From 8f2e0529b9a8a3ef6dc4e543b534f280c40a53a1 Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Thu, 26 Dec 2024 18:53:55 +0530 Subject: [PATCH 1/5] WIP tensor from classical action --- .../qualtran_dev_tools/all_call_graph_test.py | 32 ++++++++++ qualtran/bloqs/basic_gates/swap.py | 2 +- qualtran/bloqs/basic_gates/swap_test.py | 5 ++ .../tensor/tensor_from_classical.py | 61 +++++++++++++++++++ .../tensor/tensor_from_classical_test.py | 28 +++++++++ 5 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 qualtran/simulation/tensor/tensor_from_classical.py create mode 100644 qualtran/simulation/tensor/tensor_from_classical_test.py diff --git a/dev_tools/qualtran_dev_tools/all_call_graph_test.py b/dev_tools/qualtran_dev_tools/all_call_graph_test.py index be2307e7d..43a83c211 100644 --- a/dev_tools/qualtran_dev_tools/all_call_graph_test.py +++ b/dev_tools/qualtran_dev_tools/all_call_graph_test.py @@ -27,3 +27,35 @@ def test_get_all_call_graph(): g = get_all_call_graph(bes) res = list(nx.simple_cycles(g)) assert res == [] + + +@pytest.mark.parametrize("be", get_bloq_examples(), ids=lambda be: be.name) +def test_classical_tensor(be): + import numpy as np + from qualtran.simulation.tensor.tensor_from_classical import tensor_from_classical_sim + from qualtran.symbolics import is_symbolic + + if be.name in ['rsa_pe_small']: + pytest.skip('skiplist') + + LIM = 9 + + bloq = be.make() + + n = bloq.signature.n_qubits() + if is_symbolic(n): + pytest.skip(f'symbolic qubits: {n=}') + if n > LIM: + pytest.skip(f'too many qubits: {n=}') + + try: + tensor_direct = bloq.tensor_contract() + except Exception as e: + pytest.skip(f'no tensor: {e}') + + try: + tensor_classical = tensor_from_classical_sim(bloq) + except NotImplementedError as e: + pytest.skip(str(e)) + + np.testing.assert_allclose(tensor_classical, tensor_direct, rtol=1e-5, atol=1e-5) diff --git a/qualtran/bloqs/basic_gates/swap.py b/qualtran/bloqs/basic_gates/swap.py index e251333a3..43581a0ab 100644 --- a/qualtran/bloqs/basic_gates/swap.py +++ b/qualtran/bloqs/basic_gates/swap.py @@ -299,7 +299,7 @@ def _swap_small() -> Swap: return swap_small -@bloq_example +@bloq_example(generalizer=ignore_split_join) def _swap_large() -> Swap: swap_large = Swap(bitsize=64) return swap_large diff --git a/qualtran/bloqs/basic_gates/swap_test.py b/qualtran/bloqs/basic_gates/swap_test.py index 4ddb35587..c86ce7b51 100644 --- a/qualtran/bloqs/basic_gates/swap_test.py +++ b/qualtran/bloqs/basic_gates/swap_test.py @@ -35,6 +35,7 @@ _cswap_large, _cswap_small, _swap, + _swap_large, _swap_matrix, _swap_small, Swap, @@ -224,6 +225,10 @@ def test_swap_small(bloq_autotester): bloq_autotester(_swap_small) +def test_swap_large(bloq_autotester): + bloq_autotester(_swap_large) + + def test_swap_symb(bloq_autotester): if bloq_autotester.check_name == 'serialize': pytest.skip("Sympy equality with assumptions.") diff --git a/qualtran/simulation/tensor/tensor_from_classical.py b/qualtran/simulation/tensor/tensor_from_classical.py new file mode 100644 index 000000000..f5615e52d --- /dev/null +++ b/qualtran/simulation/tensor/tensor_from_classical.py @@ -0,0 +1,61 @@ +# 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. +import itertools + +import numpy as np +from numpy.typing import NDArray + +from qualtran import Bloq, Register + + +def _bits_to_classical_reg_data(reg: Register, bits): + if reg.shape == (): + return reg.dtype.from_bits(bits) + return reg.dtype.from_bits_array(np.reshape(bits, reg.shape + (reg.dtype.num_qubits,))) + + +def tensor_from_classical_sim(bloq: Bloq) -> NDArray[np.complex64]: + left_qubit_counts = tuple(r.total_bits() for r in bloq.signature.lefts()) + left_qubit_splits = np.cumsum(left_qubit_counts) + + n_qubits_left = sum(left_qubit_counts) + n_qubits_right = sum(r.total_bits() for r in bloq.signature.rights()) + + matrix = np.zeros((2,) * (n_qubits_right + n_qubits_left)) + + for input_t in itertools.product((0, 1), repeat=n_qubits_left): + *inputs_t, last = np.split(input_t, left_qubit_splits) + assert np.size(last) == 0 + + in_kwargs = { + r.name: _bits_to_classical_reg_data(r, bits) + for r, bits in zip(bloq.signature.lefts(), inputs_t) + } + out_args = bloq.call_classically(**in_kwargs) + + output_t = np.concatenate( + [r.dtype.to_bits_array(vals).flat for r, vals in zip(bloq.signature.rights(), out_args)] + ) + + matrix[*np.atleast_1d(output_t), *np.atleast_1d(input_t)] = 1 + + shape: tuple[int, ...] + if n_qubits_left == 0 and n_qubits_right == 0: + shape = () + elif n_qubits_left == 0 or n_qubits_right == 0: + shape = (2 ** max(n_qubits_left, n_qubits_right),) + else: + shape = (2**n_qubits_right, 2**n_qubits_left) + + return matrix.reshape(shape) diff --git a/qualtran/simulation/tensor/tensor_from_classical_test.py b/qualtran/simulation/tensor/tensor_from_classical_test.py new file mode 100644 index 000000000..80827ec76 --- /dev/null +++ b/qualtran/simulation/tensor/tensor_from_classical_test.py @@ -0,0 +1,28 @@ +# 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. +import numpy as np +import pytest + +from qualtran import Bloq +from qualtran.bloqs.basic_gates import TwoBitCSwap, XGate + +from .tensor_from_classical import tensor_from_classical_sim + + +@pytest.mark.parametrize("bloq", [XGate(), TwoBitCSwap()], ids=str) +def test_simple(bloq: Bloq): + from_classical = tensor_from_classical_sim(bloq) + from_tensor = bloq.tensor_contract() + + np.testing.assert_allclose(from_classical, from_tensor) From 35293d2a67ae87a95d05c5bbbe4d4e0f277681fb Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Tue, 31 Dec 2024 17:00:48 +0530 Subject: [PATCH 2/5] fix errors --- .../qualtran_dev_tools/all_call_graph_test.py | 11 ++++++----- .../simulation/tensor/tensor_from_classical.py | 14 ++++++++++---- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/dev_tools/qualtran_dev_tools/all_call_graph_test.py b/dev_tools/qualtran_dev_tools/all_call_graph_test.py index 43a83c211..d9810eefc 100644 --- a/dev_tools/qualtran_dev_tools/all_call_graph_test.py +++ b/dev_tools/qualtran_dev_tools/all_call_graph_test.py @@ -30,12 +30,13 @@ def test_get_all_call_graph(): @pytest.mark.parametrize("be", get_bloq_examples(), ids=lambda be: be.name) -def test_classical_tensor(be): +def test_classical_consistent_with_tensor(be): import numpy as np + from qualtran.simulation.tensor.tensor_from_classical import tensor_from_classical_sim from qualtran.symbolics import is_symbolic - if be.name in ['rsa_pe_small']: + if be.name in ['rsa_pe_small', 'modmul']: pytest.skip('skiplist') LIM = 9 @@ -50,12 +51,12 @@ def test_classical_tensor(be): try: tensor_direct = bloq.tensor_contract() - except Exception as e: + except (NotImplementedError, AssertionError, ValueError, RuntimeError) as e: pytest.skip(f'no tensor: {e}') try: tensor_classical = tensor_from_classical_sim(bloq) - except NotImplementedError as e: - pytest.skip(str(e)) + except (NotImplementedError, AssertionError, ValueError, RuntimeError) as e: + pytest.skip(f'no classical action: {e}') np.testing.assert_allclose(tensor_classical, tensor_direct, rtol=1e-5, atol=1e-5) diff --git a/qualtran/simulation/tensor/tensor_from_classical.py b/qualtran/simulation/tensor/tensor_from_classical.py index f5615e52d..58e6e2043 100644 --- a/qualtran/simulation/tensor/tensor_from_classical.py +++ b/qualtran/simulation/tensor/tensor_from_classical.py @@ -25,7 +25,7 @@ def _bits_to_classical_reg_data(reg: Register, bits): return reg.dtype.from_bits_array(np.reshape(bits, reg.shape + (reg.dtype.num_qubits,))) -def tensor_from_classical_sim(bloq: Bloq) -> NDArray[np.complex64]: +def tensor_from_classical_sim(bloq: Bloq) -> NDArray: left_qubit_counts = tuple(r.total_bits() for r in bloq.signature.lefts()) left_qubit_splits = np.cumsum(left_qubit_counts) @@ -44,9 +44,15 @@ def tensor_from_classical_sim(bloq: Bloq) -> NDArray[np.complex64]: } out_args = bloq.call_classically(**in_kwargs) - output_t = np.concatenate( - [r.dtype.to_bits_array(vals).flat for r, vals in zip(bloq.signature.rights(), out_args)] - ) + if out_args: + output_t = np.concatenate( + [ + r.dtype.to_bits_array(np.asarray(vals)).flat + for r, vals in zip(bloq.signature.rights(), out_args) + ] + ) + else: + output_t = np.array([]) matrix[*np.atleast_1d(output_t), *np.atleast_1d(input_t)] = 1 From f573a37cdb6490d678d630e85c047c54c2ee19c2 Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Tue, 31 Dec 2024 17:25:09 +0530 Subject: [PATCH 3/5] fix syntax error? --- qualtran/simulation/tensor/tensor_from_classical.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qualtran/simulation/tensor/tensor_from_classical.py b/qualtran/simulation/tensor/tensor_from_classical.py index 58e6e2043..b6d7559f7 100644 --- a/qualtran/simulation/tensor/tensor_from_classical.py +++ b/qualtran/simulation/tensor/tensor_from_classical.py @@ -54,7 +54,7 @@ def tensor_from_classical_sim(bloq: Bloq) -> NDArray: else: output_t = np.array([]) - matrix[*np.atleast_1d(output_t), *np.atleast_1d(input_t)] = 1 + matrix[tuple([*np.atleast_1d(output_t), *np.atleast_1d(input_t)])] = 1 shape: tuple[int, ...] if n_qubits_left == 0 and n_qubits_right == 0: From c6fb66f8a5e65637becf6ec277a2ec1026447536 Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Tue, 7 Jan 2025 12:55:35 +0100 Subject: [PATCH 4/5] move test, add timeout --- .../qualtran_dev_tools/all_call_graph_test.py | 33 --------- .../tensor_from_classical_test.py | 67 +++++++++++++++++++ qualtran/bloqs/arithmetic/sorting.py | 2 +- 3 files changed, 68 insertions(+), 34 deletions(-) create mode 100644 dev_tools/qualtran_dev_tools/tensor_from_classical_test.py diff --git a/dev_tools/qualtran_dev_tools/all_call_graph_test.py b/dev_tools/qualtran_dev_tools/all_call_graph_test.py index d9810eefc..be2307e7d 100644 --- a/dev_tools/qualtran_dev_tools/all_call_graph_test.py +++ b/dev_tools/qualtran_dev_tools/all_call_graph_test.py @@ -27,36 +27,3 @@ def test_get_all_call_graph(): g = get_all_call_graph(bes) res = list(nx.simple_cycles(g)) assert res == [] - - -@pytest.mark.parametrize("be", get_bloq_examples(), ids=lambda be: be.name) -def test_classical_consistent_with_tensor(be): - import numpy as np - - from qualtran.simulation.tensor.tensor_from_classical import tensor_from_classical_sim - from qualtran.symbolics import is_symbolic - - if be.name in ['rsa_pe_small', 'modmul']: - pytest.skip('skiplist') - - LIM = 9 - - bloq = be.make() - - n = bloq.signature.n_qubits() - if is_symbolic(n): - pytest.skip(f'symbolic qubits: {n=}') - if n > LIM: - pytest.skip(f'too many qubits: {n=}') - - try: - tensor_direct = bloq.tensor_contract() - except (NotImplementedError, AssertionError, ValueError, RuntimeError) as e: - pytest.skip(f'no tensor: {e}') - - try: - tensor_classical = tensor_from_classical_sim(bloq) - except (NotImplementedError, AssertionError, ValueError, RuntimeError) as e: - pytest.skip(f'no classical action: {e}') - - np.testing.assert_allclose(tensor_classical, tensor_direct, rtol=1e-5, atol=1e-5) diff --git a/dev_tools/qualtran_dev_tools/tensor_from_classical_test.py b/dev_tools/qualtran_dev_tools/tensor_from_classical_test.py new file mode 100644 index 000000000..5c083f8c4 --- /dev/null +++ b/dev_tools/qualtran_dev_tools/tensor_from_classical_test.py @@ -0,0 +1,67 @@ +# 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 Callable + +import numpy as np +import pytest + +from qualtran.simulation.tensor.tensor_from_classical import tensor_from_classical_sim +from qualtran.symbolics import is_symbolic + +from .bloq_finder import get_bloq_examples +from .tensor_report_card import ExecuteWithTimeout + + +def exec_with_timeout(fn: Callable, *, timeout: float = 10.0): + def _run_fn(f, cxn): + res, err = None, None + try: + res = f() + except Exception as e: # pylint: disable=broad-exception-caught + err = str(e) + cxn.send((res, err)) + + runner = ExecuteWithTimeout(timeout=timeout, max_workers=1) + runner.submit(_run_fn, {'f': fn}) + _, output = runner.next_result() + return output + + +@pytest.mark.parametrize("be", get_bloq_examples(), ids=lambda be: be.name) +def test_classical_consistent_with_tensor(be): + LIM = 30 + + bloq = be.make() + + n = bloq.signature.n_qubits() + if is_symbolic(n): + pytest.skip(f'symbolic qubits: {n}') + if n > LIM: + pytest.skip(f'too many qubits: {n=} > {LIM}') + + result = exec_with_timeout(bloq.tensor_contract) + if result is None: + pytest.skip('timeout: tensor') + tensor_direct, err = result + if err is not None: + pytest.skip(f'no tensor: {err}') + + result = exec_with_timeout(lambda: tensor_from_classical_sim(bloq)) + if result is None: + pytest.skip('timeout: tensor from classical') + tensor_classical, err = result + if err is not None: + pytest.skip(f'no classical action: {err}') + + np.testing.assert_allclose(tensor_classical, tensor_direct, rtol=1e-5, atol=1e-5) diff --git a/qualtran/bloqs/arithmetic/sorting.py b/qualtran/bloqs/arithmetic/sorting.py index d18db924e..a2c319841 100644 --- a/qualtran/bloqs/arithmetic/sorting.py +++ b/qualtran/bloqs/arithmetic/sorting.py @@ -84,7 +84,7 @@ def build_composite_bloq( @bloq_example def _comparator() -> Comparator: - comparator = Comparator(7) + comparator = Comparator(3) return comparator From 13888f19d5a81fe7daa9930a04b0841844420915 Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Tue, 7 Jan 2025 13:26:26 +0100 Subject: [PATCH 5/5] rename --- .../tensor/tensor_from_classical.py | 24 +++++++++++-------- .../tensor/tensor_from_classical_test.py | 2 +- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/qualtran/simulation/tensor/tensor_from_classical.py b/qualtran/simulation/tensor/tensor_from_classical.py index b6d7559f7..00c3f8b1f 100644 --- a/qualtran/simulation/tensor/tensor_from_classical.py +++ b/qualtran/simulation/tensor/tensor_from_classical.py @@ -12,25 +12,29 @@ # See the License for the specific language governing permissions and # limitations under the License. import itertools +from typing import TYPE_CHECKING import numpy as np from numpy.typing import NDArray from qualtran import Bloq, Register +if TYPE_CHECKING: + from qualtran.simulation.classical_sim import ClassicalValT -def _bits_to_classical_reg_data(reg: Register, bits): + +def _bits_to_classical_reg_data(reg: Register, bits) -> 'ClassicalValT': if reg.shape == (): return reg.dtype.from_bits(bits) return reg.dtype.from_bits_array(np.reshape(bits, reg.shape + (reg.dtype.num_qubits,))) def tensor_from_classical_sim(bloq: Bloq) -> NDArray: - left_qubit_counts = tuple(r.total_bits() for r in bloq.signature.lefts()) + left_qubit_counts = tuple(reg.total_bits() for reg in bloq.signature.lefts()) left_qubit_splits = np.cumsum(left_qubit_counts) n_qubits_left = sum(left_qubit_counts) - n_qubits_right = sum(r.total_bits() for r in bloq.signature.rights()) + n_qubits_right = sum(reg.total_bits() for reg in bloq.signature.rights()) matrix = np.zeros((2,) * (n_qubits_right + n_qubits_left)) @@ -38,17 +42,17 @@ def tensor_from_classical_sim(bloq: Bloq) -> NDArray: *inputs_t, last = np.split(input_t, left_qubit_splits) assert np.size(last) == 0 - in_kwargs = { - r.name: _bits_to_classical_reg_data(r, bits) - for r, bits in zip(bloq.signature.lefts(), inputs_t) + input_kwargs = { + reg.name: _bits_to_classical_reg_data(reg, bits) + for reg, bits in zip(bloq.signature.lefts(), inputs_t) } - out_args = bloq.call_classically(**in_kwargs) + output_args = bloq.call_classically(**input_kwargs) - if out_args: + if output_args: output_t = np.concatenate( [ - r.dtype.to_bits_array(np.asarray(vals)).flat - for r, vals in zip(bloq.signature.rights(), out_args) + reg.dtype.to_bits_array(np.asarray(vals)).flat + for reg, vals in zip(bloq.signature.rights(), output_args) ] ) else: diff --git a/qualtran/simulation/tensor/tensor_from_classical_test.py b/qualtran/simulation/tensor/tensor_from_classical_test.py index 80827ec76..2236079ef 100644 --- a/qualtran/simulation/tensor/tensor_from_classical_test.py +++ b/qualtran/simulation/tensor/tensor_from_classical_test.py @@ -21,7 +21,7 @@ @pytest.mark.parametrize("bloq", [XGate(), TwoBitCSwap()], ids=str) -def test_simple(bloq: Bloq): +def test_tensor_consistent_with_classical(bloq: Bloq): from_classical = tensor_from_classical_sim(bloq) from_tensor = bloq.tensor_contract()