From 66f00454f964e0fe694e3660e60445fc4ecb849b Mon Sep 17 00:00:00 2001 From: Craig Gidney Date: Sat, 22 Mar 2025 14:25:06 -0700 Subject: [PATCH 01/11] Allow passing `stim.PauliString` targets to `stim.circuit.append` - `stim.circuit.append` now accepts `stim.PauliString` targets, which are auto-flattened into pauli targets and combiner targets - Split the `circuit.pybind.cc` file into two files, so it takes less time to build when editing it --- file_lists/pybind_files | 1 + src/stim/circuit/circuit.pybind.cc | 1213 ++--------------------- src/stim/circuit/circuit.pybind.h | 1 + src/stim/circuit/circuit2.pybind.cc | 1157 +++++++++++++++++++++ src/stim/circuit/circuit_pybind_test.py | 31 + src/stim/gates/gates.h | 4 +- src/stim/py/stim.pybind.cc | 1 + 7 files changed, 1261 insertions(+), 1147 deletions(-) create mode 100644 src/stim/circuit/circuit2.pybind.cc diff --git a/file_lists/pybind_files b/file_lists/pybind_files index f29ee5ed6..cbcf7090e 100644 --- a/file_lists/pybind_files +++ b/file_lists/pybind_files @@ -1,4 +1,5 @@ src/stim/circuit/circuit.pybind.cc +src/stim/circuit/circuit2.pybind.cc src/stim/circuit/circuit_instruction.pybind.cc src/stim/circuit/circuit_repeat_block.pybind.cc src/stim/circuit/gate_target.pybind.cc diff --git a/src/stim/circuit/circuit.pybind.cc b/src/stim/circuit/circuit.pybind.cc index 4f3fc640f..772a7d9f7 100644 --- a/src/stim/circuit/circuit.pybind.cc +++ b/src/stim/circuit/circuit.pybind.cc @@ -37,104 +37,10 @@ #include "stim/simulators/measurements_to_detection_events.pybind.h" #include "stim/simulators/tableau_simulator.h" #include "stim/stabilizers/flow.h" -#include "stim/util_top/circuit_flow_generators.h" -#include "stim/util_top/circuit_inverse_qec.h" -#include "stim/util_top/circuit_to_detecting_regions.h" -#include "stim/util_top/circuit_vs_tableau.h" -#include "stim/util_top/count_determined_measurements.h" -#include "stim/util_top/export_crumble_url.h" -#include "stim/util_top/export_qasm.h" -#include "stim/util_top/export_quirk_url.h" -#include "stim/util_top/has_flow.h" -#include "stim/util_top/simplified_circuit.h" -#include "stim/util_top/transform_without_feedback.h" using namespace stim; using namespace stim_pybind; -std::set py_dem_filter_to_dem_target_set( - const Circuit &circuit, const CircuitStats &stats, const pybind11::object &included_targets_filter) { - std::set result; - auto add_all_dets = [&]() { - for (uint64_t k = 0; k < stats.num_detectors; k++) { - result.insert(DemTarget::relative_detector_id(k)); - } - }; - auto add_all_obs = [&]() { - for (uint64_t k = 0; k < stats.num_observables; k++) { - result.insert(DemTarget::observable_id(k)); - } - }; - - bool has_coords = false; - std::map> cached_coords; - auto get_coords_cached = [&]() -> const std::map> & { - std::set all_dets; - for (uint64_t k = 0; k < stats.num_detectors; k++) { - all_dets.insert(k); - } - if (!has_coords) { - cached_coords = circuit.get_detector_coordinates(all_dets); - has_coords = true; - } - return cached_coords; - }; - - if (included_targets_filter.is_none()) { - add_all_dets(); - add_all_obs(); - return result; - } - for (const auto &filter : included_targets_filter) { - bool fail = false; - if (pybind11::isinstance(filter)) { - result.insert(pybind11::cast(filter)); - } else if (pybind11::isinstance(filter)) { - std::string_view s = pybind11::cast(filter); - if (s == "D") { - add_all_dets(); - } else if (s == "L") { - add_all_obs(); - } else if (s.starts_with("D") || s.starts_with("L")) { - result.insert(DemTarget::from_text(s)); - } else { - fail = true; - } - } else { - std::vector prefix; - for (auto e : filter) { - if (pybind11::isinstance(e) || pybind11::isinstance(e)) { - prefix.push_back(pybind11::cast(e)); - } else { - fail = true; - break; - } - } - if (!fail) { - for (const auto &[target, coord] : get_coords_cached()) { - if (coord.size() >= prefix.size()) { - bool match = true; - for (size_t k = 0; k < prefix.size(); k++) { - match &= prefix[k] == coord[k]; - } - if (match) { - result.insert(DemTarget::relative_detector_id(target)); - } - } - } - } - } - if (fail) { - std::stringstream ss; - ss << "Don't know how to interpret '"; - ss << pybind11::cast(pybind11::repr(filter)); - ss << "' as a dem target filter."; - throw std::invalid_argument(ss.str()); - } - } - return result; -} - std::string circuit_repr(const Circuit &self) { if (self.operations.empty()) { return "stim.Circuit()"; @@ -244,6 +150,61 @@ void circuit_insert(Circuit &self, pybind11::ssize_t &index, pybind11::object &o } } +void handle_to_gate_targets(const pybind11::handle &obj, std::vector &out, bool can_iterate) { + try { + FlexPauliString ps = pybind11::cast(obj); + bool first = true; + ps.value.ref().for_each_active_pauli([&](size_t q) { + if (!first) { + out.push_back(GateTarget::combiner().data); + } + first = false; + out.push_back(GateTarget::pauli_xz(q, ps.value.xs[q], ps.value.zs[q]).data); + }); + if (first) { + throw std::invalid_argument("Don't know how to target an empty stim.PauliString"); + } + return; + } catch (const pybind11::cast_error &ex) { + } + + try { + std::string_view text = pybind11::cast(obj); + out.push_back(GateTarget::from_target_str(text).data); + return; + } catch (const pybind11::cast_error &ex) { + } + + try { + out.push_back(pybind11::cast(obj).data); + return; + } catch (const pybind11::cast_error &ex) { + } + + try { + out.push_back(GateTarget{pybind11::cast(obj)}.data); + return; + } catch (const pybind11::cast_error &ex) { + } + + if (can_iterate) { + pybind11::module collections = pybind11::module::import("collections.abc"); + pybind11::object iterable_type = collections.attr("Iterable"); + if (pybind11::isinstance(obj, iterable_type)) { + for (const auto &t : obj) { + handle_to_gate_targets(t, out, false); + } + return; + } + } + + std::stringstream ss; + ss << "Don't know how to target the object `"; + ss << pybind11::cast(pybind11::repr(obj)); + ss << "`."; + throw std::invalid_argument(ss.str()); +} + void circuit_append( Circuit &self, const pybind11::object &obj, @@ -253,13 +214,7 @@ void circuit_append( bool backwards_compat) { // Extract single target or list of targets. std::vector raw_targets; - try { - raw_targets.push_back(obj_to_gate_target(targets).data); - } catch (const std::invalid_argument &) { - for (const auto &t : targets) { - raw_targets.push_back(handle_to_gate_target(t).data); - } - } + handle_to_gate_targets(targets, raw_targets, true); if (pybind11::isinstance(obj)) { std::string_view gate_name = pybind11::cast(obj); @@ -520,79 +475,6 @@ void stim_pybind::pybind_circuit_methods(pybind11::module &, pybind11::class_, - clean_doc_string(R"DOC( - Counts the number of predictable measurements in the circuit. - - This method ignores any noise in the circuit. - - This method works by performing a tableau stabilizer simulation of the circuit - and, before each measurement is simulated, checking if its expectation is - non-zero. - - A measurement is predictable if its result can be predicted by using other - measurements that have already been performed, assuming the circuit is executed - without any noise. - - Note that, when multiple measurements occur at the same time, re-ordering the - order they are resolved can change which specific measurements are predictable - but won't change how many of them were predictable in total. - - The number of predictable measurements is a useful quantity because it's - related to the number of detectors and observables that a circuit should - declare. If circuit.num_detectors + circuit.num_observables is less than - circuit.count_determined_measurements(), this is a warning sign that you've - missed some detector declarations. - - The exact relationship between the number of determined measurements and the - number of detectors and observables can differ from code to code. For example, - the toric code has an extra redundant measurement compared to the surface code - because in the toric code the last X stabilizer to be measured is equal to the - product of all other X stabilizers even in the first round when initializing in - the Z basis. Typically this relationship is not declared as a detector, because - it's not local, or as an observable, because it doesn't store a qubit. - - Returns: - The number of measurements that were predictable. - - Examples: - >>> import stim - - >>> stim.Circuit(''' - ... R 0 - ... M 0 - ... ''').count_determined_measurements() - 1 - - >>> stim.Circuit(''' - ... R 0 - ... H 0 - ... M 0 - ... ''').count_determined_measurements() - 0 - - >>> stim.Circuit(''' - ... R 0 1 - ... MZZ 0 1 - ... MYY 0 1 - ... MXX 0 1 - ... ''').count_determined_measurements() - 2 - - >>> circuit = stim.Circuit.generated( - ... "surface_code:rotated_memory_x", - ... distance=5, - ... rounds=9, - ... ) - >>> circuit.count_determined_measurements() - 217 - >>> circuit.num_detectors + circuit.num_observables - 217 - )DOC") - .data()); - c.def_property_readonly( "num_observables", &Circuit::count_observables, @@ -1113,7 +995,7 @@ void stim_pybind::pybind_circuit_methods(pybind11::module &, pybind11::class_ None: + @overload def append(self, name: str, targets: Union[int, stim.GateTarget, stim.PauliString, Iterable[Union[int, stim.GateTarget, stim.PauliString]]], arg: Union[float, Iterable[float]], *, tag: str = "") -> None: @overload def append(self, name: Union[stim.CircuitOperation, stim.CircuitRepeatBlock]) -> None: Note: `stim.Circuit.append_operation` is an alias of `stim.Circuit.append`. @@ -1127,6 +1009,7 @@ void stim_pybind::pybind_circuit_methods(pybind11::module &, pybind11::class_>> c.append("CNOT", [stim.target_rec(-1), 0]) >>> c.append("X_ERROR", [0], 0.125) >>> c.append("CORRELATED_ERROR", [stim.target_x(0), stim.target_y(2)], 0.25) + >>> c.append("MPP", [stim.PauliString("X1*Y2"), stim.GateTarget("Z3")]) >>> print(repr(c)) stim.Circuit(''' X 0 @@ -1135,6 +1018,7 @@ void stim_pybind::pybind_circuit_methods(pybind11::module &, pybind11::class_(circuit, ignore_noise, ignore_measurement, ignore_reset); - }, - pybind11::kw_only(), - pybind11::arg("ignore_noise") = false, - pybind11::arg("ignore_measurement") = false, - pybind11::arg("ignore_reset") = false, - clean_doc_string(R"DOC( - @signature def to_tableau(self, *, ignore_noise: bool = False, ignore_measurement: bool = False, ignore_reset: bool = False) -> stim.Tableau: - Converts the circuit into an equivalent stabilizer tableau. - - Args: - ignore_noise: Defaults to False. When False, any noise operations in the - circuit will cause the conversion to fail with an exception. When True, - noise operations are skipped over as if they weren't even present in the - circuit. - ignore_measurement: Defaults to False. When False, any measurement - operations in the circuit will cause the conversion to fail with an - exception. When True, measurement operations are skipped over as if they - weren't even present in the circuit. - ignore_reset: Defaults to False. When False, any reset operations in the - circuit will cause the conversion to fail with an exception. When True, - reset operations are skipped over as if they weren't even present in the - circuit. - - Returns: - A tableau equivalent to the circuit (up to global phase). - - Raises: - ValueError: - The circuit contains noise operations but ignore_noise=False. - OR - The circuit contains measurement operations but - ignore_measurement=False. - OR - The circuit contains reset operations but ignore_reset=False. - - Examples: - >>> import stim - >>> stim.Circuit(''' - ... H 0 - ... CNOT 0 1 - ... ''').to_tableau() - stim.Tableau.from_conjugated_generators( - xs=[ - stim.PauliString("+Z_"), - stim.PauliString("+_X"), - ], - zs=[ - stim.PauliString("+XX"), - stim.PauliString("+ZZ"), - ], - ) - )DOC") - .data()); - - c.def( - "to_qasm", - [](const Circuit &self, int open_qasm_version, bool skip_dets_and_obs) -> std::string { - std::stringstream out; - export_open_qasm(self, out, open_qasm_version, skip_dets_and_obs); - return out.str(); - }, - pybind11::kw_only(), - pybind11::arg("open_qasm_version"), - pybind11::arg("skip_dets_and_obs") = false, - clean_doc_string(R"DOC( - @signature def to_qasm(self, *, open_qasm_version: int, skip_dets_and_obs: bool = False) -> str: - Creates an equivalent OpenQASM implementation of the circuit. - - Args: - open_qasm_version: The version of OpenQASM to target. - This should be set to 2 or to 3. - - Differences between the versions are: - - Support for operations on classical bits (only version 3). - This means DETECTOR and OBSERVABLE_INCLUDE only work with - version 3. - - Support for feedback operations (only version 3). - - Support for subroutines (only version 3). Without subroutines, - non-standard dissipative gates like MR and RX need to decompose - inline every single time they're used. - - Minor name changes (e.g. creg -> bit, qelib1.inc -> stdgates.inc). - skip_dets_and_obs: Defaults to False. When set to False, the output will - include a `dets` register and an `obs` register (assuming the circuit - has detectors and observables). These registers will be computed as part - of running the circuit. This requires performing a simulation of the - circuit, in order to correctly account for the expected value of - measurements. - - When set to True, the `dets` and `obs` registers are not included in the - output, and no simulation of the circuit is performed. - - Returns: - The OpenQASM code as a string. - - Examples: - >>> import stim - >>> circuit = stim.Circuit(''' - ... R 0 1 - ... X 1 - ... H 0 - ... CX 0 1 - ... M 0 1 - ... DETECTOR rec[-1] rec[-2] - ... '''); - >>> qasm = circuit.to_qasm(open_qasm_version=3); - >>> print(qasm.strip().replace('\n\n', '\n')) - OPENQASM 3.0; - include "stdgates.inc"; - qreg q[2]; - creg rec[2]; - creg dets[1]; - reset q[0]; - reset q[1]; - x q[1]; - h q[0]; - cx q[0], q[1]; - measure q[0] -> rec[0]; - measure q[1] -> rec[1]; - dets[0] = rec[1] ^ rec[0] ^ 1; - )DOC") - .data()); - c.def( "__len__", [](const Circuit &self) { @@ -2444,163 +2206,6 @@ void stim_pybind::pybind_circuit_methods(pybind11::module &, pybind11::class_ std::map> { - auto stats = self.compute_stats(); - auto included_target_set = py_dem_filter_to_dem_target_set(self, stats, included_targets); - std::set included_tick_set; - - if (included_ticks.is_none()) { - for (uint64_t k = 0; k < stats.num_ticks; k++) { - included_tick_set.insert(k); - } - } else { - for (const auto &t : included_ticks) { - included_tick_set.insert(pybind11::cast(t)); - } - } - auto result = circuit_to_detecting_regions( - self, included_target_set, included_tick_set, ignore_anticommutation_errors); - std::map> exposed_result; - for (const auto &[k, v] : result) { - exposed_result.insert({ExposedDemTarget(k), std::move(v)}); - } - return exposed_result; - }, - pybind11::kw_only(), - pybind11::arg("targets") = pybind11::none(), - pybind11::arg("ticks") = pybind11::none(), - pybind11::arg("ignore_anticommutation_errors") = false, - clean_doc_string(R"DOC( - @signature def detecting_regions(self, *, targets: Optional[Iterable[stim.DemTarget | str | Iterable[float]]] = None, ticks: Optional[Iterable[int]] = None) -> Dict[stim.DemTarget, Dict[int, stim.PauliString]]: - Records where detectors and observables are sensitive to errors over time. - - The result of this method is a nested dictionary, mapping detectors/observables - and ticks to Pauli sensitivities for that detector/observable at that time. - - For example, if observable 2 has Z-type sensitivity on qubits 5 and 6 during - tick 3, then `result[stim.target_logical_observable_id(2)][3]` will be equal to - `stim.PauliString("Z5*Z6")`. - - If you want sensitivities from more places in the circuit, besides just at the - TICK instructions, you can work around this by making a version of the circuit - with more TICKs. - - Args: - targets: Defaults to everything (None). - - When specified, this should be an iterable of filters where items - matching any one filter are included. - - A variety of filters are supported: - stim.DemTarget: Includes the targeted detector or observable. - Iterable[float]: Coordinate prefix match. Includes detectors whose - coordinate data begins with the same floats. - "D": Includes all detectors. - "L": Includes all observables. - "D#" (e.g. "D5"): Includes the detector with the specified index. - "L#" (e.g. "L5"): Includes the observable with the specified index. - - ticks: Defaults to everything (None). - When specified, this should be a list of integers corresponding to - the tick indices to report sensitivities for. - - ignore_anticommutation_errors: Defaults to False. - When set to False, invalid detecting regions that anticommute with a - reset will cause the method to raise an exception. When set to True, - the offending component will simply be silently dropped. This can - result in broken detectors having apparently enormous detecting - regions. - - Returns: - Nested dictionaries keyed first by a `stim.DemTarget` identifying the - detector or observable, then by the index of the tick, leading to a - PauliString with that target's error sensitivity at that tick. - - Note you can use `stim.PauliString.pauli_indices` to quickly get to the - non-identity terms in the sensitivity. - - Examples: - >>> import stim - - >>> detecting_regions = stim.Circuit(''' - ... R 0 - ... TICK - ... H 0 - ... TICK - ... CX 0 1 - ... TICK - ... MX 0 1 - ... DETECTOR rec[-1] rec[-2] - ... ''').detecting_regions() - >>> for target, tick_regions in detecting_regions.items(): - ... print("target", target) - ... for tick, sensitivity in tick_regions.items(): - ... print(" tick", tick, "=", sensitivity) - target D0 - tick 0 = +Z_ - tick 1 = +X_ - tick 2 = +XX - - >>> circuit = stim.Circuit.generated( - ... "surface_code:rotated_memory_x", - ... rounds=5, - ... distance=4, - ... ) - - >>> detecting_regions = circuit.detecting_regions( - ... targets=["L0", (2, 4), stim.DemTarget.relative_detector_id(5)], - ... ticks=range(5, 15), - ... ) - >>> for target, tick_regions in detecting_regions.items(): - ... print("target", target) - ... for tick, sensitivity in tick_regions.items(): - ... print(" tick", tick, "=", sensitivity) - target D1 - tick 5 = +____________________X______________________ - tick 6 = +____________________Z______________________ - target D5 - tick 5 = +______X____________________________________ - tick 6 = +______Z____________________________________ - target D14 - tick 5 = +__________X_X______XXX_____________________ - tick 6 = +__________X_X______XZX_____________________ - tick 7 = +__________X_X______XZX_____________________ - tick 8 = +__________X_X______XXX_____________________ - tick 9 = +__________XXX_____XXX______________________ - tick 10 = +__________XXX_______X______________________ - tick 11 = +__________X_________X______________________ - tick 12 = +____________________X______________________ - tick 13 = +____________________Z______________________ - target D29 - tick 7 = +____________________Z______________________ - tick 8 = +____________________X______________________ - tick 9 = +____________________XX_____________________ - tick 10 = +___________________XXX_______X_____________ - tick 11 = +____________X______XXXX______X_____________ - tick 12 = +__________X_X______XXX_____________________ - tick 13 = +__________X_X______XZX_____________________ - tick 14 = +__________X_X______XZX_____________________ - target D44 - tick 14 = +____________________Z______________________ - target L0 - tick 5 = +_X________X________X________X______________ - tick 6 = +_X________X________X________X______________ - tick 7 = +_X________X________X________X______________ - tick 8 = +_X________X________X________X______________ - tick 9 = +_X________X_______XX________X______________ - tick 10 = +_X________X________X________X______________ - tick 11 = +_X________XX_______X________XX_____________ - tick 12 = +_X________X________X________X______________ - tick 13 = +_X________X________X________X______________ - tick 14 = +_X________X________X________X______________ - )DOC") - .data()); - c.def( "without_noise", &Circuit::without_noise, @@ -2672,131 +2277,6 @@ void stim_pybind::pybind_circuit_methods(pybind11::module &, pybind11::class_>> import stim - - >>> stim.Circuit(''' - ... SWAP 0 1 - ... ''').decomposed() - stim.Circuit(''' - CX 0 1 1 0 0 1 - ''') - - >>> stim.Circuit(''' - ... ISWAP 0 1 2 1 - ... TICK - ... MPP !X1*Y2*Z3 - ... ''').decomposed() - stim.Circuit(''' - H 0 - CX 0 1 1 0 - H 1 - S 1 0 - H 2 - CX 2 1 1 2 - H 1 - S 1 2 - TICK - H 1 2 - S 2 - H 2 - S 2 2 - CX 2 1 3 1 - M !1 - CX 2 1 3 1 - H 2 - S 2 - H 2 - S 2 2 - H 1 - ''') - )DOC") - .data()); - - c.def( - "with_inlined_feedback", - &circuit_with_inlined_feedback, - clean_doc_string(R"DOC( - Returns a circuit without feedback with rewritten detectors/observables. - - When a feedback operation affects the expected parity of a detector or - observable, the measurement controlling that feedback operation is implicitly - part of the measurement set that defines the detector or observable. This - method removes all feedback, but avoids changing the meaning of detectors or - observables by turning these implicit measurement dependencies into explicit - measurement dependencies added to the observable or detector. - - This method guarantees that the detector error model derived from the original - circuit, and the transformed circuit, will be equivalent (modulo floating point - rounding errors and variations in where loops are placed). Specifically, the - following should be true for any circuit: - - dem1 = circuit.flattened().detector_error_model() - dem2 = circuit.with_inlined_feedback().flattened().detector_error_model() - assert dem1.approx_equals(dem2, 1e-5) - - Returns: - A `stim.Circuit` with feedback operations removed, with rewritten DETECTOR - instructions (as needed to avoid changing the meaning of each detector), and - with additional OBSERVABLE_INCLUDE instructions (as needed to avoid changing - the meaning of each observable). - - The circuit's function is permitted to differ from the original in that - any feedback operation can be pushed to the end of the circuit and - discarded. All non-feedback operations must stay where they are, preserving - the structure of the circuit. - - Examples: - >>> import stim - - >>> stim.Circuit(''' - ... CX 0 1 # copy to measure qubit - ... M 1 # measure first time - ... CX rec[-1] 1 # use feedback to reset measurement qubit - ... CX 0 1 # copy to measure qubit - ... M 1 # measure second time - ... DETECTOR rec[-1] rec[-2] - ... OBSERVABLE_INCLUDE(0) rec[-1] - ... ''').with_inlined_feedback() - stim.Circuit(''' - CX 0 1 - M 1 - OBSERVABLE_INCLUDE(0) rec[-1] - CX 0 1 - M 1 - DETECTOR rec[-1] - OBSERVABLE_INCLUDE(0) rec[-1] - ''') - )DOC") - .data()); - c.def( "inverse", [](const Circuit &self) -> Circuit { @@ -2853,479 +2333,6 @@ void stim_pybind::pybind_circuit_methods(pybind11::module &, pybind11::class_ &flow, bool unsigned_only) -> bool { - std::span> flows = {&flow, &flow + 1}; - if (unsigned_only) { - return check_if_circuit_has_unsigned_stabilizer_flows(self, flows)[0]; - } else { - auto rng = externally_seeded_rng(); - return sample_if_circuit_has_stabilizer_flows(256, rng, self, flows)[0]; - } - }, - pybind11::arg("flow"), - pybind11::kw_only(), - pybind11::arg("unsigned") = false, - clean_doc_string(R"DOC( - @signature def has_flow(self, flow: stim.Flow, *, unsigned: bool = False) -> bool: - Determines if the circuit has the given stabilizer flow or not. - - A circuit has a stabilizer flow P -> Q if it maps the instantaneous stabilizer - P at the start of the circuit to the instantaneous stabilizer Q at the end of - the circuit. The flow may be mediated by certain measurements. For example, - a lattice surgery CNOT involves an MXX measurement and an MZZ measurement, and - the CNOT flows implemented by the circuit involve these measurements. - - A flow like P -> Q means the circuit transforms P into Q. - A flow like 1 -> P means the circuit prepares P. - A flow like P -> 1 means the circuit measures P. - A flow like 1 -> 1 means the circuit contains a check (could be a DETECTOR). - - This method ignores any noise in the circuit. - - Args: - flow: The flow to check for. - unsigned: Defaults to False. When False, the flows must be correct including - the sign of the Pauli strings. When True, only the Pauli terms need to - be correct; the signs are permitted to be inverted. In effect, this - requires the circuit to be correct up to Pauli gates. - - Returns: - True if the circuit has the given flow; False otherwise. - - Examples: - >>> import stim - - >>> m = stim.Circuit('M 0') - >>> m.has_flow(stim.Flow('Z -> Z')) - True - >>> m.has_flow(stim.Flow('X -> X')) - False - >>> m.has_flow(stim.Flow('Z -> I')) - False - >>> m.has_flow(stim.Flow('Z -> I xor rec[-1]')) - True - >>> m.has_flow(stim.Flow('Z -> rec[-1]')) - True - - >>> cx58 = stim.Circuit('CX 5 8') - >>> cx58.has_flow(stim.Flow('X5 -> X5*X8')) - True - >>> cx58.has_flow(stim.Flow('X_ -> XX')) - False - >>> cx58.has_flow(stim.Flow('_____X___ -> _____X__X')) - True - - >>> stim.Circuit(''' - ... RY 0 - ... ''').has_flow(stim.Flow( - ... output=stim.PauliString("Y"), - ... )) - True - - >>> stim.Circuit(''' - ... RY 0 - ... X_ERROR(0.1) 0 - ... ''').has_flow(stim.Flow( - ... output=stim.PauliString("Y"), - ... )) - True - - >>> stim.Circuit(''' - ... RY 0 - ... ''').has_flow(stim.Flow( - ... output=stim.PauliString("X"), - ... )) - False - - >>> stim.Circuit(''' - ... CX 0 1 - ... ''').has_flow(stim.Flow( - ... input=stim.PauliString("+X_"), - ... output=stim.PauliString("+XX"), - ... )) - True - - >>> stim.Circuit(''' - ... # Lattice surgery CNOT - ... R 1 - ... MXX 0 1 - ... MZZ 1 2 - ... MX 1 - ... ''').has_flow(stim.Flow( - ... input=stim.PauliString("+X_X"), - ... output=stim.PauliString("+__X"), - ... measurements=[0, 2], - ... )) - True - - >>> stim.Circuit(''' - ... H 0 - ... ''').has_flow( - ... stim.Flow("Y -> Y"), - ... unsigned=True, - ... ) - True - - >>> stim.Circuit(''' - ... H 0 - ... ''').has_flow( - ... stim.Flow("Y -> Y"), - ... unsigned=False, - ... ) - False - - Caveats: - Currently, the unsigned=False version of this method is implemented by - performing 256 randomized tests. Each test has a 50% chance of a false - positive, and a 0% chance of a false negative. So, when the method returns - True, there is technically still a 2^-256 chance the circuit doesn't have - the flow. This is lower than the chance of a cosmic ray flipping the result. - )DOC") - .data()); - - c.def( - "has_all_flows", - [](const Circuit &self, const std::vector> &flows, bool unsigned_only) -> bool { - std::vector results; - if (unsigned_only) { - results = check_if_circuit_has_unsigned_stabilizer_flows(self, flows); - } else { - auto rng = externally_seeded_rng(); - results = sample_if_circuit_has_stabilizer_flows(256, rng, self, flows); - } - for (auto b : results) { - if (!b) { - return false; - } - } - return true; - }, - pybind11::arg("flows"), - pybind11::kw_only(), - pybind11::arg("unsigned") = false, - clean_doc_string(R"DOC( - @signature def has_all_flows(self, flows: Iterable[stim.Flow], *, unsigned: bool = False) -> bool: - Determines if the circuit has all the given stabilizer flow or not. - - This is a faster version of `all(c.has_flow(f) for f in flows)`. It's faster - because, behind the scenes, the circuit can be iterated once instead of once - per flow. - - This method ignores any noise in the circuit. - - Args: - flows: An iterable of `stim.Flow` instances representing the flows to check. - unsigned: Defaults to False. When False, the flows must be correct including - the sign of the Pauli strings. When True, only the Pauli terms need to - be correct; the signs are permitted to be inverted. In effect, this - requires the circuit to be correct up to Pauli gates. - - Returns: - True if the circuit has the given flow; False otherwise. - - Examples: - >>> import stim - - >>> stim.Circuit('H 0').has_all_flows([ - ... stim.Flow('X -> Z'), - ... stim.Flow('Y -> Y'), - ... stim.Flow('Z -> X'), - ... ]) - False - - >>> stim.Circuit('H 0').has_all_flows([ - ... stim.Flow('X -> Z'), - ... stim.Flow('Y -> -Y'), - ... stim.Flow('Z -> X'), - ... ]) - True - - >>> stim.Circuit('H 0').has_all_flows([ - ... stim.Flow('X -> Z'), - ... stim.Flow('Y -> Y'), - ... stim.Flow('Z -> X'), - ... ], unsigned=True) - True - - Caveats: - Currently, the unsigned=False version of this method is implemented by - performing 256 randomized tests. Each test has a 50% chance of a false - positive, and a 0% chance of a false negative. So, when the method returns - True, there is technically still a 2^-256 chance the circuit doesn't have - the flow. This is lower than the chance of a cosmic ray flipping the result. - )DOC") - .data()); - - c.def( - "flow_generators", - &circuit_flow_generators, - clean_doc_string(R"DOC( - @signature def flow_generators(self) -> List[stim.Flow]: - Returns a list of flows that generate all of the circuit's flows. - - Every stabilizer flow that the circuit implements is a product of some - subset of the returned generators. Every returned flow will be a flow - of the circuit. - - Returns: - A list of flow generators for the circuit. - - Examples: - >>> import stim - - >>> stim.Circuit("H 0").flow_generators() - [stim.Flow("X -> Z"), stim.Flow("Z -> X")] - - >>> stim.Circuit("M 0").flow_generators() - [stim.Flow("1 -> Z xor rec[0]"), stim.Flow("Z -> rec[0]")] - - >>> stim.Circuit("RX 0").flow_generators() - [stim.Flow("1 -> X")] - - >>> for flow in stim.Circuit("MXX 0 1").flow_generators(): - ... print(flow) - 1 -> XX xor rec[0] - _X -> _X - X_ -> _X xor rec[0] - ZZ -> ZZ - - >>> for flow in stim.Circuit.generated( - ... "repetition_code:memory", - ... rounds=2, - ... distance=3, - ... after_clifford_depolarization=1e-3, - ... ).flow_generators(): - ... print(flow) - 1 -> rec[0] - 1 -> rec[1] - 1 -> rec[2] - 1 -> rec[3] - 1 -> rec[4] - 1 -> rec[5] - 1 -> rec[6] - 1 -> ____Z - 1 -> ___Z_ - 1 -> __Z__ - 1 -> _Z___ - 1 -> Z____ - )DOC") - .data()); - - c.def( - "time_reversed_for_flows", - [](const Circuit &self, - const std::vector> &flows, - bool dont_turn_measurements_into_resets) -> pybind11::object { - auto [inv_circuit, inv_flows] = - circuit_inverse_qec(self, flows, dont_turn_measurements_into_resets); - return pybind11::make_tuple(inv_circuit, inv_flows); - }, - pybind11::arg("flows"), - pybind11::kw_only(), - pybind11::arg("dont_turn_measurements_into_resets") = false, - clean_doc_string(R"DOC( - @signature def time_reversed_for_flows(self, flows: Iterable[stim.Flow], *, dont_turn_measurements_into_resets: bool = False) -> Tuple[stim.Circuit, List[stim.Flow]]: - Time-reverses the circuit while preserving error correction structure. - - This method returns a circuit that has the same internal detecting regions - as the given circuit, as well as the same internal-to-external flows given - in the `flows` argument, except they are all time-reversed. For example, if - you pass a fault tolerant preparation circuit into this method (1 -> Z), the - result will be a fault tolerant *measurement* circuit (Z -> 1). Or, if you - pass a fault tolerant C_XYZ circuit into this method (X->Y, Y->Z, and Z->X), - the result will be a fault tolerant C_ZYX circuit (X->Z, Y->X, and Z->Y). - - Note that this method doesn't guarantee that it will preserve the *sign* of the - detecting regions or stabilizer flows. For example, inverting a memory circuit - that preserves a logical observable (X->X and Z->Z) may produce a - memory circuit that always bit flips the logical observable (X->X and Z->-Z) or - that dynamically adjusts the logical observable in response to measurements - (like "X -> X xor rec[-1]" and "Z -> Z xor rec[-2]"). - - This method will turn time-reversed resets into measurements, and attempts to - turn time-reversed measurements into resets. A measurement will time-reverse - into a reset if some annotated detectors, annotated observables, or given flows - have detecting regions with sensitivity just before the measurement but none - have detecting regions with sensitivity after the measurement. - - In some cases this method will have to introduce new operations. In particular, - when a measurement-reset operation has a noisy result, time-reversing this - measurement noise produces reset noise. But the measure-reset operations don't - have built-in reset noise, so the reset noise is specified by adding an X_ERROR - or Z_ERROR noise instruction after the time-reversed measure-reset operation. - - Args: - flows: Flows you care about, that reach past the start/end of the given - circuit. The result will contain an inverted flow for each of these - given flows. You need this information because it reveals the - measurements needed to produce the inverted flows that you care - about. - - An exception will be raised if the circuit doesn't have all these - flows. The inverted circuit will have the inverses of these flows - (ignoring sign). - dont_turn_measurements_into_resets: Defaults to False. When set to - True, measurements will time-reverse into measurements even if - nothing is sensitive to the measured qubit after the measurement - completes. This guarantees the output circuit has *all* flows - that the input circuit has (up to sign and feedback), even ones - that aren't annotated. - - Returns: - An (inverted_circuit, inverted_flows) tuple. - - inverted_circuit is the qec inverse of the given circuit. - - inverted_flows is a list of flows, matching up by index with the flows - given as arguments to the method. The input, output, and sign fields - of these flows are boring. The useful field is measurement_indices, - because it's difficult to predict which measurements are needed for - the inverted flow due to effects such as implicitly-included resets - inverting into explicitly-included measurements. - - Caveats: - Currently, this method doesn't compute the sign of the inverted flows. - It unconditionally sets the sign to False. - - Examples: - >>> import stim - - >>> inv_circuit, inv_flows = stim.Circuit(''' - ... R 0 - ... H 0 - ... S 0 - ... MY 0 - ... DETECTOR rec[-1] - ... ''').time_reversed_for_flows([]) - >>> inv_circuit - stim.Circuit(''' - RY 0 - S_DAG 0 - H 0 - M 0 - DETECTOR rec[-1] - ''') - >>> inv_flows - [] - - >>> inv_circuit, inv_flows = stim.Circuit(''' - ... M 0 - ... ''').time_reversed_for_flows([ - ... stim.Flow("Z -> rec[-1]"), - ... ]) - >>> inv_circuit - stim.Circuit(''' - R 0 - ''') - >>> inv_flows - [stim.Flow("1 -> Z")] - >>> inv_circuit.has_all_flows(inv_flows, unsigned=True) - True - - >>> inv_circuit, inv_flows = stim.Circuit(''' - ... R 0 - ... ''').time_reversed_for_flows([ - ... stim.Flow("1 -> Z"), - ... ]) - >>> inv_circuit - stim.Circuit(''' - M 0 - ''') - >>> inv_flows - [stim.Flow("Z -> rec[-1]")] - - >>> inv_circuit, inv_flows = stim.Circuit(''' - ... M 0 - ... ''').time_reversed_for_flows([ - ... stim.Flow("1 -> Z xor rec[-1]"), - ... ]) - >>> inv_circuit - stim.Circuit(''' - M 0 - ''') - >>> inv_flows - [stim.Flow("Z -> rec[-1]")] - - >>> inv_circuit, inv_flows = stim.Circuit(''' - ... M 0 - ... ''').time_reversed_for_flows( - ... flows=[stim.Flow("Z -> rec[-1]")], - ... dont_turn_measurements_into_resets=True, - ... ) - >>> inv_circuit - stim.Circuit(''' - M 0 - ''') - >>> inv_flows - [stim.Flow("1 -> Z xor rec[-1]")] - - >>> inv_circuit, inv_flows = stim.Circuit(''' - ... MR(0.125) 0 - ... ''').time_reversed_for_flows([]) - >>> inv_circuit - stim.Circuit(''' - MR 0 - X_ERROR(0.125) 0 - ''') - >>> inv_flows - [] - - >>> inv_circuit, inv_flows = stim.Circuit(''' - ... MXX 0 1 - ... H 0 - ... ''').time_reversed_for_flows([ - ... stim.Flow("ZZ -> YY xor rec[-1]"), - ... stim.Flow("ZZ -> XZ"), - ... ]) - >>> inv_circuit - stim.Circuit(''' - H 0 - MXX 0 1 - ''') - >>> inv_flows - [stim.Flow("YY -> ZZ xor rec[-1]"), stim.Flow("XZ -> ZZ")] - - >>> stim.Circuit.generated( - ... "surface_code:rotated_memory_x", - ... distance=2, - ... rounds=1, - ... ).time_reversed_for_flows([])[0] - stim.Circuit(''' - QUBIT_COORDS(1, 1) 1 - QUBIT_COORDS(2, 0) 2 - QUBIT_COORDS(3, 1) 3 - QUBIT_COORDS(1, 3) 6 - QUBIT_COORDS(2, 2) 7 - QUBIT_COORDS(3, 3) 8 - QUBIT_COORDS(2, 4) 12 - RX 8 6 3 1 - MR 12 7 2 - TICK - H 12 2 - TICK - CX 1 7 12 6 - TICK - CX 6 7 12 8 - TICK - CX 3 7 2 1 - TICK - CX 8 7 2 3 - TICK - H 12 2 - TICK - M 12 7 2 - DETECTOR(2, 0, 1) rec[-1] - DETECTOR(2, 4, 1) rec[-3] - MX 8 6 3 1 - DETECTOR(2, 0, 0) rec[-5] rec[-2] rec[-1] - DETECTOR(2, 4, 0) rec[-7] rec[-4] rec[-3] - OBSERVABLE_INCLUDE(0) rec[-3] rec[-1] - ''') - )DOC") - .data()); - c.def( "diagram", &circuit_diagram, @@ -3458,88 +2465,4 @@ void stim_pybind::pybind_circuit_methods(pybind11::module &, pybind11::class_> mark; - if (!obj_mark.is_none()) { - mark = pybind11::cast>>(obj_mark); - } - return export_crumble_url(self, skip_detectors, mark); - }, - pybind11::kw_only(), - pybind11::arg("skip_detectors") = false, - pybind11::arg("mark") = pybind11::none(), - clean_doc_string(R"DOC( - @signature def to_crumble_url(self, *, skip_detectors: bool = False, mark: Optional[dict[int, list[stim.ExplainedError]]] = None) -> str: - Returns a URL that opens up crumble and loads this circuit into it. - - Crumble is a tool for editing stabilizer circuits, and visualizing their - stabilizer flows. Its source code is in the `glue/crumble` directory of - the stim code repository on github. A prebuilt version is made available - at https://algassert.com/crumble, which is what the URL returned by this - method will point to. - - Args: - skip_detectors: Defaults to False. If set to True, detectors from the - circuit aren't included in the crumble URL. This can reduce visual - clutter in crumble, and improve its performance, since it doesn't - need to indicate or track the sensitivity regions of detectors. - mark: Defaults to None (no marks). If set to a dictionary from int to - errors, such as `mark={1: circuit.shortest_graphlike_error()}`, - then the errors will be highlighted and tracked forward by crumble. - - Returns: - A URL that can be opened in a web browser. - - Examples: - >>> import stim - >>> stim.Circuit(''' - ... H 0 - ... CNOT 0 1 - ... S 1 - ... ''').to_crumble_url() - 'https://algassert.com/crumble#circuit=H_0;CX_0_1;S_1' - - >>> circuit = stim.Circuit(''' - ... M(0.25) 0 1 2 - ... DETECTOR rec[-1] rec[-2] - ... DETECTOR rec[-2] rec[-3] - ... OBSERVABLE_INCLUDE(0) rec[-1] - ... ''') - >>> err = circuit.shortest_graphlike_error(canonicalize_circuit_errors=True) - >>> circuit.to_crumble_url(skip_detectors=True, mark={1: err}) - 'https://algassert.com/crumble#circuit=;TICK;MARKX(1)1;MARKX(1)2;MARKX(1)0;TICK;M(0.25)0_1_2;OI(0)rec[-1]' - )DOC") - .data()); - - c.def( - "to_quirk_url", - &export_quirk_url, - clean_doc_string(R"DOC( - Returns a URL that opens up quirk and loads this circuit into it. - - Quirk is an open source drag and drop circuit editor with support for up to 16 - qubits. Its source code is available at https://github.com/strilanc/quirk - and a prebuilt version is available at https://algassert.com/quirk, which is - what the URL returned by this method will point to. - - Quirk doesn't support features like noise, feedback, or detectors. This method - will simply drop any unsupported operations from the circuit when producing - the URL. - - Returns: - A URL that can be opened in a web browser. - - Examples: - >>> import stim - >>> stim.Circuit(''' - ... H 0 - ... CNOT 0 1 - ... S 1 - ... ''').to_quirk_url() - 'https://algassert.com/quirk#circuit={"cols":[["H"],["•","X"],[1,"Z^½"]]}' - )DOC") - .data()); } diff --git a/src/stim/circuit/circuit.pybind.h b/src/stim/circuit/circuit.pybind.h index 28420618d..f6b4e2d3d 100644 --- a/src/stim/circuit/circuit.pybind.h +++ b/src/stim/circuit/circuit.pybind.h @@ -23,6 +23,7 @@ namespace stim_pybind { pybind11::class_ pybind_circuit(pybind11::module &m); void pybind_circuit_methods(pybind11::module &m, pybind11::class_ &c); +void pybind_circuit_methods_extra(pybind11::module &m, pybind11::class_ &c); } // namespace stim_pybind diff --git a/src/stim/circuit/circuit2.pybind.cc b/src/stim/circuit/circuit2.pybind.cc new file mode 100644 index 000000000..022054b77 --- /dev/null +++ b/src/stim/circuit/circuit2.pybind.cc @@ -0,0 +1,1157 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include "stim/circuit/circuit.pybind.h" +#include "stim/cmd/command_help.h" + +#include "stim/dem/detector_error_model_target.pybind.h" +#include "stim/util_top/circuit_flow_generators.h" +#include "stim/util_top/circuit_inverse_qec.h" +#include "stim/util_top/circuit_to_detecting_regions.h" +#include "stim/util_top/circuit_vs_tableau.h" +#include "stim/util_top/count_determined_measurements.h" +#include "stim/util_top/export_crumble_url.h" +#include "stim/util_top/export_qasm.h" +#include "stim/util_top/export_quirk_url.h" +#include "stim/util_top/has_flow.h" +#include "stim/util_top/simplified_circuit.h" +#include "stim/util_top/transform_without_feedback.h" + +using namespace stim; +using namespace stim_pybind; + +static std::set py_dem_filter_to_dem_target_set( + const Circuit &circuit, const CircuitStats &stats, const pybind11::object &included_targets_filter) { + std::set result; + auto add_all_dets = [&]() { + for (uint64_t k = 0; k < stats.num_detectors; k++) { + result.insert(DemTarget::relative_detector_id(k)); + } + }; + auto add_all_obs = [&]() { + for (uint64_t k = 0; k < stats.num_observables; k++) { + result.insert(DemTarget::observable_id(k)); + } + }; + + bool has_coords = false; + std::map> cached_coords; + auto get_coords_cached = [&]() -> const std::map> & { + std::set all_dets; + for (uint64_t k = 0; k < stats.num_detectors; k++) { + all_dets.insert(k); + } + if (!has_coords) { + cached_coords = circuit.get_detector_coordinates(all_dets); + has_coords = true; + } + return cached_coords; + }; + + if (included_targets_filter.is_none()) { + add_all_dets(); + add_all_obs(); + return result; + } + for (const auto &filter : included_targets_filter) { + bool fail = false; + if (pybind11::isinstance(filter)) { + result.insert(pybind11::cast(filter)); + } else if (pybind11::isinstance(filter)) { + std::string_view s = pybind11::cast(filter); + if (s == "D") { + add_all_dets(); + } else if (s == "L") { + add_all_obs(); + } else if (s.starts_with("D") || s.starts_with("L")) { + result.insert(DemTarget::from_text(s)); + } else { + fail = true; + } + } else { + std::vector prefix; + for (auto e : filter) { + if (pybind11::isinstance(e) || pybind11::isinstance(e)) { + prefix.push_back(pybind11::cast(e)); + } else { + fail = true; + break; + } + } + if (!fail) { + for (const auto &[target, coord] : get_coords_cached()) { + if (coord.size() >= prefix.size()) { + bool match = true; + for (size_t k = 0; k < prefix.size(); k++) { + match &= prefix[k] == coord[k]; + } + if (match) { + result.insert(DemTarget::relative_detector_id(target)); + } + } + } + } + } + if (fail) { + std::stringstream ss; + ss << "Don't know how to interpret '"; + ss << pybind11::cast(pybind11::repr(filter)); + ss << "' as a dem target filter."; + throw std::invalid_argument(ss.str()); + } + } + return result; +} + +void stim_pybind::pybind_circuit_methods_extra(pybind11::module &, pybind11::class_ &c) { + c.def( + "detecting_regions", + [](const Circuit &self, + const pybind11::object &included_targets, + const pybind11::object &included_ticks, + bool ignore_anticommutation_errors) -> std::map> { + auto stats = self.compute_stats(); + auto included_target_set = py_dem_filter_to_dem_target_set(self, stats, included_targets); + std::set included_tick_set; + + if (included_ticks.is_none()) { + for (uint64_t k = 0; k < stats.num_ticks; k++) { + included_tick_set.insert(k); + } + } else { + for (const auto &t : included_ticks) { + included_tick_set.insert(pybind11::cast(t)); + } + } + auto result = circuit_to_detecting_regions( + self, included_target_set, included_tick_set, ignore_anticommutation_errors); + std::map> exposed_result; + for (const auto &[k, v] : result) { + exposed_result.insert({ExposedDemTarget(k), std::move(v)}); + } + return exposed_result; + }, + pybind11::kw_only(), + pybind11::arg("targets") = pybind11::none(), + pybind11::arg("ticks") = pybind11::none(), + pybind11::arg("ignore_anticommutation_errors") = false, + clean_doc_string(R"DOC( + @signature def detecting_regions(self, *, targets: Optional[Iterable[stim.DemTarget | str | Iterable[float]]] = None, ticks: Optional[Iterable[int]] = None) -> Dict[stim.DemTarget, Dict[int, stim.PauliString]]: + Records where detectors and observables are sensitive to errors over time. + + The result of this method is a nested dictionary, mapping detectors/observables + and ticks to Pauli sensitivities for that detector/observable at that time. + + For example, if observable 2 has Z-type sensitivity on qubits 5 and 6 during + tick 3, then `result[stim.target_logical_observable_id(2)][3]` will be equal to + `stim.PauliString("Z5*Z6")`. + + If you want sensitivities from more places in the circuit, besides just at the + TICK instructions, you can work around this by making a version of the circuit + with more TICKs. + + Args: + targets: Defaults to everything (None). + + When specified, this should be an iterable of filters where items + matching any one filter are included. + + A variety of filters are supported: + stim.DemTarget: Includes the targeted detector or observable. + Iterable[float]: Coordinate prefix match. Includes detectors whose + coordinate data begins with the same floats. + "D": Includes all detectors. + "L": Includes all observables. + "D#" (e.g. "D5"): Includes the detector with the specified index. + "L#" (e.g. "L5"): Includes the observable with the specified index. + + ticks: Defaults to everything (None). + When specified, this should be a list of integers corresponding to + the tick indices to report sensitivities for. + + ignore_anticommutation_errors: Defaults to False. + When set to False, invalid detecting regions that anticommute with a + reset will cause the method to raise an exception. When set to True, + the offending component will simply be silently dropped. This can + result in broken detectors having apparently enormous detecting + regions. + + Returns: + Nested dictionaries keyed first by a `stim.DemTarget` identifying the + detector or observable, then by the index of the tick, leading to a + PauliString with that target's error sensitivity at that tick. + + Note you can use `stim.PauliString.pauli_indices` to quickly get to the + non-identity terms in the sensitivity. + + Examples: + >>> import stim + + >>> detecting_regions = stim.Circuit(''' + ... R 0 + ... TICK + ... H 0 + ... TICK + ... CX 0 1 + ... TICK + ... MX 0 1 + ... DETECTOR rec[-1] rec[-2] + ... ''').detecting_regions() + >>> for target, tick_regions in detecting_regions.items(): + ... print("target", target) + ... for tick, sensitivity in tick_regions.items(): + ... print(" tick", tick, "=", sensitivity) + target D0 + tick 0 = +Z_ + tick 1 = +X_ + tick 2 = +XX + + >>> circuit = stim.Circuit.generated( + ... "surface_code:rotated_memory_x", + ... rounds=5, + ... distance=4, + ... ) + + >>> detecting_regions = circuit.detecting_regions( + ... targets=["L0", (2, 4), stim.DemTarget.relative_detector_id(5)], + ... ticks=range(5, 15), + ... ) + >>> for target, tick_regions in detecting_regions.items(): + ... print("target", target) + ... for tick, sensitivity in tick_regions.items(): + ... print(" tick", tick, "=", sensitivity) + target D1 + tick 5 = +____________________X______________________ + tick 6 = +____________________Z______________________ + target D5 + tick 5 = +______X____________________________________ + tick 6 = +______Z____________________________________ + target D14 + tick 5 = +__________X_X______XXX_____________________ + tick 6 = +__________X_X______XZX_____________________ + tick 7 = +__________X_X______XZX_____________________ + tick 8 = +__________X_X______XXX_____________________ + tick 9 = +__________XXX_____XXX______________________ + tick 10 = +__________XXX_______X______________________ + tick 11 = +__________X_________X______________________ + tick 12 = +____________________X______________________ + tick 13 = +____________________Z______________________ + target D29 + tick 7 = +____________________Z______________________ + tick 8 = +____________________X______________________ + tick 9 = +____________________XX_____________________ + tick 10 = +___________________XXX_______X_____________ + tick 11 = +____________X______XXXX______X_____________ + tick 12 = +__________X_X______XXX_____________________ + tick 13 = +__________X_X______XZX_____________________ + tick 14 = +__________X_X______XZX_____________________ + target D44 + tick 14 = +____________________Z______________________ + target L0 + tick 5 = +_X________X________X________X______________ + tick 6 = +_X________X________X________X______________ + tick 7 = +_X________X________X________X______________ + tick 8 = +_X________X________X________X______________ + tick 9 = +_X________X_______XX________X______________ + tick 10 = +_X________X________X________X______________ + tick 11 = +_X________XX_______X________XX_____________ + tick 12 = +_X________X________X________X______________ + tick 13 = +_X________X________X________X______________ + tick 14 = +_X________X________X________X______________ + )DOC") + .data()); + + c.def( + "count_determined_measurements", + &count_determined_measurements, + clean_doc_string(R"DOC( + Counts the number of predictable measurements in the circuit. + + This method ignores any noise in the circuit. + + This method works by performing a tableau stabilizer simulation of the circuit + and, before each measurement is simulated, checking if its expectation is + non-zero. + + A measurement is predictable if its result can be predicted by using other + measurements that have already been performed, assuming the circuit is executed + without any noise. + + Note that, when multiple measurements occur at the same time, re-ordering the + order they are resolved can change which specific measurements are predictable + but won't change how many of them were predictable in total. + + The number of predictable measurements is a useful quantity because it's + related to the number of detectors and observables that a circuit should + declare. If circuit.num_detectors + circuit.num_observables is less than + circuit.count_determined_measurements(), this is a warning sign that you've + missed some detector declarations. + + The exact relationship between the number of determined measurements and the + number of detectors and observables can differ from code to code. For example, + the toric code has an extra redundant measurement compared to the surface code + because in the toric code the last X stabilizer to be measured is equal to the + product of all other X stabilizers even in the first round when initializing in + the Z basis. Typically this relationship is not declared as a detector, because + it's not local, or as an observable, because it doesn't store a qubit. + + Returns: + The number of measurements that were predictable. + + Examples: + >>> import stim + + >>> stim.Circuit(''' + ... R 0 + ... M 0 + ... ''').count_determined_measurements() + 1 + + >>> stim.Circuit(''' + ... R 0 + ... H 0 + ... M 0 + ... ''').count_determined_measurements() + 0 + + >>> stim.Circuit(''' + ... R 0 1 + ... MZZ 0 1 + ... MYY 0 1 + ... MXX 0 1 + ... ''').count_determined_measurements() + 2 + + >>> circuit = stim.Circuit.generated( + ... "surface_code:rotated_memory_x", + ... distance=5, + ... rounds=9, + ... ) + >>> circuit.count_determined_measurements() + 217 + >>> circuit.num_detectors + circuit.num_observables + 217 + )DOC") + .data()); + + c.def( + "to_tableau", + [](const Circuit &circuit, bool ignore_noise, bool ignore_measurement, bool ignore_reset) { + return circuit_to_tableau(circuit, ignore_noise, ignore_measurement, ignore_reset); + }, + pybind11::kw_only(), + pybind11::arg("ignore_noise") = false, + pybind11::arg("ignore_measurement") = false, + pybind11::arg("ignore_reset") = false, + clean_doc_string(R"DOC( + @signature def to_tableau(self, *, ignore_noise: bool = False, ignore_measurement: bool = False, ignore_reset: bool = False) -> stim.Tableau: + Converts the circuit into an equivalent stabilizer tableau. + + Args: + ignore_noise: Defaults to False. When False, any noise operations in the + circuit will cause the conversion to fail with an exception. When True, + noise operations are skipped over as if they weren't even present in the + circuit. + ignore_measurement: Defaults to False. When False, any measurement + operations in the circuit will cause the conversion to fail with an + exception. When True, measurement operations are skipped over as if they + weren't even present in the circuit. + ignore_reset: Defaults to False. When False, any reset operations in the + circuit will cause the conversion to fail with an exception. When True, + reset operations are skipped over as if they weren't even present in the + circuit. + + Returns: + A tableau equivalent to the circuit (up to global phase). + + Raises: + ValueError: + The circuit contains noise operations but ignore_noise=False. + OR + The circuit contains measurement operations but + ignore_measurement=False. + OR + The circuit contains reset operations but ignore_reset=False. + + Examples: + >>> import stim + >>> stim.Circuit(''' + ... H 0 + ... CNOT 0 1 + ... ''').to_tableau() + stim.Tableau.from_conjugated_generators( + xs=[ + stim.PauliString("+Z_"), + stim.PauliString("+_X"), + ], + zs=[ + stim.PauliString("+XX"), + stim.PauliString("+ZZ"), + ], + ) + )DOC") + .data()); + + c.def( + "to_qasm", + [](const Circuit &self, int open_qasm_version, bool skip_dets_and_obs) -> std::string { + std::stringstream out; + export_open_qasm(self, out, open_qasm_version, skip_dets_and_obs); + return out.str(); + }, + pybind11::kw_only(), + pybind11::arg("open_qasm_version"), + pybind11::arg("skip_dets_and_obs") = false, + clean_doc_string(R"DOC( + @signature def to_qasm(self, *, open_qasm_version: int, skip_dets_and_obs: bool = False) -> str: + Creates an equivalent OpenQASM implementation of the circuit. + + Args: + open_qasm_version: The version of OpenQASM to target. + This should be set to 2 or to 3. + + Differences between the versions are: + - Support for operations on classical bits (only version 3). + This means DETECTOR and OBSERVABLE_INCLUDE only work with + version 3. + - Support for feedback operations (only version 3). + - Support for subroutines (only version 3). Without subroutines, + non-standard dissipative gates like MR and RX need to decompose + inline every single time they're used. + - Minor name changes (e.g. creg -> bit, qelib1.inc -> stdgates.inc). + skip_dets_and_obs: Defaults to False. When set to False, the output will + include a `dets` register and an `obs` register (assuming the circuit + has detectors and observables). These registers will be computed as part + of running the circuit. This requires performing a simulation of the + circuit, in order to correctly account for the expected value of + measurements. + + When set to True, the `dets` and `obs` registers are not included in the + output, and no simulation of the circuit is performed. + + Returns: + The OpenQASM code as a string. + + Examples: + >>> import stim + >>> circuit = stim.Circuit(''' + ... R 0 1 + ... X 1 + ... H 0 + ... CX 0 1 + ... M 0 1 + ... DETECTOR rec[-1] rec[-2] + ... '''); + >>> qasm = circuit.to_qasm(open_qasm_version=3); + >>> print(qasm.strip().replace('\n\n', '\n')) + OPENQASM 3.0; + include "stdgates.inc"; + qreg q[2]; + creg rec[2]; + creg dets[1]; + reset q[0]; + reset q[1]; + x q[1]; + h q[0]; + cx q[0], q[1]; + measure q[0] -> rec[0]; + measure q[1] -> rec[1]; + dets[0] = rec[1] ^ rec[0] ^ 1; + )DOC") + .data()); + + c.def( + "has_flow", + [](const Circuit &self, const Flow &flow, bool unsigned_only) -> bool { + std::span> flows = {&flow, &flow + 1}; + if (unsigned_only) { + return check_if_circuit_has_unsigned_stabilizer_flows(self, flows)[0]; + } else { + auto rng = externally_seeded_rng(); + return sample_if_circuit_has_stabilizer_flows(256, rng, self, flows)[0]; + } + }, + pybind11::arg("flow"), + pybind11::kw_only(), + pybind11::arg("unsigned") = false, + clean_doc_string(R"DOC( + @signature def has_flow(self, flow: stim.Flow, *, unsigned: bool = False) -> bool: + Determines if the circuit has the given stabilizer flow or not. + + A circuit has a stabilizer flow P -> Q if it maps the instantaneous stabilizer + P at the start of the circuit to the instantaneous stabilizer Q at the end of + the circuit. The flow may be mediated by certain measurements. For example, + a lattice surgery CNOT involves an MXX measurement and an MZZ measurement, and + the CNOT flows implemented by the circuit involve these measurements. + + A flow like P -> Q means the circuit transforms P into Q. + A flow like 1 -> P means the circuit prepares P. + A flow like P -> 1 means the circuit measures P. + A flow like 1 -> 1 means the circuit contains a check (could be a DETECTOR). + + This method ignores any noise in the circuit. + + Args: + flow: The flow to check for. + unsigned: Defaults to False. When False, the flows must be correct including + the sign of the Pauli strings. When True, only the Pauli terms need to + be correct; the signs are permitted to be inverted. In effect, this + requires the circuit to be correct up to Pauli gates. + + Returns: + True if the circuit has the given flow; False otherwise. + + Examples: + >>> import stim + + >>> m = stim.Circuit('M 0') + >>> m.has_flow(stim.Flow('Z -> Z')) + True + >>> m.has_flow(stim.Flow('X -> X')) + False + >>> m.has_flow(stim.Flow('Z -> I')) + False + >>> m.has_flow(stim.Flow('Z -> I xor rec[-1]')) + True + >>> m.has_flow(stim.Flow('Z -> rec[-1]')) + True + + >>> cx58 = stim.Circuit('CX 5 8') + >>> cx58.has_flow(stim.Flow('X5 -> X5*X8')) + True + >>> cx58.has_flow(stim.Flow('X_ -> XX')) + False + >>> cx58.has_flow(stim.Flow('_____X___ -> _____X__X')) + True + + >>> stim.Circuit(''' + ... RY 0 + ... ''').has_flow(stim.Flow( + ... output=stim.PauliString("Y"), + ... )) + True + + >>> stim.Circuit(''' + ... RY 0 + ... X_ERROR(0.1) 0 + ... ''').has_flow(stim.Flow( + ... output=stim.PauliString("Y"), + ... )) + True + + >>> stim.Circuit(''' + ... RY 0 + ... ''').has_flow(stim.Flow( + ... output=stim.PauliString("X"), + ... )) + False + + >>> stim.Circuit(''' + ... CX 0 1 + ... ''').has_flow(stim.Flow( + ... input=stim.PauliString("+X_"), + ... output=stim.PauliString("+XX"), + ... )) + True + + >>> stim.Circuit(''' + ... # Lattice surgery CNOT + ... R 1 + ... MXX 0 1 + ... MZZ 1 2 + ... MX 1 + ... ''').has_flow(stim.Flow( + ... input=stim.PauliString("+X_X"), + ... output=stim.PauliString("+__X"), + ... measurements=[0, 2], + ... )) + True + + >>> stim.Circuit(''' + ... H 0 + ... ''').has_flow( + ... stim.Flow("Y -> Y"), + ... unsigned=True, + ... ) + True + + >>> stim.Circuit(''' + ... H 0 + ... ''').has_flow( + ... stim.Flow("Y -> Y"), + ... unsigned=False, + ... ) + False + + Caveats: + Currently, the unsigned=False version of this method is implemented by + performing 256 randomized tests. Each test has a 50% chance of a false + positive, and a 0% chance of a false negative. So, when the method returns + True, there is technically still a 2^-256 chance the circuit doesn't have + the flow. This is lower than the chance of a cosmic ray flipping the result. + )DOC") + .data()); + + c.def( + "has_all_flows", + [](const Circuit &self, const std::vector> &flows, bool unsigned_only) -> bool { + std::vector results; + if (unsigned_only) { + results = check_if_circuit_has_unsigned_stabilizer_flows(self, flows); + } else { + auto rng = externally_seeded_rng(); + results = sample_if_circuit_has_stabilizer_flows(256, rng, self, flows); + } + for (bool b : results) { + if (!b) { + return false; + } + } + return true; + }, + pybind11::arg("flows"), + pybind11::kw_only(), + pybind11::arg("unsigned") = false, + clean_doc_string(R"DOC( + @signature def has_all_flows(self, flows: Iterable[stim.Flow], *, unsigned: bool = False) -> bool: + Determines if the circuit has all the given stabilizer flow or not. + + This is a faster version of `all(c.has_flow(f) for f in flows)`. It's faster + because, behind the scenes, the circuit can be iterated once instead of once + per flow. + + This method ignores any noise in the circuit. + + Args: + flows: An iterable of `stim.Flow` instances representing the flows to check. + unsigned: Defaults to False. When False, the flows must be correct including + the sign of the Pauli strings. When True, only the Pauli terms need to + be correct; the signs are permitted to be inverted. In effect, this + requires the circuit to be correct up to Pauli gates. + + Returns: + True if the circuit has the given flow; False otherwise. + + Examples: + >>> import stim + + >>> stim.Circuit('H 0').has_all_flows([ + ... stim.Flow('X -> Z'), + ... stim.Flow('Y -> Y'), + ... stim.Flow('Z -> X'), + ... ]) + False + + >>> stim.Circuit('H 0').has_all_flows([ + ... stim.Flow('X -> Z'), + ... stim.Flow('Y -> -Y'), + ... stim.Flow('Z -> X'), + ... ]) + True + + >>> stim.Circuit('H 0').has_all_flows([ + ... stim.Flow('X -> Z'), + ... stim.Flow('Y -> Y'), + ... stim.Flow('Z -> X'), + ... ], unsigned=True) + True + + Caveats: + Currently, the unsigned=False version of this method is implemented by + performing 256 randomized tests. Each test has a 50% chance of a false + positive, and a 0% chance of a false negative. So, when the method returns + True, there is technically still a 2^-256 chance the circuit doesn't have + the flow. This is lower than the chance of a cosmic ray flipping the result. + )DOC") + .data()); + + c.def( + "flow_generators", + &circuit_flow_generators, + clean_doc_string(R"DOC( + @signature def flow_generators(self) -> List[stim.Flow]: + Returns a list of flows that generate all of the circuit's flows. + + Every stabilizer flow that the circuit implements is a product of some + subset of the returned generators. Every returned flow will be a flow + of the circuit. + + Returns: + A list of flow generators for the circuit. + + Examples: + >>> import stim + + >>> stim.Circuit("H 0").flow_generators() + [stim.Flow("X -> Z"), stim.Flow("Z -> X")] + + >>> stim.Circuit("M 0").flow_generators() + [stim.Flow("1 -> Z xor rec[0]"), stim.Flow("Z -> rec[0]")] + + >>> stim.Circuit("RX 0").flow_generators() + [stim.Flow("1 -> X")] + + >>> for flow in stim.Circuit("MXX 0 1").flow_generators(): + ... print(flow) + 1 -> XX xor rec[0] + _X -> _X + X_ -> _X xor rec[0] + ZZ -> ZZ + + >>> for flow in stim.Circuit.generated( + ... "repetition_code:memory", + ... rounds=2, + ... distance=3, + ... after_clifford_depolarization=1e-3, + ... ).flow_generators(): + ... print(flow) + 1 -> rec[0] + 1 -> rec[1] + 1 -> rec[2] + 1 -> rec[3] + 1 -> rec[4] + 1 -> rec[5] + 1 -> rec[6] + 1 -> ____Z + 1 -> ___Z_ + 1 -> __Z__ + 1 -> _Z___ + 1 -> Z____ + )DOC") + .data()); + + c.def( + "time_reversed_for_flows", + [](const Circuit &self, + const std::vector> &flows, + bool dont_turn_measurements_into_resets) -> pybind11::object { + auto [inv_circuit, inv_flows] = + circuit_inverse_qec(self, flows, dont_turn_measurements_into_resets); + return pybind11::make_tuple(inv_circuit, inv_flows); + }, + pybind11::arg("flows"), + pybind11::kw_only(), + pybind11::arg("dont_turn_measurements_into_resets") = false, + clean_doc_string(R"DOC( + @signature def time_reversed_for_flows(self, flows: Iterable[stim.Flow], *, dont_turn_measurements_into_resets: bool = False) -> Tuple[stim.Circuit, List[stim.Flow]]: + Time-reverses the circuit while preserving error correction structure. + + This method returns a circuit that has the same internal detecting regions + as the given circuit, as well as the same internal-to-external flows given + in the `flows` argument, except they are all time-reversed. For example, if + you pass a fault tolerant preparation circuit into this method (1 -> Z), the + result will be a fault tolerant *measurement* circuit (Z -> 1). Or, if you + pass a fault tolerant C_XYZ circuit into this method (X->Y, Y->Z, and Z->X), + the result will be a fault tolerant C_ZYX circuit (X->Z, Y->X, and Z->Y). + + Note that this method doesn't guarantee that it will preserve the *sign* of the + detecting regions or stabilizer flows. For example, inverting a memory circuit + that preserves a logical observable (X->X and Z->Z) may produce a + memory circuit that always bit flips the logical observable (X->X and Z->-Z) or + that dynamically adjusts the logical observable in response to measurements + (like "X -> X xor rec[-1]" and "Z -> Z xor rec[-2]"). + + This method will turn time-reversed resets into measurements, and attempts to + turn time-reversed measurements into resets. A measurement will time-reverse + into a reset if some annotated detectors, annotated observables, or given flows + have detecting regions with sensitivity just before the measurement but none + have detecting regions with sensitivity after the measurement. + + In some cases this method will have to introduce new operations. In particular, + when a measurement-reset operation has a noisy result, time-reversing this + measurement noise produces reset noise. But the measure-reset operations don't + have built-in reset noise, so the reset noise is specified by adding an X_ERROR + or Z_ERROR noise instruction after the time-reversed measure-reset operation. + + Args: + flows: Flows you care about, that reach past the start/end of the given + circuit. The result will contain an inverted flow for each of these + given flows. You need this information because it reveals the + measurements needed to produce the inverted flows that you care + about. + + An exception will be raised if the circuit doesn't have all these + flows. The inverted circuit will have the inverses of these flows + (ignoring sign). + dont_turn_measurements_into_resets: Defaults to False. When set to + True, measurements will time-reverse into measurements even if + nothing is sensitive to the measured qubit after the measurement + completes. This guarantees the output circuit has *all* flows + that the input circuit has (up to sign and feedback), even ones + that aren't annotated. + + Returns: + An (inverted_circuit, inverted_flows) tuple. + + inverted_circuit is the qec inverse of the given circuit. + + inverted_flows is a list of flows, matching up by index with the flows + given as arguments to the method. The input, output, and sign fields + of these flows are boring. The useful field is measurement_indices, + because it's difficult to predict which measurements are needed for + the inverted flow due to effects such as implicitly-included resets + inverting into explicitly-included measurements. + + Caveats: + Currently, this method doesn't compute the sign of the inverted flows. + It unconditionally sets the sign to False. + + Examples: + >>> import stim + + >>> inv_circuit, inv_flows = stim.Circuit(''' + ... R 0 + ... H 0 + ... S 0 + ... MY 0 + ... DETECTOR rec[-1] + ... ''').time_reversed_for_flows([]) + >>> inv_circuit + stim.Circuit(''' + RY 0 + S_DAG 0 + H 0 + M 0 + DETECTOR rec[-1] + ''') + >>> inv_flows + [] + + >>> inv_circuit, inv_flows = stim.Circuit(''' + ... M 0 + ... ''').time_reversed_for_flows([ + ... stim.Flow("Z -> rec[-1]"), + ... ]) + >>> inv_circuit + stim.Circuit(''' + R 0 + ''') + >>> inv_flows + [stim.Flow("1 -> Z")] + >>> inv_circuit.has_all_flows(inv_flows, unsigned=True) + True + + >>> inv_circuit, inv_flows = stim.Circuit(''' + ... R 0 + ... ''').time_reversed_for_flows([ + ... stim.Flow("1 -> Z"), + ... ]) + >>> inv_circuit + stim.Circuit(''' + M 0 + ''') + >>> inv_flows + [stim.Flow("Z -> rec[-1]")] + + >>> inv_circuit, inv_flows = stim.Circuit(''' + ... M 0 + ... ''').time_reversed_for_flows([ + ... stim.Flow("1 -> Z xor rec[-1]"), + ... ]) + >>> inv_circuit + stim.Circuit(''' + M 0 + ''') + >>> inv_flows + [stim.Flow("Z -> rec[-1]")] + + >>> inv_circuit, inv_flows = stim.Circuit(''' + ... M 0 + ... ''').time_reversed_for_flows( + ... flows=[stim.Flow("Z -> rec[-1]")], + ... dont_turn_measurements_into_resets=True, + ... ) + >>> inv_circuit + stim.Circuit(''' + M 0 + ''') + >>> inv_flows + [stim.Flow("1 -> Z xor rec[-1]")] + + >>> inv_circuit, inv_flows = stim.Circuit(''' + ... MR(0.125) 0 + ... ''').time_reversed_for_flows([]) + >>> inv_circuit + stim.Circuit(''' + MR 0 + X_ERROR(0.125) 0 + ''') + >>> inv_flows + [] + + >>> inv_circuit, inv_flows = stim.Circuit(''' + ... MXX 0 1 + ... H 0 + ... ''').time_reversed_for_flows([ + ... stim.Flow("ZZ -> YY xor rec[-1]"), + ... stim.Flow("ZZ -> XZ"), + ... ]) + >>> inv_circuit + stim.Circuit(''' + H 0 + MXX 0 1 + ''') + >>> inv_flows + [stim.Flow("YY -> ZZ xor rec[-1]"), stim.Flow("XZ -> ZZ")] + + >>> stim.Circuit.generated( + ... "surface_code:rotated_memory_x", + ... distance=2, + ... rounds=1, + ... ).time_reversed_for_flows([])[0] + stim.Circuit(''' + QUBIT_COORDS(1, 1) 1 + QUBIT_COORDS(2, 0) 2 + QUBIT_COORDS(3, 1) 3 + QUBIT_COORDS(1, 3) 6 + QUBIT_COORDS(2, 2) 7 + QUBIT_COORDS(3, 3) 8 + QUBIT_COORDS(2, 4) 12 + RX 8 6 3 1 + MR 12 7 2 + TICK + H 12 2 + TICK + CX 1 7 12 6 + TICK + CX 6 7 12 8 + TICK + CX 3 7 2 1 + TICK + CX 8 7 2 3 + TICK + H 12 2 + TICK + M 12 7 2 + DETECTOR(2, 0, 1) rec[-1] + DETECTOR(2, 4, 1) rec[-3] + MX 8 6 3 1 + DETECTOR(2, 0, 0) rec[-5] rec[-2] rec[-1] + DETECTOR(2, 4, 0) rec[-7] rec[-4] rec[-3] + OBSERVABLE_INCLUDE(0) rec[-3] rec[-1] + ''') + )DOC") + .data()); + + c.def( + "to_crumble_url", + [](const Circuit &self, bool skip_detectors, pybind11::object &obj_mark) { + std::map> mark; + if (!obj_mark.is_none()) { + mark = pybind11::cast>>(obj_mark); + } + return export_crumble_url(self, skip_detectors, mark); + }, + pybind11::kw_only(), + pybind11::arg("skip_detectors") = false, + pybind11::arg("mark") = pybind11::none(), + clean_doc_string(R"DOC( + @signature def to_crumble_url(self, *, skip_detectors: bool = False, mark: Optional[dict[int, list[stim.ExplainedError]]] = None) -> str: + Returns a URL that opens up crumble and loads this circuit into it. + + Crumble is a tool for editing stabilizer circuits, and visualizing their + stabilizer flows. Its source code is in the `glue/crumble` directory of + the stim code repository on github. A prebuilt version is made available + at https://algassert.com/crumble, which is what the URL returned by this + method will point to. + + Args: + skip_detectors: Defaults to False. If set to True, detectors from the + circuit aren't included in the crumble URL. This can reduce visual + clutter in crumble, and improve its performance, since it doesn't + need to indicate or track the sensitivity regions of detectors. + mark: Defaults to None (no marks). If set to a dictionary from int to + errors, such as `mark={1: circuit.shortest_graphlike_error()}`, + then the errors will be highlighted and tracked forward by crumble. + + Returns: + A URL that can be opened in a web browser. + + Examples: + >>> import stim + >>> stim.Circuit(''' + ... H 0 + ... CNOT 0 1 + ... S 1 + ... ''').to_crumble_url() + 'https://algassert.com/crumble#circuit=H_0;CX_0_1;S_1' + + >>> circuit = stim.Circuit(''' + ... M(0.25) 0 1 2 + ... DETECTOR rec[-1] rec[-2] + ... DETECTOR rec[-2] rec[-3] + ... OBSERVABLE_INCLUDE(0) rec[-1] + ... ''') + >>> err = circuit.shortest_graphlike_error(canonicalize_circuit_errors=True) + >>> circuit.to_crumble_url(skip_detectors=True, mark={1: err}) + 'https://algassert.com/crumble#circuit=;TICK;MARKX(1)1;MARKX(1)2;MARKX(1)0;TICK;M(0.25)0_1_2;OI(0)rec[-1]' + )DOC") + .data()); + + c.def( + "to_quirk_url", + &export_quirk_url, + clean_doc_string(R"DOC( + Returns a URL that opens up quirk and loads this circuit into it. + + Quirk is an open source drag and drop circuit editor with support for up to 16 + qubits. Its source code is available at https://github.com/strilanc/quirk + and a prebuilt version is available at https://algassert.com/quirk, which is + what the URL returned by this method will point to. + + Quirk doesn't support features like noise, feedback, or detectors. This method + will simply drop any unsupported operations from the circuit when producing + the URL. + + Returns: + A URL that can be opened in a web browser. + + Examples: + >>> import stim + >>> stim.Circuit(''' + ... H 0 + ... CNOT 0 1 + ... S 1 + ... ''').to_quirk_url() + 'https://algassert.com/quirk#circuit={"cols":[["H"],["•","X"],[1,"Z^½"]]}' + )DOC") + .data()); + + c.def( + "decomposed", + &simplified_circuit, + clean_doc_string(R"DOC( + Recreates the circuit using (mostly) the {H,S,CX,M,R} gate set. + + The intent of this method is to simplify the circuit to use fewer gate types, + so it's easier for other tools to consume. Currently, this method performs the + following simplifications: + + - Single qubit cliffords are decomposed into {H,S}. + - Multi-qubit cliffords are decomposed into {H,S,CX}. + - Single qubit dissipative gates are decomposed into {H,S,M,R}. + - Multi-qubit dissipative gates are decomposed into {H,S,CX,M,R}. + + Currently, the following types of gate *aren't* simplified, but they may be + in the future: + + - Noise instructions (like X_ERROR, DEPOLARIZE2, and E). + - Annotations (like TICK, DETECTOR, and SHIFT_COORDS). + - The MPAD instruction. + - Repeat blocks are not flattened. + + Returns: + A `stim.Circuit` whose function is equivalent to the original circuit, + but with most gates decomposed into the {H,S,CX,M,R} gate set. + + Examples: + >>> import stim + + >>> stim.Circuit(''' + ... SWAP 0 1 + ... ''').decomposed() + stim.Circuit(''' + CX 0 1 1 0 0 1 + ''') + + >>> stim.Circuit(''' + ... ISWAP 0 1 2 1 + ... TICK + ... MPP !X1*Y2*Z3 + ... ''').decomposed() + stim.Circuit(''' + H 0 + CX 0 1 1 0 + H 1 + S 1 0 + H 2 + CX 2 1 1 2 + H 1 + S 1 2 + TICK + H 1 2 + S 2 + H 2 + S 2 2 + CX 2 1 3 1 + M !1 + CX 2 1 3 1 + H 2 + S 2 + H 2 + S 2 2 + H 1 + ''') + )DOC") + .data()); + + c.def( + "with_inlined_feedback", + &circuit_with_inlined_feedback, + clean_doc_string(R"DOC( + Returns a circuit without feedback with rewritten detectors/observables. + + When a feedback operation affects the expected parity of a detector or + observable, the measurement controlling that feedback operation is implicitly + part of the measurement set that defines the detector or observable. This + method removes all feedback, but avoids changing the meaning of detectors or + observables by turning these implicit measurement dependencies into explicit + measurement dependencies added to the observable or detector. + + This method guarantees that the detector error model derived from the original + circuit, and the transformed circuit, will be equivalent (modulo floating point + rounding errors and variations in where loops are placed). Specifically, the + following should be true for any circuit: + + dem1 = circuit.flattened().detector_error_model() + dem2 = circuit.with_inlined_feedback().flattened().detector_error_model() + assert dem1.approx_equals(dem2, 1e-5) + + Returns: + A `stim.Circuit` with feedback operations removed, with rewritten DETECTOR + instructions (as needed to avoid changing the meaning of each detector), and + with additional OBSERVABLE_INCLUDE instructions (as needed to avoid changing + the meaning of each observable). + + The circuit's function is permitted to differ from the original in that + any feedback operation can be pushed to the end of the circuit and + discarded. All non-feedback operations must stay where they are, preserving + the structure of the circuit. + + Examples: + >>> import stim + + >>> stim.Circuit(''' + ... CX 0 1 # copy to measure qubit + ... M 1 # measure first time + ... CX rec[-1] 1 # use feedback to reset measurement qubit + ... CX 0 1 # copy to measure qubit + ... M 1 # measure second time + ... DETECTOR rec[-1] rec[-2] + ... OBSERVABLE_INCLUDE(0) rec[-1] + ... ''').with_inlined_feedback() + stim.Circuit(''' + CX 0 1 + M 1 + OBSERVABLE_INCLUDE(0) rec[-1] + CX 0 1 + M 1 + DETECTOR rec[-1] + OBSERVABLE_INCLUDE(0) rec[-1] + ''') + )DOC") + .data()); +} diff --git a/src/stim/circuit/circuit_pybind_test.py b/src/stim/circuit/circuit_pybind_test.py index 928fe320d..2bf84480d 100644 --- a/src/stim/circuit/circuit_pybind_test.py +++ b/src/stim/circuit/circuit_pybind_test.py @@ -2320,3 +2320,34 @@ def test_append_tag(): c.append(stim.CircuitRepeatBlock(10, stim.Circuit()), tag="newtag") assert c == stim.Circuit("H[test] 2 3") + + +def test_append_pauli_string(): + c = stim.Circuit() + c.append("MPP", [ + stim.PauliString("X1*Y2*Z3"), + stim.target_y(4), + stim.PauliString("Z5"), + ]) + assert c == stim.Circuit(""" + MPP X1*Y2*Z3 Y4 Z5 + """) + c.append("MPP", stim.PauliString("X1*X2")) + assert c == stim.Circuit(""" + MPP X1*Y2*Z3 Y4 Z5 X1*X2 + """) + + with pytest.raises(ValueError, match="empty stim.PauliString"): + c.append("MPP", stim.PauliString("")) + with pytest.raises(ValueError, match="empty stim.PauliString"): + c.append("MPP", [stim.PauliString("")]) + with pytest.raises(ValueError, match="empty stim.PauliString"): + c.append("MPP", [stim.PauliString("X1"), stim.PauliString("")]) + assert c == stim.Circuit(""" + MPP X1*Y2*Z3 Y4 Z5 X1*X2 + """) + + with pytest.raises(ValueError, match="Don't know how to target"): + c.append("MPP", object()) + with pytest.raises(ValueError, match="Don't know how to target"): + c.append("MPP", object()) diff --git a/src/stim/gates/gates.h b/src/stim/gates/gates.h index 9c163535d..7044f9e74 100644 --- a/src/stim/gates/gates.h +++ b/src/stim/gates/gates.h @@ -247,9 +247,9 @@ struct Gate { GateFlags flags; /// A word describing what sort of gate this is. - const char *category; + std::string_view category; /// Prose summary of what the gate is, how it fits into Stim, and how to use it. - const char *help; + std::string_view help; /// A unitary matrix describing the gate. (Size 0 if the gate is not unitary.) FixedCapVector, 4>, 4> unitary_data; /// A shorthand description of the stabilizer flows of the gate. diff --git a/src/stim/py/stim.pybind.cc b/src/stim/py/stim.pybind.cc index 4200d2e0d..25af42439 100644 --- a/src/stim/py/stim.pybind.cc +++ b/src/stim/py/stim.pybind.cc @@ -628,6 +628,7 @@ PYBIND11_MODULE(STIM_PYBIND11_MODULE_NAME, m) { pybind_gate_data_methods(m, c_gate_data); pybind_circuit_repeat_block_methods(m, c_circuit_repeat_block); pybind_circuit_methods(m, c_circuit); + pybind_circuit_methods_extra(m, c_circuit); pybind_tableau_iter_methods(m, c_tableau_iter); pybind_dem_sampler_methods(m, c_dem_sampler); From 3b7e4271cc23d6605704b5bcac1b107193f38783 Mon Sep 17 00:00:00 2001 From: Craig Gidney Date: Sat, 22 Mar 2025 14:28:23 -0700 Subject: [PATCH 02/11] regen docs --- doc/python_api_reference_vDev.md | 18 ++++++++++++------ doc/stim.pyi | 18 ++++++++++++------ file_lists/test_files | 1 + glue/python/src/stim/__init__.pyi | 18 ++++++++++++------ 4 files changed, 37 insertions(+), 18 deletions(-) diff --git a/doc/python_api_reference_vDev.md b/doc/python_api_reference_vDev.md index 184b348dd..65d29fbf2 100644 --- a/doc/python_api_reference_vDev.md +++ b/doc/python_api_reference_vDev.md @@ -834,7 +834,7 @@ def __str__( def append( self, name: str, - targets: Union[int, stim.GateTarget, Iterable[Union[int, stim.GateTarget]]], + targets: Union[int, stim.GateTarget, stim.PauliString, Iterable[Union[int, stim.GateTarget, stim.PauliString]]], arg: Union[float, Iterable[float]], *, tag: str = "", @@ -867,6 +867,7 @@ def append( >>> c.append("CNOT", [stim.target_rec(-1), 0]) >>> c.append("X_ERROR", [0], 0.125) >>> c.append("CORRELATED_ERROR", [stim.target_x(0), stim.target_y(2)], 0.25) + >>> c.append("MPP", [stim.PauliString("X1*Y2"), stim.GateTarget("Z3")]) >>> print(repr(c)) stim.Circuit(''' X 0 @@ -875,6 +876,7 @@ def append( CX rec[-1] 0 X_ERROR(0.125) 0 E(0.25) X0 Y2 + MPP X1*Y2 Z3 ''') Args: @@ -888,11 +890,15 @@ def append( (The argument being called `name` is no longer quite right, but is being kept for backwards compatibility.) targets: The objects operated on by the gate. This can be either a - single target or an iterable of multiple targets to broadcast the - gate over. Each target can be an integer (a qubit), a - stim.GateTarget, or a special target from one of the `stim.target_*` - methods (such as a measurement record target like `rec[-1]` from - `stim.target_rec(-1)`). + single target or an iterable of multiple targets. + + Each target can be: + An int: The index of a targeted qubit. + A `stim.GateTarget`: Could be a variety of things. Methods like + `stim.target_rec`, `stim.target_sweet`, `stim.target_x`, and + `stim.CircuitInstruction.__getitem__` all return this type. + A `stim.PauliString`: This will automatically be expanded into + a product of pauli targets like `X1*Y2*Z3`. arg: The "parens arguments" for the gate, such as the probability for a noise operation. A double or list of doubles parameterizing the gate. Different gates take different parens arguments. For example, diff --git a/doc/stim.pyi b/doc/stim.pyi index 19afc761e..328ece885 100644 --- a/doc/stim.pyi +++ b/doc/stim.pyi @@ -301,7 +301,7 @@ class Circuit: def append( self, name: str, - targets: Union[int, stim.GateTarget, Iterable[Union[int, stim.GateTarget]]], + targets: Union[int, stim.GateTarget, stim.PauliString, Iterable[Union[int, stim.GateTarget, stim.PauliString]]], arg: Union[float, Iterable[float]], *, tag: str = "", @@ -334,6 +334,7 @@ class Circuit: >>> c.append("CNOT", [stim.target_rec(-1), 0]) >>> c.append("X_ERROR", [0], 0.125) >>> c.append("CORRELATED_ERROR", [stim.target_x(0), stim.target_y(2)], 0.25) + >>> c.append("MPP", [stim.PauliString("X1*Y2"), stim.GateTarget("Z3")]) >>> print(repr(c)) stim.Circuit(''' X 0 @@ -342,6 +343,7 @@ class Circuit: CX rec[-1] 0 X_ERROR(0.125) 0 E(0.25) X0 Y2 + MPP X1*Y2 Z3 ''') Args: @@ -355,11 +357,15 @@ class Circuit: (The argument being called `name` is no longer quite right, but is being kept for backwards compatibility.) targets: The objects operated on by the gate. This can be either a - single target or an iterable of multiple targets to broadcast the - gate over. Each target can be an integer (a qubit), a - stim.GateTarget, or a special target from one of the `stim.target_*` - methods (such as a measurement record target like `rec[-1]` from - `stim.target_rec(-1)`). + single target or an iterable of multiple targets. + + Each target can be: + An int: The index of a targeted qubit. + A `stim.GateTarget`: Could be a variety of things. Methods like + `stim.target_rec`, `stim.target_sweet`, `stim.target_x`, and + `stim.CircuitInstruction.__getitem__` all return this type. + A `stim.PauliString`: This will automatically be expanded into + a product of pauli targets like `X1*Y2*Z3`. arg: The "parens arguments" for the gate, such as the probability for a noise operation. A double or list of doubles parameterizing the gate. Different gates take different parens arguments. For example, diff --git a/file_lists/test_files b/file_lists/test_files index b00287c50..e171db8df 100644 --- a/file_lists/test_files +++ b/file_lists/test_files @@ -67,6 +67,7 @@ src/stim/simulators/measurements_to_detection_events.test.cc src/stim/simulators/sparse_rev_frame_tracker.test.cc src/stim/simulators/tableau_simulator.test.cc src/stim/simulators/vector_simulator.test.cc +src/stim/stabilizers/clifford_string.test.cc src/stim/stabilizers/flex_pauli_string.test.cc src/stim/stabilizers/flow.test.cc src/stim/stabilizers/pauli_string.test.cc diff --git a/glue/python/src/stim/__init__.pyi b/glue/python/src/stim/__init__.pyi index 19afc761e..328ece885 100644 --- a/glue/python/src/stim/__init__.pyi +++ b/glue/python/src/stim/__init__.pyi @@ -301,7 +301,7 @@ class Circuit: def append( self, name: str, - targets: Union[int, stim.GateTarget, Iterable[Union[int, stim.GateTarget]]], + targets: Union[int, stim.GateTarget, stim.PauliString, Iterable[Union[int, stim.GateTarget, stim.PauliString]]], arg: Union[float, Iterable[float]], *, tag: str = "", @@ -334,6 +334,7 @@ class Circuit: >>> c.append("CNOT", [stim.target_rec(-1), 0]) >>> c.append("X_ERROR", [0], 0.125) >>> c.append("CORRELATED_ERROR", [stim.target_x(0), stim.target_y(2)], 0.25) + >>> c.append("MPP", [stim.PauliString("X1*Y2"), stim.GateTarget("Z3")]) >>> print(repr(c)) stim.Circuit(''' X 0 @@ -342,6 +343,7 @@ class Circuit: CX rec[-1] 0 X_ERROR(0.125) 0 E(0.25) X0 Y2 + MPP X1*Y2 Z3 ''') Args: @@ -355,11 +357,15 @@ class Circuit: (The argument being called `name` is no longer quite right, but is being kept for backwards compatibility.) targets: The objects operated on by the gate. This can be either a - single target or an iterable of multiple targets to broadcast the - gate over. Each target can be an integer (a qubit), a - stim.GateTarget, or a special target from one of the `stim.target_*` - methods (such as a measurement record target like `rec[-1]` from - `stim.target_rec(-1)`). + single target or an iterable of multiple targets. + + Each target can be: + An int: The index of a targeted qubit. + A `stim.GateTarget`: Could be a variety of things. Methods like + `stim.target_rec`, `stim.target_sweet`, `stim.target_x`, and + `stim.CircuitInstruction.__getitem__` all return this type. + A `stim.PauliString`: This will automatically be expanded into + a product of pauli targets like `X1*Y2*Z3`. arg: The "parens arguments" for the gate, such as the probability for a noise operation. A double or list of doubles parameterizing the gate. Different gates take different parens arguments. For example, From fc6e0dbb57a024e5bebabeb36207f48b08ba9154 Mon Sep 17 00:00:00 2001 From: Craig Gidney Date: Mon, 24 Mar 2025 10:14:01 -0700 Subject: [PATCH 03/11] regen file list --- file_lists/test_files | 1 - 1 file changed, 1 deletion(-) diff --git a/file_lists/test_files b/file_lists/test_files index e171db8df..b00287c50 100644 --- a/file_lists/test_files +++ b/file_lists/test_files @@ -67,7 +67,6 @@ src/stim/simulators/measurements_to_detection_events.test.cc src/stim/simulators/sparse_rev_frame_tracker.test.cc src/stim/simulators/tableau_simulator.test.cc src/stim/simulators/vector_simulator.test.cc -src/stim/stabilizers/clifford_string.test.cc src/stim/stabilizers/flex_pauli_string.test.cc src/stim/stabilizers/flow.test.cc src/stim/stabilizers/pauli_string.test.cc From 10593011fa31a998665232a999986f138b0d29b8 Mon Sep 17 00:00:00 2001 From: Craig Gidney Date: Mon, 24 Mar 2025 10:14:32 -0700 Subject: [PATCH 04/11] Add CliffordString --- file_lists/test_files | 1 + src/stim.h | 1 + src/stim/stabilizers/clifford_string.h | 194 +++++++++++++++++++ src/stim/stabilizers/clifford_string.test.cc | 12 ++ 4 files changed, 208 insertions(+) create mode 100644 src/stim/stabilizers/clifford_string.h create mode 100644 src/stim/stabilizers/clifford_string.test.cc diff --git a/file_lists/test_files b/file_lists/test_files index b00287c50..e171db8df 100644 --- a/file_lists/test_files +++ b/file_lists/test_files @@ -67,6 +67,7 @@ src/stim/simulators/measurements_to_detection_events.test.cc src/stim/simulators/sparse_rev_frame_tracker.test.cc src/stim/simulators/tableau_simulator.test.cc src/stim/simulators/vector_simulator.test.cc +src/stim/stabilizers/clifford_string.test.cc src/stim/stabilizers/flex_pauli_string.test.cc src/stim/stabilizers/flow.test.cc src/stim/stabilizers/pauli_string.test.cc diff --git a/src/stim.h b/src/stim.h index e04fcdba1..0ad32d289 100644 --- a/src/stim.h +++ b/src/stim.h @@ -92,6 +92,7 @@ #include "stim/simulators/sparse_rev_frame_tracker.h" #include "stim/simulators/tableau_simulator.h" #include "stim/simulators/vector_simulator.h" +#include "stim/stabilizers/clifford_string.h" #include "stim/stabilizers/flex_pauli_string.h" #include "stim/stabilizers/flow.h" #include "stim/stabilizers/pauli_string.h" diff --git a/src/stim/stabilizers/clifford_string.h b/src/stim/stabilizers/clifford_string.h new file mode 100644 index 000000000..618d80082 --- /dev/null +++ b/src/stim/stabilizers/clifford_string.h @@ -0,0 +1,194 @@ +///* +// * Copyright 2021 Google LLC +// * +// * Licensed under the Apache License, Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// */ +// +//#ifndef _STIM_STABILIZERS_CLIFFORD_STRING_H +//#define _STIM_STABILIZERS_CLIFFORD_STRING_H +// +//#include "stim/mem/simd_bits.h" +//#include "stim/gates/gates.h" +// +//namespace stim { +// +//template +//struct CliffordWord { +// bitword x_signs; +// bitword z_signs; +// bitword inv_x2x; +// bitword x2z; +// bitword z2x; +// bitword inv_z2z; +//}; +// +//template +//inline CliffordWord operator*(const CliffordWord &lhs, const CliffordWord &rhs) const { +// CliffordWord result; +// result.inv_x2x = (lhs.inv_x2x | rhs.inv_x2x) ^ (lhs.z2x & rhs.x2z); +// result.x2z = rhs.inv_x2x.andnot(lhs.x2z) ^ lhs.inv_z2z.andnot(rhs.x2z); +// result.z2x = lhs.inv_x2x.andnot(rhs.z2x) ^ rhs.z2z.andnot(lhs.z2x); +// result.inv_z2z = (lhs.x2z & rhs.z2x) ^ (lhs.inv_z2z | rhs.inv_z2z); +// simd_word rhs_x2y = rhs.inv_x2x.andnot(rhs.x2z); +// simd_word rhs_z2y = rhs.z2z.andnot(rhs.z2x); +// simd_word dy = (lhs.x2z & lhs.z2x) ^ lhs.inv_x2x ^ lhs.z2x ^ lhs.x2z ^ lhs.inv_z2z; +// result.x_signs = ( +// rhs.x_signs +// ^ rhs.inv_x2x.andnot(lhs.x_signs) +// ^ (rhs_x2y & dy) +// ^ (rhs.x2z & lhs.z_signs) +// ); +// result.z_signs = ( +// rhs.z_signs +// ^ (rhs.z2x & lhs.x_signs) +// ^ (rhs_z2y & dy) +// ^ rhs.inv_z2z.andnot(lhs.z_signs) +// ); +// return result +//} +// +//template +//struct CliffordString { +// size_t num_qubits; +// size_t num_words; +// CliffordWord *buf; +// +// CliffordString(size_t num_qubits, bool do_not_initialize = false) +// : num_qubits(num_qubits), +// num_words(((num_qubits + W - 1) / W) * W), +// buf(nullptr) { +// if (num_words) { +// buf = bit_word::aligned_malloc(num_words * (W / 8)); +// } +// } +// static inline CliffordString uninitialized(size_t num_qubits) { +// return CliffordString(num_qubits); +// } +// static inline CliffordString identities(size_t num_qubits) { +// CliffordString result(num_qubits); +// memset(result.buf, 0, num_words * (W / 8)); +// return result; +// } +// +// stim::GateType gate_at(size_t index) const { +// return table[flat_value]; +// } +// void set_gate_at(size_t index, GateType gate) const { +// const auto &flows = stim::GATE_DATA[gate].flow_data; +// std::string_view tx = flows[0]; +// std::string_view tz = flows[1]; +// bool inv_x2x = tx[0] == 'X' || tx[0] == 'Y'; +// bool x2z = tx[0] == 'Z' || tx[0] == 'Y'; +// bool x_sign = tx[0] == '-'; +// +// bool inv_z2x = tz[0] == 'X' || tz[0] == 'Y'; +// bool z2z = tz[0] == 'Z' || tz[0] == 'Y'; +// bool z_sign = tz[0] == '-'; +// +// +// } +// +// CliffordString(const CliffordString &other) +// : num_qubits(other.num_qubits), +// num_words(other.num_words), +// buf(nullptr) { +// buf = bit_word::aligned_malloc(num_words * (W / 8)); +// memcpy(buf, other.buf, num_words * (W / 8)); +// } +// CliffordString(CliffordString &&other) +// : num_qubits(other.num_qubits), +// num_words(other.num_words), +// buf(other.buf) { +// other.buf = nullptr; +// other.num_qubits = 0; +// other.num_words = 0; +// } +// ~CliffordString() { +// if (buf != nullptr) { +// bit_word::aligned_free(buf); +// buf = nullptr; +// } +// num_qubits = 0; +// num_words = 0; +// } +// CliffordString &operator=(const CliffordString &other) { +// if (num_words != other.num_words && buf != nullptr) { +// bit_word::aligned_free(buf); +// buf = nullptr; +// num_words = 0; +// num_qubits = 0; +// } +// if (buf == nullptr && num_words > 0) { +// buf = bit_word::aligned_malloc(other.num_words * (W / 8)); +// } +// num_words = other.num_words; +// num_qubits = other.num_qubits; +// memcpy(buf, other.buf, num_words * (W / 8)); +// return *this; +// } +// CliffordString &operator=(CliffordString &&other) { +// num_words = other.num_words; +// num_qubits = other.num_qubits; +// if (buf != nullptr) { +// bit_word::aligned_free(buf); +// } +// buf = other.buf; +// other.buf = nullptr; +// other.num_qubits = 0; +// other.num_words = 0; +// return *this; +// } +// +// CliffordString &operator*=(const CliffordString &rhs) { +// if (num_words < rhs.num_words) { +// throw std::invalid_argument("Can't inplace-multiply by a larger Clifford string."); +// } +// for (size_t k = 0; k < rhs.num_words; k++) { +// buf[k] = buf[k] * rhs.buf[k]; +// } +// return *this; +// } +// CliffordString &inplace_left_mul_by(const CliffordString &lhs) { +// if (num_words < lhs.num_words) { +// throw std::invalid_argument("Can't inplace-multiply by a larger Clifford string."); +// } +// for (size_t k = 0; k < num_words; k++) { +// buf[k] = lhs.buf[k] * buf[k]; +// } +// return *this; +// } +// CliffordString operator*(const CliffordString &rhs) const { +// CliffordString result = CliffordString::uninitialized(std::max(num_qubits, rhs.num_qubits)); +// size_t min_words = std::min(num_words, rhs.num_words); +// for (size_t k = 0; k < min_size; k++) { +// result.buf[k] = buf[k] * rhs.buf[k]; +// } +// for (size_t k = min_size; k < num_words; k++) { +// result.buf[k] = buf[k]; +// } +// for (size_t k = min_size; k < rhs.num_words; k++) { +// result.buf[k] = rhs.buf[k]; +// } +// return *this; +// } +//}; +// +///// Writes a string describing the given Clifford string to an output stream. +//template +//std::ostream &operator<<(std::ostream &out, const PauliString &ps); +// +//} // namespace stim +// +//#include "stim/stabilizers/pauli_string.inl" +// +//#endif diff --git a/src/stim/stabilizers/clifford_string.test.cc b/src/stim/stabilizers/clifford_string.test.cc new file mode 100644 index 000000000..d6d304e9e --- /dev/null +++ b/src/stim/stabilizers/clifford_string.test.cc @@ -0,0 +1,12 @@ +#include "stim/stabilizers/clifford_string.h" + +#include "gtest/gtest.h" +#include "stim/mem/simd_word.test.h" + +using namespace stim; + +TEST_EACH_WORD_SIZE_W(clifford_string, mul, { + CliffordString p1 = CliffordString::uninitialized(5); + FlexPauliString p2 = FlexPauliString::from_text("i__Z"); + ASSERT_EQ(p1 * p2, FlexPauliString::from_text("-XY_")); +} From a0ecfc81cefdb5c91418f1619a276030df9106d6 Mon Sep 17 00:00:00 2001 From: Craig Gidney Date: Mon, 24 Mar 2025 13:19:31 -0700 Subject: [PATCH 05/11] x Signed-off-by: Craig Gidney --- src/stim/stabilizers/clifford_string.h | 367 +++++++++---------- src/stim/stabilizers/clifford_string.test.cc | 36 +- 2 files changed, 209 insertions(+), 194 deletions(-) diff --git a/src/stim/stabilizers/clifford_string.h b/src/stim/stabilizers/clifford_string.h index 618d80082..a774aa735 100644 --- a/src/stim/stabilizers/clifford_string.h +++ b/src/stim/stabilizers/clifford_string.h @@ -1,194 +1,181 @@ -///* -// * Copyright 2021 Google LLC -// * -// * Licensed under the Apache License, Version 2.0 (the "License"); -// * you may not use this file except in compliance with the License. -// * You may obtain a copy of the License at -// * -// * http://www.apache.org/licenses/LICENSE-2.0 -// * -// * Unless required by applicable law or agreed to in writing, software -// * distributed under the License is distributed on an "AS IS" BASIS, -// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// * See the License for the specific language governing permissions and -// * limitations under the License. -// */ -// -//#ifndef _STIM_STABILIZERS_CLIFFORD_STRING_H -//#define _STIM_STABILIZERS_CLIFFORD_STRING_H -// -//#include "stim/mem/simd_bits.h" -//#include "stim/gates/gates.h" -// -//namespace stim { -// -//template -//struct CliffordWord { -// bitword x_signs; -// bitword z_signs; -// bitword inv_x2x; -// bitword x2z; -// bitword z2x; -// bitword inv_z2z; -//}; -// -//template -//inline CliffordWord operator*(const CliffordWord &lhs, const CliffordWord &rhs) const { -// CliffordWord result; -// result.inv_x2x = (lhs.inv_x2x | rhs.inv_x2x) ^ (lhs.z2x & rhs.x2z); -// result.x2z = rhs.inv_x2x.andnot(lhs.x2z) ^ lhs.inv_z2z.andnot(rhs.x2z); -// result.z2x = lhs.inv_x2x.andnot(rhs.z2x) ^ rhs.z2z.andnot(lhs.z2x); -// result.inv_z2z = (lhs.x2z & rhs.z2x) ^ (lhs.inv_z2z | rhs.inv_z2z); -// simd_word rhs_x2y = rhs.inv_x2x.andnot(rhs.x2z); -// simd_word rhs_z2y = rhs.z2z.andnot(rhs.z2x); -// simd_word dy = (lhs.x2z & lhs.z2x) ^ lhs.inv_x2x ^ lhs.z2x ^ lhs.x2z ^ lhs.inv_z2z; -// result.x_signs = ( -// rhs.x_signs -// ^ rhs.inv_x2x.andnot(lhs.x_signs) -// ^ (rhs_x2y & dy) -// ^ (rhs.x2z & lhs.z_signs) -// ); -// result.z_signs = ( -// rhs.z_signs -// ^ (rhs.z2x & lhs.x_signs) -// ^ (rhs_z2y & dy) -// ^ rhs.inv_z2z.andnot(lhs.z_signs) -// ); -// return result -//} -// -//template -//struct CliffordString { -// size_t num_qubits; -// size_t num_words; -// CliffordWord *buf; -// -// CliffordString(size_t num_qubits, bool do_not_initialize = false) -// : num_qubits(num_qubits), -// num_words(((num_qubits + W - 1) / W) * W), -// buf(nullptr) { -// if (num_words) { -// buf = bit_word::aligned_malloc(num_words * (W / 8)); -// } -// } -// static inline CliffordString uninitialized(size_t num_qubits) { -// return CliffordString(num_qubits); -// } -// static inline CliffordString identities(size_t num_qubits) { -// CliffordString result(num_qubits); -// memset(result.buf, 0, num_words * (W / 8)); -// return result; -// } -// +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _STIM_STABILIZERS_CLIFFORD_STRING_H +#define _STIM_STABILIZERS_CLIFFORD_STRING_H + +#include "stim/mem/simd_bits.h" +#include "stim/gates/gates.h" + +namespace stim { + +template +struct CliffordWord { + bitword x_signs; + bitword z_signs; + bitword inv_x2x; + bitword x2z; + bitword z2x; + bitword inv_z2z; +}; + +template +inline CliffordWord operator*(const CliffordWord &lhs, const CliffordWord &rhs) { + CliffordWord result; + result.inv_x2x = (lhs.inv_x2x | rhs.inv_x2x) ^ (lhs.z2x & rhs.x2z); + result.x2z = rhs.inv_x2x.andnot(lhs.x2z) ^ lhs.inv_z2z.andnot(rhs.x2z); + result.z2x = lhs.inv_x2x.andnot(rhs.z2x) ^ rhs.z2z.andnot(lhs.z2x); + result.inv_z2z = (lhs.x2z & rhs.z2x) ^ (lhs.inv_z2z | rhs.inv_z2z); + simd_word rhs_x2y = rhs.inv_x2x.andnot(rhs.x2z); + simd_word rhs_z2y = rhs.z2z.andnot(rhs.z2x); + simd_word dy = (lhs.x2z & lhs.z2x) ^ lhs.inv_x2x ^ lhs.z2x ^ lhs.x2z ^ lhs.inv_z2z; + result.x_signs = ( + rhs.x_signs + ^ rhs.inv_x2x.andnot(lhs.x_signs) + ^ (rhs_x2y & dy) + ^ (rhs.x2z & lhs.z_signs) + ); + result.z_signs = ( + rhs.z_signs + ^ (rhs.z2x & lhs.x_signs) + ^ (rhs_z2y & dy) + ^ rhs.inv_z2z.andnot(lhs.z_signs) + ); + return result; +} + +template +struct CliffordString { + size_t num_qubits; + simd_bits x_signs; + simd_bits z_signs; + simd_bits inv_x2x; + simd_bits x2z; + simd_bits z2x; + simd_bits inv_z2z; + + CliffordString(size_t num_qubits) + : num_qubits(num_qubits), + x_signs(num_qubits), + z_signs(num_qubits), + inv_x2x(num_qubits), + x2z(num_qubits), + z2x(num_qubits), + inv_z2z(num_qubits) { + } + + inline CliffordWord word_at(size_t k) const { + return CliffordWord{ + x_signs.ptr_simd[k], + z_signs.ptr_simd[k], + inv_x2x.ptr_simd[k], + x2z.ptr_simd[k], + z2x.ptr_simd[k], + inv_z2z.ptr_simd[k], + }; + } + inline void set_word_at(size_t k, CliffordWord new_value) const { + x_signs.ptr_simd[k] = new_value.x_signs; + z_signs.ptr_simd[k] = new_value.z_signs; + inv_x2x.ptr_simd[k] = new_value.inv_x2x; + x2z.ptr_simd[k] = new_value.x2z; + z2x.ptr_simd[k] = new_value.z2x; + inv_z2z.ptr_simd[k] = new_value.inv_z2z; + } + // stim::GateType gate_at(size_t index) const { // return table[flat_value]; // } -// void set_gate_at(size_t index, GateType gate) const { -// const auto &flows = stim::GATE_DATA[gate].flow_data; -// std::string_view tx = flows[0]; -// std::string_view tz = flows[1]; -// bool inv_x2x = tx[0] == 'X' || tx[0] == 'Y'; -// bool x2z = tx[0] == 'Z' || tx[0] == 'Y'; -// bool x_sign = tx[0] == '-'; -// -// bool inv_z2x = tz[0] == 'X' || tz[0] == 'Y'; -// bool z2z = tz[0] == 'Z' || tz[0] == 'Y'; -// bool z_sign = tz[0] == '-'; -// -// -// } -// -// CliffordString(const CliffordString &other) -// : num_qubits(other.num_qubits), -// num_words(other.num_words), -// buf(nullptr) { -// buf = bit_word::aligned_malloc(num_words * (W / 8)); -// memcpy(buf, other.buf, num_words * (W / 8)); -// } -// CliffordString(CliffordString &&other) -// : num_qubits(other.num_qubits), -// num_words(other.num_words), -// buf(other.buf) { -// other.buf = nullptr; -// other.num_qubits = 0; -// other.num_words = 0; -// } -// ~CliffordString() { -// if (buf != nullptr) { -// bit_word::aligned_free(buf); -// buf = nullptr; -// } -// num_qubits = 0; -// num_words = 0; -// } -// CliffordString &operator=(const CliffordString &other) { -// if (num_words != other.num_words && buf != nullptr) { -// bit_word::aligned_free(buf); -// buf = nullptr; -// num_words = 0; -// num_qubits = 0; -// } -// if (buf == nullptr && num_words > 0) { -// buf = bit_word::aligned_malloc(other.num_words * (W / 8)); -// } -// num_words = other.num_words; -// num_qubits = other.num_qubits; -// memcpy(buf, other.buf, num_words * (W / 8)); -// return *this; -// } -// CliffordString &operator=(CliffordString &&other) { -// num_words = other.num_words; -// num_qubits = other.num_qubits; -// if (buf != nullptr) { -// bit_word::aligned_free(buf); -// } -// buf = other.buf; -// other.buf = nullptr; -// other.num_qubits = 0; -// other.num_words = 0; -// return *this; -// } -// -// CliffordString &operator*=(const CliffordString &rhs) { -// if (num_words < rhs.num_words) { -// throw std::invalid_argument("Can't inplace-multiply by a larger Clifford string."); -// } -// for (size_t k = 0; k < rhs.num_words; k++) { -// buf[k] = buf[k] * rhs.buf[k]; -// } -// return *this; -// } -// CliffordString &inplace_left_mul_by(const CliffordString &lhs) { -// if (num_words < lhs.num_words) { -// throw std::invalid_argument("Can't inplace-multiply by a larger Clifford string."); -// } -// for (size_t k = 0; k < num_words; k++) { -// buf[k] = lhs.buf[k] * buf[k]; -// } -// return *this; -// } -// CliffordString operator*(const CliffordString &rhs) const { -// CliffordString result = CliffordString::uninitialized(std::max(num_qubits, rhs.num_qubits)); -// size_t min_words = std::min(num_words, rhs.num_words); -// for (size_t k = 0; k < min_size; k++) { -// result.buf[k] = buf[k] * rhs.buf[k]; -// } -// for (size_t k = min_size; k < num_words; k++) { -// result.buf[k] = buf[k]; -// } -// for (size_t k = min_size; k < rhs.num_words; k++) { -// result.buf[k] = rhs.buf[k]; -// } -// return *this; -// } -//}; -// -///// Writes a string describing the given Clifford string to an output stream. + + void set_gate_at(size_t index, GateType gate) const { + const auto &flows = stim::GATE_DATA[gate].flow_data; + std::string_view tx = flows[0]; + std::string_view tz = flows[1]; + bool new_inv_x2x = !(tx[0] == 'X' || tx[0] == 'Y'); + bool new_x2z = tx[0] == 'Z' || tx[0] == 'Y'; + bool new_x_sign = tx[0] == '-'; + + bool new_z2x = tz[0] == 'X' || tz[0] == 'Y'; + bool new_inv_z2z = !(tz[0] == 'Z' || tz[0] == 'Y'); + bool new_z_sign = tz[0] == '-'; + + x_signs[index] = new_x_sign; + z_signs[index] = new_z_sign; + inv_x2x[index] = new_inv_x2x; + x2z[index] = new_x2z; + z2x[index] = new_z2x; + inv_z2z[index] = new_inv_z2z; + } + + CliffordString &operator*=(const CliffordString &rhs) { + if (num_qubits < rhs.num_qubits) { + throw std::invalid_argument("Can't inplace-multiply by a larger Clifford string."); + } + for (size_t k = 0; k < rhs.num_words; k++) { + auto lhs_w = word_at(k); + auto rhs_w = rhs.word_at(k); + set_word_at(k, lhs_w * rhs_w); + } + return *this; + } + CliffordString &inplace_left_mul_by(const CliffordString &lhs) { + if (num_qubits < lhs.num_qubits) { + throw std::invalid_argument("Can't inplace-multiply by a larger Clifford string."); + } + for (size_t k = 0; k < x_signs.num_simd_words; k++) { + auto lhs_w = lhs.word_at(k); + auto rhs_w = word_at(k); + set_word_at(k, lhs_w * rhs_w); + } + return *this; + } + CliffordString operator*(const CliffordString &rhs) const { + CliffordString result = CliffordString(std::max(num_qubits, rhs.num_qubits)); + size_t min_words = std::min(x_signs.num_simd_words, rhs.x_signs.num_words); + for (size_t k = 0; k < min_words; k++) { + auto lhs_w = word_at(k); + auto rhs_w = rhs.word_at(k); + result.set_word_at(k, lhs_w * rhs_w); + } + + // The longer string copies its tail into the result. + size_t min_qubits = std::min(num_qubits, rhs.num_qubits); + for (size_t q = min_qubits; q < num_qubits; q++) { + result.x_signs[q] = x_signs[q]; + result.z_signs[q] = z_signs[q]; + result.inv_x2x[q] = inv_x2x[q]; + result.x2z[q] = x2z[q]; + result.z2x[q] = z2x[q]; + result.inv_z2z[q] = inv_z2z[q]; + } + for (size_t q = min_qubits; q < rhs.num_qubits; q++) { + result.x_signs[q] = rhs.x_signs[q]; + result.z_signs[q] = rhs.z_signs[q]; + result.inv_x2x[q] = rhs.inv_x2x[q]; + result.x2z[q] = rhs.x2z[q]; + result.z2x[q] = rhs.z2x[q]; + result.inv_z2z[q] = rhs.inv_z2z[q]; + } + return *this; + } +}; + //template -//std::ostream &operator<<(std::ostream &out, const PauliString &ps); -// -//} // namespace stim -// -//#include "stim/stabilizers/pauli_string.inl" -// -//#endif +//std::ostream &operator<<(std::ostream &out, const CliffordString &v); + +} // namespace stim + +#endif diff --git a/src/stim/stabilizers/clifford_string.test.cc b/src/stim/stabilizers/clifford_string.test.cc index d6d304e9e..5c184fae0 100644 --- a/src/stim/stabilizers/clifford_string.test.cc +++ b/src/stim/stabilizers/clifford_string.test.cc @@ -6,7 +6,35 @@ using namespace stim; TEST_EACH_WORD_SIZE_W(clifford_string, mul, { - CliffordString p1 = CliffordString::uninitialized(5); - FlexPauliString p2 = FlexPauliString::from_text("i__Z"); - ASSERT_EQ(p1 * p2, FlexPauliString::from_text("-XY_")); -} + std::vector single_qubit_gates; + for (size_t g = 0; g < NUM_DEFINED_GATES; g++) { + Gate gate = GATE_DATA[(GateType)g]; + if ((gate.flags & GateFlags::GATE_IS_SINGLE_QUBIT_GATE) && (gate.flags & GateFlags::GATE_IS_UNITARY)) { + single_qubit_gates.push_back(gate); + } + } + ASSERT_EQ(single_qubit_gates.size(), 24); + + std::map t2g; + for (const auto &g : single_qubit_gates) { + t2g[g.tableau().str()] = g.id; + } + + CliffordString p1 = CliffordString::uninitialized(24 * 24); + CliffordString p2 = CliffordString::uninitialized(24 * 24); + CliffordString p12 = CliffordString::uninitialized(24 * 24); + for (size_t k1 = 0; k1 < 24; k1++) { + for (size_t k2 = 0; k2 < 24; k2++) { + size_t k = k1 * 24 + k2; + Gate g1 = single_qubit_gates[k1]; + Gate g2 = single_qubit_gates[k2]; + p1.set_gate_at(k, g1.id); + p2.set_gate_at(k, g2.id); + auto t1 = g1.tableau(); + auto t2 = g2.tableau(); + auto t3 = t2.then(t1); + auto g3 = t2g[t3.str()]; + p12.set_gate_at(k, g3); + } + } +}) From 141a873575d494382e11b4b645a804151d664beb Mon Sep 17 00:00:00 2001 From: Craig Gidney Date: Mon, 24 Mar 2025 14:23:13 -0700 Subject: [PATCH 06/11] pass test --- src/stim/gates/gate_data_period_3.cc | 12 +- src/stim/stabilizers/clifford_string.h | 189 ++++++++++++++++--- src/stim/stabilizers/clifford_string.test.cc | 90 ++++++++- 3 files changed, 247 insertions(+), 44 deletions(-) diff --git a/src/stim/gates/gate_data_period_3.cc b/src/stim/gates/gate_data_period_3.cc index be8014697..88ff17dce 100644 --- a/src/stim/gates/gate_data_period_3.cc +++ b/src/stim/gates/gate_data_period_3.cc @@ -40,7 +40,7 @@ Parens Arguments: Qubits to operate on. )MARKDOWN", .unitary_data = {{0.5f - i * 0.5f, -0.5f - 0.5f * i}, {0.5f - 0.5f * i, 0.5f + 0.5f * i}}, - .flow_data = {"Y", "X"}, + .flow_data = {"+Y", "+X"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( S 0 S 0 @@ -102,7 +102,7 @@ Parens Arguments: Qubits to operate on. )MARKDOWN", .unitary_data = {{0.5f + i * 0.5f, -0.5f + 0.5f * i}, {0.5f + 0.5f * i, 0.5f - 0.5f * i}}, - .flow_data = {"-Y", "X"}, + .flow_data = {"-Y", "+X"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( S 0 H 0 @@ -130,7 +130,7 @@ Parens Arguments: Qubits to operate on. )MARKDOWN", .unitary_data = {{0.5f - i * 0.5f, 0.5f + 0.5f * i}, {-0.5f + 0.5f * i, 0.5f + 0.5f * i}}, - .flow_data = {"Y", "-X"}, + .flow_data = {"+Y", "-X"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( S 0 H 0 @@ -160,7 +160,7 @@ Parens Arguments: Qubits to operate on. )MARKDOWN", .unitary_data = {{0.5f + i * 0.5f, 0.5f + 0.5f * i}, {-0.5f + 0.5f * i, 0.5f - 0.5f * i}}, - .flow_data = {"Z", "Y"}, + .flow_data = {"+Z", "+Y"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( H 0 S 0 @@ -188,7 +188,7 @@ Parens Arguments: Qubits to operate on. )MARKDOWN", .unitary_data = {{0.5f - i * 0.5f, -0.5f + 0.5f * i}, {0.5f + 0.5f * i, 0.5f + 0.5f * i}}, - .flow_data = {"-Z", "Y"}, + .flow_data = {"-Z", "+Y"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( S 0 S 0 @@ -218,7 +218,7 @@ Parens Arguments: Qubits to operate on. )MARKDOWN", .unitary_data = {{0.5f - i * 0.5f, 0.5f - 0.5f * i}, {-0.5f - 0.5f * i, 0.5f + 0.5f * i}}, - .flow_data = {"Z", "-Y"}, + .flow_data = {"+Z", "-Y"}, .h_s_cx_m_r_decomposition = R"CIRCUIT( H 0 S 0 diff --git a/src/stim/stabilizers/clifford_string.h b/src/stim/stabilizers/clifford_string.h index a774aa735..ccd96de7b 100644 --- a/src/stim/stabilizers/clifford_string.h +++ b/src/stim/stabilizers/clifford_string.h @@ -35,39 +35,50 @@ struct CliffordWord { template inline CliffordWord operator*(const CliffordWord &lhs, const CliffordWord &rhs) { CliffordWord result; - result.inv_x2x = (lhs.inv_x2x | rhs.inv_x2x) ^ (lhs.z2x & rhs.x2z); + + result.inv_x2x = (lhs.inv_x2x | rhs.inv_x2x) ^ lhs.z2x & rhs.x2z; result.x2z = rhs.inv_x2x.andnot(lhs.x2z) ^ lhs.inv_z2z.andnot(rhs.x2z); - result.z2x = lhs.inv_x2x.andnot(rhs.z2x) ^ rhs.z2z.andnot(lhs.z2x); - result.inv_z2z = (lhs.x2z & rhs.z2x) ^ (lhs.inv_z2z | rhs.inv_z2z); + result.z2x = lhs.inv_x2x.andnot(rhs.z2x) ^ rhs.inv_z2z.andnot(lhs.z2x); + result.inv_z2z = lhs.x2z & rhs.z2x ^ (lhs.inv_z2z | rhs.inv_z2z); + simd_word rhs_x2y = rhs.inv_x2x.andnot(rhs.x2z); - simd_word rhs_z2y = rhs.z2z.andnot(rhs.z2x); - simd_word dy = (lhs.x2z & lhs.z2x) ^ lhs.inv_x2x ^ lhs.z2x ^ lhs.x2z ^ lhs.inv_z2z; - result.x_signs = ( - rhs.x_signs + simd_word rhs_z2y = rhs.inv_z2z.andnot(rhs.z2x); + simd_word dy = lhs.x2z & lhs.z2x ^ lhs.inv_x2x ^ lhs.z2x ^ lhs.x2z ^ lhs.inv_z2z; + result.x_signs = rhs.x_signs ^ rhs.inv_x2x.andnot(lhs.x_signs) - ^ (rhs_x2y & dy) - ^ (rhs.x2z & lhs.z_signs) - ); - result.z_signs = ( - rhs.z_signs - ^ (rhs.z2x & lhs.x_signs) - ^ (rhs_z2y & dy) - ^ rhs.inv_z2z.andnot(lhs.z_signs) - ); + ^ rhs_x2y & dy + ^ rhs.x2z & lhs.z_signs; + result.z_signs = rhs.z_signs + ^ rhs.z2x & lhs.x_signs + ^ rhs_z2y & dy + ^ rhs.inv_z2z.andnot(lhs.z_signs); return result; } +template +struct CliffordString; + +template +std::ostream &operator<<(std::ostream &out, const CliffordString &v); + +/// A string of single-qubit Clifford rotations. template struct CliffordString { size_t num_qubits; + + // The 2 sign bits of a single qubit Clifford, packed into arrays for easy processing. simd_bits x_signs; simd_bits z_signs; + + // The 4 tableau bits of a single qubit Clifford, packed into arrays for easy processing. + // The x2x and z2z terms are inverted so that zero-initializing produces the identity gate. simd_bits inv_x2x; simd_bits x2z; simd_bits z2x; simd_bits inv_z2z; - CliffordString(size_t num_qubits) + /// Constructs an identity CliffordString for the given number of qubits. + explicit CliffordString(size_t num_qubits) : num_qubits(num_qubits), x_signs(num_qubits), z_signs(num_qubits), @@ -96,20 +107,106 @@ struct CliffordString { inv_z2z.ptr_simd[k] = new_value.inv_z2z; } -// stim::GateType gate_at(size_t index) const { -// return table[flat_value]; -// } + GateType gate_at(size_t q) const { + constexpr std::array table{ + GateType::I, + GateType::X, + GateType::Z, + GateType::Y, + + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + + GateType::S, + GateType::H_XY, + GateType::S_DAG, + GateType::H_NXY, + + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + + GateType::SQRT_X_DAG, + GateType::SQRT_X, + GateType::H_YZ, + GateType::H_NYZ, + + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + + GateType::C_ZYX, + GateType::C_ZNYX, + GateType::C_ZYNX, + GateType::C_NZYX, + + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, - void set_gate_at(size_t index, GateType gate) const { - const auto &flows = stim::GATE_DATA[gate].flow_data; + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + + GateType::C_XYZ, + GateType::C_XYNZ, + GateType::C_XNYZ, + GateType::C_NXYZ, + + GateType::H, + GateType::SQRT_Y_DAG, + GateType::SQRT_Y, + GateType::H_NXZ, + }; + int k = x_signs[q]*2 + z_signs[q] + inv_x2x[q]*4 + x2z[q]*8 + z2x[q]*16 + inv_z2z[q]*32; + return table[k]; + } + + void set_gate_at(size_t index, GateType gate_type) { + Gate g = GATE_DATA[gate_type]; + if (!(g.flags & GATE_IS_SINGLE_QUBIT_GATE) || !(g.flags & GATE_IS_UNITARY)) { + throw std::invalid_argument("Not a single qubit gate: " + std::string(g.name)); + } + const auto &flows = g.flow_data; std::string_view tx = flows[0]; std::string_view tz = flows[1]; - bool new_inv_x2x = !(tx[0] == 'X' || tx[0] == 'Y'); - bool new_x2z = tx[0] == 'Z' || tx[0] == 'Y'; + bool new_inv_x2x = !(tx[1] == 'X' || tx[1] == 'Y'); + bool new_x2z = tx[1] == 'Z' || tx[1] == 'Y'; bool new_x_sign = tx[0] == '-'; - bool new_z2x = tz[0] == 'X' || tz[0] == 'Y'; - bool new_inv_z2z = !(tz[0] == 'Z' || tz[0] == 'Y'); + bool new_z2x = tz[1] == 'X' || tz[1] == 'Y'; + bool new_inv_z2z = !(tz[1] == 'Z' || tz[1] == 'Y'); bool new_z_sign = tz[0] == '-'; x_signs[index] = new_x_sign; @@ -124,7 +221,7 @@ struct CliffordString { if (num_qubits < rhs.num_qubits) { throw std::invalid_argument("Can't inplace-multiply by a larger Clifford string."); } - for (size_t k = 0; k < rhs.num_words; k++) { + for (size_t k = 0; k < rhs.x_signs.num_simd_words; k++) { auto lhs_w = word_at(k); auto rhs_w = rhs.word_at(k); set_word_at(k, lhs_w * rhs_w); @@ -144,7 +241,7 @@ struct CliffordString { } CliffordString operator*(const CliffordString &rhs) const { CliffordString result = CliffordString(std::max(num_qubits, rhs.num_qubits)); - size_t min_words = std::min(x_signs.num_simd_words, rhs.x_signs.num_words); + size_t min_words = std::min(x_signs.num_simd_words, rhs.x_signs.num_simd_words); for (size_t k = 0; k < min_words; k++) { auto lhs_w = word_at(k); auto rhs_w = rhs.word_at(k); @@ -169,12 +266,42 @@ struct CliffordString { result.z2x[q] = rhs.z2x[q]; result.inv_z2z[q] = rhs.inv_z2z[q]; } - return *this; + + return result; + } + + bool operator==(const CliffordString &other) const { + return x_signs == other.x_signs + && z_signs == other.z_signs + && inv_x2x == other.inv_x2x + && x2z == other.x2z + && z2x == other.z2x + && inv_z2z == other.inv_z2z; + } + bool operator!=(const CliffordString &other) const { + return !(*this == other); + } + + std::string str() const { + std::stringstream ss; + ss << *this; + return ss.str(); } }; -//template -//std::ostream &operator<<(std::ostream &out, const CliffordString &v); +template +std::ostream &operator<<(std::ostream &out, const CliffordString &v) { + for (size_t q = 0; q < v.num_qubits; q++) { + if (q > 0) { + out << " "; + } + int c = v.inv_x2x[q] + v.x2z[q] * 2 + v.z2x[q] * 4 + v.inv_z2z[q] * 8; + int p = v.z_signs[q] + v.x_signs[q] * 2; + out << "_?S?V??D??????UH"[c]; + out << "IXZY"[p]; + } + return out; +} } // namespace stim diff --git a/src/stim/stabilizers/clifford_string.test.cc b/src/stim/stabilizers/clifford_string.test.cc index 5c184fae0..f01004bfa 100644 --- a/src/stim/stabilizers/clifford_string.test.cc +++ b/src/stim/stabilizers/clifford_string.test.cc @@ -2,27 +2,102 @@ #include "gtest/gtest.h" #include "stim/mem/simd_word.test.h" +#include "stim/stabilizers/tableau.h" using namespace stim; -TEST_EACH_WORD_SIZE_W(clifford_string, mul, { - std::vector single_qubit_gates; +std::vector single_qubit_clifford_rotations() { + std::vector result; for (size_t g = 0; g < NUM_DEFINED_GATES; g++) { Gate gate = GATE_DATA[(GateType)g]; if ((gate.flags & GateFlags::GATE_IS_SINGLE_QUBIT_GATE) && (gate.flags & GateFlags::GATE_IS_UNITARY)) { - single_qubit_gates.push_back(gate); + result.push_back(gate); } } - ASSERT_EQ(single_qubit_gates.size(), 24); + assert(result.size() == 24); + return result; +} + +TEST_EACH_WORD_SIZE_W(clifford_string, set_gate_at_vs_str_vs_gate_at, { + CliffordString p = CliffordString(24); + int x = 0; + + p.set_gate_at(x++, GateType::I); + p.set_gate_at(x++, GateType::X); + p.set_gate_at(x++, GateType::Y); + p.set_gate_at(x++, GateType::Z); + + p.set_gate_at(x++, GateType::H); + p.set_gate_at(x++, GateType::SQRT_Y_DAG); + p.set_gate_at(x++, GateType::H_NXZ); + p.set_gate_at(x++, GateType::SQRT_Y); + + p.set_gate_at(x++, GateType::S); + p.set_gate_at(x++, GateType::H_XY); + p.set_gate_at(x++, GateType::H_NXY); + p.set_gate_at(x++, GateType::S_DAG); + + p.set_gate_at(x++, GateType::SQRT_X_DAG); + p.set_gate_at(x++, GateType::SQRT_X); + p.set_gate_at(x++, GateType::H_NYZ); + p.set_gate_at(x++, GateType::H_YZ); + + p.set_gate_at(x++, GateType::C_XYZ); + p.set_gate_at(x++, GateType::C_XYNZ); + p.set_gate_at(x++, GateType::C_NXYZ); + p.set_gate_at(x++, GateType::C_XNYZ); + + p.set_gate_at(x++, GateType::C_ZYX); + p.set_gate_at(x++, GateType::C_ZNYX); + p.set_gate_at(x++, GateType::C_NZYX); + p.set_gate_at(x++, GateType::C_ZYNX); + + ASSERT_EQ(p.str(), "_I _X _Y _Z HI HX HY HZ SI SX SY SZ VI VX VY VZ UI UX UY UZ DI DX DY DZ"); + + x = 0; + + EXPECT_EQ(p.gate_at(x++), GateType::I); + EXPECT_EQ(p.gate_at(x++), GateType::X); + EXPECT_EQ(p.gate_at(x++), GateType::Y); + EXPECT_EQ(p.gate_at(x++), GateType::Z); + + EXPECT_EQ(p.gate_at(x++), GateType::H); + EXPECT_EQ(p.gate_at(x++), GateType::SQRT_Y_DAG); + EXPECT_EQ(p.gate_at(x++), GateType::H_NXZ); + EXPECT_EQ(p.gate_at(x++), GateType::SQRT_Y); + + EXPECT_EQ(p.gate_at(x++), GateType::S); + EXPECT_EQ(p.gate_at(x++), GateType::H_XY); + EXPECT_EQ(p.gate_at(x++), GateType::H_NXY); + EXPECT_EQ(p.gate_at(x++), GateType::S_DAG); + + EXPECT_EQ(p.gate_at(x++), GateType::SQRT_X_DAG); + EXPECT_EQ(p.gate_at(x++), GateType::SQRT_X); + EXPECT_EQ(p.gate_at(x++), GateType::H_NYZ); + EXPECT_EQ(p.gate_at(x++), GateType::H_YZ); + + EXPECT_EQ(p.gate_at(x++), GateType::C_XYZ); + EXPECT_EQ(p.gate_at(x++), GateType::C_XYNZ); + EXPECT_EQ(p.gate_at(x++), GateType::C_NXYZ); + EXPECT_EQ(p.gate_at(x++), GateType::C_XNYZ); + + EXPECT_EQ(p.gate_at(x++), GateType::C_ZYX); + EXPECT_EQ(p.gate_at(x++), GateType::C_ZNYX); + EXPECT_EQ(p.gate_at(x++), GateType::C_NZYX); + EXPECT_EQ(p.gate_at(x++), GateType::C_ZYNX); +}); + +TEST_EACH_WORD_SIZE_W(clifford_string, multiplication_table_vs_tableau_multiplication, { + std::vector single_qubit_gates = single_qubit_clifford_rotations(); std::map t2g; for (const auto &g : single_qubit_gates) { t2g[g.tableau().str()] = g.id; } - CliffordString p1 = CliffordString::uninitialized(24 * 24); - CliffordString p2 = CliffordString::uninitialized(24 * 24); - CliffordString p12 = CliffordString::uninitialized(24 * 24); + CliffordString p1 = CliffordString(24 * 24); + CliffordString p2 = CliffordString(24 * 24); + CliffordString p12 = CliffordString(24 * 24); for (size_t k1 = 0; k1 < 24; k1++) { for (size_t k2 = 0; k2 < 24; k2++) { size_t k = k1 * 24 + k2; @@ -37,4 +112,5 @@ TEST_EACH_WORD_SIZE_W(clifford_string, mul, { p12.set_gate_at(k, g3); } } + ASSERT_EQ(p1 * p2, p12); }) From d61443d1002a2d1967d41cd210659f59cdd349da Mon Sep 17 00:00:00 2001 From: Craig Gidney Date: Mon, 24 Mar 2025 14:26:47 -0700 Subject: [PATCH 07/11] doc --- src/stim/stabilizers/clifford_string.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/stim/stabilizers/clifford_string.h b/src/stim/stabilizers/clifford_string.h index ccd96de7b..99b915724 100644 --- a/src/stim/stabilizers/clifford_string.h +++ b/src/stim/stabilizers/clifford_string.h @@ -36,11 +36,15 @@ template inline CliffordWord operator*(const CliffordWord &lhs, const CliffordWord &rhs) { CliffordWord result; + // I don't have a simple explanation of why this is correct. It was produced by starting from something that was + // obviously correct, having tests to check all 24*24 cases, then iteratively applying simple rewrites to reduce + // the number of operations. So the result is correct, but somewhat incomprehensible. result.inv_x2x = (lhs.inv_x2x | rhs.inv_x2x) ^ lhs.z2x & rhs.x2z; result.x2z = rhs.inv_x2x.andnot(lhs.x2z) ^ lhs.inv_z2z.andnot(rhs.x2z); result.z2x = lhs.inv_x2x.andnot(rhs.z2x) ^ rhs.inv_z2z.andnot(lhs.z2x); result.inv_z2z = lhs.x2z & rhs.z2x ^ (lhs.inv_z2z | rhs.inv_z2z); + // I *especially* don't have an explanation of why this part is correct. But every case is tested and verified. simd_word rhs_x2y = rhs.inv_x2x.andnot(rhs.x2z); simd_word rhs_z2y = rhs.inv_z2z.andnot(rhs.z2x); simd_word dy = lhs.x2z & lhs.z2x ^ lhs.inv_x2x ^ lhs.z2x ^ lhs.x2z ^ lhs.inv_z2z; @@ -52,6 +56,7 @@ inline CliffordWord operator*(const CliffordWord &lhs, const CliffordWord< ^ rhs.z2x & lhs.x_signs ^ rhs_z2y & dy ^ rhs.inv_z2z.andnot(lhs.z_signs); + return result; } From 2313484332226aa9caae15420c58b23bf68a0b53 Mon Sep 17 00:00:00 2001 From: Craig Gidney Date: Mon, 24 Mar 2025 15:13:30 -0700 Subject: [PATCH 08/11] f --- src/stim/stabilizers/clifford_string.h | 12 ++++++------ src/stim/stabilizers/clifford_string.test.cc | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/stim/stabilizers/clifford_string.h b/src/stim/stabilizers/clifford_string.h index 99b915724..e96aec3a2 100644 --- a/src/stim/stabilizers/clifford_string.h +++ b/src/stim/stabilizers/clifford_string.h @@ -39,22 +39,22 @@ inline CliffordWord operator*(const CliffordWord &lhs, const CliffordWord< // I don't have a simple explanation of why this is correct. It was produced by starting from something that was // obviously correct, having tests to check all 24*24 cases, then iteratively applying simple rewrites to reduce // the number of operations. So the result is correct, but somewhat incomprehensible. - result.inv_x2x = (lhs.inv_x2x | rhs.inv_x2x) ^ lhs.z2x & rhs.x2z; + result.inv_x2x = (lhs.inv_x2x | rhs.inv_x2x) ^ (lhs.z2x & rhs.x2z); result.x2z = rhs.inv_x2x.andnot(lhs.x2z) ^ lhs.inv_z2z.andnot(rhs.x2z); result.z2x = lhs.inv_x2x.andnot(rhs.z2x) ^ rhs.inv_z2z.andnot(lhs.z2x); - result.inv_z2z = lhs.x2z & rhs.z2x ^ (lhs.inv_z2z | rhs.inv_z2z); + result.inv_z2z = (lhs.x2z & rhs.z2x) ^ (lhs.inv_z2z | rhs.inv_z2z); // I *especially* don't have an explanation of why this part is correct. But every case is tested and verified. simd_word rhs_x2y = rhs.inv_x2x.andnot(rhs.x2z); simd_word rhs_z2y = rhs.inv_z2z.andnot(rhs.z2x); - simd_word dy = lhs.x2z & lhs.z2x ^ lhs.inv_x2x ^ lhs.z2x ^ lhs.x2z ^ lhs.inv_z2z; + simd_word dy = (lhs.x2z & lhs.z2x) ^ lhs.inv_x2x ^ lhs.z2x ^ lhs.x2z ^ lhs.inv_z2z; result.x_signs = rhs.x_signs ^ rhs.inv_x2x.andnot(lhs.x_signs) ^ rhs_x2y & dy ^ rhs.x2z & lhs.z_signs; result.z_signs = rhs.z_signs - ^ rhs.z2x & lhs.x_signs - ^ rhs_z2y & dy + ^ (rhs.z2x & lhs.x_signs) + ^ (rhs_z2y & dy) ^ rhs.inv_z2z.andnot(lhs.z_signs); return result; @@ -302,7 +302,7 @@ std::ostream &operator<<(std::ostream &out, const CliffordString &v) { } int c = v.inv_x2x[q] + v.x2z[q] * 2 + v.z2x[q] * 4 + v.inv_z2z[q] * 8; int p = v.z_signs[q] + v.x_signs[q] * 2; - out << "_?S?V??D??????UH"[c]; + out << "_?S?V??d??????uH"[c]; out << "IXZY"[p]; } return out; diff --git a/src/stim/stabilizers/clifford_string.test.cc b/src/stim/stabilizers/clifford_string.test.cc index f01004bfa..f516cd346 100644 --- a/src/stim/stabilizers/clifford_string.test.cc +++ b/src/stim/stabilizers/clifford_string.test.cc @@ -52,7 +52,7 @@ TEST_EACH_WORD_SIZE_W(clifford_string, set_gate_at_vs_str_vs_gate_at, { p.set_gate_at(x++, GateType::C_NZYX); p.set_gate_at(x++, GateType::C_ZYNX); - ASSERT_EQ(p.str(), "_I _X _Y _Z HI HX HY HZ SI SX SY SZ VI VX VY VZ UI UX UY UZ DI DX DY DZ"); + ASSERT_EQ(p.str(), "_I _X _Y _Z HI HX HY HZ SI SX SY SZ VI VX VY VZ uI uX uY uZ dI dX dY dZ"); x = 0; From b7bf4a435a2c83bd39506826a1a92b8744d7002c Mon Sep 17 00:00:00 2001 From: Craig Gidney Date: Mon, 24 Mar 2025 15:15:21 -0700 Subject: [PATCH 09/11] f2 --- src/stim/util_bot/arg_parse.h | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/stim/util_bot/arg_parse.h b/src/stim/util_bot/arg_parse.h index 8896150c2..ef3feac1a 100644 --- a/src/stim/util_bot/arg_parse.h +++ b/src/stim/util_bot/arg_parse.h @@ -17,12 +17,9 @@ #ifndef _STIM_UTIL_BOT_ARG_PARSE_H #define _STIM_UTIL_BOT_ARG_PARSE_H -#include #include -#include #include #include -#include #include #include #include @@ -197,7 +194,7 @@ const T &find_enum_argument( } return values.at(default_key); } - if (values.find(text) == values.end()) { + if (!values.contains(text)) { std::stringstream msg; msg << "\033[31mUnrecognized value '" << text << "' for enum flag '" << name << "'.\n"; msg << "Recognized values are:\n"; @@ -240,7 +237,7 @@ struct ostream_else_cout { std::unique_ptr held; public: - ostream_else_cout(std::unique_ptr &&held); + explicit ostream_else_cout(std::unique_ptr &&held); std::ostream &stream(); }; @@ -249,7 +246,7 @@ struct ostream_else_cout { /// Args: /// name: The name of the command line flag that will specify the file path. /// default_std_out: If true, defaults to stdout when the command line argument isn't given. Otherwise exits with -/// failure when the command line argumen tisn't given. +/// failure when the command line argument isn't given. /// argc: Number of command line arguments. /// argv: Array of command line argument strings. /// From 9df180f917a4e986b149f372f8bbc0e3622a1d07 Mon Sep 17 00:00:00 2001 From: Craig Gidney Date: Mon, 24 Mar 2025 23:20:55 -0700 Subject: [PATCH 10/11] perf --- file_lists/perf_files | 1 + src/stim/stabilizers/clifford_string.h | 4 ++-- src/stim/stabilizers/clifford_string.perf.cc | 16 ++++++++++++++++ src/stim/util_bot/arg_parse.cc | 4 ++-- 4 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 src/stim/stabilizers/clifford_string.perf.cc diff --git a/file_lists/perf_files b/file_lists/perf_files index 2d6ac07e1..857fa1e01 100644 --- a/file_lists/perf_files +++ b/file_lists/perf_files @@ -12,6 +12,7 @@ src/stim/simulators/dem_sampler.perf.cc src/stim/simulators/error_analyzer.perf.cc src/stim/simulators/frame_simulator.perf.cc src/stim/simulators/tableau_simulator.perf.cc +src/stim/stabilizers/clifford_string.perf.cc src/stim/stabilizers/pauli_string.perf.cc src/stim/stabilizers/pauli_string_iter.perf.cc src/stim/stabilizers/tableau.perf.cc diff --git a/src/stim/stabilizers/clifford_string.h b/src/stim/stabilizers/clifford_string.h index e96aec3a2..9f66609c7 100644 --- a/src/stim/stabilizers/clifford_string.h +++ b/src/stim/stabilizers/clifford_string.h @@ -50,8 +50,8 @@ inline CliffordWord operator*(const CliffordWord &lhs, const CliffordWord< simd_word dy = (lhs.x2z & lhs.z2x) ^ lhs.inv_x2x ^ lhs.z2x ^ lhs.x2z ^ lhs.inv_z2z; result.x_signs = rhs.x_signs ^ rhs.inv_x2x.andnot(lhs.x_signs) - ^ rhs_x2y & dy - ^ rhs.x2z & lhs.z_signs; + ^ (rhs_x2y & dy) + ^ (rhs.x2z & lhs.z_signs); result.z_signs = rhs.z_signs ^ (rhs.z2x & lhs.x_signs) ^ (rhs_z2y & dy) diff --git a/src/stim/stabilizers/clifford_string.perf.cc b/src/stim/stabilizers/clifford_string.perf.cc new file mode 100644 index 000000000..00eb90bd3 --- /dev/null +++ b/src/stim/stabilizers/clifford_string.perf.cc @@ -0,0 +1,16 @@ +#include "stim/stabilizers/clifford_string.h" + +#include "stim/perf.perf.h" + +using namespace stim; + +BENCHMARK(CliffordString_multiplication_10K) { + size_t n = 10 * 1000; + CliffordString p1(n); + CliffordString p2(n); + benchmark_go([&]() { + p1 *= p2; + }) + .goal_nanos(430) + .show_rate("Rots", n); +} diff --git a/src/stim/util_bot/arg_parse.cc b/src/stim/util_bot/arg_parse.cc index e5ad0af6a..873258000 100644 --- a/src/stim/util_bot/arg_parse.cc +++ b/src/stim/util_bot/arg_parse.cc @@ -365,7 +365,7 @@ ostream_else_cout stim::find_output_stream_argument( msg << "Missing command line argument: '" << name_c_str << "'"; throw std::invalid_argument(msg.str()); } - return {nullptr}; + return ostream_else_cout(nullptr); } if (*path_c_str == '\0') { std::stringstream msg; @@ -378,7 +378,7 @@ ostream_else_cout stim::find_output_stream_argument( msg << "Failed to open '" << path_c_str << "'"; throw std::invalid_argument(msg.str()); } - return {std::move(f)}; + return ostream_else_cout(std::move(f)); } std::vector stim::split_view(char splitter, std::string_view text) { From 039878baf43164f43819682736aa51f58eefea23 Mon Sep 17 00:00:00 2001 From: Craig Gidney Date: Fri, 4 Apr 2025 17:48:33 -0700 Subject: [PATCH 11/11] to/from circuit --- src/stim/mem/bitword.h | 20 ++ src/stim/stabilizers/clifford_string.h | 320 +++++++++++-------- src/stim/stabilizers/clifford_string.test.cc | 66 ++++ 3 files changed, 281 insertions(+), 125 deletions(-) diff --git a/src/stim/mem/bitword.h b/src/stim/mem/bitword.h index b0bce4636..3cc718ca9 100644 --- a/src/stim/mem/bitword.h +++ b/src/stim/mem/bitword.h @@ -178,6 +178,26 @@ inline bitword operator^(const bitword &self, int64_t mask) { return self ^ bitword(mask); } +template +inline bitword andnot(const bitword &inv, const bitword &val) { + return inv.andnot(val); +} +inline uint64_t andnot(uint64_t inv, uint64_t val) { + return ~inv & val; +} +inline uint32_t andnot(uint32_t inv, uint32_t val) { + return ~inv & val; +} +inline uint16_t andnot(uint16_t inv, uint16_t val) { + return ~inv & val; +} +inline uint8_t andnot(uint8_t inv, uint8_t val) { + return ~inv & val; +} +inline bool andnot(bool inv, bool val) { + return !inv && val; +} + } // namespace stim #endif diff --git a/src/stim/stabilizers/clifford_string.h b/src/stim/stabilizers/clifford_string.h index 9f66609c7..fb370735b 100644 --- a/src/stim/stabilizers/clifford_string.h +++ b/src/stim/stabilizers/clifford_string.h @@ -19,43 +19,154 @@ #include "stim/mem/simd_bits.h" #include "stim/gates/gates.h" +#include "stim/circuit/circuit.h" namespace stim { -template +/// A fixed-size list of W single-qubit Clifford rotations. +template struct CliffordWord { - bitword x_signs; - bitword z_signs; - bitword inv_x2x; - bitword x2z; - bitword z2x; - bitword inv_z2z; + Word x_signs; + Word z_signs; + Word inv_x2x; // Inverted so that zero-initializing gives the identity gate. + Word x2z; + Word z2x; + Word inv_z2z; // Inverted so that zero-initializing gives the identity gate. }; -template -inline CliffordWord operator*(const CliffordWord &lhs, const CliffordWord &rhs) { - CliffordWord result; +inline GateType bits2gate(std::array bits) { + constexpr std::array table{ + GateType::I, + GateType::X, + GateType::Z, + GateType::Y, + + GateType::NOT_A_GATE, // These should be impossible if the class is in a good state. + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + + GateType::S, + GateType::H_XY, + GateType::S_DAG, + GateType::H_NXY, + + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + + GateType::SQRT_X_DAG, + GateType::SQRT_X, + GateType::H_YZ, + GateType::H_NYZ, + + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + + GateType::C_ZYX, + GateType::C_ZNYX, + GateType::C_ZYNX, + GateType::C_NZYX, + + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + GateType::NOT_A_GATE, + + GateType::C_XYZ, + GateType::C_XYNZ, + GateType::C_XNYZ, + GateType::C_NXYZ, + + GateType::H, + GateType::SQRT_Y_DAG, + GateType::SQRT_Y, + GateType::H_NXZ, + }; + int k = (bits[0] << 0) + | (bits[1] << 1) + | (bits[2] << 2) + | (bits[3] << 3) + | (bits[4] << 4) + | (bits[5] << 5); + return table[k]; +} + +inline std::array gate_to_bits(GateType gate_type) { + Gate g = GATE_DATA[gate_type]; + if (!(g.flags & GATE_IS_SINGLE_QUBIT_GATE) || !(g.flags & GATE_IS_UNITARY)) { + throw std::invalid_argument("Not a single qubit gate: " + std::string(g.name)); + } + const auto &flows = g.flow_data; + std::string_view tx = flows[0]; + std::string_view tz = flows[1]; + bool z_sign = tz[0] == '-'; + bool inv_x2x = !(tx[1] == 'X' || tx[1] == 'Y'); + bool x2z = tx[1] == 'Z' || tx[1] == 'Y'; + bool x_sign = tx[0] == '-'; + bool z2x = tz[1] == 'X' || tz[1] == 'Y'; + bool inv_z2z = !(tz[1] == 'Z' || tz[1] == 'Y'); + return {z_sign, x_sign, inv_x2x, x2z, z2x, inv_z2z}; +} + +/// Returns the result of multiplying W rotations pair-wise. +template +inline CliffordWord operator*(const CliffordWord &lhs, const CliffordWord &rhs) { + CliffordWord result; // I don't have a simple explanation of why this is correct. It was produced by starting from something that was // obviously correct, having tests to check all 24*24 cases, then iteratively applying simple rewrites to reduce // the number of operations. So the result is correct, but somewhat incomprehensible. result.inv_x2x = (lhs.inv_x2x | rhs.inv_x2x) ^ (lhs.z2x & rhs.x2z); - result.x2z = rhs.inv_x2x.andnot(lhs.x2z) ^ lhs.inv_z2z.andnot(rhs.x2z); - result.z2x = lhs.inv_x2x.andnot(rhs.z2x) ^ rhs.inv_z2z.andnot(lhs.z2x); + result.x2z = andnot(rhs.inv_x2x, lhs.x2z) ^ andnot(lhs.inv_z2z, rhs.x2z); + result.z2x = andnot(lhs.inv_x2x, rhs.z2x) ^ andnot(rhs.inv_z2z, lhs.z2x); result.inv_z2z = (lhs.x2z & rhs.z2x) ^ (lhs.inv_z2z | rhs.inv_z2z); // I *especially* don't have an explanation of why this part is correct. But every case is tested and verified. - simd_word rhs_x2y = rhs.inv_x2x.andnot(rhs.x2z); - simd_word rhs_z2y = rhs.inv_z2z.andnot(rhs.z2x); - simd_word dy = (lhs.x2z & lhs.z2x) ^ lhs.inv_x2x ^ lhs.z2x ^ lhs.x2z ^ lhs.inv_z2z; + Word rhs_x2y = andnot(rhs.inv_x2x, rhs.x2z); + Word rhs_z2y = andnot(rhs.inv_z2z, rhs.z2x); + Word dy = (lhs.x2z & lhs.z2x) ^ lhs.inv_x2x ^ lhs.z2x ^ lhs.x2z ^ lhs.inv_z2z; result.x_signs = rhs.x_signs - ^ rhs.inv_x2x.andnot(lhs.x_signs) + ^ andnot(rhs.inv_x2x, lhs.x_signs) ^ (rhs_x2y & dy) ^ (rhs.x2z & lhs.z_signs); result.z_signs = rhs.z_signs ^ (rhs.z2x & lhs.x_signs) ^ (rhs_z2y & dy) - ^ rhs.inv_z2z.andnot(lhs.z_signs); + ^ andnot(rhs.inv_z2z, lhs.z_signs); return result; } @@ -93,8 +204,9 @@ struct CliffordString { inv_z2z(num_qubits) { } - inline CliffordWord word_at(size_t k) const { - return CliffordWord{ + /// Extracts rotations k*W through (k+1)*W into a CliffordWord. + inline CliffordWord> word_at(size_t k) const { + return CliffordWord>{ x_signs.ptr_simd[k], z_signs.ptr_simd[k], inv_x2x.ptr_simd[k], @@ -103,7 +215,8 @@ struct CliffordString { inv_z2z.ptr_simd[k], }; } - inline void set_word_at(size_t k, CliffordWord new_value) const { + /// Writes rotations k*W through (k+1)*W from a CliffordWord. + inline void set_word_at(size_t k, CliffordWord> new_value) const { x_signs.ptr_simd[k] = new_value.x_signs; z_signs.ptr_simd[k] = new_value.z_signs; inv_x2x.ptr_simd[k] = new_value.inv_x2x; @@ -112,116 +225,23 @@ struct CliffordString { inv_z2z.ptr_simd[k] = new_value.inv_z2z; } + /// Converts the internal rotation representation into a GateType. GateType gate_at(size_t q) const { - constexpr std::array table{ - GateType::I, - GateType::X, - GateType::Z, - GateType::Y, - - GateType::NOT_A_GATE, - GateType::NOT_A_GATE, - GateType::NOT_A_GATE, - GateType::NOT_A_GATE, - - GateType::S, - GateType::H_XY, - GateType::S_DAG, - GateType::H_NXY, - - GateType::NOT_A_GATE, - GateType::NOT_A_GATE, - GateType::NOT_A_GATE, - GateType::NOT_A_GATE, - - GateType::SQRT_X_DAG, - GateType::SQRT_X, - GateType::H_YZ, - GateType::H_NYZ, - - GateType::NOT_A_GATE, - GateType::NOT_A_GATE, - GateType::NOT_A_GATE, - GateType::NOT_A_GATE, - - GateType::NOT_A_GATE, - GateType::NOT_A_GATE, - GateType::NOT_A_GATE, - GateType::NOT_A_GATE, - - GateType::C_ZYX, - GateType::C_ZNYX, - GateType::C_ZYNX, - GateType::C_NZYX, - - GateType::NOT_A_GATE, - GateType::NOT_A_GATE, - GateType::NOT_A_GATE, - GateType::NOT_A_GATE, - - GateType::NOT_A_GATE, - GateType::NOT_A_GATE, - GateType::NOT_A_GATE, - GateType::NOT_A_GATE, - - GateType::NOT_A_GATE, - GateType::NOT_A_GATE, - GateType::NOT_A_GATE, - GateType::NOT_A_GATE, - - GateType::NOT_A_GATE, - GateType::NOT_A_GATE, - GateType::NOT_A_GATE, - GateType::NOT_A_GATE, - - GateType::NOT_A_GATE, - GateType::NOT_A_GATE, - GateType::NOT_A_GATE, - GateType::NOT_A_GATE, - - GateType::NOT_A_GATE, - GateType::NOT_A_GATE, - GateType::NOT_A_GATE, - GateType::NOT_A_GATE, - - GateType::C_XYZ, - GateType::C_XYNZ, - GateType::C_XNYZ, - GateType::C_NXYZ, - - GateType::H, - GateType::SQRT_Y_DAG, - GateType::SQRT_Y, - GateType::H_NXZ, - }; - int k = x_signs[q]*2 + z_signs[q] + inv_x2x[q]*4 + x2z[q]*8 + z2x[q]*16 + inv_z2z[q]*32; - return table[k]; + return bits2gate(std::array{z_signs[q], x_signs[q], inv_x2x[q], x2z[q], z2x[q], inv_z2z[q]}); } - void set_gate_at(size_t index, GateType gate_type) { - Gate g = GATE_DATA[gate_type]; - if (!(g.flags & GATE_IS_SINGLE_QUBIT_GATE) || !(g.flags & GATE_IS_UNITARY)) { - throw std::invalid_argument("Not a single qubit gate: " + std::string(g.name)); - } - const auto &flows = g.flow_data; - std::string_view tx = flows[0]; - std::string_view tz = flows[1]; - bool new_inv_x2x = !(tx[1] == 'X' || tx[1] == 'Y'); - bool new_x2z = tx[1] == 'Z' || tx[1] == 'Y'; - bool new_x_sign = tx[0] == '-'; - - bool new_z2x = tz[1] == 'X' || tz[1] == 'Y'; - bool new_inv_z2z = !(tz[1] == 'Z' || tz[1] == 'Y'); - bool new_z_sign = tz[0] == '-'; - - x_signs[index] = new_x_sign; - z_signs[index] = new_z_sign; - inv_x2x[index] = new_inv_x2x; - x2z[index] = new_x2z; - z2x[index] = new_z2x; - inv_z2z[index] = new_inv_z2z; + /// Sets an internal rotation from a GateType. + void set_gate_at(size_t q, GateType gate_type) { + std::array bits = gate_to_bits(gate_type);; + z_signs[q] = bits[0]; + x_signs[q] = bits[1]; + inv_x2x[q] = bits[2]; + x2z[q] = bits[3]; + z2x[q] = bits[4]; + inv_z2z[q] = bits[5]; } + /// Inplace right-multiplication of rotations. CliffordString &operator*=(const CliffordString &rhs) { if (num_qubits < rhs.num_qubits) { throw std::invalid_argument("Can't inplace-multiply by a larger Clifford string."); @@ -233,6 +253,51 @@ struct CliffordString { } return *this; } + + void inplace_then(CircuitInstruction inst) { + std::array v = gate_to_bits(inst.gate_type); + for (const auto &t : inst.targets) { + if (!t.is_qubit_target()) { + continue; + } + uint32_t q = t.qubit_value(); + if (q >= num_qubits) { + throw std::invalid_argument("Circuit acted on qubit past end of string."); + } + size_t w = q / W; + size_t k = q % W; + CliffordWord> tmp{}; + bit_ref(&tmp.z_signs, k) ^= v[0]; + bit_ref(&tmp.x_signs, k) ^= v[1]; + bit_ref(&tmp.inv_x2x, k) ^= v[2]; + bit_ref(&tmp.x2z, k) ^= v[3]; + bit_ref(&tmp.z2x, k) ^= v[4]; + bit_ref(&tmp.inv_z2z, k) ^= v[5]; + set_word_at(w, tmp * word_at(w)); + } + } + + static CliffordString from_circuit(const Circuit &circuit) { + CliffordString result(circuit.count_qubits()); + circuit.for_each_operation([&](CircuitInstruction inst) { + result.inplace_then(inst); + }); + return result; + } + + Circuit to_circuit() const { + Circuit result; + for (size_t q = 0; q < num_qubits; q++) { + GateType g = gate_at(q); + if (g != GateType::I || q + 1 == num_qubits) { + GateTarget t = GateTarget::qubit(q); + result.safe_append(CircuitInstruction{g, {}, &t, {}}); + } + } + return result; + } + + /// Inplace left-multiplication of rotations. CliffordString &inplace_left_mul_by(const CliffordString &lhs) { if (num_qubits < lhs.num_qubits) { throw std::invalid_argument("Can't inplace-multiply by a larger Clifford string."); @@ -244,6 +309,8 @@ struct CliffordString { } return *this; } + + /// Out-of-place multiplication of rotations. CliffordString operator*(const CliffordString &rhs) const { CliffordString result = CliffordString(std::max(num_qubits, rhs.num_qubits)); size_t min_words = std::min(x_signs.num_simd_words, rhs.x_signs.num_simd_words); @@ -275,6 +342,7 @@ struct CliffordString { return result; } + /// Determines if two Clifford strings have the same length and contents. bool operator==(const CliffordString &other) const { return x_signs == other.x_signs && z_signs == other.z_signs @@ -283,10 +351,12 @@ struct CliffordString { && z2x == other.z2x && inv_z2z == other.inv_z2z; } + /// Determines if two Clifford strings have different lengths or contents. bool operator!=(const CliffordString &other) const { return !(*this == other); } + /// Returns a description of the Clifford string. std::string str() const { std::stringstream ss; ss << *this; diff --git a/src/stim/stabilizers/clifford_string.test.cc b/src/stim/stabilizers/clifford_string.test.cc index f516cd346..4521f4c02 100644 --- a/src/stim/stabilizers/clifford_string.test.cc +++ b/src/stim/stabilizers/clifford_string.test.cc @@ -114,3 +114,69 @@ TEST_EACH_WORD_SIZE_W(clifford_string, multiplication_table_vs_tableau_multiplic } ASSERT_EQ(p1 * p2, p12); }) + +TEST_EACH_WORD_SIZE_W(clifford_string, to_from_circuit, { + Circuit circuit(R"CIRCUIT( + H 0 + H_XY 1 + H_YZ 2 + H_NXY 3 + H_NXZ 4 + H_NYZ 5 + S_DAG 6 + X 7 + Y 8 + Z 9 + C_XYZ 10 + C_ZYX 11 + C_NXYZ 12 + C_XNYZ 13 + C_XYNZ 14 + C_NZYX 15 + C_ZNYX 16 + C_ZYNX 17 + SQRT_X 18 + SQRT_X_DAG 19 + SQRT_Y 20 + SQRT_Y_DAG 21 + S 22 + I 23 + )CIRCUIT"); + CliffordString s = CliffordString::from_circuit(circuit); + ASSERT_EQ(s.to_circuit(), circuit); +}) + +TEST_EACH_WORD_SIZE_W(clifford_string, known_identities, { + auto s1 = CliffordString::from_circuit(Circuit(R"CIRCUIT( + H 0 + )CIRCUIT")); + auto s2 = CliffordString::from_circuit(Circuit(R"CIRCUIT( + H 0 + )CIRCUIT")); + auto s3 = CliffordString::from_circuit(Circuit(R"CIRCUIT( + I 0 + )CIRCUIT")); + ASSERT_EQ(s2 * s1, s3); + + s1 = CliffordString::from_circuit(Circuit(R"CIRCUIT( + S 0 + )CIRCUIT")); + s2 = CliffordString::from_circuit(Circuit(R"CIRCUIT( + S 0 + )CIRCUIT")); + s3 = CliffordString::from_circuit(Circuit(R"CIRCUIT( + Z 0 + )CIRCUIT")); + ASSERT_EQ(s2 * s1, s3); + + s1 = CliffordString::from_circuit(Circuit(R"CIRCUIT( + S_DAG 0 + )CIRCUIT")); + s2 = CliffordString::from_circuit(Circuit(R"CIRCUIT( + H 0 + )CIRCUIT")); + s3 = CliffordString::from_circuit(Circuit(R"CIRCUIT( + C_XYZ 0 + )CIRCUIT")); + ASSERT_EQ(s2 * s1, s3); +})