diff --git a/qualtran/_infra/registers.py b/qualtran/_infra/registers.py index a6a90f1fd5..e62e713110 100644 --- a/qualtran/_infra/registers.py +++ b/qualtran/_infra/registers.py @@ -188,9 +188,11 @@ def n_qubits(self) -> int: is taken to be the greater of the number of left or right qubits. A bloq with this signature uses at least this many qubits. """ + from qualtran.resource_counting.symbolic_counting_utils import smax + left_size = sum(reg.total_bits() for reg in self.lefts()) right_size = sum(reg.total_bits() for reg in self.rights()) - return max(left_size, right_size) + return smax(left_size, right_size) def __repr__(self): return f'Signature({repr(self._registers)})' diff --git a/qualtran/_infra/registers_test.py b/qualtran/_infra/registers_test.py index c9394c3370..cdf63ed85b 100644 --- a/qualtran/_infra/registers_test.py +++ b/qualtran/_infra/registers_test.py @@ -15,6 +15,7 @@ import cirq import numpy as np import pytest +import sympy from qualtran import BoundedQUInt, QAny, QBit, QInt, Register, Side, Signature from qualtran._infra.gate_with_registers import get_named_qubits @@ -196,3 +197,12 @@ def test_dtypes_converter(): r2 = Register("my_reg", QAny(5)) r2 = Register("my_reg", QInt(5)) assert r1 != r2 + + +def test_symbolic_reg(): + n = sympy.Symbol('n', positive=True, integer=True) + sig = Signature( + [Register('x', QAny(n), side=Side.LEFT), Register('y', QAny(2 * n), side=Side.RIGHT)] + ) + + assert sig.n_qubits() == 2 * n diff --git a/qualtran/bloqs/basic_gates/hadamard.py b/qualtran/bloqs/basic_gates/hadamard.py index 06855aa08f..5ae1d30d5f 100644 --- a/qualtran/bloqs/basic_gates/hadamard.py +++ b/qualtran/bloqs/basic_gates/hadamard.py @@ -98,6 +98,9 @@ def short_name(self) -> 'str': def wire_symbol(self, soq: 'Soquet') -> 'WireSymbol': return TextBox('H') + def __str__(self): + return 'H' + @bloq_example def _hadamard() -> Hadamard: diff --git a/qualtran/bloqs/basic_gates/on_each_test.py b/qualtran/bloqs/basic_gates/on_each_test.py index 6782a24ad9..7ae02d5013 100644 --- a/qualtran/bloqs/basic_gates/on_each_test.py +++ b/qualtran/bloqs/basic_gates/on_each_test.py @@ -42,8 +42,7 @@ def test_classical_simulation(): h_on_each = OnEach(10, Hadamard()) with pytest.raises( NotImplementedError, - match=r'.*does not support classical simulation: ' - r'Hadamard\(\) is not classically simulable\.', + match=r'.*does not support classical simulation: ' r'H is not classically simulable\.', ): h_on_each.call_classically(q=0) diff --git a/qualtran/bloqs/basic_gates/t_gate.py b/qualtran/bloqs/basic_gates/t_gate.py index 2844d3cf76..4d12cdadec 100644 --- a/qualtran/bloqs/basic_gates/t_gate.py +++ b/qualtran/bloqs/basic_gates/t_gate.py @@ -108,8 +108,8 @@ def pretty_name(self) -> str: return f'T{maybe_dag}' def __str__(self): - maybe_dag = 'is_adjoint=True' if self.is_adjoint else '' - return f'TGate({maybe_dag})' + maybe_dag = '†' if self.is_adjoint else '' + return f'T{maybe_dag}' def wire_symbol(self, soq: 'Soquet') -> 'WireSymbol': return TextBox(self.pretty_name()) diff --git a/qualtran/bloqs/basic_gates/toffoli.py b/qualtran/bloqs/basic_gates/toffoli.py index ff754c335f..f0de4c2091 100644 --- a/qualtran/bloqs/basic_gates/toffoli.py +++ b/qualtran/bloqs/basic_gates/toffoli.py @@ -121,6 +121,9 @@ def wire_symbol(self, soq: 'Soquet') -> 'WireSymbol': return ModPlus() raise ValueError(f'Bad wire symbol soquet: {soq}') + def __str__(self): + return 'Toffoli' + @bloq_example def _toffoli() -> Toffoli: diff --git a/qualtran/bloqs/chemistry/resource_estimation.ipynb b/qualtran/bloqs/chemistry/resource_estimation.ipynb index 8a70c1aa35..5d44dd7c0e 100644 --- a/qualtran/bloqs/chemistry/resource_estimation.ipynb +++ b/qualtran/bloqs/chemistry/resource_estimation.ipynb @@ -133,7 +133,6 @@ "from qualtran.drawing.musical_score import get_musical_score_data, draw_musical_score\n", "msd = get_musical_score_data(block_encoding_bloq.decompose_bloq())\n", "fig, ax = draw_musical_score(msd)\n", - "plt.tick_params(left=False, right=False, labelleft=False, labelbottom=False, bottom=False)\n", "fig.set_size_inches(8, 4)" ] }, @@ -185,6 +184,47 @@ "print(f'qualtran = {num_toff} vs. ref = 10880, delta = {num_toff - 10880}')" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "e79d3c99-cd23-4333-a177-6d6ab3dca72a", + "metadata": {}, + "outputs": [], + "source": [ + "# qualtran = 26749.0 vs. ref = 10880, delta = 15869.0" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c61a4b30-b875-4414-b198-e08774df0c4a", + "metadata": {}, + "outputs": [], + "source": [ + "from qualtran.resource_counting import BloqCount, query_costs, get_cost_value, QECGatesCost\n", + "from qualtran.resource_counting.generalizers import ignore_alloc_free, ignore_split_join, generalize_rotation_angle" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a126c934-1528-425a-aa4d-93a4bb880236", + "metadata": {}, + "outputs": [], + "source": [ + "get_cost_value(block_encoding_bloq, BloqCount.for_gateset(\"t+tof+cswap\"))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e68450ff-d582-400f-abd1-f3d24dd43979", + "metadata": {}, + "outputs": [], + "source": [ + "46976/4 + 30480/4 + 7105 + 280" + ] + }, { "cell_type": "markdown", "id": "dbd1615f", diff --git a/qualtran/bloqs/data_loading/qrom.py b/qualtran/bloqs/data_loading/qrom.py index 10b1eab0cb..4996d4da59 100644 --- a/qualtran/bloqs/data_loading/qrom.py +++ b/qualtran/bloqs/data_loading/qrom.py @@ -15,7 +15,7 @@ """Quantum read-only memory.""" from functools import cached_property -from typing import Callable, Dict, Iterable, Sequence, Set, Tuple +from typing import Any, Callable, Dict, Iterable, Sequence, Set, Tuple, Union import attrs import cirq @@ -28,7 +28,7 @@ from qualtran.bloqs.mcmt.and_bloq import And, MultiAnd from qualtran.bloqs.multiplexers.unary_iteration_bloq import UnaryIterationGate from qualtran.drawing import Circle, TextBox, WireSymbol -from qualtran.resource_counting import BloqCountT +from qualtran.resource_counting import BloqCountT, CostKey, QubitCount from qualtran.simulation.classical_sim import ClassicalValT @@ -203,6 +203,11 @@ def on_classical_vals(self, **vals: 'ClassicalValT') -> Dict[str, 'ClassicalValT targets = {k: v ^ vals[k] for k, v in targets.items()} return controls | selections | targets + def my_static_costs(self, cost_key: 'CostKey') -> Union[Any, NotImplemented]: + if cost_key == QubitCount(): + return self.num_controls + 2 * sum(self.selection_bitsizes) + sum(self.target_bitsizes) + return super().my_static_costs(cost_key) + def _circuit_diagram_info_(self, _) -> cirq.CircuitDiagramInfo: wire_symbols = ["@"] * self.num_controls wire_symbols += ["In"] * total_bits(self.selection_registers) @@ -240,6 +245,9 @@ def nth_operation_callgraph(self, **kwargs: int) -> Set['BloqCountT']: selection_idx = tuple(kwargs[reg.name] for reg in self.selection_registers) return {(CNOT(), sum(int(d[selection_idx]).bit_count() for d in self.data))} + def __str__(self): + return 'QROM' + @bloq_example def _qrom_small() -> QROM: diff --git a/qualtran/bloqs/data_loading/select_swap_qrom.py b/qualtran/bloqs/data_loading/select_swap_qrom.py index fbc95c1801..d7d927adbe 100644 --- a/qualtran/bloqs/data_loading/select_swap_qrom.py +++ b/qualtran/bloqs/data_loading/select_swap_qrom.py @@ -254,3 +254,6 @@ def short_name(self) -> str: def _value_equality_values_(self): return self.block_size, self._target_bitsizes, self.data + + def __str__(self): + return 'SelectSwapQROM' diff --git a/qualtran/bloqs/for_testing/costing_test.py b/qualtran/bloqs/for_testing/costing_test.py index b6b43598b2..e7dc511fd6 100644 --- a/qualtran/bloqs/for_testing/costing_test.py +++ b/qualtran/bloqs/for_testing/costing_test.py @@ -24,9 +24,9 @@ def test_costing_bloqs(): == """\ Algo -- 1 -> Func1 Algo -- 1 -> Func2 -Func1 -- 10 -> Hadamard() -Func1 -- 10 -> TGate() -Func1 -- 10 -> TGate(is_adjoint=True) -Func2 -- 100 -> Toffoli() -Toffoli() -- 4 -> TGate()""" +Func1 -- 10 -> H +Func1 -- 10 -> T +Func1 -- 10 -> T† +Func2 -- 100 -> Toffoli +Toffoli -- 4 -> T""" ) diff --git a/qualtran/bloqs/mcmt/and_bloq.py b/qualtran/bloqs/mcmt/and_bloq.py index 4827d8ee04..6f71261bf4 100644 --- a/qualtran/bloqs/mcmt/and_bloq.py +++ b/qualtran/bloqs/mcmt/and_bloq.py @@ -215,6 +215,13 @@ def _t_complexity_(self) -> TComplexity: else: return TComplexity(t=4 * 1, clifford=9 + 2 * pre_post_cliffords) + def __str__(self): + dag = '†' if self.uncompute else '' + + if self.cv1 == 0 or self.cv2 == 0: + return f'And{dag}_{self.cv1}{self.cv2}' + return f'And{dag}' + @bloq_example( generalizer=[cirq_to_bloqs, ignore_cliffords, ignore_alloc_free, generalize_rotation_angle] diff --git a/qualtran/bloqs/multiplexers/selected_majorana_fermion.py b/qualtran/bloqs/multiplexers/selected_majorana_fermion.py index 4f43766a58..17126fa2ec 100644 --- a/qualtran/bloqs/multiplexers/selected_majorana_fermion.py +++ b/qualtran/bloqs/multiplexers/selected_majorana_fermion.py @@ -13,7 +13,7 @@ # limitations under the License. from functools import cached_property -from typing import Sequence, Tuple, Union +from typing import Any, Sequence, Tuple, Union import attrs import cirq @@ -24,6 +24,7 @@ from qualtran._infra.data_types import BoundedQUInt from qualtran._infra.gate_with_registers import total_bits from qualtran.bloqs.multiplexers.unary_iteration_bloq import UnaryIterationGate +from qualtran.resource_counting import CostKey @attrs.frozen @@ -120,3 +121,10 @@ def nth_operation( # type: ignore[override] yield cirq.CNOT(control, *accumulator) yield self.target_gate(target[target_idx]).controlled_by(control) yield cirq.CZ(*accumulator, target[target_idx]) + + def my_static_costs(self, cost_key: 'CostKey') -> Union[Any, NotImplemented]: + from qualtran.resource_counting import QubitCount + + if isinstance(cost_key, QubitCount): + return self.signature.n_qubits() + return super().my_static_costs(cost_key) diff --git a/qualtran/drawing/bloq_counts_graph_test.py b/qualtran/drawing/bloq_counts_graph_test.py index c46d725ed4..466160dbbc 100644 --- a/qualtran/drawing/bloq_counts_graph_test.py +++ b/qualtran/drawing/bloq_counts_graph_test.py @@ -35,7 +35,7 @@ def test_format_counts_sigma(): == """\ #### Counts totals: - `ArbitraryClifford(n=2)`: 45 - - `TGate()`: 20""" + - `T`: 20""" ) @@ -46,10 +46,10 @@ def test_format_counts_graph_markdown(): ret == """\ - `MultiAnd(cvs=(1, 1, 1, 1, 1, 1))` - - `And(cv1=1, cv2=1, uncompute=False)`: $\\displaystyle 5$ - - `And(cv1=1, cv2=1, uncompute=False)` + - `And`: $\\displaystyle 5$ + - `And` - `ArbitraryClifford(n=2)`: $\\displaystyle 9$ - - `TGate()`: $\\displaystyle 4$ + - `T`: $\\displaystyle 4$ """ ) diff --git a/qualtran/drawing/flame_graph.py b/qualtran/drawing/flame_graph.py index 0366fd8e88..2c9a267ca8 100644 --- a/qualtran/drawing/flame_graph.py +++ b/qualtran/drawing/flame_graph.py @@ -58,7 +58,7 @@ def _pretty_name(bloq: Bloq) -> str: @functools.lru_cache(maxsize=1024) def _t_counts_for_bloq(bloq: Bloq, graph: nx.DiGraph) -> Union[int, sympy.Expr]: - sigma = _compute_sigma(bloq, graph) + sigma = _compute_sigma(bloq, graph, generalizer=lambda b: b) return t_counts_from_sigma(sigma) diff --git a/qualtran/resource_counting/_call_graph.py b/qualtran/resource_counting/_call_graph.py index 864310c411..4e56bff2f7 100644 --- a/qualtran/resource_counting/_call_graph.py +++ b/qualtran/resource_counting/_call_graph.py @@ -168,27 +168,14 @@ def _build_call_graph( g.add_edge(bloq, callee, n=n) -def _compute_sigma(root_bloq: Bloq, g: nx.DiGraph) -> Dict[Bloq, Union[int, sympy.Expr]]: - """Iterate over nodes to sum up the counts of leaf bloqs.""" - bloq_sigmas: Dict[Bloq, Dict[Bloq, Union[int, sympy.Expr]]] = defaultdict( - lambda: defaultdict(lambda: 0) - ) - for bloq in reversed(list(nx.topological_sort(g))): - callees = list(g.successors(bloq)) - sigma = bloq_sigmas[bloq] - if not callees: - # 1. `bloq` is a leaf node. Its count is one of itself. - sigma[bloq] = 1 - continue - - for callee in callees: - callee_sigma = bloq_sigmas[callee] - # 2. Otherwise, sigma of the caller is sum(n * sigma of callee) for all the callees. - n = g.edges[bloq, callee]['n'] - for k in callee_sigma.keys(): - sigma[k] += callee_sigma[k] * n +def _compute_sigma( + root_bloq: Bloq, g: nx.DiGraph, generalizer: 'GeneralizerT' +) -> Dict[Bloq, Union[int, sympy.Expr]]: + """Shim for compatibility with old 'sigma' that used the call graph to count leaf bloqs.""" + from qualtran.resource_counting import BloqCount, get_cost_value - return dict(bloq_sigmas[root_bloq]) + leaf_counts = BloqCount.for_call_graph_leaf_bloqs(g) + return get_cost_value(root_bloq, leaf_counts, generalizer=generalizer) def get_bloq_call_graph( @@ -236,7 +223,7 @@ def get_bloq_call_graph( if bloq is None: raise ValueError("You can't generalize away the root bloq.") _build_call_graph(bloq, generalizer, ssa, keep, max_depth, g=g, depth=0) - sigma = _compute_sigma(bloq, g) + sigma = _compute_sigma(bloq, g, generalizer) return g, sigma diff --git a/qualtran/resource_counting/costs.ipynb b/qualtran/resource_counting/costs.ipynb new file mode 100644 index 0000000000..c1de1e6c76 --- /dev/null +++ b/qualtran/resource_counting/costs.ipynb @@ -0,0 +1,200 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d4d7bfac-b2c7-414e-921e-92da858fe216", + "metadata": {}, + "source": [ + "# Costs\n", + "\n", + "We cannot (yet) run the large ('fault-tolerant scale') quantum algorithms expressed in Qualtran. Instead, a major research goal is to estimate the resources required to implement interesting quantum algorithms. Resources can be time, qubits, T gates, precision, depth, or any number of things. Throughout Qualtran, we use the less ambiguous term \"costs\" to represent these quantities.\n", + "\n", + "In the research literature, there is a surface level agreement on what costs to count: Often gate counts of T, Toffoli, Clifford, and Rotation; as well as the number of qubits are tabulated as the most probable limiting factors of implementing these algorithms on real quantum computers. When it comes to details, agreement on costs is not guaranteed. Researchers may want to consider additional target gatesets or architectures; disagree on how to lump or count cliffords of various sizes; and treat rotations in a variety of ways.\n", + "\n", + "In Qualtran, we provide a set of configurable `CostKey`s that can be used to query algorithms expressed as bloqs. Developers can also implement their own costs by overriding `CostKey` to provide even more customization." + ] + }, + { + "cell_type": "markdown", + "id": "42306c58-584f-4b85-89e7-9a64a1398a64", + "metadata": {}, + "source": [ + "## Getting costs\n", + "\n", + "The following functions can be used to query costs for a bloq. Each takes a bloq and a cost key:\n", + "\n", + " - `get_cost_value`\n", + " - `get_cost_cache`\n", + "\n", + "The former will return a single value whereas the latter will return a dictionary mapping every bloq for which a cost was computed to its value.\n", + "\n", + "Before we start, we'll create an example bloq with enough complexity to be interesting:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0e4801d3-2e55-4c0a-a113-5d6c9263f0e4", + "metadata": {}, + "outputs": [], + "source": [ + "# Construct a PrepTHC bloq for demonstration purposes.\n", + "\n", + "import numpy as np\n", + "from qualtran.bloqs.chemistry.thc import PrepareTHC\n", + "\n", + "\n", + "num_spin_orb = 25\n", + "num_mu = 100\n", + "t_l = np.random.normal(0, 1, size=num_spin_orb//2)\n", + "zeta = np.random.normal(0, 1, size=(num_mu, num_mu))\n", + "zeta = 0.5 * (zeta + zeta.T)\n", + "\n", + "prep_thc = PrepareTHC.from_hamiltonian_coeffs(t_l, zeta, num_bits_state_prep=10)" + ] + }, + { + "cell_type": "markdown", + "id": "b0023aa8-7cfe-4f22-8625-e6bbe509c721", + "metadata": {}, + "source": [ + "`get_cost_value` takes a `Bloq` and a `CostKey` and returns a value. The type of value depends on the particular cost key. For example, if we ask for the qubit count by using the `QubitCount` cost key, we get an integer." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "600e6e48-e888-4021-aecc-3fbc6b26ab6a", + "metadata": {}, + "outputs": [], + "source": [ + "from qualtran.resource_counting import get_cost_value, QubitCount\n", + "\n", + "get_cost_value(prep_thc, QubitCount())" + ] + }, + { + "cell_type": "markdown", + "id": "94920338-1145-4f81-8b30-c306c44f470a", + "metadata": {}, + "source": [ + "`get_cost_cache` has the same input arguments, but instead returns a dictionary containing the (sub-)costs for each bloq that was encountered during the recursive computation. In the following demo, we only print the top 5 entries to avoid overwhelming the output." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c077411d-8f4f-4aa1-b5a4-c3e8fca90e69", + "metadata": {}, + "outputs": [], + "source": [ + "from qualtran.resource_counting import get_cost_cache\n", + "\n", + "cost_cache = get_cost_cache(prep_thc, QubitCount())\n", + "print(f'cost_cache contains {len(cost_cache)} entries. Displaying top five.\\n')\n", + "\n", + "top_five = sorted(cost_cache.items(), key=lambda x: x[1])[-5:]\n", + "for bloq, val in top_five:\n", + " print(f'{bloq}: {val}')" + ] + }, + { + "cell_type": "markdown", + "id": "dd7c069d-3820-4a1b-95b4-326dbda87fce", + "metadata": {}, + "source": [ + "## Configurable Cost Keys\n", + "\n", + "The behavior of a cost computation can be modified by arguments to the cost key. The `QubitCount()` cost is simple: it has no parameters and the value of the cost is a simple integer. `QECGatesCost` returns a richer cost value type and has more options for controlling the cost query." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e1963547-c294-4a0f-bb57-5e2239968eb4", + "metadata": {}, + "outputs": [], + "source": [ + "from qualtran.resource_counting import QECGatesCost\n", + "\n", + "get_cost_value(prep_thc, QECGatesCost())" + ] + }, + { + "cell_type": "markdown", + "id": "68bfe8db-e5f2-41ac-bf7d-26f38022eb67", + "metadata": {}, + "source": [ + "Here, we can see that the output is a `GateCounts` data class that tallys up the number of T, Toffoli, CSwap (aka Fredkin), AND, and Clifford Gates. We can provide additional configuration to count things differently." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9ff68d5f-5a78-4c46-ac69-eb738c083af5", + "metadata": {}, + "outputs": [], + "source": [ + "t_cost = QECGatesCost(ts_per_toffoli=4, ts_per_cswap=7, ts_per_and=4)\n", + "get_cost_value(prep_thc, QECGatesCost(ts_per_toffoli=4, ts_per_cswap=7, ts_per_and=4))" + ] + }, + { + "cell_type": "markdown", + "id": "ecfbf912-fae9-4dd9-a6d3-a8bf1532e084", + "metadata": {}, + "source": [ + "Now, the result is entirely in terms of T gate counts." + ] + }, + { + "cell_type": "markdown", + "id": "a375c726-4a97-4640-9ced-d7a1bcb700a4", + "metadata": {}, + "source": [ + "## Querying Multiple Costs\n", + "\n", + "The `query_costs` function will get multiple costs for multiple bloqs simultaneously. Its output is suitable for annotating a call graph diagram" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20a95e27-9b69-459f-9f39-56a98ec3ad67", + "metadata": {}, + "outputs": [], + "source": [ + "from qualtran.resource_counting import query_costs\n", + "\n", + "from qualtran.drawing import GraphvizCallGraph\n", + "from qualtran.resource_counting.generalizers import ignore_split_join, ignore_alloc_free\n", + "\n", + "costs = query_costs(prep_thc, [t_cost, QubitCount()])\n", + "g, _ = prep_thc.call_graph(max_depth=3, generalizer=[ignore_split_join, ignore_alloc_free])\n", + "\n", + "GraphvizCallGraph(g, costs).get_svg()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}