diff --git a/dev_tools/autogenerate-bloqs-notebooks-v2.py b/dev_tools/autogenerate-bloqs-notebooks-v2.py index 24fe5050f..778409140 100644 --- a/dev_tools/autogenerate-bloqs-notebooks-v2.py +++ b/dev_tools/autogenerate-bloqs-notebooks-v2.py @@ -60,6 +60,7 @@ import qualtran.bloqs.block_encoding.chebyshev_polynomial import qualtran.bloqs.block_encoding.lcu_block_encoding import qualtran.bloqs.block_encoding.lcu_select_and_prepare +import qualtran.bloqs.block_encoding.linear_combination import qualtran.bloqs.block_encoding.phase import qualtran.bloqs.bookkeeping import qualtran.bloqs.chemistry.df.double_factorization @@ -592,6 +593,7 @@ qualtran.bloqs.block_encoding.unitary._UNITARY_DOC, qualtran.bloqs.block_encoding.tensor_product._TENSOR_PRODUCT_DOC, qualtran.bloqs.block_encoding.product._PRODUCT_DOC, + qualtran.bloqs.block_encoding.linear_combination._LINEAR_COMBINATION_DOC, qualtran.bloqs.block_encoding.phase._PHASE_DOC, ], directory=f'{SOURCE_DIR}/bloqs/block_encoding/', diff --git a/qualtran/bloqs/block_encoding/__init__.py b/qualtran/bloqs/block_encoding/__init__.py index 2e1325119..0418f6c41 100644 --- a/qualtran/bloqs/block_encoding/__init__.py +++ b/qualtran/bloqs/block_encoding/__init__.py @@ -21,6 +21,7 @@ LCUBlockEncoding, LCUBlockEncodingZeroState, ) +from qualtran.bloqs.block_encoding.linear_combination import LinearCombination from qualtran.bloqs.block_encoding.phase import Phase from qualtran.bloqs.block_encoding.product import Product from qualtran.bloqs.block_encoding.tensor_product import TensorProduct diff --git a/qualtran/bloqs/block_encoding/block_encoding.ipynb b/qualtran/bloqs/block_encoding/block_encoding.ipynb index 6ff9d4fe9..8d2a01b2e 100644 --- a/qualtran/bloqs/block_encoding/block_encoding.ipynb +++ b/qualtran/bloqs/block_encoding/block_encoding.ipynb @@ -1175,6 +1175,135 @@ "show_call_graph(phase_block_encoding_g)\n", "show_counts_sigma(phase_block_encoding_sigma)" ] + }, + { + "cell_type": "markdown", + "id": "5f9d88fa", + "metadata": { + "cq.autogen": "LinearCombination.bloq_doc.md" + }, + "source": [ + "## `LinearCombination`\n", + "Linear combination of a sequence of block encodings.\n", + "\n", + "Builds the block encoding $B[\\lambda_1 U_1 + \\lambda_2 U_2 + \\cdots + \\lambda_n U_n]$ given\n", + "block encodings $B[U_1], \\ldots, B[U_n]$ and coefficients $\\lambda_i \\in \\mathbb{R}$.\n", + "\n", + "When each $B[U_i]$ is a $(\\alpha_i, a_i, \\epsilon_i)$-block encoding of $U_i$, we have that\n", + "$B[\\lambda_1 U_1 + \\cdots + \\lambda_n U_n]$ is a $(\\alpha, a, \\epsilon)$-block encoding\n", + "of $\\lambda_1 U_1 + \\cdots + \\lambda_n U_n$ where the normalization constant\n", + "$\\alpha = \\sum_i \\lvert\\lambda_i\\rvert\\alpha_i$, number of ancillas\n", + "$a = \\lceil \\log_2 n \\rceil + \\max_i a_i$, and precision\n", + "$\\epsilon = (\\sum_i \\lvert\\lambda_i\\rvert)\\max_i \\epsilon_i$.\n", + "\n", + "Under the hood, this bloq uses LCU Prepare and Select oracles to build the block encoding.\n", + "These oracles will be automatically instantiated if not specified by the user.\n", + "\n", + "#### Parameters\n", + " - `block_encodings`: A sequence of block encodings.\n", + " - `lambd`: Corresponding coefficients.\n", + " - `lambd_bits`: Number of bits needed to represent coefficients precisely.\n", + " - `prepare`: If specified, oracle preparing $\\sum_i \\sqrt{|\\lambda_i|} |i\\rangle$ (state should be normalized and can have junk).\n", + " - `select`: If specified, oracle taking $|i\\rangle|\\psi\\rangle \\mapsto \\text{sgn}(\\lambda_i) |i\\rangle U_i|\\psi\\rangle$. \n", + "\n", + "#### Registers\n", + " - `system`: The system register.\n", + " - `ancilla`: The ancilla register (present only if bitsize > 0).\n", + " - `resource`: The resource register (present only if bitsize > 0). \n", + "\n", + "#### References\n", + " - [Quantum algorithms: A survey of applications and end-to-end complexities]( https://arxiv.org/abs/2310.03011). Dalzell et al. (2023). Ch. 10.2.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6e60f66d", + "metadata": { + "cq.autogen": "LinearCombination.bloq_doc.py" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.block_encoding import LinearCombination" + ] + }, + { + "cell_type": "markdown", + "id": "7c3b1b39", + "metadata": { + "cq.autogen": "LinearCombination.example_instances.md" + }, + "source": [ + "### Example Instances" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7048adb0", + "metadata": { + "cq.autogen": "LinearCombination.linear_combination_block_encoding" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.basic_gates import Hadamard, TGate, XGate, ZGate\n", + "from qualtran.bloqs.block_encoding.unitary import Unitary\n", + "\n", + "linear_combination_block_encoding = LinearCombination(\n", + " (Unitary(TGate()), Unitary(Hadamard()), Unitary(XGate()), Unitary(ZGate())),\n", + " lambd=(0.25, -0.25, 0.25, -0.25),\n", + " lambd_bits=1,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "a3c0e843", + "metadata": { + "cq.autogen": "LinearCombination.graphical_signature.md" + }, + "source": [ + "#### Graphical Signature" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "403264e7", + "metadata": { + "cq.autogen": "LinearCombination.graphical_signature.py" + }, + "outputs": [], + "source": [ + "from qualtran.drawing import show_bloqs\n", + "show_bloqs([linear_combination_block_encoding],\n", + " ['`linear_combination_block_encoding`'])" + ] + }, + { + "cell_type": "markdown", + "id": "a506b326", + "metadata": { + "cq.autogen": "LinearCombination.call_graph.md" + }, + "source": [ + "### Call Graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9df2892b", + "metadata": { + "cq.autogen": "LinearCombination.call_graph.py" + }, + "outputs": [], + "source": [ + "from qualtran.resource_counting.generalizers import ignore_split_join\n", + "linear_combination_block_encoding_g, linear_combination_block_encoding_sigma = linear_combination_block_encoding.call_graph(max_depth=1, generalizer=ignore_split_join)\n", + "show_call_graph(linear_combination_block_encoding_g)\n", + "show_counts_sigma(linear_combination_block_encoding_sigma)" + ] } ], "metadata": { diff --git a/qualtran/bloqs/block_encoding/linear_combination.py b/qualtran/bloqs/block_encoding/linear_combination.py new file mode 100644 index 000000000..2ec187b6f --- /dev/null +++ b/qualtran/bloqs/block_encoding/linear_combination.py @@ -0,0 +1,357 @@ +# Copyright 2024 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 +# +# https://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. + +from functools import cached_property +from typing import cast, Dict, List, Optional, Tuple, Union + +import numpy as np +from attrs import evolve, field, frozen, validators + +from qualtran import ( + bloq_example, + BloqBuilder, + BloqDocSpec, + BoundedQUInt, + QAny, + Register, + Signature, + Soquet, + SoquetT, +) +from qualtran._infra.bloq import DecomposeTypeError +from qualtran.bloqs.block_encoding import BlockEncoding +from qualtran.bloqs.block_encoding.lcu_block_encoding import BlackBoxPrepare, BlackBoxSelect +from qualtran.bloqs.block_encoding.lcu_select_and_prepare import PrepareOracle +from qualtran.bloqs.block_encoding.phase import Phase +from qualtran.bloqs.bookkeeping.auto_partition import AutoPartition, Unused +from qualtran.bloqs.bookkeeping.partition import Partition +from qualtran.linalg.lcu_util import preprocess_probabilities_for_reversible_sampling +from qualtran.symbolics import smax, ssum, SymbolicFloat, SymbolicInt +from qualtran.symbolics.types import is_symbolic + + +@frozen +class LinearCombination(BlockEncoding): + r"""Linear combination of a sequence of block encodings. + + Builds the block encoding $B[\lambda_1 U_1 + \lambda_2 U_2 + \cdots + \lambda_n U_n]$ given + block encodings $B[U_1], \ldots, B[U_n]$ and coefficients $\lambda_i \in \mathbb{R}$. + + When each $B[U_i]$ is a $(\alpha_i, a_i, \epsilon_i)$-block encoding of $U_i$, we have that + $B[\lambda_1 U_1 + \cdots + \lambda_n U_n]$ is a $(\alpha, a, \epsilon)$-block encoding + of $\lambda_1 U_1 + \cdots + \lambda_n U_n$ where the normalization constant + $\alpha = \sum_i \lvert\lambda_i\rvert\alpha_i$, number of ancillas + $a = \lceil \log_2 n \rceil + \max_i a_i$, and precision + $\epsilon = (\sum_i \lvert\lambda_i\rvert)\max_i \epsilon_i$. + + Under the hood, this bloq uses LCU Prepare and Select oracles to build the block encoding. + These oracles will be automatically instantiated if not specified by the user. + + Args: + block_encodings: A sequence of block encodings. + lambd: Corresponding coefficients. + lambd_bits: Number of bits needed to represent coefficients precisely. + prepare: If specified, oracle preparing $\sum_i \sqrt{|\lambda_i|} |i\rangle$ + (state should be normalized and can have junk). + select: If specified, oracle taking + $|i\rangle|\psi\rangle \mapsto \text{sgn}(\lambda_i) |i\rangle U_i|\psi\rangle$. + + Registers: + system: The system register. + ancilla: The ancilla register (present only if bitsize > 0). + resource: The resource register (present only if bitsize > 0). + + References: + [Quantum algorithms: A survey of applications and end-to-end complexities]( + https://arxiv.org/abs/2310.03011). Dalzell et al. (2023). Ch. 10.2. + """ + + _block_encodings: Tuple[BlockEncoding, ...] = field( + converter=lambda x: x if isinstance(x, tuple) else tuple(x), validator=validators.min_len(2) + ) + _lambd: Tuple[float, ...] = field(converter=lambda x: x if isinstance(x, tuple) else tuple(x)) + lambd_bits: SymbolicInt + + _prepare: Optional[BlackBoxPrepare] = None + _select: Optional[BlackBoxSelect] = None + + def __attrs_post_init__(self): + if len(self._block_encodings) != len(self._lambd): + raise ValueError("Must provide the same number of block encodings and coefficients.") + if sum(abs(x) for x in self._lambd) == 0: + raise ValueError("Coefficients must not sum to zero.") + if not all(be.system_bitsize == self.system_bitsize for be in self._block_encodings): + raise ValueError("All block encodings must have the same dtype.") + if ( + self._prepare is not None or self._select is not None + ) and self.prepare.selection_registers != self.select.selection_registers: + raise ValueError( + "If given, prepare and select oracles must have same selection registers." + ) + if self._select is not None and self._select.target_registers != ( + self.signature.get_left("system"), + ): + raise ValueError( + "If given, select oracle must have block encoding `system` register as target." + ) + + @cached_property + def signed_block_encodings(self): + """Appropriately negated constituent block encodings.""" + return tuple( + Phase(be, phi=1, eps=0) if l < 0 else be + for l, be in zip(self._lambd, self._block_encodings) + ) + + @cached_property + def rescaled_lambd(self): + """Rescaled and padded array of coefficients.""" + x = np.abs(np.array(self._lambd)) + x /= np.linalg.norm(x, ord=1) + x.resize(2 ** int(np.ceil(np.log2(len(x)))), refcheck=False) + return x + + @cached_property + def signature(self) -> Signature: + return Signature.build_from_dtypes( + system=QAny(self.system_bitsize), + ancilla=QAny(self.ancilla_bitsize), + resource=QAny(self.resource_bitsize), + ) + + @cached_property + def system_bitsize(self) -> SymbolicInt: + return self.signed_block_encodings[0].system_bitsize + + def pretty_name(self) -> str: + return f"B[{'+'.join(be.pretty_name()[2:-1] for be in self.signed_block_encodings)}]" + + @cached_property + def alpha(self) -> SymbolicFloat: + return ssum( + abs(l) * be.alpha for be, l in zip(self.signed_block_encodings, self.rescaled_lambd) + ) + + @cached_property + def be_ancilla_bitsize(self) -> SymbolicInt: + return smax(be.ancilla_bitsize for be in self.signed_block_encodings) + + @cached_property + def ancilla_bitsize(self) -> SymbolicInt: + return self.be_ancilla_bitsize + self.prepare.selection_bitsize + + @cached_property + def be_resource_bitsize(self) -> SymbolicInt: + return smax(be.resource_bitsize for be in self.signed_block_encodings) + + @cached_property + def resource_bitsize(self) -> SymbolicInt: + return self.be_resource_bitsize + self.prepare.junk_bitsize + + @cached_property + def epsilon(self) -> SymbolicFloat: + return ssum(abs(l) for l in self.rescaled_lambd) * smax( + be.epsilon for be in self.signed_block_encodings + ) + + @property + def target_registers(self) -> Tuple[Register, ...]: + return (self.signature.get_right("system"),) + + @property + def junk_registers(self) -> Tuple[Register, ...]: + return (self.signature.get_right("resource"),) + + @property + def selection_registers(self) -> Tuple[Register, ...]: + return (self.signature.get_right("ancilla"),) + + @property + def signal_state(self) -> PrepareOracle: + # This method will be implemented in the future after PrepareOracle + # is updated for the BlockEncoding interface. + # GitHub issue: https://github.com/quantumlib/Qualtran/issues/1104 + raise NotImplementedError + + @cached_property + def prepare(self) -> BlackBoxPrepare: + if self._prepare is not None: + return self._prepare + if is_symbolic(self.lambd_bits): + raise DecomposeTypeError(f"Cannot decompose symbolic {self=}") + + alt, keep, mu = preprocess_probabilities_for_reversible_sampling( + unnormalized_probabilities=tuple(self.rescaled_lambd), + sub_bit_precision=cast(int, self.lambd_bits), + ) + N = len(self.rescaled_lambd) + + # import here to avoid circular dependency of StatePreparationAliasSampling + # on PrepareOracle in qualtran.bloq.block_encoding + from qualtran.bloqs.state_preparation.state_preparation_alias_sampling import ( + StatePreparationAliasSampling, + ) + + # disable spurious pylint + # pylint: disable=abstract-class-instantiated + prep = StatePreparationAliasSampling( + selection_registers=Register('selection', BoundedQUInt((N - 1).bit_length(), N)), + alt=np.array(alt), + keep=np.array(keep), + mu=mu, + sum_of_unnormalized_probabilities=1, + ) + return BlackBoxPrepare(prep) + + @cached_property + def select(self) -> BlackBoxSelect: + if self._select is not None: + return self._select + if ( + is_symbolic(self.system_bitsize) + or is_symbolic(self.ancilla_bitsize) + or is_symbolic(self.resource_bitsize) + ): + raise DecomposeTypeError(f"Cannot decompose symbolic {self=}") + assert isinstance(self.be_ancilla_bitsize, int) + assert isinstance(self.be_resource_bitsize, int) + + # make all bloqs have same ancilla and resource registers + bloqs = [] + for be in self.signed_block_encodings: + assert isinstance(be.ancilla_bitsize, int) + assert isinstance(be.resource_bitsize, int) + + partitions: List[Tuple[Register, List[Union[str, Unused]]]] = [ + (Register("system", QAny(self.system_bitsize)), ["system"]) + ] + if self.be_ancilla_bitsize > 0: + regs: List[Union[str, Unused]] = [] + if be.ancilla_bitsize > 0: + regs.append("ancilla") + if self.be_ancilla_bitsize > be.ancilla_bitsize: + regs.append(Unused(self.be_ancilla_bitsize - be.ancilla_bitsize)) + partitions.append((Register("ancilla", QAny(self.be_ancilla_bitsize)), regs)) + if self.be_resource_bitsize > 0: + regs = [] + if be.resource_bitsize > 0: + regs.append("resource") + if self.be_resource_bitsize > be.resource_bitsize: + regs.append(Unused(self.be_resource_bitsize - be.resource_bitsize)) + partitions.append((Register("resource", QAny(self.be_resource_bitsize)), regs)) + bloqs.append(AutoPartition(be, partitions, left_only=False)) + + # import here to avoid circular dependency of ApplyLthBloq + # on SelectOracle in qualtran.bloqs.block_encoding + from qualtran.bloqs.multiplexers.apply_lth_bloq import ApplyLthBloq + + return BlackBoxSelect(ApplyLthBloq(np.array(bloqs))) + + def build_composite_bloq( + self, bb: BloqBuilder, system: Soquet, ancilla: Soquet, **soqs: SoquetT + ) -> Dict[str, SoquetT]: + if ( + is_symbolic(self.system_bitsize) + or is_symbolic(self.ancilla_bitsize) + or is_symbolic(self.resource_bitsize) + ): + raise DecomposeTypeError(f"Cannot decompose symbolic {self=}") + assert isinstance(self.be_ancilla_bitsize, int) + assert isinstance(self.ancilla_bitsize, int) + assert isinstance(self.be_resource_bitsize, int) + assert isinstance(self.resource_bitsize, int) + + # partition ancilla register + be_system_soqs: Dict[str, SoquetT] = {"system": system} + anc_regs = [Register("selection", QAny(self.prepare.selection_bitsize))] + if self.be_ancilla_bitsize > 0: + anc_regs.append(Register("ancilla", QAny(self.be_ancilla_bitsize))) + anc_part = Partition(cast(int, self.ancilla_bitsize), tuple(anc_regs)) + anc_soqs = bb.add_d(anc_part, x=ancilla) + if self.be_ancilla_bitsize > 0: + be_system_soqs["ancilla"] = anc_soqs.pop("ancilla") + prepare_in_soqs = {"selection": anc_soqs.pop("selection")} + + # partition resource register if necessary + if self.resource_bitsize > 0: + res_regs = [] + if self.be_resource_bitsize > 0: + res_regs.append(Register("resource", QAny(self.be_resource_bitsize))) + if self.prepare.junk_bitsize > 0: + res_regs.append(Register("prepare_junk", QAny(self.prepare.junk_bitsize))) + res_part = Partition(cast(int, self.resource_bitsize), tuple(res_regs)) + res_soqs = bb.add_d(res_part, x=soqs.pop("resource")) + if self.be_resource_bitsize > 0: + be_system_soqs["resource"] = res_soqs.pop("resource") + if self.prepare.junk_bitsize > 0: + prepare_in_soqs["junk"] = res_soqs.pop("prepare_junk") + + # merge system, ancilla, resource of block encoding into system register of Select oracle + be_regs = [Register("system", QAny(self.system_bitsize))] + if self.be_ancilla_bitsize > 0: + be_regs.append(Register("ancilla", QAny(self.be_ancilla_bitsize))) + if self.be_resource_bitsize > 0: + be_regs.append(Register("resource", QAny(self.be_resource_bitsize))) + be_part = Partition(cast(int, self.select.system_bitsize), tuple(be_regs)) + + prepare_soqs = bb.add_d(self.prepare, **prepare_in_soqs) + select_out_soqs = bb.add_d( + self.select, + selection=prepare_soqs.pop("selection"), + system=cast(Soquet, bb.add(evolve(be_part, partition=False), **be_system_soqs)), + ) + prep_adj_soqs = bb.add_d( + self.prepare.adjoint(), selection=select_out_soqs.pop("selection"), **prepare_soqs + ) + + # partition system register of Select into system, ancilla, resource of block encoding + be_soqs = bb.add_d(be_part, x=select_out_soqs.pop("system")) + out: Dict[str, SoquetT] = {"system": be_soqs.pop("system")} + + # merge ancilla registers of block encoding and Prepare oracle + anc_soqs = {"selection": prep_adj_soqs.pop("selection")} + if self.be_ancilla_bitsize > 0: + anc_soqs["ancilla"] = be_soqs.pop("ancilla") + out["ancilla"] = cast(Soquet, bb.add(evolve(anc_part, partition=False), **anc_soqs)) + + # merge resource registers of block encoding and Prepare oracle + if self.resource_bitsize > 0: + res_soqs = dict() + if self.be_resource_bitsize > 0: + res_soqs["resource"] = be_soqs.pop("resource") + if self.prepare.junk_bitsize > 0: + res_soqs["prepare_junk"] = prep_adj_soqs.pop("junk") + out["resource"] = cast(Soquet, bb.add(evolve(res_part, partition=False), **res_soqs)) + + return out + + +@bloq_example +def _linear_combination_block_encoding() -> LinearCombination: + from qualtran.bloqs.basic_gates import Hadamard, TGate, XGate, ZGate + from qualtran.bloqs.block_encoding.unitary import Unitary + + linear_combination_block_encoding = LinearCombination( + (Unitary(TGate()), Unitary(Hadamard()), Unitary(XGate()), Unitary(ZGate())), + lambd=(0.25, -0.25, 0.25, -0.25), + lambd_bits=1, + ) + return linear_combination_block_encoding + + +_LINEAR_COMBINATION_DOC = BloqDocSpec( + bloq_cls=LinearCombination, + import_line="from qualtran.bloqs.block_encoding import LinearCombination", + examples=[_linear_combination_block_encoding], +) diff --git a/qualtran/bloqs/block_encoding/linear_combination_test.py b/qualtran/bloqs/block_encoding/linear_combination_test.py new file mode 100644 index 000000000..0e80ac62f --- /dev/null +++ b/qualtran/bloqs/block_encoding/linear_combination_test.py @@ -0,0 +1,183 @@ +# Copyright 2024 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 +# +# https://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. + +from typing import cast + +import numpy as np +import pytest +from attrs import evolve + +from qualtran import BloqBuilder, QAny, Register, Signature, Soquet +from qualtran.bloqs.basic_gates import ( + CNOT, + Hadamard, + IntEffect, + IntState, + Ry, + Swap, + TGate, + XGate, + ZGate, +) +from qualtran.bloqs.block_encoding.linear_combination import ( + _linear_combination_block_encoding, + LinearCombination, +) +from qualtran.bloqs.block_encoding.unitary import Unitary + + +def test_linear_combination(bloq_autotester): + bloq_autotester(_linear_combination_block_encoding) + + +def test_linear_combination_signature(): + assert _linear_combination_block_encoding().signature == Signature( + [Register("system", QAny(1)), Register("ancilla", QAny(2)), Register("resource", QAny(5))] + ) + + +def test_linear_combination_checks(): + with pytest.raises(ValueError): + _ = LinearCombination((), (), lambd_bits=1) + with pytest.raises(ValueError): + _ = LinearCombination((Unitary(TGate()),), (), lambd_bits=1) + with pytest.raises(ValueError): + _ = LinearCombination((Unitary(TGate()),), (1.0,), lambd_bits=1) + with pytest.raises(ValueError): + _ = LinearCombination((Unitary(TGate()), Unitary(CNOT())), (1.0,), lambd_bits=1) + with pytest.raises(ValueError): + _ = LinearCombination((Unitary(TGate()), Unitary(Hadamard())), (0.0, 0.0), lambd_bits=1) + + +def test_linear_combination_params(): + u1 = evolve(Unitary(TGate()), alpha=0.5, ancilla_bitsize=2, resource_bitsize=1, epsilon=0.01) + u2 = evolve(Unitary(Hadamard()), alpha=0.5, ancilla_bitsize=1, resource_bitsize=1, epsilon=0.1) + bloq = LinearCombination((u1, u2), (0.5, 0.5), lambd_bits=1) + assert bloq.system_bitsize == 1 + assert bloq.alpha == 0.5 * 0.5 + 0.5 * 0.5 + assert bloq.epsilon == (0.5 + 0.5) * max(0.01, 0.1) + assert bloq.ancilla_bitsize == 1 + max(1, 2) + assert bloq.resource_bitsize == max(1, 1) + 4 # dependent on state preparation + + +def get_tensors(bloq): + bb = BloqBuilder() + system = bb.add_register("system", cast(int, bloq.system_bitsize)) + ancilla = cast(Soquet, bb.add(IntState(0, bloq.ancilla_bitsize))) + resource = cast(Soquet, bb.add(IntState(0, bloq.resource_bitsize))) + system, ancilla, resource = bb.add_t(bloq, system=system, ancilla=ancilla, resource=resource) + bb.add(IntEffect(0, cast(int, bloq.ancilla_bitsize)), val=ancilla) + bb.add(IntEffect(0, cast(int, bloq.resource_bitsize)), val=resource) + bloq = bb.finalize(system=system) + return bloq.tensor_contract() + + +def test_linear_combination_tensors(): + bloq = _linear_combination_block_encoding() + from_gate = ( + 0.25 * TGate().tensor_contract() + + -0.25 * Hadamard().tensor_contract() + + 0.25 * XGate().tensor_contract() + + -0.25 * ZGate().tensor_contract() + ) + from_tensors = get_tensors(bloq) + np.testing.assert_allclose(from_gate, from_tensors) + + +def run_gate_test(gates, lambd, lambd_bits=1, atol=1e-07): + bloq = LinearCombination(tuple(Unitary(g) for g in gates), lambd, lambd_bits) + lambd = np.array(lambd) / np.linalg.norm(lambd, ord=1) + from_gate = sum(l * g.tensor_contract() for l, g in zip(lambd, gates)) + from_tensors = get_tensors(bloq) + np.testing.assert_allclose(from_gate, from_tensors, atol=atol) + + +# all coefficients are multiples of small negative powers of 2 after normalization +exact2 = [[0.0, 1.0], [1 / 3, 1 / 3], [0.5, 0.5], [0.25, 0.25], [2.0, 6.0], [1.0, 0.0]] +exact3 = [ + [0.0, 0.0, 1.0], + [1.0, -0.5, 0.5], + [1 / 4, 1 / 4, -1 / 2], + [1 / 2, 1 / 4, 1 / 4], + [3 / 16, -1 / 4, 1 / 16], + [-1.0, 0.0, 0.0], +] +exact5 = [ + [1.0, 0.0, 0.0, 0.0, 0.0], + [9 / 16, -1 / 16, 1 / 8, -1 / 8, 1 / 8], + [1 / 4, 1 / 8, -1 / 16, 1 / 32, -1 / 32], + [0.0, 0.0, 0.0, 0.0, 1.0], + [1.0, 0.5, 0.5, -1.0, 1.0], +] + + +@pytest.mark.parametrize('lambd', exact2) +def test_linear_combination2(lambd): + run_gate_test([TGate(), Hadamard()], lambd) + + +@pytest.mark.parametrize('lambd', exact2) +def test_linear_combination_twogate(lambd): + run_gate_test([CNOT(), Hadamard().controlled()], lambd) + + +@pytest.mark.parametrize('lambd', exact3) +def test_linear_combination3(lambd): + run_gate_test([TGate(), Hadamard(), XGate()], lambd) + + +@pytest.mark.parametrize('lambd', exact5) +def test_linear_combination5(lambd): + run_gate_test([TGate(), Hadamard(), XGate(), ZGate(), Ry(angle=np.pi / 4.0)], lambd) + + +# coefficients are not multiples of small negative powers of 2 after normalization +approx2 = [ + [1 / 3, 2 / 3], + [2 / 3, 1 / 3], + [1 / 2, -1 / 4], + [1 / 8, 1 / 2], + [-1 / 3, 1 / 2], + [1 / 3, 1 / 5], + [1.0, -7.0], + [3.7, 2.1], + [1.0, 0.0], +] +approx5 = [ + [1.0, 0.0, 0.0, 1.0, 0.0], + [1.0, 1.0, 1.0, -1.0, 1.0], + [1.5, 2.4, -3.3, 4.2, 5.1], + [1 / 7, -2 / 7, 3 / 7, 4 / 7, -5 / 7], +] + + +@pytest.mark.parametrize('lambd', approx2) +def test_linear_combination_approx2(lambd): + run_gate_test([TGate(), Hadamard()], lambd, lambd_bits=9, atol=0.001) + + +@pytest.mark.parametrize('lambd', approx5) +def test_linear_combination_approx5(lambd): + run_gate_test( + [ + TGate().controlled(), + Hadamard().controlled(), + CNOT(), + Swap(1), + Ry(angle=np.pi / 4.0).controlled(), + ], + lambd, + lambd_bits=9, + atol=0.001, + ) diff --git a/qualtran/bloqs/multiplexers/apply_lth_bloq.py b/qualtran/bloqs/multiplexers/apply_lth_bloq.py index 852c28534..608e858e8 100644 --- a/qualtran/bloqs/multiplexers/apply_lth_bloq.py +++ b/qualtran/bloqs/multiplexers/apply_lth_bloq.py @@ -22,13 +22,14 @@ from qualtran import Bloq, bloq_example, BloqDocSpec, BoundedQUInt, QBit, Register, Side from qualtran._infra.gate_with_registers import merge_qubits, SpecializedSingleQubitControlledGate +from qualtran.bloqs.block_encoding.lcu_select_and_prepare import SelectOracle from qualtran.bloqs.multiplexers.unary_iteration_bloq import UnaryIterationGate from qualtran.resource_counting import BloqCountT from qualtran.symbolics import ceil, log2 @frozen -class ApplyLthBloq(UnaryIterationGate, SpecializedSingleQubitControlledGate): # type: ignore[misc] +class ApplyLthBloq(UnaryIterationGate, SpecializedSingleQubitControlledGate, SelectOracle): # type: ignore[misc] r"""A SELECT operation that executes one of a list of bloqs $U_l$ based on a quantum index: $$ diff --git a/qualtran/conftest.py b/qualtran/conftest.py index ff39888a7..241fd04dd 100644 --- a/qualtran/conftest.py +++ b/qualtran/conftest.py @@ -101,6 +101,7 @@ def assert_bloq_example_serializes_for_pytest(bloq_ex: BloqExample): 'product_block_encoding_properties', 'product_block_encoding_symb', 'apply_lth_bloq', + 'linear_combination_block_encoding', 'phase_block_encoding', ]: pytest.xfail("Skipping serialization test for bloq examples that cannot yet be serialized.") diff --git a/qualtran/serialization/resolver_dict.py b/qualtran/serialization/resolver_dict.py index 6213db611..a14d73143 100644 --- a/qualtran/serialization/resolver_dict.py +++ b/qualtran/serialization/resolver_dict.py @@ -34,6 +34,7 @@ import qualtran.bloqs.basic_gates.z_basis import qualtran.bloqs.block_encoding import qualtran.bloqs.block_encoding.lcu_select_and_prepare +import qualtran.bloqs.block_encoding.linear_combination import qualtran.bloqs.block_encoding.phase import qualtran.bloqs.block_encoding.product import qualtran.bloqs.block_encoding.tensor_product @@ -196,6 +197,7 @@ "qualtran.bloqs.block_encoding.unitary.Unitary": qualtran.bloqs.block_encoding.unitary.Unitary, "qualtran.bloqs.block_encoding.tensor_product.TensorProduct": qualtran.bloqs.block_encoding.tensor_product.TensorProduct, "qualtran.bloqs.block_encoding.product.Product": qualtran.bloqs.block_encoding.product.Product, + "qualtran.bloqs.block_encoding.linear_combination.LinearCombination": qualtran.bloqs.block_encoding.linear_combination.LinearCombination, "qualtran.bloqs.block_encoding.phase.phase": qualtran.bloqs.block_encoding.phase.Phase, "qualtran.bloqs.bookkeeping.allocate.Allocate": qualtran.bloqs.bookkeeping.allocate.Allocate, "qualtran.bloqs.bookkeeping.arbitrary_clifford.ArbitraryClifford": qualtran.bloqs.bookkeeping.arbitrary_clifford.ArbitraryClifford,