diff --git a/src/stim/circuit/circuit.pybind.cc b/src/stim/circuit/circuit.pybind.cc index c1b3a0d2a..d0731737d 100644 --- a/src/stim/circuit/circuit.pybind.cc +++ b/src/stim/circuit/circuit.pybind.cc @@ -150,59 +150,99 @@ 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(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 + // 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) { + return; } - 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"); + progress = true; } - 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) { - } + 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; + } - try { - out.push_back(pybind11::cast(obj).data); - return; - } catch (const pybind11::cast_error &ex) { - } + pybind11::detail::make_caster caster_string_view; + 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; + } - try { - out.push_back(GateTarget{pybind11::cast(obj)}.data); - return; - } catch (const pybind11::cast_error &ex) { - } + 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) { + 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"); + } + ++it; + if (it == end) { + return; + } + progress = true; + } - 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); + if (!progress && can_iterate && pybind11::isinstance(*it, pybind11::module::import("collections.abc").attr("Iterable"))) { + auto it2 = pybind11::iter(*it); + handle_to_gate_targets_helper(it2, pybind11::iterator::sentinel(), out, false); + ++it; + if (it == end) { + return; } - return; + progress = true; + } + + if (!progress) { + std::stringstream ss; + ss << "Don't know how to target the object `"; + ss << pybind11::cast(pybind11::repr(*it)); + 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) { + 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( @@ -220,29 +260,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 new file mode 100644 index 000000000..f015c476f --- /dev/null +++ b/src/stim/circuit/circuit_pybind_perf.py @@ -0,0 +1,262 @@ +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_micros=27, units={"targets": 2000}) +def benchmark_circuit_append_int_in_large_chunks(): + c = stim.Circuit() + targets = [0, 1] * 1000 + + def run(): + c.clear() + c.append("CX", targets) + return benchmark_go(run) + + +@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 + + def run(): + c.clear() + c.append("CX", targets) + return benchmark_go(run) + + +@benchmark(goal_micros=46, units={"targets": 2000}) +def benchmark_circuit_append_pauli_strings_in_large_chunks(): + 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_in_large_chunks(): + c = stim.Circuit() + content = "CX" + " 0 1" * 1000 + + def run(): + c.clear() + c.append_from_stim_program_text(content) + 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()) + + +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 + """)