From 269ec9e9ca0a6d208ea9c89818ac9a3d40229602 Mon Sep 17 00:00:00 2001 From: Craig Gidney Date: Mon, 13 Jul 2026 15:45:05 -0700 Subject: [PATCH 1/4] Measure initial performance --- src/stim/circuit/circuit_pybind_perf.py | 238 ++++++++++++++++++++++++ src/stim/circuit/circuit_pybind_test.py | 24 ++- 2 files changed, 260 insertions(+), 2 deletions(-) create mode 100644 src/stim/circuit/circuit_pybind_perf.py diff --git a/src/stim/circuit/circuit_pybind_perf.py b/src/stim/circuit/circuit_pybind_perf.py new file mode 100644 index 000000000..6568508d8 --- /dev/null +++ b/src/stim/circuit/circuit_pybind_perf.py @@ -0,0 +1,238 @@ +import math +import time +from typing import Callable, Iterable + +import stim + + +def si2(val: float) -> str: + """Describe quantity as an SI-prefixed value with two significant figures.""" + unit = ' ' + if val < 1: + if val < 1: + val *= 1000 + unit = 'm' + if val < 1: + val *= 1000 + unit = 'u' + if val < 1: + val *= 1000 + unit = 'n' + if val < 1: + val *= 1000 + unit = 'p' + else: + if val > 1000: + val /= 1000 + unit = 'k' + if val > 1000: + val /= 1000 + unit = 'M' + if val > 1000: + val /= 1000 + unit = 'G' + if val > 1000: + val /= 1000 + unit = 'T' + if 1 <= val < 10: + return f'''{math.floor(val)}.{math.floor(val * 10) % 10} {unit}''' + elif 10 <= val < 100: + return f''' {math.floor(val)} {unit}''' + elif 100 <= val < 1000: + return f'''{math.floor(val / 10) * 10} {unit}''' + else: + return f'''{val} {unit}''' + +BENCHMARK_CONFIG_TARGET_SECONDS = 0.5 + +class BenchmarkResult: + def __init__( + self, + *, + name: str | None, + total_seconds: float, + total_reps: int, + units: Iterable[tuple[str, float]], + goal_seconds: None | float, + ): + self.name = name + self.total_seconds = total_seconds + self.total_reps = total_reps + self.goal_seconds: float | None = goal_seconds + self.units: tuple[tuple[str, float], ...] = tuple(units) + + def __str__(self) -> str: + parts = [] + actual_seconds_per_rep = self.total_seconds / self.total_reps + if self.goal_seconds is not None: + deviation = round((math.log(self.goal_seconds) - math.log(actual_seconds_per_rep)) / (math.log(10) / 10.0)) + parts.append("[") + for k in range(-20, 21): + if (k < deviation and k < 0) or (k > deviation and k > 0): + parts.append('.') + elif k == deviation: + parts.append('*') + elif k == 0: + parts.append('|') + elif deviation < 0: + parts.append('<') + else: + parts.append('>') + parts.append("] ") + parts.append(si2(actual_seconds_per_rep)) + parts.append("s") + parts.append(" (vs ") + parts.append(si2(self.goal_seconds)) + parts.append("s) ") + else: + parts.append(si2(actual_seconds_per_rep)) + parts.append("s ") + for unit, multiplier in self.units: + parts.append("(") + parts.append(si2(self.total_reps / self.total_seconds * multiplier)) + parts.append(unit) + parts.append("/s) ") + if self.name is not None: + parts.append(self.name) + return ''.join(parts) + + +def benchmark_go( + body: Callable, + *, + target_wait_time_seconds: float = BENCHMARK_CONFIG_TARGET_SECONDS, +) -> BenchmarkResult: + """Benchmarks how long it takes to run a method. + + Args: + body: The method to time. + target_wait_time_seconds: How long to spend timing. + + Returns: + The number of shots completed and the amount of time spent. + """ + + total_reps: int = 0 + total_seconds: float = 0.0 + + rep_limit = 1 + while total_seconds < target_wait_time_seconds: + remaining_time: float = target_wait_time_seconds - total_seconds + reps: int = rep_limit + if total_seconds > 0: + reps = int(remaining_time * total_reps // total_seconds) + if reps < total_reps * 0.1: + break + if reps > rep_limit: + reps = rep_limit + if reps < 1: + reps = 1 + start_s = time.monotonic() + for _ in range(reps): + body() + end_s = time.monotonic() + dt_s = end_s - start_s + total_reps += reps + total_seconds += dt_s + rep_limit *= 100 + + return BenchmarkResult( + total_seconds=total_seconds, + total_reps=total_reps, + name=None, + units=(), + goal_seconds=None, + ) + + +_REGISTERED_BENCHMARKS = [] + +def benchmark( + original_method: Callable[[], BenchmarkResult] | None = None, + /, + *, + goal_nanos: float | None = None, + goal_micros: float | None = None, + goal_millis: float | None = None, + units: dict[str, float | int] | None = None, +): + """A decorator marking a method as a benchmark.""" + assert (goal_micros is not None) + (goal_millis is not None) + (goal_nanos is not None) <= 1, "Specified mulitple goal units." + goal_seconds: float | None = None + if goal_micros is not None: + goal_seconds = goal_micros * 1e-6 + if goal_millis is not None: + goal_seconds = goal_millis * 1e-3 + if goal_nanos is not None: + goal_seconds = goal_nanos * 1e-9 + + def wrap(method: Callable[[], BenchmarkResult], /): + def run(): + result = method() + return BenchmarkResult( + name=method.__name__, + total_reps=result.total_reps, + total_seconds=result.total_seconds, + units=() if units is None else units.items(), + goal_seconds=goal_seconds, + ) + _REGISTERED_BENCHMARKS.append(run) + return run + + if original_method is None: + return wrap + else: + return wrap(original_method) + + +@benchmark(goal_millis=8.4, units={"targets": 2000}) +def benchmark_circuit_append_int(): + c = stim.Circuit() + targets = [0, 1] * 1000 + + def run(): + c.clear() + c.append("CX", targets) + return benchmark_go(run) + + +@benchmark(goal_millis=6.5, units={"targets": 2000}) +def benchmark_circuit_append_gate_targets(): + c = stim.Circuit() + targets = [stim.GateTarget(0), stim.GateTarget(1)] * 1000 + + def run(): + c.clear() + c.append("CX", targets) + return benchmark_go(run) + + +@benchmark(goal_micros=84, units={"targets": 2000}) +def benchmark_circuit_append_pauli_strings(): + c = stim.Circuit() + targets = [stim.PauliString("XX")] * 1000 + + def run(): + c.clear() + c.append("MPP", targets) + return benchmark_go(run) + + +@benchmark(goal_micros=15, units={"targets": 2000}) +def benchmark_circuit_append_text(): + c = stim.Circuit() + content = "CX" + " 0 1" * 1000 + + def run(): + c.clear() + c.append_from_stim_program_text(content) + return benchmark_go(run) + + +def main(): + for e in _REGISTERED_BENCHMARKS: + print(e()) + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/src/stim/circuit/circuit_pybind_test.py b/src/stim/circuit/circuit_pybind_test.py index be2c05446..6cd2d6993 100644 --- a/src/stim/circuit/circuit_pybind_test.py +++ b/src/stim/circuit/circuit_pybind_test.py @@ -734,8 +734,6 @@ def test_shortest_graphlike_error_msgs(): """) with pytest.raises(ValueError, match=r"NO OBSERVABLES(.|\n)*NO DETECTORS"): c.shortest_graphlike_error() - with pytest.raises(ValueError, match=""): - c.shortest_graphlike_error() c = stim.Circuit(""" M 0 @@ -2415,3 +2413,25 @@ def test_append_circuit_to_circuit(): X 1 Z 2 """) + + +def test_append_odd_types(): + circuit = stim.Circuit() + circuit.append("CX", np.array([0, 1], dtype=np.uint8)) + circuit.append("H", np.array([2, 3], dtype=np.int16)) + circuit.append("MPP", ["X2", "Z3"]) + circuit.append("H", 5) + circuit.append("H", "6") + circuit.append("MPP", "Z3") + circuit.append("CX", ["rec[-1]", 3]) + circuit.append("MPP", stim.PauliString("XYZ")) + circuit.append("MPP", [stim.PauliString("XX")]) + assert circuit == stim.Circuit(""" + CX 0 1 + H 2 3 + MPP X2 Z3 + H 5 6 + MPP Z3 + CX rec[-1] 3 + MPP X0*Y1*Z2 X0*X1 + """) From 3977ba87dba5a1bf2d5ca705b88abe0c559e7278 Mon Sep 17 00:00:00 2001 From: Craig Gidney Date: Mon, 13 Jul 2026 15:44:04 -0700 Subject: [PATCH 2/4] Improve performance --- src/stim/circuit/circuit.pybind.cc | 92 ++++++++++++------------- src/stim/circuit/circuit_pybind_perf.py | 6 +- 2 files changed, 48 insertions(+), 50 deletions(-) diff --git a/src/stim/circuit/circuit.pybind.cc b/src/stim/circuit/circuit.pybind.cc index c1b3a0d2a..5da48ac90 100644 --- a/src/stim/circuit/circuit.pybind.cc +++ b/src/stim/circuit/circuit.pybind.cc @@ -150,59 +150,57 @@ 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); +template +static void handle_to_gate_targets_helper(const T &iterable_obj, std::vector &out, bool can_iterate) { + pybind11::detail::make_caster caster_gate_target; + pybind11::detail::make_caster caster_u32; + pybind11::detail::make_caster caster_string_view; + pybind11::detail::make_caster caster_pauli_string; + + for (const pybind11::handle &obj : iterable_obj) { + if (caster_u32.load(obj, true)) { + uint32_t value = caster_u32; + // Note: for backwards compatibility, it isn't checked that the u32 actually + // corresponds to a qubit target. + out.push_back(GateTarget{value}.data); + + } else if (caster_gate_target.load(obj, false)) { + GateTarget value = caster_gate_target; + out.push_back(value.data); + + } else if (caster_string_view.load(obj, false)) { + std::string_view text = pybind11::cast(obj); + out.push_back(GateTarget::from_target_str(text).data); + + } else if (caster_pauli_string.load(obj, false)) { + 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"); } - 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) { - } + } else if (can_iterate && pybind11::isinstance(obj, pybind11::module::import("collections.abc").attr("Iterable"))) { + handle_to_gate_targets_helper(obj, out, false); - 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; + } else { + 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()); } } +} - 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 handle_to_gate_targets(const pybind11::handle &obj, std::vector &out, bool can_iterate) { + handle_to_gate_targets_helper(std::span{&obj, 1}, out, can_iterate); } void circuit_append( diff --git a/src/stim/circuit/circuit_pybind_perf.py b/src/stim/circuit/circuit_pybind_perf.py index 6568508d8..64c7caa23 100644 --- a/src/stim/circuit/circuit_pybind_perf.py +++ b/src/stim/circuit/circuit_pybind_perf.py @@ -185,7 +185,7 @@ def run(): return wrap(original_method) -@benchmark(goal_millis=8.4, units={"targets": 2000}) +@benchmark(goal_micros=27, units={"targets": 2000}) def benchmark_circuit_append_int(): c = stim.Circuit() targets = [0, 1] * 1000 @@ -196,7 +196,7 @@ def run(): return benchmark_go(run) -@benchmark(goal_millis=6.5, units={"targets": 2000}) +@benchmark(goal_micros=100, units={"targets": 2000}) def benchmark_circuit_append_gate_targets(): c = stim.Circuit() targets = [stim.GateTarget(0), stim.GateTarget(1)] * 1000 @@ -207,7 +207,7 @@ def run(): return benchmark_go(run) -@benchmark(goal_micros=84, units={"targets": 2000}) +@benchmark(goal_micros=370, units={"targets": 2000}) def benchmark_circuit_append_pauli_strings(): c = stim.Circuit() targets = [stim.PauliString("XX")] * 1000 From 0c8c3d59dfd7b28e21818b819be91856efed3b98 Mon Sep 17 00:00:00 2001 From: Craig Gidney Date: Tue, 14 Jul 2026 13:57:08 -0700 Subject: [PATCH 3/4] - Use same-type hinting when looping - Special case list/tuple arguments - Improve double conversion as well --- src/stim/circuit/circuit.pybind.cc | 106 +++++++++++++++--------- src/stim/circuit/circuit_pybind_perf.py | 36 ++++++-- 2 files changed, 97 insertions(+), 45 deletions(-) diff --git a/src/stim/circuit/circuit.pybind.cc b/src/stim/circuit/circuit.pybind.cc index 5da48ac90..83b438595 100644 --- a/src/stim/circuit/circuit.pybind.cc +++ b/src/stim/circuit/circuit.pybind.cc @@ -150,30 +150,46 @@ void circuit_insert(Circuit &self, pybind11::ssize_t &index, pybind11::object &o } } -template -static void handle_to_gate_targets_helper(const T &iterable_obj, std::vector &out, bool can_iterate) { - pybind11::detail::make_caster caster_gate_target; - pybind11::detail::make_caster caster_u32; - pybind11::detail::make_caster caster_string_view; - pybind11::detail::make_caster caster_pauli_string; - - for (const pybind11::handle &obj : iterable_obj) { - if (caster_u32.load(obj, true)) { +template +static void handle_to_gate_targets_helper(T &it, const T2 &end, std::vector &out, bool can_iterate) { + while (it != end) { + bool progress = false; + + pybind11::detail::make_caster caster_u32; + while (caster_u32.load(*it, true)) { uint32_t value = caster_u32; // Note: for backwards compatibility, it isn't checked that the u32 actually // corresponds to a qubit target. out.push_back(GateTarget{value}.data); + ++it; + if (it == end) { + return; + } + progress = true; + } - } else if (caster_gate_target.load(obj, false)) { + pybind11::detail::make_caster caster_gate_target; + while (caster_gate_target.load(*it, false)) { GateTarget value = caster_gate_target; out.push_back(value.data); + ++it; + if (it == end) { + return; + } + progress = true; + } - } else if (caster_string_view.load(obj, false)) { - std::string_view text = pybind11::cast(obj); + pybind11::detail::make_caster caster_string_view; + while (it != end && caster_string_view.load(*it, false)) { + std::string_view text = caster_string_view; out.push_back(GateTarget::from_target_str(text).data); + ++it; + progress = true; + } - } else if (caster_pauli_string.load(obj, false)) { - FlexPauliString ps = pybind11::cast(obj); + pybind11::detail::make_caster caster_pauli_string; + while (caster_pauli_string.load(*it, false)) { + FlexPauliString ps = caster_pauli_string; bool first = true; ps.value.ref().for_each_active_pauli([&](size_t q) { if (!first) { @@ -185,14 +201,27 @@ static void handle_to_gate_targets_helper(const T &iterable_obj, std::vector(pybind11::repr(obj)); + ss << pybind11::cast(pybind11::repr(*it)); ss << "`."; throw std::invalid_argument(ss.str()); } @@ -200,7 +229,15 @@ static void handle_to_gate_targets_helper(const T &iterable_obj, std::vector &out, bool can_iterate) { - handle_to_gate_targets_helper(std::span{&obj, 1}, out, can_iterate); + if (pybind11::isinstance(obj) || pybind11::isinstance(obj)) { + // Fast path for appending items in a list or tuple. + auto it = pybind11::iter(obj); + handle_to_gate_targets_helper(it, pybind11::iterator::sentinel(), out, false); + } else { + auto span = std::span{&obj, 1}; + auto it = span.begin(); + handle_to_gate_targets_helper(it, span.end(), out, can_iterate); + } } void circuit_append( @@ -218,29 +255,20 @@ void circuit_append( std::string_view gate_name = pybind11::cast(obj); // Maintain backwards compatibility to when there was always exactly one argument. - pybind11::object used_arg; - if (!arg.is_none()) { - used_arg = arg; - } else if (backwards_compat && GATE_DATA.at(gate_name).arg_count == 1) { - used_arg = pybind11::make_tuple(0.0); + std::vector converted_args; + if (arg.is_none()) { + if (backwards_compat && GATE_DATA.at(gate_name).arg_count == 1) { + converted_args.push_back(0); + } + } else if (pybind11::detail::make_caster caster_double; caster_double.load(arg, true)) { + converted_args.push_back(caster_double); + } else if (pybind11::detail::make_caster> caster_vec_double; caster_vec_double.load(arg, true)) { + converted_args = caster_vec_double; } else { - used_arg = pybind11::make_tuple(); + throw std::invalid_argument("Arg must be a double or sequence of doubles."); } - // Extract single argument or list of arguments. - try { - auto d = pybind11::cast(used_arg); - self.safe_append_ua(gate_name, raw_targets, d, tag); - return; - } catch (const pybind11::cast_error &) { - } - try { - auto args = pybind11::cast>(used_arg); - self.safe_append_u(gate_name, raw_targets, args, tag); - return; - } catch (const pybind11::cast_error &) { - } - throw std::invalid_argument("Arg must be a double or sequence of doubles."); + self.safe_append_u(gate_name, raw_targets, converted_args, tag); } else if (pybind11::isinstance(obj)) { if (!raw_targets.empty() || !arg.is_none() || !tag.empty()) { throw std::invalid_argument( diff --git a/src/stim/circuit/circuit_pybind_perf.py b/src/stim/circuit/circuit_pybind_perf.py index 64c7caa23..f015c476f 100644 --- a/src/stim/circuit/circuit_pybind_perf.py +++ b/src/stim/circuit/circuit_pybind_perf.py @@ -186,7 +186,7 @@ def run(): @benchmark(goal_micros=27, units={"targets": 2000}) -def benchmark_circuit_append_int(): +def benchmark_circuit_append_int_in_large_chunks(): c = stim.Circuit() targets = [0, 1] * 1000 @@ -196,8 +196,20 @@ def run(): return benchmark_go(run) -@benchmark(goal_micros=100, units={"targets": 2000}) -def benchmark_circuit_append_gate_targets(): +@benchmark(goal_micros=320, units={"targets": 2000}) +def benchmark_circuit_append_int_in_small_chunks(): + c = stim.Circuit() + targets = [0, 1] + + def run(): + c.clear() + for _ in range(1000): + c.append("CX", targets) + return benchmark_go(run) + + +@benchmark(goal_micros=25, units={"targets": 2000}) +def benchmark_circuit_append_gate_targets_in_large_chunks(): c = stim.Circuit() targets = [stim.GateTarget(0), stim.GateTarget(1)] * 1000 @@ -207,8 +219,8 @@ def run(): return benchmark_go(run) -@benchmark(goal_micros=370, units={"targets": 2000}) -def benchmark_circuit_append_pauli_strings(): +@benchmark(goal_micros=46, units={"targets": 2000}) +def benchmark_circuit_append_pauli_strings_in_large_chunks(): c = stim.Circuit() targets = [stim.PauliString("XX")] * 1000 @@ -219,7 +231,7 @@ def run(): @benchmark(goal_micros=15, units={"targets": 2000}) -def benchmark_circuit_append_text(): +def benchmark_circuit_append_text_in_large_chunks(): c = stim.Circuit() content = "CX" + " 0 1" * 1000 @@ -229,6 +241,18 @@ def run(): return benchmark_go(run) +@benchmark(goal_micros=190, units={"targets": 2000}) +def benchmark_circuit_append_text_in_small_chunks(): + c = stim.Circuit() + content = "CX 0 1" + + def run(): + c.clear() + for _ in range(1000): + c.append_from_stim_program_text(content) + return benchmark_go(run) + + def main(): for e in _REGISTERED_BENCHMARKS: print(e()) From 016d8b7fe20de00bbcfe350c8d24671004e4769f Mon Sep 17 00:00:00 2001 From: Craig Gidney Date: Tue, 14 Jul 2026 15:22:58 -0700 Subject: [PATCH 4/4] typos --- src/stim/circuit/circuit.pybind.cc | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/stim/circuit/circuit.pybind.cc b/src/stim/circuit/circuit.pybind.cc index 83b438595..d0731737d 100644 --- a/src/stim/circuit/circuit.pybind.cc +++ b/src/stim/circuit/circuit.pybind.cc @@ -158,8 +158,10 @@ static void handle_to_gate_targets_helper(T &it, const T2 &end, std::vector caster_u32; while (caster_u32.load(*it, true)) { uint32_t value = caster_u32; - // Note: for backwards compatibility, it isn't checked that the u32 actually - // corresponds to a qubit target. + // Note: for backwards compatibility, it isn't checked that the u32 + // corresponds to a qubit target rather than e.g. a measurement record. + // This means the GateTarget can be malformed in various ways, which + // must be caught by later validation (e.g. CircuitInstruction::validate). out.push_back(GateTarget{value}.data); ++it; if (it == end) { @@ -180,10 +182,13 @@ static void handle_to_gate_targets_helper(T &it, const T2 &end, std::vector caster_string_view; - while (it != end && caster_string_view.load(*it, false)) { + while (caster_string_view.load(*it, false)) { std::string_view text = caster_string_view; out.push_back(GateTarget::from_target_str(text).data); ++it; + if (it == end) { + return; + } progress = true; }