diff --git a/README.md b/README.md index e1093d9..b50bc9f 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ English | [简体中文](README_CN.md) - [Install PaddlePaddle](#install-paddlepaddle) - [Install Paddle Quantum](#install-paddle-quantum) - [Environment setup for Quantum Chemistry module](#environment_setup_for_quantum_chemistry_module) - - [Run programs](#run-programs) + - [Run Example](#run-example) - [Introduction and developments](#introduction-and-developments) - [Quick start](#quick-start) - [Tutorials](#tutorials) @@ -18,7 +18,7 @@ English | [简体中文](README_CN.md) - [Copyright and License](#copyright-and-license) - [References](#references) -[Paddle Quantum (量桨)](https://qml.baidu.com/) is a quantum machine learning (QML) toolkit developed based on Baidu PaddlePaddle. It provides a platform to construct and train quantum neural networks (QNNs) with easy-to-use QML development kits supporting combinatorial optimization, quantum chemistry and other cutting-edge quantum applications, making PaddlePaddle the first deep learning framework in China that supports quantum machine learning. +[Paddle Quantum (量桨)](https://qml.baidu.com/) is the world's first cloud-integrated quantum machine learning platform based on Baidu PaddlePaddle. It supports the building and training of quantum neural networks, making PaddlePaddle the first deep learning framework in China. Paddle Quantum is feature-rich and easy to use. It provides comprehensive API documentation and tutorials help users get started right away.

@@ -33,7 +33,7 @@ English | [简体中文](README_CN.md) - + @@ -55,7 +55,7 @@ Paddle Quantum aims at establishing a bridge between artificial intelligence (AI ## Features - Easy-to-use - - Many online learning resources (Nearly 40 tutorials) + - Many online learning resources (Nearly 50 tutorials) - High efficiency in building QNN with various QNN templates - Automatic differentiation - Versatile @@ -71,7 +71,7 @@ Paddle Quantum aims at establishing a bridge between artificial intelligence (AI ### Install PaddlePaddle -This dependency will be automatically satisfied when users install Paddle Quantum. Please refer to [PaddlePaddle](https://www.paddlepaddle.org.cn/install/quick)'s official installation and configuration page. This project requires PaddlePaddle 2.2.0+. +This dependency will be automatically satisfied when users install Paddle Quantum. Please refer to [PaddlePaddle](https://www.paddlepaddle.org.cn/install/quick)'s official installation and configuration page. This project requires PaddlePaddle 2.2.0 to 2.3.0. ### Install Paddle Quantum @@ -151,6 +151,7 @@ We provide tutorials covering quantum simulation, machine learning, combinatoria 8. [Hamiltonian Simulation with Product Formula](./tutorials/quantum_simulation/HamiltonianSimulation_EN.ipynb) 9. [Simulate the Spin Dynamics on a Heisenberg Chain](./tutorials/quantum_simulation/SimulateHeisenberg_EN.ipynb) 10. [Distributed Variational Quantum Eigensolver Based on Schmidt Decomposition](./tutorials/quantum_simulation/DistributedVQE_EN.ipynb) + 11. [Quantum Signal Processing and Quantum Singular Value Transformation](./tutorials/quantum_simulation/QSP_and_QSVT_EN.ipynb) - [Machine Learning](./tutorials/machine_learning) 1. [Encoding Classical Data into Quantum States](./tutorials/machine_learning/DataEncoding_EN.ipynb) @@ -160,6 +161,8 @@ We provide tutorials covering quantum simulation, machine learning, combinatoria 5. [Quantum Autoencoder](./tutorials/machine_learning/QAutoencoder_EN.ipynb) 6. [Quantum GAN](./tutorials/machine_learning/QGAN_EN.ipynb) 7. [Variational Quantum Singular Value Decomposition (VQSVD)](./tutorials/machine_learning/VQSVD_EN.ipynb) + 8. [Data Encoding Analysis](./tutorials/machine_learning/EncodingAnalysis_EN.ipynb) + 9. [Quantum Neural Network Approximating Functions](./tutorials/machine_learning/QApproximating_EN.ipynb) - [Combinatorial Optimization](./tutorials/combinatorial_optimization) 1. [Quantum Approximation Optimization Algorithm (QAOA)](./tutorials/combinatorial_optimization/QAOA_EN.ipynb) diff --git a/README_CN.md b/README_CN.md index d0f18ca..da89185 100644 --- a/README_CN.md +++ b/README_CN.md @@ -19,7 +19,7 @@ - [Copyright and License](#copyright-and-license) - [References](#references) -[Paddle Quantum(量桨)](https://qml.baidu.com/)是基于百度飞桨开发的量子机器学习工具集,支持量子神经网络的搭建与训练,提供易用的量子机器学习开发套件与量子优化、量子化学等前沿量子应用工具集,使得百度飞桨也因此成为国内首个支持量子机器学习的深度学习框架。 +[Paddle Quantum(量桨)](https://qml.baidu.com/)是基于百度飞桨研发的全球首个云量一体的量子机器学习平台。量桨支持量子神经网络的搭建与训练等功能,使得百度飞桨也因此成为国内首个支持量子机器学习的深度学习框架。量桨具备轻松上手、功能丰富等特点,提供了完善的API文档和用例教程,使用户可以快速入门和上手。

@@ -34,7 +34,7 @@ - + @@ -55,7 +55,7 @@ ## 特色 - 轻松上手 - - 丰富的在线学习资源(近 40 篇教程案例) + - 丰富的在线学习资源(近 50 篇教程案例) - 通过模板高效搭建量子神经网络 - 自动微分框架 - 功能丰富 @@ -71,7 +71,7 @@ ### 安装 PaddlePaddle -当用户安装 Paddle Quantum 时会自动下载安装这个关键依赖包。关于 PaddlePaddle 更全面的安装信息请参考 [PaddlePaddle](https://www.paddlepaddle.org.cn/install/quick) 安装配置页面。此项目需求 PaddlePaddle 2.2.0+。 +当用户安装 Paddle Quantum 时会自动下载安装这个关键依赖包。关于 PaddlePaddle 更全面的安装信息请参考 [PaddlePaddle](https://www.paddlepaddle.org.cn/install/quick) 安装配置页面。此项目需求 PaddlePaddle 2.2.0 到 2.3.0。 ### 安装 Paddle Quantum @@ -80,6 +80,7 @@ ```bash pip install paddle-quantum ``` + 用户也可以选择下载全部文件后进行本地安装, ```bash @@ -88,7 +89,6 @@ cd quantum pip install -e . ``` - ### 量子化学模块的环境设置 我们的量子化学模块是基于 `Psi4` 进行开发的,所以在运行量子化学模块之前需要先行安装该 Python 包。 @@ -160,6 +160,8 @@ Paddle Quantum(量桨)建立起了人工智能与量子计算的桥梁,为 8. [利用 Product Formula 模拟时间演化](./tutorials/quantum_simulation/HamiltonianSimulation_CN.ipynb) 9. [模拟一维海森堡链的自旋动力学](./tutorials/quantum_simulation/SimulateHeisenberg_CN.ipynb) 10. [基于施密特分解的分布式变分量子本征求解器](./tutorials/quantum_simulation/DistributedVQE_CN.ipynb) + 11. [量子信号处理与量子奇异值变换](./tutorials/quantum_simulation/QSP_and_QSVT_CN.ipynb) + - [机器学习](./tutorials/machine_learning) 1. [量子态编码经典数据](./tutorials/machine_learning/DataEncoding_CN.ipynb) @@ -169,6 +171,8 @@ Paddle Quantum(量桨)建立起了人工智能与量子计算的桥梁,为 5. [量子变分自编码器(Quantum Autoencoder)](./tutorials/machine_learning/QAutoencoder_CN.ipynb) 6. [量子生成对抗网络(Quantum GAN)](./tutorials/machine_learning/QGAN_CN.ipynb) 7. [变分量子奇异值分解(VQSVD)](./tutorials/machine_learning/VQSVD_CN.ipynb) + 8. [数据编码分析](./tutorials/machine_learning/EncodingAnalysis_CN.ipynb) + 9. [量子神经网络模拟函数](./tutorials/machine_learning/QApproximating_CN.ipynb) - [组合优化](./tutorials/combinatorial_optimization) 1. [量子近似优化算法(QAOA)](./tutorials/combinatorial_optimization/QAOA_CN.ipynb) diff --git a/docs/source/conf.py b/docs/source/conf.py index 691f71f..a4a0839 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -22,7 +22,7 @@ author = 'Baidu Inc' # The full version, including alpha/beta/rc tags -release = '2.2.0' +release = '2.2.1' # -- General configuration --------------------------------------------------- diff --git a/docs/source/introduction.rst b/docs/source/introduction.rst index b413171..21d0396 100644 --- a/docs/source/introduction.rst +++ b/docs/source/introduction.rst @@ -3,7 +3,7 @@ Paddle Quantum ======================= -`Paddle Quantum `__\ is a quantum machine learning toolkit developed based on Baidu PaddlePaddle. It provides a platform to construct and train quantum neural networks (QNN) with easy-to-use QML development kits supporting combinatorial optimization, quantum chemistry and other cutting-edge quantum applications, making PaddlePaddle the first deep learning framework in China that supports quantum machine learning. +`Paddle Quantum `__\ is the world's first cloud-integrated quantum machine learning platform based on Baidu PaddlePaddle. It supports the building and training of quantum neural networks, making PaddlePaddle the first deep learning framework in China. Paddle Quantum is feature-rich and easy to use. It provides comprehensive API documentation and tutorials help users get started right away. .. figure:: https://release-data.cdn.bcebos.com/Paddle%20Quantum.png :target: https://github.com/PaddlePaddle/Quantum @@ -19,7 +19,7 @@ Features - Easy-to-use - - Many online learning resources (Nearly 40 tutorials) + - Many online learning resources (Nearly 50 tutorials) - High efficiency in building QNN with various QNN templates - Automatic differentiation @@ -45,7 +45,7 @@ Install Install PaddlePaddle ~~~~~~~~~~~~~~~~~~~~ -This dependency will be automatically satisfied when users install Paddle Quantum. Please refer to `PaddlePaddle `__ Install and configuration page. This project requires PaddlePaddle 2.2.0+. +This dependency will be automatically satisfied when users install Paddle Quantum. Please refer to `PaddlePaddle `__ Install and configuration page. This project requires PaddlePaddle 2.2.0 to 2.3.0. .. _header-n19: diff --git a/docs/source/locale/en/LC_MESSAGES/introduction.po b/docs/source/locale/en/LC_MESSAGES/introduction.po index 36a1b8c..0464b72 100644 --- a/docs/source/locale/en/LC_MESSAGES/introduction.po +++ b/docs/source/locale/en/LC_MESSAGES/introduction.po @@ -113,7 +113,7 @@ msgid "" "This dependency will be automatically satisfied when users install Paddle" " Quantum. Please refer to `PaddlePaddle " "`__ Install and " -"configuration page. This project requires PaddlePaddle 2.2.0+." +"configuration page. This project requires PaddlePaddle 2.2.0 to 2.3.0." msgstr "" #: ../../source/introduction.rst:53 diff --git a/docs/source/modules.rst b/docs/source/modules.rst index a81010d..dab2a01 100644 --- a/docs/source/modules.rst +++ b/docs/source/modules.rst @@ -22,3 +22,4 @@ paddle_quantum.shadow paddle_quantum.trotter paddle_quantum.visual + paddle_quantum.qsvt \ No newline at end of file diff --git a/docs/source/paddle_quantum.qsvt.qsp.rst b/docs/source/paddle_quantum.qsvt.qsp.rst new file mode 100644 index 0000000..a4efb5d --- /dev/null +++ b/docs/source/paddle_quantum.qsvt.qsp.rst @@ -0,0 +1,7 @@ +paddle\_quantum.qsvt.qsp +============================ + +.. automodule:: paddle_quantum.qsvt.qsp + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/paddle_quantum.qsvt.qsp_utils.rst b/docs/source/paddle_quantum.qsvt.qsp_utils.rst new file mode 100644 index 0000000..fa9986e --- /dev/null +++ b/docs/source/paddle_quantum.qsvt.qsp_utils.rst @@ -0,0 +1,7 @@ +paddle\_quantum.qsvt.qsp\_utils +=============================== + +.. automodule:: paddle_quantum.qsvt.qsp_utils + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/paddle_quantum.qsvt.qsvt.rst b/docs/source/paddle_quantum.qsvt.qsvt.rst new file mode 100644 index 0000000..7880aba --- /dev/null +++ b/docs/source/paddle_quantum.qsvt.qsvt.rst @@ -0,0 +1,7 @@ +paddle\_quantum.qsvt.qsvt +============================ + +.. automodule:: paddle_quantum.qsvt.qsvt + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/paddle_quantum.qsvt.rst b/docs/source/paddle_quantum.qsvt.rst new file mode 100644 index 0000000..451777f --- /dev/null +++ b/docs/source/paddle_quantum.qsvt.rst @@ -0,0 +1,16 @@ +paddle\_quantum.qsvt +============================ + +.. automodule:: paddle_quantum.qsvt + :members: + :undoc-members: + :show-inheritance: + +.. rubric:: Submodules + +.. toctree:: + :maxdepth: 4 + + paddle_quantum.qsvt.qsp_utils + paddle_quantum.qsvt.qsp + paddle_quantum.qsvt.qsvt \ No newline at end of file diff --git a/docs/source/tutorial.rst b/docs/source/tutorial.rst index 219901d..ae0d849 100644 --- a/docs/source/tutorial.rst +++ b/docs/source/tutorial.rst @@ -21,14 +21,14 @@ Tutorials --------- We provide use cases covering quantum simulation, machine learning, combinatorial optimization, local operations and classical communication (LOCC), and other popular quantum machine learning research topics. -Similar to the \ `quick start guide `__\ , you can read each tutorial `online `__ or download and run `Jupyter Notebooks `__\ locally. +Similar to the \ `quick start guide `__\ , you can read each tutorial `online `__ or download and run `Jupyter Notebooks `__\ locally. For interested developers, we recommend them to download Jupyter Notebooks and play around with it. Here are the tutorial categories: -- `Quantum Simulation `__ -- `Machine Learning `__ -- `Combinatorial Optimization `__ -- `LOCCNet `__ -- `QNN Research `__ +- `Quantum Simulation `__ +- `Machine Learning `__ +- `Combinatorial Optimization `__ +- `LOCCNet `__ +- `QNN Research `__ With the latest LOCCNet module, Paddle Quantum can efficiently simulate distributed quantum information processing tasks. Interested readers can start with this `tutorial on LOCCNet `__. In addition, Paddle Quantum supports quantum neural diff --git a/docs_zh_CN/source/conf.py b/docs_zh_CN/source/conf.py index 7697801..bb8e5c9 100644 --- a/docs_zh_CN/source/conf.py +++ b/docs_zh_CN/source/conf.py @@ -22,7 +22,7 @@ author = 'Baidu Inc' # The full version, including alpha/beta/rc tags -release = '2.2.0' +release = '2.2.1' # -- General configuration --------------------------------------------------- diff --git a/docs_zh_CN/source/introduction.rst b/docs_zh_CN/source/introduction.rst index 5e6f534..9eba1a9 100644 --- a/docs_zh_CN/source/introduction.rst +++ b/docs_zh_CN/source/introduction.rst @@ -3,7 +3,7 @@ Paddle Quantum(量桨) ======================= -`Paddle Quantum(量桨) `__\ 是基于百度飞桨开发的量子机器学习工具集,支持量子神经网络的搭建与训练,提供易用的量子机器学习开发套件与量子优化、量子化学等前沿量子应用工具集,使得百度飞桨也因此成为国内首个支持量子机器学习的深度学习框架。 +`Paddle Quantum(量桨) `__\ 是基于百度飞桨研发的全球首个云量一体的量子机器学习平台。量桨支持量子神经网络的搭建与训练等功能,使得百度飞桨也因此成为国内首个支持量子机器学习的深度学习框架。量桨具备轻松上手、功能丰富等特点,提供了完善的API文档和用例教程,使用户可以快速入门和上手。 .. figure:: https://release-data.cdn.bcebos.com/Paddle%20Quantum.png :target: https://github.com/PaddlePaddle/Quantum @@ -19,7 +19,7 @@ Paddle Quantum(量桨) - 轻松上手 - - 丰富的在线学习资源(近 40 篇教程案例) + - 丰富的在线学习资源(近 50 篇教程案例) - 通过模板高效搭建量子神经网络 - 自动微分框架 @@ -47,7 +47,7 @@ Paddle Quantum(量桨) 当用户安装 Paddle Quantum 时会自动下载安装这个关键依赖包。关于 PaddlePaddle 更全面的安装信息请参考 `PaddlePaddle `__ -安装配置页面。此项目需求 PaddlePaddle 2.2.0+。 +安装配置页面。此项目需求 PaddlePaddle 2.2.0 到 2.3.0。 .. _header-n19: diff --git a/docs_zh_CN/source/modules.rst b/docs_zh_CN/source/modules.rst index a81010d..9d4382d 100644 --- a/docs_zh_CN/source/modules.rst +++ b/docs_zh_CN/source/modules.rst @@ -22,3 +22,4 @@ paddle_quantum.shadow paddle_quantum.trotter paddle_quantum.visual + paddle_quantum.qsvt diff --git a/docs_zh_CN/source/paddle_quantum.ansatz.circuit.rst b/docs_zh_CN/source/paddle_quantum.ansatz.circuit.rst index 8da5b5e..3326916 100644 --- a/docs_zh_CN/source/paddle_quantum.ansatz.circuit.rst +++ b/docs_zh_CN/source/paddle_quantum.ansatz.circuit.rst @@ -3,7 +3,7 @@ paddle\_quantum.ansatz.circuit 量子电路类的功能实现。 -.. py:class:: Circuit(num_qubits = None) +.. py:class:: Circuit(num_qubits=None) 基类: :py:class:`paddle_quantum.ansatz.container.Sequential` @@ -28,7 +28,11 @@ paddle\_quantum.ansatz.circuit 展平后的电路参数梯度。 - .. py:method:: update_param(theta, idx = None) + .. py:property:: depth() + + 电路深度 + + .. py:method:: update_param(theta, idx=None) 替换单层或所有的电路参数。 @@ -39,7 +43,11 @@ paddle\_quantum.ansatz.circuit :raises ValueError: 索引必须是整数或者 None。 - .. py:method:: randomize_param(self, low = 0, high = 2 * pi) + .. py:method:: transfer_static() + + 将该线路的所有参数的 ``stop_grdient`` 设为 ``True`` + + .. py:method:: randomize_param(low=0, high=2 * pi) 在 ``[low, high)`` 的范围内随机化电路参数 @@ -48,7 +56,7 @@ paddle\_quantum.ansatz.circuit :param high: 随机参数的上界, 默认为 ``2*pi``。 :type high: float, optional - .. py:method:: h(qubits_idx = 'full', num_qubits = None, depth = 1) + .. py:method:: h(qubits_idx='full', num_qubits=None, depth=1) 添加一个单量子比特的 Hadamard 门。 @@ -69,7 +77,7 @@ paddle\_quantum.ansatz.circuit :param depth: 层数,默认为 ``1``。 :type depth: int, optional - .. py:method:: s(qubits_idx = 'full', num_qubits = None, depth = 1) + .. py:method:: s(qubits_idx='full', num_qubits=None, depth=1) 添加单量子比特 S 门。 @@ -90,7 +98,7 @@ paddle\_quantum.ansatz.circuit :param depth: 层数,默认为 ``1``。 :type depth: int, optional - .. py:method:: t(qubits_idx = 'full', num_qubits = None, depth = 1) + .. py:method:: t(qubits_idx='full', num_qubits=None, depth=1) 添加单量子比特 T 门。 @@ -111,7 +119,7 @@ paddle\_quantum.ansatz.circuit :param depth: 层数,默认为 ``1``。 :type depth: int, optional - .. py:method:: x(qubits_idx = 'full', num_qubits = None, depth = 1) + .. py:method:: x(qubits_idx='full', num_qubits=None, depth=1) 添加单量子比特 X 门。 @@ -701,7 +709,7 @@ paddle\_quantum.ansatz.circuit :param depth: 层数,默认为 ``1``。 :type depth: int, optional - .. py:method:: ccx(qubits_idx = 'cycle', num_qubits = None, depth = 1) + .. py:method:: ccx(qubits_idx='cycle', num_qubits=None, depth=1) 添加 CCX 门。 @@ -771,6 +779,8 @@ paddle\_quantum.ansatz.circuit :type num_qubits: int, optional :param depth: 层数,默认为 ``1``。 :type depth: int, optional + :param gate_name: oracle 的名字,默认为 ``O``。 + :type gate_name: str, optional .. py:method:: control_oracle(oracle, qubits_idx, num_qubits=None, depth=1) @@ -784,8 +794,28 @@ paddle\_quantum.ansatz.circuit :type num_qubits: int, optional :param depth: 层数,默认为 ``1``。 :type depth: int, optional + :param gate_name: oracle 的名字,默认为 ``cO``。 + :type gate_name: str, optional + + .. py:method:: collapse(qubits_idx='full', num_qubits=None, desired_result=None, if_print=False, measure_basis='z') + + 添加一个坍缩算子 + + :param qubits_idx: 作用的量子比特的编号。 + :type qubits_idx: Union[Iterable[Iterable[int]], Iterable[int], int] + :param num_qubits: 总共的量子比特数量,默认为 ``None``。 + :type num_qubits: int, optional + :param desired_result: 期望的坍缩态(现只支持输入计算基),默认为 ``None`` (随机坍缩)。 + :type desired_result: Union[int, str] + :param if_print: 是否要打印坍缩的信息,默认为 ``True``。 + :type if_print: bool, optional + :param measure_basis: 要观测的测量基底,默认为 ``z``。 + :type measure_basis: Union[Iterable[paddle.Tensor], str] + + :raises NotImplementdError: 要观测的测量基底只能为 ``z``,其他测量基底会在之后推出。 + :raises TypeError: 当 ``backend`` 为 ``unitary_matrix`` 时,无法获取输入态的概率。 - .. py:method:: superposition_layer(qubits_idx = 'full', num_qubits = None, depth = 1) + .. py:method:: superposition_layer(qubits_idx='full', num_qubits=None, depth=1) 添加一个 Hadamard 门组成的层。 @@ -796,7 +826,7 @@ paddle\_quantum.ansatz.circuit :param depth: 层数,默认为 ``1``。 :type depth: int, optional - .. py:method:: weak_superposition_layer(qubits_idx = 'full', num_qubits = None, depth = 1) + .. py:method:: weak_superposition_layer(qubits_idx='full', num_qubits=None, depth=1) 转角度为 :math:`\pi/4` 的 Ry 门组成的层。 @@ -807,7 +837,7 @@ paddle\_quantum.ansatz.circuit :param depth: 层数,默认为 ``1``。 :type depth: int, optional - .. py:method:: linear_entangled_layer(qubits_idx = 'full', num_qubits = None, depth = 1) + .. py:method:: linear_entangled_layer(qubits_idx='full', num_qubits=None, depth=1) 包含 Ry 门、Rz 门,和 CNOT 门的线性纠缠层。 @@ -818,7 +848,7 @@ paddle\_quantum.ansatz.circuit :param depth: 层数,默认为 ``1``。 :type depth: int, optional - .. py:method:: real_entangled_layer(qubits_idx = 'full', num_qubits = None, depth = 1) + .. py:method:: real_entangled_layer(qubits_idx='full', num_qubits=None, depth=1) 包含 Ry 门和 CNOT 门的强纠缠层。 @@ -829,7 +859,7 @@ paddle\_quantum.ansatz.circuit :param depth: 层数,默认为 ``1``。 :type depth: int, optional - .. py:method:: complex_entangled_layer(qubits_idx = 'full', num_qubits = None, depth = 1) + .. py:method:: complex_entangled_layer(qubits_idx='full', num_qubits=None, depth=1) 包含 U3 门和 CNOT 门的强纠缠层。 @@ -840,7 +870,7 @@ paddle\_quantum.ansatz.circuit :param depth: 层数,默认为 ``1``。 :type depth: int, optional - .. py:method:: real_block_layer(qubits_idx = 'full', num_qubits = None, depth = 1) + .. py:method:: real_block_layer(qubits_idx='full', num_qubits=None, depth=1) 包含 Ry 门和 CNOT 门的弱纠缠层。 @@ -851,7 +881,7 @@ paddle\_quantum.ansatz.circuit :param depth: 层数,默认为 ``1``。 :type depth: int, optional - .. py:method:: complex_block_layer(qubits_idx = 'full', num_qubits = None, depth = 1) + .. py:method:: complex_block_layer(qubits_idx='full', num_qubits=None, depth=1) 包含 U3 门和 CNOT 门的弱纠缠层。 @@ -862,7 +892,7 @@ paddle\_quantum.ansatz.circuit :param depth: 层数,默认为 ``1``。 :type depth: int, optional - .. py:method:: bit_flip(prob, qubits_idx = 'full', num_qubits = None) + .. py:method:: bit_flip(prob, qubits_idx='full', num_qubits=None) 添加比特反转信道。 @@ -873,7 +903,7 @@ paddle\_quantum.ansatz.circuit :param num_qubits: 总的量子比特个数,默认为 ``None``。 :type num_qubits: int, optional - .. py:method:: phase_flip(prob, qubits_idx = 'full', num_qubits = None) + .. py:method:: phase_flip(prob, qubits_idx='full', num_qubits=None) 添加相位反转信道。 @@ -884,7 +914,7 @@ paddle\_quantum.ansatz.circuit :param num_qubits: 总的量子比特个数,默认为 ``None``。 :type num_qubits: int, optional - .. py:method:: bit_phase_flip(prob, qubits_idx = 'full', num_qubits = None) + .. py:method:: bit_phase_flip(prob, qubits_idx='full', num_qubits=None) 添加比特相位反转信道。 @@ -895,7 +925,7 @@ paddle\_quantum.ansatz.circuit :param num_qubits: 总的量子比特个数,默认为 ``None``。 :type num_qubits: int, optional - .. py:method:: amplitude_damping(gamma, qubits_idx = 'full', num_qubits = None) + .. py:method:: amplitude_damping(gamma, qubits_idx='full', num_qubits=None) 添加振幅阻尼信道。 @@ -906,7 +936,7 @@ paddle\_quantum.ansatz.circuit :param num_qubits: 总的量子比特个数,默认为 ``None``。 :type num_qubits: int, optional - .. py:method:: generalized_amplitude_damping(gamma, qubits_idx = 'full', num_qubits = None) + .. py:method:: generalized_amplitude_damping(gamma, qubits_idx='full', num_qubits=None) 添加广义振幅阻尼信道。 @@ -919,7 +949,7 @@ paddle\_quantum.ansatz.circuit :param num_qubits: 总的量子比特个数,默认为 ``None``。 :type num_qubits: int, optional - .. py:method:: phase_damping(gamma, qubits_idx = 'full', num_qubits = None) + .. py:method:: phase_damping(gamma, qubits_idx='full', num_qubits=None) 添加相位阻尼信道。 @@ -930,7 +960,7 @@ paddle\_quantum.ansatz.circuit :param num_qubits: 总的量子比特个数,默认为 ``None``。 :type num_qubits: int, optional - .. py:method:: depolarizing(prob, qubits_idx = 'full', num_qubits = None) + .. py:method:: depolarizing(prob, qubits_idx='full', num_qubits=None) 添加去极化信道。 @@ -941,7 +971,7 @@ paddle\_quantum.ansatz.circuit :param num_qubits: 总的量子比特个数,默认为 ``None``。 :type num_qubits: int, optional - .. py:method:: pauli_channel(prob, qubits_idx = 'full', num_qubits = None) + .. py:method:: pauli_channel(prob, qubits_idx='full', num_qubits=None) 添加泡利信道。 @@ -952,7 +982,7 @@ paddle\_quantum.ansatz.circuit :param num_qubits: 总的量子比特个数,默认为 ``None``。 :type num_qubits: int, optional - .. py:method:: reset_channel(prob, qubits_idx = 'full', num_qubits = None) + .. py:method:: reset_channel(prob, qubits_idx='full', num_qubits=None) 添加重置信道。 @@ -963,7 +993,7 @@ paddle\_quantum.ansatz.circuit :param num_qubits: 总的量子比特个数,默认为 ``None``。 :type num_qubits: int, optional - .. py:method:: thermal_relaxation(const_t, exec_time, qubits_idx = 'full', num_qubits = None) + .. py:method:: thermal_relaxation(const_t, exec_time, qubits_idx='full', num_qubits=None) 添加热弛豫信道。 @@ -976,7 +1006,7 @@ paddle\_quantum.ansatz.circuit :param num_qubits: 总的量子比特个数,默认为 ``None``。 :type num_qubits: int, optional - .. py:method:: kraus_repr(kraus_oper, qubits_idx, num_qubits = None) + .. py:method:: kraus_repr(kraus_oper, qubits_idx, num_qubits=None) 添加一个 Kraus 表示的自定义量子信道。 @@ -987,7 +1017,7 @@ paddle\_quantum.ansatz.circuit :param num_qubits: 总的量子比特个数,默认为 ``None``。 :type num_qubits: int, optional - .. py:method:: unitary_matrix(num_qubits = None) + .. py:method:: unitary_matrix(num_qubits=None) 电路的酉矩阵形式 diff --git a/docs_zh_CN/source/paddle_quantum.ansatz.vans.rst b/docs_zh_CN/source/paddle_quantum.ansatz.vans.rst index 18d3b80..437f92f 100644 --- a/docs_zh_CN/source/paddle_quantum.ansatz.vans.rst +++ b/docs_zh_CN/source/paddle_quantum.ansatz.vans.rst @@ -41,16 +41,18 @@ paddle\_quantum.ansatz.vans .. py:function:: cir_decompose(cir) - 将电路中的 Layer 分解成量子门。 + 将电路中的 Layer 分解成量子门, 如果需要的话可以把所有参数门的输入转为可训练参数 - :param cir: 待分解电路。 + :param cir: 待分解电路 :type cir: Circuit - :return: 分解后的电路。 + :param trainable: 是否将分解后的参数量子门输入转为参数 + :type trainable: bool, optional + :return: 分解后的电路 :rtype: Circuit .. note:: - 该函数不支持自定义门,如 oracle 和 control-oracle。 + 该量子电路稳定支持原生门,不支持 oracle 等其他自定义量子门。 .. py:class:: VAns(n, loss_func, *loss_func_args, epsilon=0.1, insert_rate=2, iter=100, iter_out=10, LR =0.1, threshold=0.002, accept_wall=100, zero_init_state=True) diff --git a/docs_zh_CN/source/paddle_quantum.dataset.rst b/docs_zh_CN/source/paddle_quantum.dataset.rst index 1e3cedf..b307b2f 100644 --- a/docs_zh_CN/source/paddle_quantum.dataset.rst +++ b/docs_zh_CN/source/paddle_quantum.dataset.rst @@ -175,7 +175,7 @@ paddle\_quantum.dataset :param dimension: 编码数据的维度。 :type dimension: int - .. py:method:: encode(feature, encoding, num_qubits, return_state = True, full_return = False) + .. py:method:: encode(feature, encoding, num_qubits, return_state=True, full_return=False) 用 ``num_qubits`` 的量子比特对 ``feature`` 进行编码 ``encoding``。 diff --git a/docs_zh_CN/source/paddle_quantum.fisher.rst b/docs_zh_CN/source/paddle_quantum.fisher.rst index 361e207..f08f45c 100644 --- a/docs_zh_CN/source/paddle_quantum.fisher.rst +++ b/docs_zh_CN/source/paddle_quantum.fisher.rst @@ -113,7 +113,7 @@ Fisher 信息的功能实现。 :return: 量子费舍信息矩阵的秩 :rtype: int -.. py:class:: ClassicalFisher(model, num_thetas, num_inputs, model_type = 'quantum', **kwargs) +.. py:class:: ClassicalFisher(model, num_thetas, num_inputs, model_type='quantum', **kwargs) :param model: 经典或量子神经网络模型的实例 :type model: paddle.nn.Layer @@ -164,7 +164,7 @@ Fisher 信息的功能实现。 :rtype: Tuple[np.ndarray, float] - .. py:method:: get_eff_dim(normalized_cfisher, list_num_samples, gamma = 1) + .. py:method:: get_eff_dim(normalized_cfisher, list_num_samples, gamma=1) 计算经典的有效维数 diff --git a/docs_zh_CN/source/paddle_quantum.gate.base.rst b/docs_zh_CN/source/paddle_quantum.gate.base.rst index c2bfc56..521785d 100644 --- a/docs_zh_CN/source/paddle_quantum.gate.base.rst +++ b/docs_zh_CN/source/paddle_quantum.gate.base.rst @@ -19,3 +19,38 @@ paddle\_quantum.gate.base 参数名为"mylayer_0.w_n",其中 "w" 是参数的名称,"n" 为自动生成的具有唯一性的后缀。如果为 ``None``, 前缀名将为小写的类名。默认为 ``None``。 :type name_scope: str, optional + + .. py:method:: gate_history_generation() + + 生成量子门的历史记录 + +.. py:class:: ParamGate + + 基类::py:class:`paddle_quantum.gate.base.Gate` + + 可参数化量子门的基类。 + + .. py:method:: theta_generation(param, param_shape) + + 规范可参数化量子门的输入,并根据输入决定是否要管理或者生成参数 + + :param param: 可参数化量子门的输入 + :type param: Union[paddle.Tensor, float, List[float]] + :param param_shape: 输入的形状 + :type param_shape: List[int] + + .. note:: + + 在以下情况 ``param`` 会被转为一个参数 + - ``param`` 是 ``None`` + 在以下情况 ``param`` 会被记录为一个参数 + - ``param`` 是 `ParamBase` + 在以下情况 ``param`` 会保持不变 + - ``param`` 是一个 `paddle.Tensor` 但不是 `ParamBase` + - ``param`` 是一个 `float` 或者 `List[float]` + + .. py:method:: gate_history_generation() + + 生成可参数化量子门的历史记录 + + diff --git a/docs_zh_CN/source/paddle_quantum.gate.custom.rst b/docs_zh_CN/source/paddle_quantum.gate.custom.rst index 135b061..fd9d10f 100644 --- a/docs_zh_CN/source/paddle_quantum.gate.custom.rst +++ b/docs_zh_CN/source/paddle_quantum.gate.custom.rst @@ -3,7 +3,7 @@ paddle\_quantum.gate.custom 自定义量子门和受控量子门的功能实现。 -.. py:class:: Oracle(oracle, qubits_idx, num_qubits=None, depth=1) +.. py:class:: Oracle(oracle, qubits_idx, num_qubits=None, depth=1, gate_name='O') 基类::py:class:`paddle_quantum.gate.base.Gate` @@ -17,8 +17,10 @@ paddle\_quantum.gate.custom :type num_qubits: int, optional :param depth: 层数,默认为 ``1``。 :type depth: int, optional + :param gate_name: oracle 的名字,默认为 ``O``。 + :type gate_name: str, optional -.. py:class:: ControlOracle(oracle, qubits_idx, num_qubits=None, depth=1) +.. py:class:: ControlOracle(oracle, qubits_idx, num_qubits=None, depth=1, gate_name='cO') 基类::py:class:`paddle_quantum.gate.base.Gate` @@ -32,3 +34,5 @@ paddle\_quantum.gate.custom :type num_qubits: int, optional :param depth: 层数,默认为 ``1``。 :type depth: int, optional + :param gate_name: oracle 的名字,默认为 ``cO``。 + :type gate_name: str, optional diff --git a/docs_zh_CN/source/paddle_quantum.gate.layer.rst b/docs_zh_CN/source/paddle_quantum.gate.layer.rst index c378e89..d2b7d43 100644 --- a/docs_zh_CN/source/paddle_quantum.gate.layer.rst +++ b/docs_zh_CN/source/paddle_quantum.gate.layer.rst @@ -124,3 +124,31 @@ paddle\_quantum.gate.layer :type depth: int, optional .. py:class:: QAOALayer(Gate) + + 基类::py:class:`paddle_quantum.gate.base.Gate` + + QAOA 驱动层 + + .. note:: + + 仅支持 MaxCut 问题 + + :param edges: 图的边 + :type edges: Iterable + :param nodes: 图的节点 + :type nodes: Iterable + :param depth: 层数,默认为 ``1``。 + :type depth: int, optional + +.. py:class:: QAOALayer(Gate) + + 基类::py:class:`paddle_quantum.gate.base.Gate` + + 带权重的 QAOA 驱动层 + + :param edges: 带权重的图的边 + :type edges: Dict[Tuple[int, int], float] + :param nodes: 带权重的图的节点 + :type nodes: Dict[int, float] + :param depth: 层数,默认为 ``1``。 + :type depth: int, optional diff --git a/docs_zh_CN/source/paddle_quantum.gate.multi_qubit_gate.rst b/docs_zh_CN/source/paddle_quantum.gate.multi_qubit_gate.rst index faca52e..59e3512 100644 --- a/docs_zh_CN/source/paddle_quantum.gate.multi_qubit_gate.rst +++ b/docs_zh_CN/source/paddle_quantum.gate.multi_qubit_gate.rst @@ -129,7 +129,7 @@ paddle\_quantum.gate.multi\_qubit\_gate .. py:class:: CP(qubits_idx='cycle', num_qubits=None, depth=1, param=None, param_sharing=False) - 基类::py:class:`paddle_quantum.gate.base.Gate` + 基类::py:class:`paddle_quantum.gate.base.ParamGate` 受控 P 门。 @@ -158,7 +158,7 @@ paddle\_quantum.gate.multi\_qubit\_gate .. py:class:: CRX(qubits_idx='cycle', num_qubits=None, depth=1, param=None, param_sharing=False) - 基类::py:class:`paddle_quantum.gate.base.Gate` + 基类::py:class:`paddle_quantum.gate.base.ParamGate` 关于 x 轴的受控单量子比特旋转门。 @@ -191,7 +191,7 @@ paddle\_quantum.gate.multi\_qubit\_gate .. py:class:: CRY(qubits_idx='cycle', num_qubits=None, depth=1, param=None, param_sharing=False) - 基类::py:class:`paddle_quantum.gate.base.Gate` + 基类::py:class:`paddle_quantum.gate.base.ParamGate` 关于 y 轴的受控单量子比特旋转门。 @@ -224,7 +224,7 @@ paddle\_quantum.gate.multi\_qubit\_gate .. py:class:: CRZ(qubits_idx='cycle', num_qubits=None, depth=1, param=None, param_sharing=False) - 基类::py:class:`paddle_quantum.gate.base.Gate` + 基类::py:class:`paddle_quantum.gate.base.ParamGate` 关于 z 轴的受控单量子比特旋转门。 @@ -257,7 +257,7 @@ paddle\_quantum.gate.multi\_qubit\_gate .. py:class:: CU(qubits_idx='cycle', num_qubits=None, depth=1, param=None, param_sharing=False) - 基类::py:class:`paddle_quantum.gate.base.Gate` + 基类::py:class:`paddle_quantum.gate.base.ParamGate` 受控单量子比特旋转门。 @@ -290,7 +290,7 @@ paddle\_quantum.gate.multi\_qubit\_gate .. py:class:: RXX(qubits_idx='cycle', num_qubits=None, depth=1, param=None, param_sharing=False) - 基类::py:class:`paddle_quantum.gate.base.Gate` + 基类::py:class:`paddle_quantum.gate.base.ParamGate` RXX 门。 @@ -322,7 +322,7 @@ paddle\_quantum.gate.multi\_qubit\_gate .. py:class:: RYY(qubits_idx='cycle', num_qubits=None, depth=1, param=None, param_sharing=False) - 基类::py:class:`paddle_quantum.gate.base.Gate` + 基类::py:class:`paddle_quantum.gate.base.ParamGate` RYY 门。 @@ -354,7 +354,7 @@ paddle\_quantum.gate.multi\_qubit\_gate .. py:class:: RZZ(qubits_idx='cycle', num_qubits=None, depth=1, param=None, param_sharing=False) - 基类::py:class:`paddle_quantum.gate.base.Gate` + 基类::py:class:`paddle_quantum.gate.base.ParamGate` RZZ 门。 @@ -474,7 +474,7 @@ paddle\_quantum.gate.multi\_qubit\_gate .. py:class:: UniversalTwoQubits(qubits_idx='cycle', num_qubits=None, depth=1, param=None, param_sharing=False) - 基类::py:class:`paddle_quantum.gate.base.Gate` + 基类::py:class:`paddle_quantum.gate.base.ParamGate` 两量子比特通用门,该通用门需要 15 个参数。 @@ -492,7 +492,7 @@ paddle\_quantum.gate.multi\_qubit\_gate .. py:class:: UniversalThreeQubits(qubits_idx='cycle', num_qubits=None, depth=1, param=None, param_sharing=False) - 基类::py:class:`paddle_quantum.gate.base.Gate` + 基类::py:class:`paddle_quantum.gate.base.ParamGate` 三量子比特通用门,该通用门需要 81 个参数。 diff --git a/docs_zh_CN/source/paddle_quantum.gate.single_qubit_gate.rst b/docs_zh_CN/source/paddle_quantum.gate.single_qubit_gate.rst index a560aa7..9bc63b9 100644 --- a/docs_zh_CN/source/paddle_quantum.gate.single_qubit_gate.rst +++ b/docs_zh_CN/source/paddle_quantum.gate.single_qubit_gate.rst @@ -140,7 +140,7 @@ paddle\_quantum.gate.single\_qubit\_gate .. py:class:: P(qubits_idx='full', num_qubits=None, depth=1, param=None, param_sharing=False) - 基类::py:class:`paddle_quantum.gate.base.Gate` + 基类::py:class:`paddle_quantum.gate.base.ParamGate` 单量子比特 P 门。 @@ -167,7 +167,7 @@ paddle\_quantum.gate.single\_qubit\_gate .. py:class:: RX(qubits_idx='full', num_qubits=None, depth=1, param=None, param_sharing=False) - 基类::py:class:`paddle_quantum.gate.base.Gate` + 基类::py:class:`paddle_quantum.gate.base.ParamGate` 关于 x 轴的单量子比特旋转门。 @@ -194,7 +194,7 @@ paddle\_quantum.gate.single\_qubit\_gate .. py:class:: RY(qubits_idx='full', num_qubits=None, depth=1, param=None, param_sharing=False) - 基类::py:class:`paddle_quantum.gate.base.Gate` + 基类::py:class:`paddle_quantum.gate.base.ParamGate` 关于 y 轴的单量子比特旋转门。 @@ -221,7 +221,7 @@ paddle\_quantum.gate.single\_qubit\_gate .. py:class:: RZ(qubits_idx='full', num_qubits=None, depth=1, param=None, param_sharing=False) - 基类::py:class:`paddle_quantum.gate.base.Gate` + 基类::py:class:`paddle_quantum.gate.base.ParamGate` 关于 z 轴的单量子比特旋转门。 @@ -248,7 +248,7 @@ paddle\_quantum.gate.single\_qubit\_gate .. py:class:: U3(qubits_idx='full', num_qubits=None, depth=1, param=None, param_sharing=False) - 基类::py:class:`paddle_quantum.gate.base.Gate` + 基类::py:class:`paddle_quantum.gate.base.ParamGate` 单量子比特旋转门。 diff --git a/docs_zh_CN/source/paddle_quantum.gradtool.rst b/docs_zh_CN/source/paddle_quantum.gradtool.rst index bb4586a..0dbfa04 100644 --- a/docs_zh_CN/source/paddle_quantum.gradtool.rst +++ b/docs_zh_CN/source/paddle_quantum.gradtool.rst @@ -98,6 +98,8 @@ paddle\_quantum.gradtool :param \*args: 用于损失函数计算的额外参数列表。 :type \*args: Any + :raise Exception: 训练数据必须是 ``paddle.Tensor`` 类型 + :return: 包含如下两个元素: - loss_list: 损失函数值随训练次数变化的列表。 - grad_list: 各参数梯度随训练次变化的列表。 @@ -130,6 +132,8 @@ paddle\_quantum.gradtool :param \*args: 用于损失函数计算的额外参数列表。 :type \*args: Any + :raise Exception: 训练数据必须是 ``paddle.Tensor`` 类型 + .. note:: 在本函数中提供了三种计算模式,``mode`` 分别可以选择 ``'single'``, ``'max'``, 以及 ``'random'``。 - mode='single': 表示计算电路中的每个可变参数梯度的平均值和方差。 diff --git a/docs_zh_CN/source/paddle_quantum.hamiltonian.rst b/docs_zh_CN/source/paddle_quantum.hamiltonian.rst index daeb076..44374f3 100644 --- a/docs_zh_CN/source/paddle_quantum.hamiltonian.rst +++ b/docs_zh_CN/source/paddle_quantum.hamiltonian.rst @@ -75,7 +75,7 @@ paddle\_quantum.hamiltonian - pauli_words: 元素为每一项的泡利字符串,例如 'Z0, Z1, X3' 这一项的泡利字符串为 'ZZIX'。 :rtype: Tuple[list] - .. py:method:: construct_h_matrix(qubit_num = None) + .. py:method:: construct_h_matrix(qubit_num=None) 构建 Hamiltonian 在 Z 基底下的矩阵。 diff --git a/docs_zh_CN/source/paddle_quantum.linalg.rst b/docs_zh_CN/source/paddle_quantum.linalg.rst index b18b0c0..52d9cda 100644 --- a/docs_zh_CN/source/paddle_quantum.linalg.rst +++ b/docs_zh_CN/source/paddle_quantum.linalg.rst @@ -27,7 +27,7 @@ paddle\_quantum.linalg 验证矩阵 ``P`` 是否为厄密矩阵 - :param mat: 矩阵 + :param mat: 厄密矩阵 :type mat: paddle.Tensor :param eps: 容错率 :type eps: float, optional @@ -39,7 +39,7 @@ paddle\_quantum.linalg 验证矩阵 ``P`` 是否为映射算子 - :param mat: 矩阵 + :param mat: 映射算子 :type mat: paddle.Tensor :param eps: 容错率 :type eps: float, optional @@ -47,11 +47,11 @@ paddle\_quantum.linalg :return: 决定是否 :math:`PP - P = 0` :rtype: bool -.. py:function:: is_unitary(mat, eps = 1e-5) +.. py:function:: is_unitary(mat, eps=1e-5) 验证矩阵 ``P`` 是否为酉矩阵 - :param mat: 矩阵 + :param mat: 酉矩阵 :type mat: paddle.Tensor :param eps: 容错率 :type eps: float, optional @@ -66,7 +66,7 @@ paddle\_quantum.linalg :param num_qubits: 量子比特数 n :type num_qubits: int - :return: 一个 :math:`2^n \times 2^n` 厄密矩阵 + :return: 一个 :math:`2^n \times 2^n` 厄密矩阵 (n 为量子比特数) :rtype: paddle.Tensor .. py:function:: orthogonal_projection_random(num_qubits) @@ -76,37 +76,49 @@ paddle\_quantum.linalg :param num_qubits: 量子比特数 n :type num_qubits: int - :return: 一个 :math:`2^n \times 2^n` 正交投影算子 + :return: 一个 :math:`2^n \times 2^n` 正交投影算子 (n 为量子比特数) :rtype: paddle.Tensor -.. py:function:: unitary_hermitian_random(num_qubits) +.. py:function:: density_matrix_random(num_qubits) - 随机生成一个厄密酉矩阵 + 随机生成一个密度矩阵 :param num_qubits: 量子比特数 n :type num_qubits: int - :return: 一个 :math:`2^n \times 2^n` 厄密共轭酉矩阵 + :return: 一个 :math:`2^n \times 2^n` 密度矩阵 (n 为量子比特数) :rtype: paddle.Tensor -.. py:function:: unitary_random_with_hermitian_block(num_qubits) +.. py:function:: unitary_random(num_qubits) - 随机生成一个左上半部分为厄密矩阵的酉矩阵 + 随机生成一个酉矩阵 :param num_qubits: 量子比特数 n :type num_qubits: int - :return: 一个左上半部分为厄密矩阵的 :math:`2^n \times 2^n` 酉矩阵 + :return: 一个 :math:`2^n \times 2^n` 酉矩阵 (n 为量子比特数) :rtype: paddle.Tensor -.. py:function:: unitary_random(num_qubits) +.. py:function:: unitary_hermitian_random(num_qubits) - 随机生成一个酉矩阵 + 随机生成一个厄密酉矩阵 + + :param num_qubits: 量子比特数 n + :type num_qubits: int + + :return: 一个 :math:`2^n \times 2^n` 厄密共轭酉矩阵 (n 为量子比特数) + :rtype: paddle.Tensor + +.. py:function:: unitary_random_with_hermitian_block(num_qubits, is_unitary) + + 随机生成一个左上半部分为厄密矩阵的酉矩阵 :param num_qubits: 量子比特数 n :type num_qubits: int + :param is_unitary: 厄密矩阵块是否是酉矩阵的 1/2 + :type is_unitary: bool - :return: 一个 :math:`2^n \times 2^n` 酉矩阵 + :return: 一个左上半部分为厄密矩阵的 :math:`2^n \times 2^n` 酉矩阵 (n 为量子比特数) :rtype: paddle.Tensor .. py:function:: haar_orthogonal(num_qubits) @@ -116,7 +128,7 @@ paddle\_quantum.linalg :param num_qubits: 量子比特数 n :type num_qubits: int - :return: 一个 :math:`2^n \times 2^n` 正交矩阵 + :return: 一个 :math:`2^n \times 2^n` 正交矩阵 (n 为量子比特数) :rtype: paddle.Tensor .. py:function:: haar_unitary(num_qubits) @@ -126,7 +138,7 @@ paddle\_quantum.linalg :param num_qubits: 量子比特数 n :type num_qubits: int - :return: 一个 :math:`2^n \times 2^n` 酉矩阵 + :return: 一个 :math:`2^n \times 2^n` 酉矩阵 (n 为量子比特数) :rtype: paddle.Tensor .. py:function:: haar_state_vector(num_qubits, is_real=False) @@ -138,7 +150,7 @@ paddle\_quantum.linalg :param is_real: 生成的态矢量是否为实数 :type is_real: bool, optional - :return: 一个 :math:`2^n \times 1` 态矢量 + :return: 一个 :math:`2^n \times 1` 态矢量 (n 为量子比特数) :rtype: paddle.Tensor .. py:function:: haar_density_operator(num_qubits, rank=None, is_real=False) @@ -152,7 +164,7 @@ paddle\_quantum.linalg :param is_real: 生成的态矢量是否为实数 :type is_real: bool, optional - :return: 一个 :math:`2^n x 2^n` 密度矩阵 + :return: 一个 :math:`2^n \times 2^n` 密度矩阵 (n 为量子比特数) :rtype: paddle.Tensor .. py:function:: NKron(matrix_A, matrix_B, *args) @@ -160,11 +172,11 @@ paddle\_quantum.linalg 计算两个及以上的矩阵的克罗内克乘积 :param matrix_A: 矩阵 - :type num_qubits: np.ndarray + :type num_qubits: Union[np.ndarray, paddle.Tensor] :param matrix_B: 矩阵 - :type matrix_B: np.ndarray + :type matrix_B: Union[np.ndarray, paddle.Tensor] :param \*args: 更多矩阵 - :type \*args: np.ndarray + :type \*args: Union[np.ndarray, paddle.Tensor] :return: 克罗内克乘积 - :rtype: np.ndarray + :rtype: Union[np.ndarray, paddle.Tensor] diff --git a/docs_zh_CN/source/paddle_quantum.loss.measure.rst b/docs_zh_CN/source/paddle_quantum.loss.measure.rst index f084e88..416e719 100644 --- a/docs_zh_CN/source/paddle_quantum.loss.measure.rst +++ b/docs_zh_CN/source/paddle_quantum.loss.measure.rst @@ -49,5 +49,8 @@ paddle\_quantum.loss.measure :type qubits_idx: Union[Iterable[int], int, str], optional :param desired_result: 指定要返回的测量结果的概率值。默认为 ``None``,返回所有测量结果的概率值。 :type desired_result: Union[Iterable[str], str], optional + :raises NotImplementedError: 所指定的后端必须为量桨已经实现的后端 + :raises NotImplementedError: ``qubits_idx`` 须为 ``Iterable`` 或 ``'full'``。 + :raises NotImplementedError: 目前我们只支持在Z方向上测量。 :return: 测量结果所对应的概率值。 :rtype: paddle.Tensor diff --git a/docs_zh_CN/source/paddle_quantum.operator.operator.rst b/docs_zh_CN/source/paddle_quantum.operator.operator.rst index ae0c958..4358e16 100644 --- a/docs_zh_CN/source/paddle_quantum.operator.operator.rst +++ b/docs_zh_CN/source/paddle_quantum.operator.operator.rst @@ -35,6 +35,14 @@ paddle\_quantum.operator.operator 该类可以让你使用对量子态进行坍缩,坍缩到某一本征态。 + :param qubits_idx: 坍缩的量子比特编号,默认为 ``'full'``. + :type qubits_idx: Union[Iterable[int], int, str], optional + :param num_qubits: 总的量子比特数量,默认为 ``None``。 + :type num_qubits: int, optional + :param desired_result: 想要坍缩到的特定结果。 + :type desired_result: Union[int, str] + :param if_print: 是否打印坍缩后的量子态的信息,默认为 ``'False'``. + :type if_print: bool :param measure_basis: 测量基底。量子态会坍缩到对应的本征态上。 :type measure_basis: Union[Iterable[paddle.Tensor], str] :raises NotImplementedError: 所输入的测量基底还没有实现。 @@ -45,8 +53,5 @@ paddle\_quantum.operator.operator :param state: 输入的量子态,其将会被坍缩。 :type state: paddle_quantum.State - :param desired_result: 想要坍缩到的特定结果。 - :type desired_result: Union[int, str] - :raises NotImplementedError: 当前仅支持 z 基底。 :return: 坍缩后的量子态。 :rtype: paddle_quantum.State diff --git a/docs_zh_CN/source/paddle_quantum.qchem.hardware_efficient.rst b/docs_zh_CN/source/paddle_quantum.qchem.hardware_efficient.rst index 5817e62..90e8d52 100644 --- a/docs_zh_CN/source/paddle_quantum.qchem.hardware_efficient.rst +++ b/docs_zh_CN/source/paddle_quantum.qchem.hardware_efficient.rst @@ -3,7 +3,7 @@ paddle\_quantum.qchem.hardware\_efficient Hardware Efficient 电路模板。 -.. py:class:: HardwareEfficientModel(n_qubits, depth, theta = None) +.. py:class:: HardwareEfficientModel(n_qubits, depth, theta=None) 基类: :py:class:`paddle_quantum.gate.base.Gate` diff --git a/docs_zh_CN/source/paddle_quantum.qchem.loss.rst b/docs_zh_CN/source/paddle_quantum.qchem.loss.rst index adfdffe..b572908 100644 --- a/docs_zh_CN/source/paddle_quantum.qchem.loss.rst +++ b/docs_zh_CN/source/paddle_quantum.qchem.loss.rst @@ -3,7 +3,7 @@ paddle\_quantum.qchem.loss 量子化学中的损失函数。 -.. py:class:: MolEnergyLoss(geometry, basis, multiplicity = 1, charge = 0) +.. py:class:: MolEnergyLoss(geometry, basis, multiplicity=1, charge=0) 基类::py:class:`paddle_quantum.loss.ExpecVal` @@ -18,7 +18,7 @@ paddle\_quantum.qchem.loss :param charge: 分子电荷量, 默认值为 ``0``。 :type charge: int, optional -.. py:class:: RHFEnergyLoss(geometry, basis, multiplicity = 1, charge = 0) +.. py:class:: RHFEnergyLoss(geometry, basis, multiplicity=1, charge=0) 基类: :py:class:`paddle_quantum.Operator` diff --git a/docs_zh_CN/source/paddle_quantum.qchem.qchem.rst b/docs_zh_CN/source/paddle_quantum.qchem.qchem.rst index 7b68ba1..e02d7b8 100644 --- a/docs_zh_CN/source/paddle_quantum.qchem.qchem.rst +++ b/docs_zh_CN/source/paddle_quantum.qchem.qchem.rst @@ -3,6 +3,17 @@ paddle\_quantum.qchem.qchem 量子化学中的功能函数。 +.. py:function:: qubitOperator_to_Hamiltonian(spin_h,tol) + 将openfermion形式转化为量桨的哈密顿量形式。 + + :param spin_h: openfermion形式的哈密顿量。 + :type spin_h: openfermion.ops.operators.qubit_operator.QubitOperator + :param tol: 阈值 + :type tol: float, optional + + :return: 返回转换成量桨形式的哈密顿量 + :rtype: Hamiltonian + .. py:function:: geometry(structure, file) 读取分子几何信息。 diff --git a/docs_zh_CN/source/paddle_quantum.qchem.slater_determinant.rst b/docs_zh_CN/source/paddle_quantum.qchem.slater_determinant.rst index 3fc277a..3954ba6 100644 --- a/docs_zh_CN/source/paddle_quantum.qchem.slater_determinant.rst +++ b/docs_zh_CN/source/paddle_quantum.qchem.slater_determinant.rst @@ -22,7 +22,7 @@ paddle\_quantum.qchem.slater\_determinant :param theta: 给定旋转角度。 :type theta: float -.. py:class:: RHFSlaterDeterminantModel(n_qubits, n_electrons, mo_coeff = None) +.. py:class:: RHFSlaterDeterminantModel(n_qubits, n_electrons, mo_coeff=None) 基类::py:class:`paddle_quantum.gate.Gate` diff --git a/docs_zh_CN/source/paddle_quantum.qchem.uccsd.rst b/docs_zh_CN/source/paddle_quantum.qchem.uccsd.rst index 088fbc3..a22cccd 100644 --- a/docs_zh_CN/source/paddle_quantum.qchem.uccsd.rst +++ b/docs_zh_CN/source/paddle_quantum.qchem.uccsd.rst @@ -3,7 +3,7 @@ paddle\_quantum.qchem.uccsd UCCSD 电路模板。 -.. py:class:: UCCSDModel(n_qubits, n_electrons, n_trotter_steps, single_excitation_amplitude = None, double_excitation_amplitude = None) +.. py:class:: UCCSDModel(n_qubits, n_electrons, n_trotter_steps, single_excitation_amplitude=None, double_excitation_amplitude=None) 基类::py:class:`paddle_quantum.gate.Gate` diff --git a/docs_zh_CN/source/paddle_quantum.qinfo.rst b/docs_zh_CN/source/paddle_quantum.qinfo.rst index 7d7d447..b6af6c8 100644 --- a/docs_zh_CN/source/paddle_quantum.qinfo.rst +++ b/docs_zh_CN/source/paddle_quantum.qinfo.rst @@ -3,12 +3,12 @@ paddle\_quantum.qinfo 量子信息中的常用功能实现。 -.. py:function:: partial_trace(rho_AB, dim1, dim2, A_or_B) +.. py:function:: partial_trace(state, dim1, dim2, A_or_B) 计算量子态的偏迹。 - :param rho_AB: 输入的量子态。 - :type rho_AB: paddle_quantum.State + :param state: 输入的量子态。 + :type state: Union[paddle_quantum.State, paddle.Tensor] :param dim1: 系统A的维数。 :type dim1: int :param dim2: 系统B的维数。 @@ -19,12 +19,12 @@ paddle\_quantum.qinfo :return: 输入的量子态的偏迹。 :rtype: paddle.Tensor -.. py:function:: partial_trace_discontiguous(rho, preserve_qubits = None) +.. py:function:: partial_trace_discontiguous(state, preserve_qubits=None) 计算量子态的偏迹,可选取任意子系统。 - :param rho: 输入的量子态。 - :type rho: paddle_quantum.State + :param state: 输入的量子态。 + :type state: Union[paddle_quantum.State, paddle.Tensor] :param preserve_qubits: 要保留的量子比特,默认为 None,表示全保留。 :type preserve_qubits: list, optional @@ -40,9 +40,9 @@ paddle\_quantum.qinfo D(\rho, \sigma) = 1 / 2 * \text{tr}|\rho-\sigma| :param rho: 量子态的密度矩阵形式。 - :type rho: paddle_quantum.State + :type rho: Union[paddle_quantum.State, paddle.Tensor] :param sigma: 量子态的密度矩阵形式。 - :type sigma: paddle_quantum.State + :type sigma: Union[paddle_quantum.State, paddle.Tensor] :return: 输入的量子态之间的迹距离。 :rtype: paddle.Tensor @@ -56,9 +56,9 @@ paddle\_quantum.qinfo F(\rho, \sigma) = \text{tr}(\sqrt{\sqrt{\rho}\sigma\sqrt{\rho}}) :param rho: 量子态的密度矩阵形式。 - :type rho: paddle_quantum.State + :type rho: Union[paddle_quantum.State, paddle.Tensor] :param sigma: 量子态的密度矩阵形式。 - :type sigma: paddle_quantum.State + :type sigma: Union[paddle_quantum.State, paddle.Tensor] :return: 输入的量子态之间的保真度。 :rtype: paddle.Tensor @@ -89,7 +89,7 @@ paddle\_quantum.qinfo P = \text{tr}(\rho^2) :param rho: 量子态的密度矩阵形式。 - :type rho: paddle_quantum.State + :type rho: Union[paddle_quantum.State, paddle.Tensor] :return: 输入的量子态的纯度。 :rtype: paddle.Tensor @@ -103,7 +103,7 @@ paddle\_quantum.qinfo S = -\text{tr}(\rho \log(\rho)) :param rho: 量子态的密度矩阵形式。 - :type rho: paddle_quantum.State + :type rho: Union[paddle_quantum.State, paddle.Tensor] :return: 输入的量子态的冯诺依曼熵。 :rtype: paddle.Tensor @@ -117,14 +117,14 @@ paddle\_quantum.qinfo S(\rho \| \sigma)=\text{tr} \rho(\log \rho-\log \sigma) :param rho: 量子态的密度矩阵形式 - :type rho: paddle_quantum.State + :type rho: Union[paddle_quantum.State, paddle.Tensor] :param sig: 量子态的密度矩阵形式 - :type sig: paddle_quantum.State + :type sig: Union[paddle_quantum.State, paddle.Tensor] :return: 输入的量子态之间的相对熵 :rtype: paddle.Tensor -.. py:function:: random_pauli_str_generator(n, terms = 3) +.. py:function:: random_pauli_str_generator(n, terms=3) 随机生成一个可观测量(observable)的列表( ``list`` )形式。 @@ -158,12 +158,12 @@ paddle\_quantum.qinfo :return: 输入列表对应的可观测量的矩阵形式。 :rtype: paddle.Tensor -.. py:function:: partial_transpose_2(density_op, sub_system = None) +.. py:function:: partial_transpose_2(density_op, sub_system=None) 计算输入量子态的 partial transpose :math:`\rho^{T_A}`。 :param density_op: 量子态的密度矩阵形式。 - :type density_op: paddle_quantum.State + :type density_op: Union[paddle_quantum.State, paddle.Tensor] :param sub_system: 1或2,表示关于哪个子系统进行 partial transpose,默认为第二个。 :type sub_system: int, optional @@ -175,7 +175,7 @@ paddle\_quantum.qinfo 计算输入量子态的 partial transpose :math:`\rho^{T_A}`。 :param density_op: 量子态的密度矩阵形式。 - :type density_op: paddle_quantum.State + :type density_op: Union[paddle_quantum.State, paddle.Tensor] :param n: 需要转置系统的量子比特数量。 :type n: int @@ -187,7 +187,7 @@ paddle\_quantum.qinfo 计算输入量子态的 Negativity :math:`N = ||\frac{\rho^{T_A}-1}{2}||`。 :param density_op: 量子态的密度矩阵形式。 - :type density_op: paddle_quantum.State + :type density_op: Union[paddle_quantum.State, paddle.Tensor] :return: 输入的量子态的 Negativity。 :rtype: paddle.Tensor @@ -197,27 +197,27 @@ paddle\_quantum.qinfo 计算输入量子态的 Logarithmic Negativity :math:`E_N = ||\rho^{T_A}||`。 :param density_op: 量子态的密度矩阵形式。 - :type density_op: paddle_quantum.State + :type density_op: Union[paddle_quantum.State, paddle.Tensor] :return: 输入的量子态的 Logarithmic Negativity。 :rtype: paddle.Tensor -.. py:function:: is_ppt(density_op: paddle_quantum.State) +.. py:function:: is_ppt(density_op) 计算输入量子态是否满足 PPT 条件。 :param density_op: 量子态的密度矩阵形式。 - :type density_op: paddle_quantum.State + :type density_op: Union[paddle_quantum.State, paddle.Tensor] :return: 输入的量子态是否满足 PPT 条件。 :rtype: bool -.. py:function:: schmidt_decompose(psi, sys_A = None) +.. py:function:: schmidt_decompose(psi, sys_A=None) 计算输入量子态的施密特分解 :math:`\lvert\psi\rangle=\sum_ic_i\lvert i_A\rangle\otimes\lvert i_B \rangle`。 :param psi: 量子态的向量形式,形状为(2**n)。 - :type psi: paddle_quantum.State + :type psi: Union[paddle_quantum.State, paddle.Tensor] :param sys_A: 包含在子系统 A 中的 qubit 下标(其余 qubit 包含在子系统B中),默认为量子态 :math:`\lvert \psi\rangle` 的前半数 qubit。 :type sys_A: List[int], optional @@ -240,7 +240,7 @@ paddle\_quantum.qinfo :return: 编码得到的密度矩阵。 :rtype: paddle_quantum.State -.. py:function:: shadow_trace(state, hamiltonian, sample_shots, method = 'CS') +.. py:function:: shadow_trace(state, hamiltonian, sample_shots, method='CS') 估计可观测量 :math:`H` 的期望值 :math:`\text{trace}(H\rho)`。 @@ -253,5 +253,27 @@ paddle\_quantum.qinfo :param method: 使用 shadow 来进行估计的方法,可选 "CS"、"LBCS"、"APS" 三种方法,默认为 ``CS``。 :type method: str, optional + :raises ValueError: 输入的哈密顿量 (Hamiltonian) 形式不合法 + :return: 估计可观测量 :math:`H` 的期望值。 :rtype: float + +.. py:function:: tensor_product(state_a, state_b, *args) + + 计算输入的量子态(至少两个)的直积形式, 输出将自动返回 State 实例 + + :param state_a: 量子态A + :type state_a: Union[State, paddle.Tensor] + :param state_b: 量子态B + :type state_b: Union[State, paddle.Tensor] + :param args: 其他量子态 + :type args: Union[State, paddle.Tensor] + + :raises NotImplementedError: 当前只接收输入类型为 State 或 paddle.Tensor + + .. note:: + + 使用的 backend 必须为 DensityMatrix + + :return: 输入量子态的直积 + :rtype: State \ No newline at end of file diff --git a/docs_zh_CN/source/paddle_quantum.qsvt.qsp.rst b/docs_zh_CN/source/paddle_quantum.qsvt.qsp.rst new file mode 100644 index 0000000..d3a3228 --- /dev/null +++ b/docs_zh_CN/source/paddle_quantum.qsvt.qsp.rst @@ -0,0 +1,225 @@ +paddle\_quantum.qsvt.qsp +============================ + +量子信号处理相关类与函数,具体参考论文 https://arxiv.org/abs/1806.01838 + +.. py:function:: signal_unitary(signal_x) + + 实现论文中的信号矩阵 :math:`W(x)` + + :param signal_x: 输入信号,区间为[-1, 1] + :type signal_x: float + + :return: matrix :math:`W(x=\text{signal_x})` + :rtype: ndarray + +.. py:function:: poly_parity_verification(poly_p, k, error) + + 对输入多项式进行奇偶校验,判断 :math:`P` 奇偶性是否为 (k mod 2),详见论文定理 3 中的条件 2. + + :param poly_p: 多项式 :math:`P(x)` + :type poly_p: Polynomial + :param k: 项数 k + :type k: int + :param error: 误差阈值,默认为 `1e-6`. + :type error: float + + :return: poly_p 奇偶性是否为 (k mod 2) + :rtype: bool + +.. py:function:: normalization_verification(poly_p, poly_q, trials, error) + + 归一化验证,判断多项式 :math:`P(x)` 和 :math:`Q(x)` 是否满足归一化条件,详见论文定理 3 中的条件 3 + + :param poly_p: 多项式 :math:`P(x)` + :type poly_p: Polynomial + :param poly_q: 多项式 :math:`Q(x)` + :type poly_q: Polynomial + :param trials: 验证次数,默认为 `10` + :type trials: int + :param error: 误差阈值,默认为 `1e-2`. + :type error: float + + :return: 多项式是否满足归一化条件 :math:`|P|^2 + (1 - x^2)|Q|^2 = 1` + :rtype: bool + +.. py:function:: angle_phi_verification(phi, poly_p, poly_p_hat, poly_q_hat, trials, error) + + 验证角度 :math:`\phi` 是否满足论文中的等式 6 + + :param phi: 旋转角 :math:`\phi` + :type phi: float + :param poly_p: 多项式 :math:`P(x)` + :type poly_p: Polynomial + :param poly_q: 多项式 :math:`Q(x)` + :type poly_q: Polynomial + :param poly_p_hat: 多项式 :math:`\tilde{P}(x)` + :type poly_p: Polynomial + :param poly_q_hat: 多项式 :math:`\tilde{Q}(x)` + :type poly_q: Polynomial + :param trials: 验证次数,默认为 `10` + :type trials: int + :param error: 误差阈值,默认为 `1e-2`. + :type error: float + + :return: 角度 :math:`\phi` 是否满足论文中的等式 6. + :rtype: bool + +.. py:function:: processing_unitary(list_matrices, signal_x) + + 构造量子信号处理矩阵 :math:`W_\Phi(x)`,详见论文中的等式 1 + + :param list_matrices: 一个包含信号处理矩阵的数组 + :type list_matrices: List[ndarray] + :param signal_x: 输入信号 x,范围为 [-1, 1] + :type signal_x: float + + :return: 量子信号处理矩阵 :math:`W_\Phi(x)` + :rtype: ndarray + +.. py:function:: Phi_verification(list_phi, poly_p, trials, error) + + 验证完整的角度 :math:`\Phi` + + :param list_phi: 包含所有角度 :math:`\phi` 的数组 + :type list_phi: ndarray + :param poly_p: 多项式 :math:`P(x)` + :type poly_p: Polynomial + :param trials: 验证次数,默认为 `100` + :type trials: trials + :param error: 误差阈值,默认为 `1e-6` + :type error: float + + :return: 角度 :math:`\Phi` 是否使得 :math:`W_\Phi(x)` 为 :math:`P(x)` 的块编码 + :rtype: bool + +.. py:function:: update_polynomial(poly_p, poly_q, phi) + + 计算 :math:`P, Q` 经过一层量子信号处理后的多项式 :math:`\tilde{P}, \tilde{Q}` + + :param poly_p: 多项式 :math:`P(x)` + :type poly_p: Polynomial + :param poly_q: 多项式 :math:`Q(x)` + :type poly_q: Polynomial + :param phi: 量子信号处理的旋转角 :math:`\phi` + :type phi: float + + :return: 更新之后的多项式 :math:`\tilde{P}(x), \tilde{Q}(x)` + :rtype: Tuple[Polynomial, Polynomial] + + +.. py:function:: alg_find_Phi(poly_p, poly_q, length) + + 计算角度 :math:`\Phi` 的算法 + + :param poly_p: 多项式 :math:`P(x)` + :type poly_p: Polynomial + :param poly_q: 多项式 :math:`Q(x)` + :type poly_q: Polynomial + :param length: 返回角度的个数,即量子信号处理的层数 + :type length: int + + :return: 包含角度的数组 :math:`\Phi` + :rtype: ndarray + + +.. py:function:: poly_A_hat_generation(poly_p) + + 计算多项式 :math:`\hat{A}(y) = 1 - P(x)P^*(x)`,其中 :math:`y = x^2` + + :param poly_p: 多项式 :math:`P(x)` + :type poly_p: Polynomial + + :return: 多项式 :math:`\hat{A}(y)` + :rtype: Polynomial + +.. py:function:: poly_A_hat_decomposition(A_hat, error) + + 通过求根的方式分解多项式 :math:`\hat{A}(y)` + + :param poly_p: 多项式 :math:`P(x)` + :type poly_p: Polynomial + :param error: 误差阈值,默认为 `0.001` + :type error: float + + :return: 多项式 :math:`\hat{A}(y)` 的最高项系数以及根 + :rtype: Tuple[float, List[float]] + +.. py:function:: poly_Q_generation(leading_coef, roots, parity) + + 根据多项式 :math:`\hat{A}(y)` 的分解,构造多项式 :math:`Q(x)` + + :param leading_coef: 多项式 :math:`\hat{A}(y)` 的最高项系数 + :type leading_coef: float + :param roots: 多项式 :math:`\hat{A}(y)` 的根 + :type roots: List[float] + :param parity: 多项式 :math:`Q(x)` 的奇偶性 + :type parity: int + + :return: 多项式 :math:`Q(x)` + :rtype: Polynomial + +.. py:function:: alg_find_Q(poly_p, k) + + 根据多项式 :math:`P(x)` 构造多项式 :math:`Q(x)` 的算法 + + :param poly_p: 多项式 :math:`P(x)` + :type poly_p: Polynomial + :param k: 多项式 :math:`Q(x)` 的项数 + :type k: int + + :return: 多项式 :math:`Q(x)` + :rtype: Polynomial + +.. py:function:: quantum_signal_processing(poly_p, length) + + 量子信号处理函数,找到一组角度 :math:`\Phi` 使得量子信号处理算子 :math:`W_\Phi(x)` 是一个多项式 :math:`P(x)` 的块编码 + + :param poly_p: 多项式 :math:`P(x)` + :type poly_p: Polynomial + :param length: 角度的个数,即量子信号处理的层数,默认 `None` 为多项式 :math:`P(x)` 的度 + :type length: int + + :return: 角度 :math:`\Phi` + :rtype: ndarray + +.. py:function:: reflection_based_quantum_signal_processing(P) + + 基于反射的量子信号处理函数,找到一组角度 :math:`\Phi` 使得量子信号处理算子 :math:`W_\Phi(x)` 是一个多项式 :math:`P(x)` 的块编码,详见论文引理 8 + + :param poly_p: 多项式 :math:`P(x)` + :type poly_p: Polynomial + + :return: 角度 :math:`\Phi` + :rtype: ndarray + +.. py:class:: ScalarQSP + + 基类: :py:class:`object` + + 基于量子信号处理的类 + + :param poly_p: 多项式 :math:`P(x)` + :type poly_p: Polynomial + :param length: 角度的个数,即量子信号处理的层数,默认 `None` 为多项式 :math:`P(x)` 的度 + :type length: int + + .. py:method:: block_encoding(signal_x) + + 构造一个量子信号处理的电路,即实现多项式 :math:`P(x)` 的块编码电路 + + :param signal_x: 输入的信号 x + :type signal_x: float + + :return: 量子信号处理的电路 + :rtype: Circuit + + .. py:method:: block_encoding_matrix(signal_x) + + 构造一个量子信号处理的矩阵,即实现多项式 :math:`P(x)` 的块编码矩阵 + + :param signal_x: 输入的信号 x + :type signal_x: float + + :return: 量子信号处理的矩阵 + :rtype: paddle.Tensor diff --git a/docs_zh_CN/source/paddle_quantum.qsvt.qsp_utils.rst b/docs_zh_CN/source/paddle_quantum.qsvt.qsp_utils.rst new file mode 100644 index 0000000..7f7e241 --- /dev/null +++ b/docs_zh_CN/source/paddle_quantum.qsvt.qsp_utils.rst @@ -0,0 +1,82 @@ +paddle\_quantum.qsvt.qsp\_utils +=============================== + +量子信号处理相关工具函数包 + +.. py:function:: random_odd_poly_generation(degree, odd) + + 生成一个随机的满足量子信号处理要求的多项式 + + :param degree: 多项式的度 + :type degree: int + :param odd: 多项式的奇偶性,输入 `True` 则为奇函数, `False` 则为偶函数 + :type odd: bool + + :return: 一个随机生成的多项式 + :rtype: Polynomial + +.. py:function:: clean_small_error(array) + + 清除相对较小的项,以提升计算精度 + + :param array: 目标数组 + :type array: ndarray + + :return: 经过清除后的数组 + :rtype: ndarray + +.. py:function:: poly_norm(poly, p=1) + + 计算一个多项式的 p 范数 + + :param poly: 目标多项式 + :type poly: Polynomial + :param p: p 范数,默认为 `1`,输入 `0` 则是无穷范数 + :type p: Optional[int] + + :return: 目标多项式的 p 范数 + :rtype: float + +.. py:function:: poly_real(poly) + + 取一个多项式的实部 + + :param poly: 目标多项式 + :type poly: Polynomial + + :return: 目标多项式的实部 + :rtype: Polynomial + +.. py:function:: poly_imag(poly) + + 取一个多项式的实部 + + :param poly: 目标多项式 + :type poly: Polynomial + + :return: 目标多项式的虚部 + :rtype: Polynomial + +.. py:function:: poly_matrix(poly, matrix_A) + + 计算一个矩阵的多项式 poly(matrix_A) + + :param poly: 输入多项式 + :type poly: Polynomial + :param matrix_A: 输入矩阵 + :type matrix_A: paddle.Tensor + + :return: 矩阵的多项式结果 poly(matrix_A) + :rtype: paddle.Tensor + +.. py:function:: exp_matrix(t, matrix_A) + + 计算矩阵指数 :math:`e^{itA}` + + :param t: 演化时间 + :type t: float + :param matrix_A: 目标矩阵 A + :type matrix_A: paddle.Tensor + + :return: 矩阵指数 :math:`e^{itA}` + :rtype: paddle.Tensor \ No newline at end of file diff --git a/docs_zh_CN/source/paddle_quantum.qsvt.qsvt.rst b/docs_zh_CN/source/paddle_quantum.qsvt.qsvt.rst new file mode 100644 index 0000000..f5f0ba9 --- /dev/null +++ b/docs_zh_CN/source/paddle_quantum.qsvt.qsvt.rst @@ -0,0 +1,62 @@ +paddle\_quantum.qsvt.qsvt +============================ + +量子奇异值变换 + +.. py:function:: block_encoding_projector(num_qubits, num_projected_qubits) + + 生成块编码的投影算子 + + :param num_qubits: 量子比特数量 + :type num_qubits: int + :param num_projected_qubits: 被投影的量子比特数量,默认为 `num_qubits - 1` + :type num_projected_qubits: int + + :return: 投影算子 :math:`|0\rangle\langle0| \otimes I` + :rtype: paddle.Tensor + + +.. py:function:: qubitization(proj, phi) + + 单比特化操作,生成等同于 :math:`e^{i \phi (2P - I)}` 的电路 + + :param proj: 正交投影算子 :math:`P` + :type proj: paddle.Tensor + :param phi: 角度 :math:`\phi` + :type phi: paddle.Tensor + + :return: :math:`e^{i \phi (2P - I)}` 的电路 + :rtype: Circuit + + +.. py:class:: QSVT + + 基类: :py:class:`object` + + :param poly_p: 多项式 :math:`P(x)` + :type poly_p: Polynomial + :param oracle: 酉算子 :math:`U`,为一个厄米特矩阵 :math:`X` 的块编码 + :type oracle: paddle.Tensor + :param m: 厄米特矩阵 :math:`X` 的系统量子比特数量,默认为酉算子 :math:`U` 量子比特数量 - 1 + + .. py:method:: block_encoding_matrix() + + 构造一个对于厄米特矩阵 :math:`X` 的量子奇异值变换矩阵,即实现多项式 :math:`P(X)` 的块编码矩阵 + + :return: 量子奇异值变换矩阵 + :rtype: paddle.Tensor + + + .. py:method:: block_encoding_circuit() + + 构造一个对于厄米特矩阵 :math:`X` 的量子奇异值变换电路,即实现多项式 :math:`P(X)` 的块编码电路 + + :return: 量子奇异值变换电路 + :rtype: Circuit + + .. py:method:: block_encoding_unitary() + + 返回一个对于厄米特矩阵 :math:`X` 的量子奇异值变换电路的酉矩阵形式,用于验证正确性 + + :return: 量子奇异值变换电路的酉矩阵 + :rtype: paddle.Tensor \ No newline at end of file diff --git a/docs_zh_CN/source/paddle_quantum.qsvt.rst b/docs_zh_CN/source/paddle_quantum.qsvt.rst new file mode 100644 index 0000000..ac79f14 --- /dev/null +++ b/docs_zh_CN/source/paddle_quantum.qsvt.rst @@ -0,0 +1,12 @@ +paddle\_quantum.qsvt +============================ +量子奇异值变换模块 + +.. rubric:: Submodules + +.. toctree:: + :maxdepth: 4 + + paddle_quantum.qsvt.qsp_utils + paddle_quantum.qsvt.qsp + paddle_quantum.qsvt.qsvt \ No newline at end of file diff --git a/docs_zh_CN/source/paddle_quantum.shadow.rst b/docs_zh_CN/source/paddle_quantum.shadow.rst index 4648983..0abbca1 100644 --- a/docs_zh_CN/source/paddle_quantum.shadow.rst +++ b/docs_zh_CN/source/paddle_quantum.shadow.rst @@ -20,5 +20,8 @@ paddle\_quantum.shadow :param method: 进行随机采样的方法,有 ``'CS'`` 、 ``'LBCS'`` 、 ``'APS'`` 三种方法,默认为 ``'CS'``。 :type method: str, optional + :raises ValueError: 输入的哈密顿量 (Hamiltonian) 形式不合法 + :raises NotImplementedError: 输入 ``state`` 的 ``backend`` 必须是 ``StateVector`` 或 ``DensityMatrix`` + :return: 随机选择的泡利测量基和测量结果,形状为 ``(sample_shots, 2)`` 的list。 :rtype: list diff --git a/docs_zh_CN/source/paddle_quantum.state.common.rst b/docs_zh_CN/source/paddle_quantum.state.common.rst index 9c2731c..f6c5ab7 100644 --- a/docs_zh_CN/source/paddle_quantum.state.common.rst +++ b/docs_zh_CN/source/paddle_quantum.state.common.rst @@ -77,6 +77,8 @@ paddle\_quantum.state.common p_{1}|\Phi^{+}\rangle\langle\Phi^{+}|+p_{2}| \Psi^{+}\rangle\langle\Psi^{+}|+p_{3}| \Phi^{-}\rangle\langle\Phi^{-}| + p_{4}|\Psi^{-}\rangle\langle\Psi^{-}| + :param prob: 各个贝尔态的概率。 + :type: List[float] :raises Exception: 当后端为态矢量时,所输入量子态应该为纯态。 :raises NotImplementedError: 所指定的后端必须为量桨已经实现的后端。 @@ -123,7 +125,7 @@ paddle\_quantum.state.common :param num_qubits: 量子态所包含的量子比特数。 :type num_qubits: int - :raises Exception: 所指定的后端必须为态矢量。 + :raises Exception: 当后端为态矢量时,所输入量子态应该为纯态。 :raises NotImplementedError: 所指定的后端必须为量桨已经实现的后端。 :returns: 生成的 GHZ-state。 :rtype: paddle_quantum.State diff --git a/docs_zh_CN/source/paddle_quantum.state.state.rst b/docs_zh_CN/source/paddle_quantum.state.state.rst index 74bb0e6..7633a79 100644 --- a/docs_zh_CN/source/paddle_quantum.state.state.rst +++ b/docs_zh_CN/source/paddle_quantum.state.state.rst @@ -17,6 +17,17 @@ paddle\_quantum.state.state :type backend: paddle_quantum.Backend, optional :param dtype: 量子态的数据类型。默认为 None,使用全局的默认数据类型。 :type dtype: str, optional + :raises Exception: 所输入的量子态维度不正确。 + :raises NotImplementedError: 所指定的后端必须为量桨已经实现的后端。 + + + .. py:property:: ket() + + 得到量子态的列向量形式。 + + .. py:property:: bra() + + 得到量子态的行向量形式。 .. py:method:: numpy() @@ -55,6 +66,7 @@ paddle\_quantum.state.state :type hamiltonian: paddle_quantum.Hamiltonian :param shots: 测量次数。 :type shots: int + :raises NotImplementedError: 所指定的后端必须为量桨已经实现的后端。 :return: 该量子态关于可观测量的期望值。 :rtype: float @@ -69,5 +81,8 @@ paddle\_quantum.state.state :type qubits_idx: Union[Iterable[int], int], optional :param plot: 是否画图。默认为 Flase,表示不画图。 :type plot: bool, optional + :raises Exception: 测量的次数必须大于0。 + :raises NotImplementedError: 所指定的后端必须为量桨已经实现的后端。 + :raises NotImplementedError: 输入的量子比特下标有误。 :return: 测量结果。 :rtype: dict diff --git a/docs_zh_CN/source/paddle_quantum.trotter.rst b/docs_zh_CN/source/paddle_quantum.trotter.rst index b188203..2501b73 100644 --- a/docs_zh_CN/source/paddle_quantum.trotter.rst +++ b/docs_zh_CN/source/paddle_quantum.trotter.rst @@ -4,7 +4,7 @@ paddle\_quantum.trotter Trotter 哈密顿量时间演化的功能实现。 -.. py:function:: construct_trotter_circuit(circuit, hamiltonian, tau, steps, method = 'suzuki', order = 1, grouping = None, coefficient = None, permutation = None) +.. py:function:: construct_trotter_circuit(circuit, hamiltonian, tau, steps, method='suzuki', order=1, grouping=None, coefficient=None, permutation=None) 向 circuit 的后面添加 trotter 时间演化电路,即给定一个系统的哈密顿量 H,该电路可以模拟系统的时间演化 :math:`U_{cir} e^{-iHt}`。 @@ -27,6 +27,11 @@ Trotter 哈密顿量时间演化的功能实现。 :param permutation: 自定义哈密顿量的排列方式,默认为 ``None``,仅在 ``method='custom'`` 时有效。 :type permutation: np.ndarray, optional + :raises ValueError: Trotter-Suzuki 分解的阶数 ``order`` 必须为 ``1``, ``2``, 或 ``2k``, 其中 ``k`` 是一个整数 + :raises ValueError: ``permutation`` 和 ``coefficient`` 的形状不一致 + :raises ValueError: 重排策略 ``grouping`` 的方法不支持, 仅支持 ``'xyz'``, ``'even_odd'`` + :raises ValueError: 搭建时间演化电路的方法 ``method`` 不支持, 仅支持 ``'suzuki'``, ``'custom'`` + .. Hint:: 想知道该函数是如何模拟的?更多信息请移步至量桨官网教程: https://qml.baidu.com/tutorials/overview.html. @@ -91,7 +96,7 @@ Trotter 哈密顿量时间演化的功能实现。 :return: 系数数组。 :rtype: np.ndarray -.. py:function:: get_1d_heisenberg_hamiltonian(length, j_x = 1.0, j_y = 1.0, j_z = 1.0, h_z = 0.0, periodic_boundary_condition = True) +.. py:function:: get_1d_heisenberg_hamiltonian(length, j_x=1.0, j_y=1.0, j_z=1.0, h_z=0.0, periodic_boundary_condition=True) 生成一个一维海森堡链的哈密顿量。 diff --git a/docs_zh_CN/source/paddle_quantum.visual.rst b/docs_zh_CN/source/paddle_quantum.visual.rst index 75dd53a..1318dec 100644 --- a/docs_zh_CN/source/paddle_quantum.visual.rst +++ b/docs_zh_CN/source/paddle_quantum.visual.rst @@ -73,4 +73,7 @@ paddle\_quantum.visual :param density_matrix: 多量子比特的量子态的状态向量或者密度矩阵,要求量子数大于 1。 :type density_matrix: paddle_quantum.State :param size: 条宽度,在 0 到 1 之间,默认为 ``0.3``。 - :type size: float, optional \ No newline at end of file + :type size: float, optional + + :raises TypeError: 要求输入的 ``density_matrix`` 类型为 ``numpy.ndarray``, ``paddle.Tensor``, 或者 ``paddle_quantum.State`` + :raises ValueError: 要求输入的 ``density_matrix`` 是一个方阵 \ No newline at end of file diff --git a/docs_zh_CN/source/tutorial.rst b/docs_zh_CN/source/tutorial.rst index 6113f30..e91ae74 100644 --- a/docs_zh_CN/source/tutorial.rst +++ b/docs_zh_CN/source/tutorial.rst @@ -23,14 +23,14 @@ Paddle Quantum。目前支持网页阅览和\ `下载运行 Jupyter Notebook `__\ 类似,每个教程目前支持 -\ `网页阅览 `__\ 和\ `下载运行 Jupyter Notebook `__\ 两种方式。我们推荐用户下载 Notebook +\ `网页阅览 `__\ 和\ `下载运行 Jupyter Notebook `__\ 两种方式。我们推荐用户下载 Notebook 后,本地运行进行实践。 -- `量子模拟 `__ -- `机器学习 `__ -- `组合优化 `__ -- `LOCCNet `__ -- `量子神经网络研究 `__ +- `量子模拟 `__ +- `机器学习 `__ +- `组合优化 `__ +- `LOCCNet `__ +- `量子神经网络研究 `__ 随着 LOCCNet 模组的推出,量桨现已支持分布式量子信息处理任务的高效模拟和开发。感兴趣的读者请参见 `教程 `__。 Paddle Quantum 也支持在 GPU diff --git a/introduction/PaddleQuantum_QuLeaf_CN.ipynb b/introduction/PaddleQuantum_QuLeaf_CN.ipynb new file mode 100644 index 0000000..cf308e6 --- /dev/null +++ b/introduction/PaddleQuantum_QuLeaf_CN.ipynb @@ -0,0 +1,218 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 使用量桨通过量易伏平台连接量子计算机\n", + "\n", + "*Copyright (c) 2022 Institute for Quantum Computing, Baidu Inc. All Rights Reserved.*\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 简介\n", + "\n", + "在量子机器学习中,我们最终想实现的目标就是使用量子计算机和经典计算机进行混合计算,从而实现高效的量子机器学习(Quantum Machine Learning, QML)算法。在量桨(Paddle Quantum)中,我们也支持通过[量易伏](https://quantum-hub.baidu.com/)这一云原生量子计算平台连接到真实的量子计算机,从而使用量子计算机实现 QML 算法。\n", + "\n", + "### 量易伏简介\n", + "\n", + "量易伏是由百度研究院量子计算研究所推出的云原生量子计算平台。量易伏很好地实现了量子计算和云计算的深度融合。量易包含本地模拟器、云端模拟器和云端量子计算机等多种使用方式。其中,云端模拟器和云端量子计算机的使用需要消耗量易伏点数,用户需要在[量易伏网站](https://quantum-hub.baidu.com/)上进行注册后可获得量易伏账号和点数。量易伏包含 QComposer、PyOnline、YunOnline、QCompute SDK 等多种调用方式。其中 QCompute SDK 允许我们通过 Python 进行调用。在 Python 环境中,需要输入账号对应的 token 来进行调用,token 可在 https://quantum-hub.baidu.com/token 上进行查看。如果想使用云端模拟器或云端量子计算机,需要消耗量易伏点数,点数可在 https://quantum-hub.baidu.com/feedback 页面发送邮件进行获取。\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 量桨连接量易伏\n", + "\n", + "目前量桨已经支持连接量易伏,只需要使用 `paddle_quantum.set_backend('quleaf')` 即可将量桨的后端设置为量易伏。除此之外,我们还需要设置量易伏的模拟方式。如果使用云端算力,则还需要输入 token。因此,完整的设置代码如下:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import paddle_quantum\n", + "from QCompute import BackendName\n", + "paddle_quantum.set_backend('quleaf')\n", + "paddle_quantum.backend.quleaf.set_quleaf_backend(BackendName.LocalBaiduSim2)\n", + "# 如果使用本地模拟器,则可以不需要输入 token\n", + "# paddle_quantum.backend.quleaf.set_quleaf_token('your token')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "在 `set_quleaf_backend()` 函数中可以设置量易伏的后端模拟方式。\n", + "\n", + "目前,量易伏的所有后端如下:\n", + "\n", + "| 类型 | 量子端 | 说明 |\n", + "| :--: | :--: | :--: |\n", + "| 本地 | LocalBaiduSim2 | 使用 Python 编写的 Sim2 本地版 |\n", + "| 云端 | CloudBaiduSim2Water | 使用 C++ 编写的多实例 Sim2 模拟器云版 |\n", + "| 云端 | CloudBaiduSim2Earth | 使用 Python 编写的单一实例高配置 Sim2 模拟器云版 |\n", + "| 云端 | CloudBaiduSim2Thunder | 使用 C++ 编写的单一实例高配置 Sim2 模拟器云版 |\n", + "| 云端 | CloudBaiduSim2Wind | 使用 C++ 编写的单一实例 Sim2 模拟器云版(支持稀疏模式) |\n", + "| 云端 | CloudBaiduSim2Heaven | 使用 C++ 编写的单一实例集群 Sim2 模拟器云版 |\n", + "| 云端 | CloudBaiduSim2Lake | 使用 C++ 编写的单一实例 Sim2 模拟器云版 (支持 GPU 运算) |\n", + "| 云端 | CloudAerAtBD | 开源 Aer(C++ 版) 模拟器云版 |\n", + "| 云端 | CloudIoPCAS | 来自中科院物理所的 10 比特量子真机 (VIP) |\n", + "\n", + "其中,`LocalBaiduSim2` 为本地模拟器,不需要输入 token,不会使用量易伏点数;`CloudIoPCAS` 为真实的量子计算机,云端的量子计算机和模拟器都需要输入token,且会消耗量易伏点数。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 贝尔态制备\n", + "\n", + "这里,我们以制备贝尔态为例测试量桨是否成功连接量易伏。在执行了上面的代码之后,用户就已经将量桨设置为了量易伏模式。" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "num_qubits = 2\n", + "init_state = paddle_quantum.state.zero_state(num_qubits)\n", + "circuit = paddle_quantum.ansatz.Circuit(num_qubits)\n", + "circuit.h(0)\n", + "circuit.cnot([0, 1])\n", + "bell_state = circuit(init_state)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "如果上面的代码能成功运行,则说明可以连接到量易伏,并执行量子电路。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## VQE 例子\n", + "\n", + "这里,我们以VQE为例来介绍如何通过量桨调用量易伏算力。我们使用量易伏的本地模拟器来进行 demo 演示。" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The iter is 0, loss is -0.32104.\n", + "The iter is 10, loss is -0.49239.\n", + "The iter is 20, loss is -0.80956.\n", + "The iter is 30, loss is -1.07812.\n", + "The iter is 40, loss is -1.09850.\n", + "The iter is 50, loss is -1.13334.\n", + "The iter is 60, loss is -1.13445.\n", + "The iter is 70, loss is -1.13492.\n", + "The theoretical value is -1.137283834485513.\n" + ] + } + ], + "source": [ + "import paddle\n", + "\n", + "# 定义哈密顿量\n", + "hamiltonian_list = [\n", + " [-0.0970662686176252, 'I'],\n", + " [-0.04530261550868938, 'X0, X1, Y2, Y3'],\n", + " [0.04530261550868938, 'X0, Y1, Y2, X3'],\n", + " [0.04530261550868938, 'Y0, X1, X2, Y3'],\n", + " [-0.04530261550868938, 'Y0, Y1, X2, X3'],\n", + " [0.1714128263940238, 'Z0'],\n", + " [0.16868898168693292, 'Z0, Z1'],\n", + " [0.12062523481381847, 'Z0, Z2'],\n", + " [0.1659278503225078, 'Z0, Z3'],\n", + " [0.17141282639402383, 'Z1'],\n", + " [0.1659278503225078, 'Z1, Z2'],\n", + " [0.12062523481381847, 'Z1, Z3'],\n", + " [-0.22343153674664024, 'Z2'],\n", + " [0.17441287610651632, 'Z2, Z3'],\n", + " [-0.2234315367466403, 'Z3'],\n", + "]\n", + "\n", + "# 定义电路\n", + "num_qubits = 4\n", + "circuit = paddle_quantum.ansatz.Circuit(num_qubits)\n", + "circuit.ry('full')\n", + "circuit.cnot('cycle')\n", + "circuit.ry('full')\n", + "circuit.cnot('cycle')\n", + "circuit.ry('full')\n", + "# print(circuit)\n", + "\n", + "# 定义初态和优化器\n", + "init_state = paddle_quantum.state.zero_state(num_qubits)\n", + "optimizer = paddle.optimizer.Adam(learning_rate=0.1, parameters=circuit.parameters())\n", + "hamiltonian = paddle_quantum.Hamiltonian(hamiltonian_list)\n", + "loss_func = paddle_quantum.loss.ExpecVal(hamiltonian, shots=10000)\n", + "# 进行迭代训练\n", + "num_itr = 80\n", + "for itr in range(0, num_itr):\n", + " state = circuit(init_state)\n", + " loss = loss_func(state)\n", + " loss.backward()\n", + " optimizer.minimize(loss)\n", + " optimizer.clear_grad()\n", + " if itr % 10 == 0:\n", + " print(f\"The iter is {itr:3d}, loss is {loss.item():3.5f}.\")\n", + "print(\"The theoretical value is -1.137283834485513.\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "由上面的训练过程可以看出,我们可以通过量桨调用量易伏来实现量子机器学习算法。\n", + "\n", + "最后,我们总结一下量桨调用量易伏的用法。只需要在程序的最开头部分设置量桨和量易伏的 backend 就行。需要注意的是,很多函数都不支持在量易伏上运行,只有量子电路相关的功能才支持。" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.8.0 ('paddle-quantum-dev')", + "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.8.0" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "9043b12ec77a531919bc05f05830335d23baf822720cbea14b03018197d26545" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/introduction/PaddleQuantum_QuLeaf_EN.ipynb b/introduction/PaddleQuantum_QuLeaf_EN.ipynb new file mode 100644 index 0000000..78d1b8b --- /dev/null +++ b/introduction/PaddleQuantum_QuLeaf_EN.ipynb @@ -0,0 +1,218 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Using Paddle Quantum to connect to a quantum computer via the QuLeaf platform\n", + "\n", + "*Copyright (c) 2022 Institute for Quantum Computing, Baidu Inc. All Rights Reserved.*\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Introduction\n", + "\n", + "In quantum machine learning, the ultimate goal we want to achieve is to use quantum computers and classical computers for hybrid computation, thus enabling efficient Quantum Machine Learning (QML) algorithms. In Paddle Quantum, we also support the implementation of QML algorithms using quantum computers by connecting to a real quantum computer via [QuLeaf](https://quantum-hub.baidu.com/), a cloud-native quantum computing platform.\n", + "\n", + "### Introduction to QuLeaf\n", + "\n", + "QuLeaf is a cloud-native quantum computing platform launched by the Institute of Quantum Computing of Baidu Research Institute. QuLeaf is a good implementation of the deep integration of quantum computing and cloud computing. QuLeaf includes various usage methods such as local simulator, cloud-based simulator and cloud-based quantum computer. Among them, the use of cloud simulator and cloud quantum computer needs to consume QuLeaf credits, users need to register on the [QuLeaf website](https://quantum-hub.baidu.com/) to get QuLeaf account and points. QuLeaf includes QComposer, PyOnline, YunOnline, QCompute SDK and so on. Among them, QCompute SDK allows us to make calls through Python. In the Python environment, you need to enter the token corresponding to your account to make the call, and the token can be viewed at https://quantum-hub.baidu.com/token. If you want to use the cloud-based simulator or cloud-based quantum computer, you need to consume QuLeaf credits which can be obtained in the https://quantum-hub.baidu.com/feedback." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Paddle Quantum calls QuLeaf\n", + "\n", + "In the paddle quantum, we already support the backend implementation of QuLeaf. Just use `paddle_quantum.set_backend('quleaf')` to set the backend of the quantum paddle to QuLeaf. In addition to that, we need to set the simulation method of the QuLeaf. If we use cloud computation power, we also need to enter the token, so the complete setup code is as follows." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import paddle_quantum\n", + "from QCompute import BackendName\n", + "paddle_quantum.set_backend('quleaf')\n", + "paddle_quantum.backend.quleaf.set_quleaf_backend(BackendName.LocalBaiduSim2)\n", + "# If you are using the local simulator, you don't need to enter your token\n", + "# paddle_quantum.backend.quleaf.set_quleaf_token('your token')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `set_quleaf_backend()` function allows you to set the quantum end of QuLeaf.\n", + "\n", + "Currently, all the end the QuLeaf are as follows:\n", + "\n", + "| Type | Quantum End | Description |\n", + "| :---: | :---: | :---: |\n", + "| Local | LocalBaiduSim2 | Sim2, local simulator, Python version |\n", + "| Cloud | CloudBaiduSim2Water | Sim2, Cloud Simulator, C++ Version, Dockerd Multiple Instances |\n", + "| Cloud | CloudBaiduSim2Earth | Sim2, Cloud Simulator, Python Version, Single Instance, high-performance |\n", + "| Cloud | CloudBaiduSim2Thunder | Sim2, Cloud Simulator, C++ Version, Single Instance, high-performance |\n", + "| Cloud | CloudBaiduSim2Wind | Sim2, Cloud Simulator, C++ Version, Single Instance, Sparse |\n", + "| Cloud | CloudBaiduSim2Heaven | Sim2, Cloud Simulator, C++ Version, Single Instance, Cluster |\n", + "| Cloud | CloudBaiduSim2Lake | Sim2, Cloud Simulator, C++ Version, Single Instance, GPU |\n", + "| Cloud | CloudAerAtBD | Aer, Cloud Simulator, C++ Version (open-source) |\n", + "| Cloud | CloudIoPCAS | QPU 10-qubit quantum computer, from the Institute of Physics CAS |\n", + "\n", + "In this case, `LocalBaiduSim2` is a local simulator, which does not require token input and does not consume the QuLeaf credits; `CloudIoPCAS` is a real quantum computer, both the quantum computer and the simulator in the cloud require token input and consume the QuLeaf credits." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Bell state preparation\n", + "\n", + "Here, we test whether the paddle quantum is successfully connected to the QuLeaf by the example of the Bell state preparation. After executing the above code, the user has already set the paddle quantum to the QuLeaf mode." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "num_qubits = 2\n", + "init_state = paddle_quantum.state.zero_state(num_qubits)\n", + "circuit = paddle_quantum.ansatz.Circuit(num_qubits)\n", + "circuit.h(0)\n", + "circuit.cnot([0, 1])\n", + "bell_state = circuit(init_state)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If the above code runs successfully, it means that it can connect to the QuLeaf and execute the quantum circuit." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## VQE Example\n", + "\n", + "Here, we use VQE as an example to show how to use the QuLeaf through the paddle quantum. We use QuLeaf's local simulator for the demo." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The iter is 0, loss is 0.15198.\n", + "The iter is 10, loss is -0.76691.\n", + "The iter is 20, loss is -0.91600.\n", + "The iter is 30, loss is -0.97056.\n", + "The iter is 40, loss is -1.05518.\n", + "The iter is 50, loss is -1.13397.\n", + "The iter is 60, loss is -1.12326.\n", + "The iter is 70, loss is -1.13693.\n", + "The theoretical value is -1.137283834485513.\n" + ] + } + ], + "source": [ + "import paddle\n", + "\n", + "# define the hamiltonian\n", + "hamiltonian_list = [\n", + " [-0.0970662686176252, 'I'],\n", + " [-0.04530261550868938, 'X0, X1, Y2, Y3'],\n", + " [0.04530261550868938, 'X0, Y1, Y2, X3'],\n", + " [0.04530261550868938, 'Y0, X1, X2, Y3'],\n", + " [-0.04530261550868938, 'Y0, Y1, X2, X3'],\n", + " [0.1714128263940238, 'Z0'],\n", + " [0.16868898168693292, 'Z0, Z1'],\n", + " [0.12062523481381847, 'Z0, Z2'],\n", + " [0.1659278503225078, 'Z0, Z3'],\n", + " [0.17141282639402383, 'Z1'],\n", + " [0.1659278503225078, 'Z1, Z2'],\n", + " [0.12062523481381847, 'Z1, Z3'],\n", + " [-0.22343153674664024, 'Z2'],\n", + " [0.17441287610651632, 'Z2, Z3'],\n", + " [-0.2234315367466403, 'Z3'],\n", + "]\n", + "\n", + "# define the quantum circuit\n", + "num_qubits = 4\n", + "circuit = paddle_quantum.ansatz.Circuit(num_qubits)\n", + "circuit.ry('full')\n", + "circuit.cnot('cycle')\n", + "circuit.ry('full')\n", + "circuit.cnot('cycle')\n", + "circuit.ry('full')\n", + "# print(circuit)\n", + "\n", + "# define the initial quantum state and the optimizer\n", + "init_state = paddle_quantum.state.zero_state(num_qubits)\n", + "optimizer = paddle.optimizer.Adam(learning_rate=0.1, parameters=circuit.parameters())\n", + "hamiltonian = paddle_quantum.Hamiltonian(hamiltonian_list)\n", + "loss_func = paddle_quantum.loss.ExpecVal(hamiltonian, shots=10000)\n", + "# iterative training\n", + "num_itr = 80\n", + "for itr in range(0, num_itr):\n", + " state = circuit(init_state)\n", + " loss = loss_func(state)\n", + " loss.backward()\n", + " optimizer.minimize(loss)\n", + " optimizer.clear_grad()\n", + " if itr % 10 == 0:\n", + " print(f\"The iter is {itr:3d}, loss is {loss.item():3.5f}.\")\n", + "print(\"The theoretical value is -1.137283834485513.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "From the above training process, we can see that we can implement quantum machine learning algorithm by paddle quantum calling the QuLeaf.\n", + "\n", + "Finally, let's summarize the usage of the paddle quantum to call the QuLeaf. All you need to do is to set the backend of the paddle quantum and the QuLeaf in the beginning of the program. Note that many functions are not supported on the QuLeaf, only quantum circuit related functions are supported." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.8.0 ('paddle-quantum-dev')", + "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.8.0" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "9043b12ec77a531919bc05f05830335d23baf822720cbea14b03018197d26545" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/paddle_quantum/QAOA/example/main.py b/paddle_quantum/QAOA/example/main.py index 36fc53d..973b812 100644 --- a/paddle_quantum/QAOA/example/main.py +++ b/paddle_quantum/QAOA/example/main.py @@ -35,7 +35,8 @@ } -def main(n=4): +if __name__ == "__main__": + n = 4 paddle.seed(SEED) p = 4 # number of layers in the circuit @@ -73,7 +74,3 @@ def main(n=4): ax.margins(0.20) plt.axis("off") plt.show() - - -if __name__ == "__main__": - main() diff --git a/paddle_quantum/QAOA/maxcut.py b/paddle_quantum/QAOA/maxcut.py index 4be9bc0..9dfb2b9 100644 --- a/paddle_quantum/QAOA/maxcut.py +++ b/paddle_quantum/QAOA/maxcut.py @@ -33,13 +33,13 @@ def maxcut_hamiltonian(E): - r"""生成最大割问题对应的哈密顿量。 + r"""Generate the Hamiltonian for Max-Cut problem Args: - E (list): 图的边 + E (list): Edges of the graph Returns: - list: 生成的哈密顿量的列表形式 + list: list form of the generated Hamiltonian """ H_D_list = [] for (u, v) in E: @@ -49,22 +49,22 @@ def maxcut_hamiltonian(E): def find_cut(G, p, ITR, LR, print_loss=False, shots=0, plot=False): - r"""运行 QAOA 寻找最大割问题的近似解。 + r"""Find the approximated solution of given Max-Cut problem via QAOA Args: - G (NetworkX graph): 图 - p (int): QAOA 电路的层数 - ITR (int): 梯度下降优化参数的迭代次数 - LR (float): Adam 优化器的学习率 - print_loss (bool, optional): 优化过程中是否输出损失函数的值,默认为 ``False``,即不输出 - shots (int, optional): QAOA 电路最终输出的量子态的测量次数,默认 0,则返回测量结果的精确概率分布 - plot (bool, optional): 是否绘制测量结果图,默认为 ``False`` ,即不绘制 + G (NetworkX graph): Graph + p (int): depth of the QAOA circuit + ITR (int): maximum iteration times for optimization + LR (float): learning rate of the Adam optimizer + print_loss (bool, optional): whether print the loss value during optimization. Defaults to ``False``, not print + shots (int, optional): measurement times at the final output of QAOA circuit, Defaults to ``0``, exact probability distribution + plot (bool, optional): whether plot the result of measurement, Defaults to ``False``, not plot Returns: tuple: tuple containing: - string: 寻找到的近似解 - dict: 所有测量结果和其对应的出现次数 + string: approximated solution + dict: measurement results and their frequencies """ V = list(G.nodes()) # Map nodes' labels to integers from 0 to |V|-1 diff --git a/paddle_quantum/QAOA/tsp.py b/paddle_quantum/QAOA/tsp.py index 2ed5025..656fbc2 100644 --- a/paddle_quantum/QAOA/tsp.py +++ b/paddle_quantum/QAOA/tsp.py @@ -34,13 +34,15 @@ def tsp_hamiltonian(g, A, n): - """ - This is to construct Hamiltonia H_C + r"""This is to construct Hamiltonia H_C + Args: - G: the graph to solve + g: the graph to solve + A: the penality parameter + n: the number of vertices of graph g + Returns: - Hamiltonian list - Hamiltonian H_C + Hamiltonian with list form """ H_C_list1 = [] for i in range(n - 1): @@ -93,8 +95,7 @@ def tsp_hamiltonian(g, A, n): def solve_tsp(g, A, p=2, ITR=120, LR=0.4, print_loss=False, shots=0): - """ - This is the core function to solve the TSP. + r"""This is the core function to solve the TSP. Args: g: the graph to solve @@ -102,6 +103,9 @@ def solve_tsp(g, A, p=2, ITR=120, LR=0.4, print_loss=False, shots=0): p: number of layers of blocks in the complex entangled circuit (default value p=2) ITR: number of iteration steps for the complex entangled circuit (default value ITR=120) LR: learning rate for the gradient-based optimization method (default value LR=0.4) + print_loss (bool, optional): whether print the loss value during optimization. Defaults to ``False``, not print + shots (int, optional): measurement times at the final output of QAOA circuit, Defaults to ``0``, exact probability distribution + Returns: string representation for the optimized walk for the salesman """ diff --git a/paddle_quantum/SSVQE/HGenerator.py b/paddle_quantum/SSVQE/HGenerator.py index cbb3b9a..2e80d1d 100644 --- a/paddle_quantum/SSVQE/HGenerator.py +++ b/paddle_quantum/SSVQE/HGenerator.py @@ -23,10 +23,15 @@ def H_generator(N): + r"""Generate a Hamiltonian with trivial descriptions + + Args: + N: Number of Pauli strings + + Returns: + A Hamiltonian """ - Generate a Hamiltonian with trivial descriptions - Returns: A Hamiltonian - """ + # Generate the Pauli string representing a random Hamiltonian hamiltonian = random_pauli_str_generator(N, terms=10) print("Random Hamiltonian in Pauli string format = \n", hamiltonian) diff --git a/paddle_quantum/SSVQE/Paddle_SSVQE.py b/paddle_quantum/SSVQE/Paddle_SSVQE.py index 9f780ce..41094c5 100644 --- a/paddle_quantum/SSVQE/Paddle_SSVQE.py +++ b/paddle_quantum/SSVQE/Paddle_SSVQE.py @@ -37,6 +37,16 @@ def loss_func(U, H): + r"""Compute the loss function of SSVQE + + Args: + H: Hamiltonian + U: unitary of the circuit + + Returns: Tutle: inlcuding following elements + - loss function + - loss components + """ # Calculate loss function loss_struct = paddle.real(matmul(matmul(dagger(U), H), U)) # Use computational basis to calculate each expectation value, which is the same @@ -55,13 +65,16 @@ def loss_func(U, H): def Paddle_SSVQE(H, N=2, ITR=50, LR=0.3): - r""" - Paddle_SSVQE - :param H: Hamiltonian - :param N: Number of qubits/Width of QNN - :param ITR: Number of iterations - :param LR: Learning rate - :return: First several smallest eigenvalues of the Hamiltonian + r"""Paddle_SSVQE + + Args: + H: Hamiltonian + N: Number of qubits/Width of QNN + ITR: Number of iterations + LR: Learning rate + + Returns: + First several smallest eigenvalues of the Hamiltonian """ # We need to convert Numpy array to variable supported in PaddlePaddle @@ -89,7 +102,7 @@ def Paddle_SSVQE(H, N=2, ITR=50, LR=0.3): return loss_components -def main(): +if __name__ == '__main__': paddle.seed(SEED) N = 2 H = H_generator(N) @@ -127,6 +140,3 @@ def output_ordinalvalue(num): output_ordinalvalue(i), numpy.linalg.eigh(H)[0][i]) ) - -if __name__ == '__main__': - main() diff --git a/paddle_quantum/SSVQE/example/main.py b/paddle_quantum/SSVQE/example/main.py index f8c28da..e39ad11 100644 --- a/paddle_quantum/SSVQE/example/main.py +++ b/paddle_quantum/SSVQE/example/main.py @@ -20,9 +20,10 @@ import numpy from paddle_quantum.SSVQE.HGenerator import H_generator from paddle_quantum.SSVQE.Paddle_SSVQE import Paddle_SSVQE + -def main(): +if __name__ == '__main__': N = 2 H = H_generator(N) @@ -38,8 +39,4 @@ def main(): print('The theoretical 2nd excited state energy: ', numpy.linalg.eigh(H)[0][2]) print('The estimated 3rd excited state energy is: ', loss_components[3].numpy()) - print('The theoretical 3rd excited state energy: ', numpy.linalg.eigh(H)[0][3]) - - -if __name__ == '__main__': - main() + print('The theoretical 3rd excited state energy: ', numpy.linalg.eigh(H)[0][3]) \ No newline at end of file diff --git a/paddle_quantum/VQE/Paddle_VQE.py b/paddle_quantum/VQE/Paddle_VQE.py index 5a893bf..b39afed 100644 --- a/paddle_quantum/VQE/Paddle_VQE.py +++ b/paddle_quantum/VQE/Paddle_VQE.py @@ -37,14 +37,14 @@ def Paddle_VQE(Hamiltonian, N, D=2, ITR=80, LR=0.2): - r""" - Main Learning network using dynamic graph - :param Hamiltonian: Hamiltonian - :param N: Width of QNN - :param D: Depth of QNN - :param ITR: Number of iterations - :param LR: Learning rate - :return: No return + r"""Main Learning network using dynamic graph + + Args: + Hamiltonian: Hamiltonian + N: Width of QNN + D: Depth of QNN. Defaults to 2. + ITR: Number of iterations. Defaults to 80. + LR: Learning rate. Defaults to 0.2. """ # Determine the dimensions of network @@ -86,7 +86,7 @@ def Paddle_VQE(Hamiltonian, N, D=2, ITR=80, LR=0.2): savez("./output/summary_data", iter=summary_iter, energy=summary_loss) -def main(): +if __name__ == '__main__': # Read data from built-in function or xyz file depending on OS sysStr = platform.system() @@ -110,7 +110,3 @@ def main(): Paddle_VQE(hamiltonian, N) benchmark_result() - - -if __name__ == '__main__': - main() diff --git a/paddle_quantum/VQE/chemistrygen.py b/paddle_quantum/VQE/chemistrygen.py index 07b9787..70bdbd6 100644 --- a/paddle_quantum/VQE/chemistrygen.py +++ b/paddle_quantum/VQE/chemistrygen.py @@ -29,10 +29,15 @@ ] -def Hamiltonian_str_convert(qubit_op): - ''' - Convert provided Hamiltonian information to Pauli string - ''' +def _Hamiltonian_str_convert(qubit_op): + r"""Convert provided Hamiltonian information to Pauli string + + Args: + qubit_op: instance of ``QubitOperator`` class defined in ``openfermion`` + + Returns: + H_info for Hamiltonian + """ info_dic = qubit_op.terms def process_tuple(tup): @@ -55,10 +60,21 @@ def process_tuple(tup): def calc_H_rho_from_qubit_operator(qubit_op, n_qubits): + r"""Generate a Hamiltonian from QubitOperator + + Args: + qubit_op: instance of ``QubitOperator`` class defined in ``openfermion`` + n_qubits: number of qubits + + Raises: + Exception: unrecognized basis + + Returns: + Tuple: + - H: (2**n, 2**n) complex128 array, as the Hamiltonian (n == n_qubits) + - rho: (2**n, 2**n) complex128 array, as the density matrix (n == n_qubits) """ - Generate a Hamiltonian from QubitOperator - Returns: H, rho - """ + # const beta = 1 @@ -94,9 +110,20 @@ def calc_H_rho_from_qubit_operator(qubit_op, n_qubits): def read_calc_H(geo_fn, multiplicity=1, charge=0): - """ - Read and calc the H and rho - Returns: H,rho matrix + r"""Read and calc the H and rho + + Args: + geo_fn (str): geometry filename + multiplicity (int, optional): used in openfermionpyscf, Defaults to 1. + charge (int, optional): used in openfermionpyscf, Defaults to 0. + + Raises: + Exception: filename should be a string + + Returns: + Tuple: + - H: the Hamiltonian + - nqubit: qubit 的个数 """ if not isinstance(geo_fn, str): # geo_fn = 'h2.xyz' @@ -117,19 +144,11 @@ def read_calc_H(geo_fn, multiplicity=1, charge=0): qubit_op = openfermion.transforms.jordan_wigner(molecular_hamiltonian) # calc H - Hamiltonian = Hamiltonian_str_convert(qubit_op) + Hamiltonian = _Hamiltonian_str_convert(qubit_op) return Hamiltonian, molecular_hamiltonian.n_qubits -def main(): - """ - The main function - """ - +if __name__ == '__main__': filename = 'h2.xyz' H, N = read_calc_H(geo_fn=filename) print('H', H) - - -if __name__ == '__main__': - main() diff --git a/paddle_quantum/VQE/chemistrysub.py b/paddle_quantum/VQE/chemistrysub.py index ab7330e..f187f19 100644 --- a/paddle_quantum/VQE/chemistrysub.py +++ b/paddle_quantum/VQE/chemistrysub.py @@ -27,9 +27,12 @@ def H_generator(): - """ - Generate a Hamiltonian with trivial descriptions - :return: a Hamiltonian, 'mat' + r"""Generate a Hamiltonian with trivial descriptions + + Returns: + Tuple: including following elements + - H: the Hamiltonian + - rho: density matrix """ beta = 1 @@ -47,9 +50,12 @@ def H_generator(): def H2_generator(): - """ - Generate a Hamiltonian with trivial descriptions - Returns: A Hamiltonian, 'mat' + r"""Generate a Hamiltonian with trivial descriptions + + Returns: + tuple contains + - H: Hamiltonian, a list of Pauli string + - N: the number of qubits """ beta = 1 diff --git a/paddle_quantum/VQE/example/main.py b/paddle_quantum/VQE/example/main.py index b68e389..c864443 100644 --- a/paddle_quantum/VQE/example/main.py +++ b/paddle_quantum/VQE/example/main.py @@ -23,11 +23,8 @@ from paddle_quantum.VQE.chemistrysub import H2_generator -def main(): - """ - Main Learning network using dynamic graph - :return: Plot or No return - """ +if __name__ == '__main__': + #Main Learning network using dynamic graph # Read data from built-in function or xyz file depending on OS sysStr = platform.system() @@ -51,7 +48,3 @@ def main(): Paddle_VQE(hamiltonian, N) benchmark_result() - - -if __name__ == '__main__': - main() diff --git a/paddle_quantum/VQSD/HGenerator.py b/paddle_quantum/VQSD/HGenerator.py index 9a8a8a5..71ceb67 100644 --- a/paddle_quantum/VQSD/HGenerator.py +++ b/paddle_quantum/VQSD/HGenerator.py @@ -27,6 +27,14 @@ def generate_rho_sigma(): + r"""Generate two quantum states with specific eigenvalues + + Returns: Tuple: including following elements + - rho + - sigma + + """ + numpy.random.seed(SEED) V = scipy.stats.unitary_group.rvs(4) # Generate a random unitary matrix D = numpy.diag([0.5, 0.3, 0.1, 0.1]) # Input the spectrum of the target state rho diff --git a/paddle_quantum/VQSD/Paddle_VQSD.py b/paddle_quantum/VQSD/Paddle_VQSD.py index a9d6cd9..e994c81 100644 --- a/paddle_quantum/VQSD/Paddle_VQSD.py +++ b/paddle_quantum/VQSD/Paddle_VQSD.py @@ -36,6 +36,18 @@ def loss_func(U, rho, sigma): + r"""Compute the loss function of VQSD + + Args: + rho: Quantum state to be diagonalized + sigma: Quantum state sigma + U: Unitary of the circuit + + Returns: + Overlap between sigma and the state that is generated by applying the unitary to rho + + """ + # rho_tilda is the quantum state obtained by acting U on rho, which is U*rho*U^dagger rho_tilde = matmul(matmul(U, rho), dagger(U)) @@ -46,14 +58,17 @@ def loss_func(U, rho, sigma): def Paddle_VQSD(rho, sigma, N=2, ITR=50, LR=0.2): - r""" - Paddle_VQSD - :param rho: Quantum state to be diagonalized - :param sigma: Quantum state sigma - :param N: Width of QNN - :param ITR: Number of iterations - :param LR: Learning rate - :return: Diagonalized quantum state after optimization + r"""Paddle_VQSD + + Args: + rho: Quantum state to be diagonalized + sigma: Quantum state sigma + N: Width of QNN + ITR: Number of iterations + LR: Learning rate + + Returns: + Diagonalized quantum state after optimization """ rho = paddle.to_tensor(rho, dtype=paddle_quantum.get_dtype()) sigma = paddle.to_tensor(sigma, dtype=paddle_quantum.get_dtype()) @@ -82,9 +97,9 @@ def Paddle_VQSD(rho, sigma, N=2, ITR=50, LR=0.2): return rho_tilde_np + -def main(): - +if __name__ == '__main__': D = [0.5, 0.3, 0.1, 0.1] rho, sigma = generate_rho_sigma() @@ -93,7 +108,3 @@ def main(): print("The estimated spectrum is:", numpy.real(numpy.diag(rho_tilde_np))) print('The target spectrum is:', D) - - -if __name__ == '__main__': - main() diff --git a/paddle_quantum/VQSD/example/main.py b/paddle_quantum/VQSD/example/main.py index c05f6ad..d891c2f 100644 --- a/paddle_quantum/VQSD/example/main.py +++ b/paddle_quantum/VQSD/example/main.py @@ -20,9 +20,10 @@ import numpy from paddle_quantum.VQSD.HGenerator import generate_rho_sigma from paddle_quantum.VQSD.Paddle_VQSD import Paddle_VQSD + -def main(): +if __name__ == '__main__': """ main """ @@ -34,7 +35,3 @@ def main(): print("The estimated spectrum is:", numpy.real(numpy.diag(rho_tilde_np))) print('The target spectrum is:', D) - - -if __name__ == '__main__': - main() diff --git a/paddle_quantum/__init__.py b/paddle_quantum/__init__.py index 07be7e4..ff87617 100644 --- a/paddle_quantum/__init__.py +++ b/paddle_quantum/__init__.py @@ -16,7 +16,8 @@ r""" Paddle Quantum Library. """ - +import os +os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"]="python" from .backend import Backend from .state import State from .base import Operator @@ -32,7 +33,6 @@ from . import loss from . import mbqc from . import operator -from . import qchem from . import base from . import dataset from . import finance @@ -46,4 +46,4 @@ from . import visual name = 'paddle_quantum' -__version__ = '2.2.0' +__version__ = '2.2.1' diff --git a/paddle_quantum/ansatz/circuit.py b/paddle_quantum/ansatz/circuit.py index ff41b4d..e5a058d 100644 --- a/paddle_quantum/ansatz/circuit.py +++ b/paddle_quantum/ansatz/circuit.py @@ -17,6 +17,7 @@ The source file of the Circuit class. """ +import warnings import paddle from .container import Sequential from ..gate import Gate, H, S, T, X, Y, Z, P, RX, RY, RZ, U3 @@ -31,7 +32,9 @@ from ..gate import AmplitudeEncoding from ..channel import BitFlip, PhaseFlip, BitPhaseFlip, AmplitudeDamping, GeneralizedAmplitudeDamping, PhaseDamping from ..channel import Depolarizing, PauliChannel, ResetChannel, ThermalRelaxation, KrausRepr +from ..intrinsic import _get_float_dtype from ..state import zero_state +from ..operator import Collapse from typing import Union, Iterable, Optional, Dict, List, Tuple from paddle_quantum import State, get_backend, get_dtype, Backend from math import pi @@ -44,14 +47,14 @@ class Circuit(Sequential): Args: num_qubits: Number of qubits. Defaults to None. """ - + def __init__(self, num_qubits: Optional[int] = None): super().__init__() self.__num_qubits = num_qubits - + # whether the circuit is a dynamic quantum circuit self.__isdynamic = True if num_qubits is None else False - + # alias for ccx self.toffoli = self.ccx @@ -66,18 +69,20 @@ def isdynamic(self) -> bool: r"""Whether the circuit is dynamic """ return self.__dynamic - + @num_qubits.setter def num_qubits(self, value: int) -> None: assert isinstance(value, int) self.__num_qubits = value - - @property + + @property def param(self) -> paddle.Tensor: r"""Flattened parameters in the circuit. """ + if len(self.parameters()) == 0: + return [] return paddle.concat([paddle.flatten(param) for param in self.parameters()]) - + @property def grad(self) -> np.ndarray: r"""Gradients with respect to the flattened parameters. @@ -88,20 +93,32 @@ def grad(self) -> np.ndarray: ' otherwise check where the gradient chain is broken' grad_list.append(paddle.flatten(param.grad)) return paddle.concat(grad_list).numpy() - - def update_param(self, theta: Union[paddle.Tensor, np.ndarray, float], idx: Optional[int] = None) -> None: + + @property + def depth(self) -> int: + r"""(current) Depth of this Circuit + """ + qubit_depth = [len(qubit_gates) for qubit_gates in self.qubit_history] + return max(qubit_depth) + + def update_param(self, theta: Union[paddle.Tensor, np.ndarray, float], idx: int = None) -> None: r"""Replace parameters of all/one layer(s) by ``theta``. Args: theta: New parameters - idx: Index of replacement. Defaults to None, refering to all layers. + idx: Index of replacement. Defaults to None, referring to all layers. """ if not isinstance(theta, paddle.Tensor): theta = paddle.to_tensor(theta, dtype='float32') theta = paddle.flatten(theta) - + + backend_dtype = _get_float_dtype(get_dtype()) + if backend_dtype != 'float32': + warnings.warn( + f"\ndtype of parameters will be float32 instead of {backend_dtype}", UserWarning) + if idx is None: - assert self.param.shape == theta.shape, "the shapen of input paramters is not correct" + assert self.param.shape == theta.shape, "the shape of input parameters is not correct" for layer in self.sublayers(): for name, _ in layer.named_parameters(): param = getattr(layer, name) @@ -109,9 +126,8 @@ def update_param(self, theta: Union[paddle.Tensor, np.ndarray, float], idx: Opt param = paddle.create_parameter( shape=param.shape, - dtype=param.dtype, - default_initializer=paddle.nn.initializer.Assign( - theta[:num_param].reshape(param.shape)), + dtype='float32', + default_initializer=paddle.nn.initializer.Assign(theta[:num_param].reshape(param.shape)), ) setattr(layer, 'theta', param) @@ -119,21 +135,19 @@ def update_param(self, theta: Union[paddle.Tensor, np.ndarray, float], idx: Opt return theta = theta[num_param:] elif isinstance(idx, int): - assert idx < len(self.sublayers( - )), "the index is out of range, expect below " + str(len(self.sublayers())) + assert idx < len(self.sublayers()), "the index is out of range, expect below " + str(len(self.sublayers())) layer = self.sublayers()[idx] assert theta.shape == paddle.concat([paddle.flatten(param) for param in layer.parameters()]).shape, \ - "the shape of input paramters is not correct," - + "the shape of input parameters is not correct," + for name, _ in layer.named_parameters(): param = getattr(layer, name) num_param = int(paddle.numel(param)) param = paddle.create_parameter( shape=param.shape, - dtype=param.dtype, - default_initializer=paddle.nn.initializer.Assign( - theta[:num_param].reshape(param.shape)), + dtype='float32', + default_initializer=paddle.nn.initializer.Assign(theta[:num_param].reshape(param.shape)), ) setattr(layer, 'theta', param) @@ -142,10 +156,20 @@ def update_param(self, theta: Union[paddle.Tensor, np.ndarray, float], idx: Opt theta = theta[num_param:] else: raise ValueError("idx must be an integer or None") - - def randomize_param(self, low: float = 0, high: Optional[float] = 2 * pi) -> None: + + def transfer_static(self) -> None: + r""" set ``stop_gradient`` of all parameters of the circuit as ``True`` + + """ + for layer in self.sublayers(): + for name, _ in layer.named_parameters(): + param = getattr(layer, name) + param.stop_gradient = True + setattr(layer, 'theta', param) + + def randomize_param(self, low: float = 0, high: Optional[float] = 2 * pi) -> None: r"""Randomize parameters of the circuit in a range from low to high. - + Args: low: Lower bound. high: Upper bound. @@ -157,14 +181,13 @@ def randomize_param(self, low: float = 0, high: Optional[float] = 2 * pi) -> No param = paddle.create_parameter( shape=param.shape, dtype=param.dtype, - default_initializer=paddle.nn.initializer.Uniform( - low=low, high=high), + default_initializer=paddle.nn.initializer.Uniform(low=low, high=high), ) setattr(layer, 'theta', param) def __num_qubits_update(self, qubits_idx: Union[Iterable[int], int, str]) -> None: r"""Update ``self.num_qubits`` according to ``qubits_idx``, or report error. - + Args: qubits_idx: Input qubit indices of a quantum gate. """ @@ -183,8 +206,7 @@ def __num_qubits_update(self, qubits_idx: Union[Iterable[int], int, str]) -> Non return assert max_idx + 1 <= num_qubits or self.__isdynamic, \ - "The circuit is not a dynamic quantum circuit. Invalid input qubit idx: " + \ - str(max_idx) + " num_qubit: " + str(self.__num_qubits) + f"The circuit is not a dynamic quantum circuit. Invalid input qubit idx: {max_idx} num_qubit: {self.__num_qubits}" self.__num_qubits = int(max(max_idx + 1, num_qubits)) def h( @@ -208,8 +230,7 @@ def h( depth: Number of layers. Defaults to 1. """ self.__num_qubits_update(qubits_idx) - self.append( - H(qubits_idx, self.num_qubits if num_qubits is None else num_qubits, depth)) + self.append(H(qubits_idx, self.num_qubits if num_qubits is None else num_qubits, depth)) def s( self, qubits_idx: Union[Iterable[int], int, str] = 'full', num_qubits: Optional[int] = None, depth: int = 1 @@ -266,10 +287,10 @@ def x( The matrix form of such a gate is: .. math:: - X = \begin{bmatrix} - 0 & 1 \\ - 1 & 0 - \end{bmatrix} + X = \begin{bmatrix} + 0 & 1 \\ + 1 & 0 + \end{bmatrix} Args: qubits_idx: Indices of the qubits on which the gates are applied. Defaults to 'full'. @@ -290,9 +311,9 @@ def y( .. math:: Y = \begin{bmatrix} - 0 & -i \\ - i & 0 - \end{bmatrix} + 0 & -i \\ + i & 0 + \end{bmatrix} Args: qubits_idx: Indices of the qubits on which the gates are applied. Defaults to 'full'. @@ -349,8 +370,8 @@ def p( param_sharing: Whether gates in the same layer share a parameter. Defaults to False. """ self.__num_qubits_update(qubits_idx) - self.append(P(qubits_idx, self.num_qubits if num_qubits is None else num_qubits, - depth, param, param_sharing)) + self.append( + P(qubits_idx, self.num_qubits if num_qubits is None else num_qubits, depth, param, param_sharing)) def rx( self, qubits_idx: Union[Iterable[int], int, str] = 'full', num_qubits: Optional[int] = None, depth: int = 1, @@ -375,8 +396,8 @@ def rx( param_sharing: Whether gates in the same layer share a parameter. Defaults to False. """ self.__num_qubits_update(qubits_idx) - self.append(RX(qubits_idx, self.num_qubits if num_qubits is None else num_qubits, - depth, param, param_sharing)) + self.append(RX(qubits_idx, self.num_qubits if num_qubits is None else num_qubits, + depth, param, param_sharing)) def ry( self, qubits_idx: Union[Iterable[int], int, str] = 'full', num_qubits: Optional[int] = None, depth: int = 1, @@ -401,8 +422,8 @@ def ry( param_sharing: Whether gates in the same layer share a parameter. Defaults to False. """ self.__num_qubits_update(qubits_idx) - self.append(RY(qubits_idx, self.num_qubits if num_qubits is None else num_qubits, - depth, param, param_sharing)) + self.append(RY(qubits_idx, self.num_qubits if num_qubits is None else num_qubits, + depth, param, param_sharing)) def rz( self, qubits_idx: Union[Iterable[int], int, str] = 'full', num_qubits: Optional[int] = None, depth: int = 1, @@ -427,8 +448,8 @@ def rz( param_sharing: Whether gates in the same layer share a parameter. Defaults to False. """ self.__num_qubits_update(qubits_idx) - self.append(RZ(qubits_idx, self.num_qubits if num_qubits is None else num_qubits, - depth, param, param_sharing)) + self.append(RZ(qubits_idx, self.num_qubits if num_qubits is None else num_qubits, + depth, param, param_sharing)) def u3( self, qubits_idx: Union[Iterable[int], int, str] = 'full', num_qubits: int = None, depth: int = 1, @@ -456,8 +477,8 @@ def u3( param_sharing: Whether gates in the same layer share a parameter. Defaults to False. """ self.__num_qubits_update(qubits_idx) - self.append(U3(qubits_idx, self.num_qubits if num_qubits is None else num_qubits, - depth, param, param_sharing)) + self.append(U3(qubits_idx, self.num_qubits if num_qubits is None else num_qubits, + depth, param, param_sharing)) def cnot( self, qubits_idx: Union[Iterable[int], str] = 'cycle', num_qubits: int = None, depth: int = 1 @@ -616,8 +637,8 @@ def cp( param_sharing: Whether gates in the same layer share a parameter. Defaults to False. """ self.__num_qubits_update(qubits_idx) - self.append(CP(qubits_idx, self.num_qubits if num_qubits is None else num_qubits, - depth, param, param_sharing)) + self.append(CP(qubits_idx, self.num_qubits if num_qubits is None else num_qubits, + depth, param, param_sharing)) def crx( self, qubits_idx: Union[Iterable[int], str] = 'cycle', num_qubits: int = None, depth: int = 1, @@ -648,8 +669,8 @@ def crx( param_sharing: Whether gates in the same layer share a parameter. Defaults to False. """ self.__num_qubits_update(qubits_idx) - self.append(CRX( - qubits_idx, self.num_qubits if num_qubits is None else num_qubits, depth, param, param_sharing)) + self.append(CRX(qubits_idx, self.num_qubits if num_qubits is None else num_qubits, + depth, param, param_sharing)) def cry( self, qubits_idx: Union[Iterable[int], str] = 'cycle', num_qubits: int = None, depth: int = 1, @@ -680,8 +701,8 @@ def cry( param_sharing: Whether gates in the same layer share a parameter. Defaults to False. """ self.__num_qubits_update(qubits_idx) - self.append(CRY( - qubits_idx, self.num_qubits if num_qubits is None else num_qubits, depth, param, param_sharing)) + self.append(CRY(qubits_idx, self.num_qubits if num_qubits is None else num_qubits, + depth, param, param_sharing)) def crz( self, qubits_idx: Union[Iterable[int], str] = 'cycle', num_qubits: int = None, depth: int = 1, @@ -712,8 +733,8 @@ def crz( param_sharing: Whether gates in the same layer share a parameter. Defaults to False. """ self.__num_qubits_update(qubits_idx) - self.append(CRZ( - qubits_idx, self.num_qubits if num_qubits is None else num_qubits, depth, param, param_sharing)) + self.append(CRZ(qubits_idx, self.num_qubits if num_qubits is None else num_qubits, + depth, param, param_sharing)) def cu( self, qubits_idx: Union[Iterable[int], str] = 'cycle', num_qubits: int = None, depth: int = 1, @@ -744,8 +765,8 @@ def cu( param_sharing: Whether gates in the same layer share a parameter. Defaults to False. """ self.__num_qubits_update(qubits_idx) - self.append(CU(qubits_idx, self.num_qubits if num_qubits is None else num_qubits, - depth, param, param_sharing)) + self.append(CU(qubits_idx, self.num_qubits if num_qubits is None else num_qubits, + depth, param, param_sharing)) def rxx( self, qubits_idx: Union[Iterable[int], str] = 'cycle', num_qubits: int = None, depth: int = 1, @@ -775,8 +796,8 @@ def rxx( param_sharing: Whether gates in the same layer share a parameter. Defaults to False. """ self.__num_qubits_update(qubits_idx) - self.append(RXX( - qubits_idx, self.num_qubits if num_qubits is None else num_qubits, depth, param, param_sharing)) + self.append(RXX(qubits_idx, self.num_qubits if num_qubits is None else num_qubits, + depth, param, param_sharing)) def ryy( self, qubits_idx: Union[Iterable[int], str] = 'cycle', num_qubits: int = None, depth: int = 1, @@ -806,8 +827,8 @@ def ryy( param_sharing: Whether gates in the same layer share a parameter. Defaults to False. """ self.__num_qubits_update(qubits_idx) - self.append(RYY( - qubits_idx, self.num_qubits if num_qubits is None else num_qubits, depth, param, param_sharing)) + self.append(RYY(qubits_idx, self.num_qubits if num_qubits is None else num_qubits, + depth, param, param_sharing)) def rzz( self, qubits_idx: Union[Iterable[int], str] = 'cycle', num_qubits: int = None, depth: int = 1, @@ -837,8 +858,8 @@ def rzz( param_sharing: Whether gates in the same layer share a parameter. Defaults to False. """ self.__num_qubits_update(qubits_idx) - self.append(RZZ( - qubits_idx, self.num_qubits if num_qubits is None else num_qubits, depth, param, param_sharing)) + self.append(RZZ(qubits_idx, self.num_qubits if num_qubits is None else num_qubits, + depth, param, param_sharing)) def ms( self, qubits_idx: Union[Iterable[int], str] = 'cycle', num_qubits: int = None, depth: int = 1 @@ -928,8 +949,8 @@ def ccx( depth: Number of layers. Defaults to 1. """ self.__num_qubits_update(qubits_idx) - self.append(Toffoli( - qubits_idx, self.num_qubits if num_qubits is None else num_qubits, depth)) + self.append( + Toffoli(qubits_idx, self.num_qubits if num_qubits is None else num_qubits, depth)) def universal_two_qubits( self, qubits_idx: Union[Iterable[int], str] = 'cycle', num_qubits: int = None, depth: int = 1, @@ -945,8 +966,8 @@ def universal_two_qubits( param_sharing: Whether gates in the same layer share a parameter. Defaults to False. """ self.__num_qubits_update(qubits_idx) - self.append(UniversalTwoQubits( - qubits_idx, self.num_qubits if num_qubits is None else num_qubits, depth, param, param_sharing)) + self.append(UniversalTwoQubits(qubits_idx, self.num_qubits if num_qubits is None else num_qubits, + depth, param, param_sharing)) def universal_three_qubits( self, qubits_idx: Union[Iterable[int], str] = 'cycle', num_qubits: int = None, depth: int = 1, @@ -965,12 +986,12 @@ def universal_three_qubits( ValueError: The ``param`` must be paddle.Tensor or float. """ self.__num_qubits_update(qubits_idx) - self.append(UniversalThreeQubits( - qubits_idx, self.num_qubits if num_qubits is None else num_qubits, depth, param, param_sharing)) + self.append(UniversalThreeQubits(qubits_idx, self.num_qubits if num_qubits is None else num_qubits, + depth, param, param_sharing)) def oracle( self, oracle: paddle.tensor, qubits_idx: Union[Iterable[Iterable[int]], Iterable[int], int], - num_qubits: int = None, depth: int = 1 + num_qubits: int = None, depth: int = 1, gate_name: str = 'O' ) -> None: """Add an oracle gate. @@ -979,16 +1000,15 @@ def oracle( qubits_idx: Indices of the qubits on which the gates are applied. num_qubits: Total number of qubits. Defaults to None. depth: Number of layers. Defaults to 1. + gate_name: name of this oracle """ self.__num_qubits_update(qubits_idx) - self.append(Oracle(oracle, qubits_idx, - self.num_qubits if num_qubits is None else num_qubits, depth)) + self.append(Oracle(oracle, qubits_idx, + self.num_qubits if num_qubits is None else num_qubits, depth, gate_name)) def control_oracle( - self, oracle: paddle.Tensor, - # num_control_qubits: int, controlled_value: 'str', - qubits_idx: Union[Iterable[Iterable[int]], Iterable[int]], - num_qubits: int = None, depth: int = 1 + self, oracle: paddle.Tensor, qubits_idx: Union[Iterable[Iterable[int]], Iterable[int]], + num_qubits: int = None, depth: int = 1, gate_name: str = 'cO' ) -> None: """Add a controlled oracle gate. @@ -997,10 +1017,33 @@ def control_oracle( qubits_idx: Indices of the qubits on which the gates are applied. num_qubits: Total number of qubits. Defaults to None. depth: Number of layers. Defaults to 1. + gate_name: name of this oracle """ self.__num_qubits_update(qubits_idx) - self.append(ControlOracle(oracle, qubits_idx, - self.num_qubits if num_qubits is None else num_qubits, depth)) + self.append(ControlOracle(oracle, qubits_idx, + self.num_qubits if num_qubits is None else num_qubits, depth, gate_name)) + + def collapse(self, qubits_idx: Union[Iterable[int], int, str] = 'full', num_qubits: int = None, + desired_result: Union[int, str] = None, if_print: bool = False, + measure_basis: Union[Iterable[paddle.Tensor], str] = 'z') -> None: + r""" + Args: + qubits_idx: list of qubits to be collapsed. Defaults to ``'full'``. + num_qubits: Total number of qubits. Defaults to ``None``. + desired_result: The desired result you want to collapse. Defaults to ``None`` meaning randomly choose one. + if_print: whether print the information about the collapsed state. Defaults to ``False``. + measure_basis: The basis of the measurement. The quantum state will collapse to the corresponding eigenstate. + + Raises: + NotImplementedError: If the basis of measurement is not z. Other bases will be implemented in future. + TypeError: cannot get probability of state when the backend is unitary_matrix. + + Note: + When desired_result is `None`, Collapse does not support gradient calculation + """ + self.__num_qubits_update(qubits_idx) + self.append(Collapse(qubits_idx, self.num_qubits if num_qubits is None else num_qubits, + desired_result, if_print, measure_basis)) def superposition_layer( self, qubits_idx: Iterable[int] = 'full', num_qubits: int = None, depth: int = 1 @@ -1013,9 +1056,9 @@ def superposition_layer( depth: Number of layers. Defaults to 1. """ self.__num_qubits_update(qubits_idx) - self.append(SuperpositionLayer( - qubits_idx, self.num_qubits if num_qubits is None else num_qubits, depth)) - + self.append( + SuperpositionLayer(qubits_idx, self.num_qubits if num_qubits is None else num_qubits, depth)) + def weak_superposition_layer( self, qubits_idx: Iterable[int] = 'full', num_qubits: int = None, depth: int = 1 ) -> None: @@ -1027,9 +1070,9 @@ def weak_superposition_layer( depth: Number of layers. Defaults to 1. """ self.__num_qubits_update(qubits_idx) - self.append(WeakSuperpositionLayer( - qubits_idx, self.num_qubits if num_qubits is None else num_qubits, depth)) - + self.append( + WeakSuperpositionLayer(qubits_idx, self.num_qubits if num_qubits is None else num_qubits, depth)) + def linear_entangled_layer( self, qubits_idx: Iterable[int] = 'full', num_qubits: int = None, depth: int = 1 ) -> None: @@ -1041,8 +1084,8 @@ def linear_entangled_layer( depth: Number of layers. Defaults to 1. """ self.__num_qubits_update(qubits_idx) - self.append(LinearEntangledLayer( - qubits_idx, self.num_qubits if num_qubits is None else num_qubits, depth)) + self.append( + LinearEntangledLayer(qubits_idx, self.num_qubits if num_qubits is None else num_qubits, depth)) def real_entangled_layer( self, qubits_idx: Iterable[int] = 'full', num_qubits: int = None, depth: int = 1 @@ -1055,8 +1098,8 @@ def real_entangled_layer( depth: Number of layers. Defaults to 1. """ self.__num_qubits_update(qubits_idx) - self.append(RealEntangledLayer( - qubits_idx, self.num_qubits if num_qubits is None else num_qubits, depth)) + self.append( + RealEntangledLayer(qubits_idx, self.num_qubits if num_qubits is None else num_qubits, depth)) def complex_entangled_layer( self, qubits_idx: Iterable[int] = 'full', num_qubits: int = None, depth: int = 1 @@ -1069,8 +1112,8 @@ def complex_entangled_layer( depth: Number of layers. Defaults to 1. """ self.__num_qubits_update(qubits_idx) - self.append(ComplexEntangledLayer( - qubits_idx, self.num_qubits if num_qubits is None else num_qubits, depth)) + self.append( + ComplexEntangledLayer(qubits_idx, self.num_qubits if num_qubits is None else num_qubits, depth)) def real_block_layer( self, qubits_idx: Iterable[int] = 'full', num_qubits: int = None, depth: int = 1 @@ -1083,9 +1126,9 @@ def real_block_layer( depth: Number of layers. Defaults to 1. """ self.__num_qubits_update(qubits_idx) - self.append(RealBlockLayer( - qubits_idx, self.num_qubits if num_qubits is None else num_qubits, depth)) - + self.append( + RealBlockLayer(qubits_idx, self.num_qubits if num_qubits is None else num_qubits, depth)) + def complex_block_layer( self, qubits_idx: Iterable[int] = 'full', num_qubits: int = None, depth: int = 1 ) -> None: @@ -1097,8 +1140,8 @@ def complex_block_layer( depth: Number of layers. Defaults to 1. """ self.__num_qubits_update(qubits_idx) - self.append(ComplexBlockLayer( - qubits_idx, self.num_qubits if num_qubits is None else num_qubits, depth)) + self.append( + ComplexBlockLayer(qubits_idx, self.num_qubits if num_qubits is None else num_qubits, depth)) def bit_flip( self, prob: Union[paddle.Tensor, float], qubits_idx: Union[Iterable[int], int, str] = 'full', num_qubits: int = None @@ -1111,9 +1154,9 @@ def bit_flip( num_qubits: Total number of qubits. Defaults to None. """ self.__num_qubits_update(qubits_idx) - self.append(BitFlip(prob, qubits_idx, - self.num_qubits if num_qubits is None else num_qubits)) - + self.append(BitFlip(prob, qubits_idx, + self.num_qubits if num_qubits is None else num_qubits)) + def phase_flip( self, prob: Union[paddle.Tensor, float], qubits_idx: Union[Iterable[int], int, str] = 'full', num_qubits: int = None ) -> None: @@ -1125,9 +1168,9 @@ def phase_flip( num_qubits: Total number of qubits. Defaults to None. """ self.__num_qubits_update(qubits_idx) - self.append(PhaseFlip(prob, qubits_idx, - self.num_qubits if num_qubits is None else num_qubits)) - + self.append(PhaseFlip(prob, qubits_idx, + self.num_qubits if num_qubits is None else num_qubits)) + def bit_phase_flip( self, prob: Union[paddle.Tensor, float], qubits_idx: Union[Iterable[int], int, str] = 'full', num_qubits: int = None ) -> None: @@ -1139,9 +1182,9 @@ def bit_phase_flip( num_qubits: Total number of qubits. Defaults to None. """ self.__num_qubits_update(qubits_idx) - self.append(BitPhaseFlip(prob, qubits_idx, - self.num_qubits if num_qubits is None else num_qubits)) - + self.append(BitPhaseFlip(prob, qubits_idx, + self.num_qubits if num_qubits is None else num_qubits)) + def amplitude_damping( self, gamma: Union[paddle.Tensor, float], qubits_idx: Union[Iterable[int], int, str] = 'full', num_qubits: int = None ) -> None: @@ -1153,24 +1196,24 @@ def amplitude_damping( num_qubits: Total number of qubits. Defaults to None. """ self.__num_qubits_update(qubits_idx) - self.append(AmplitudeDamping(gamma, qubits_idx, - self.num_qubits if num_qubits is None else num_qubits)) - + self.append(AmplitudeDamping(gamma, qubits_idx, + self.num_qubits if num_qubits is None else num_qubits)) + + #TODO: change bug def generalized_amplitude_damping( - self, gamma: Union[paddle.Tensor, float], prob: Union[paddle.Tensor, float], qubits_idx: Union[Iterable[int], int, str] = 'full', num_qubits: int = None + self, gamma: Union[paddle.Tensor, float], qubits_idx: Union[Iterable[int], int, str] = 'full', num_qubits: int = None ) -> None: r"""Add generalized amplitude damping channels. Args: gamma: Damping probability. - prob: Excitation probability. qubits_idx: Indices of the qubits on which the channels are applied. Defaults to 'full'. num_qubits: Total number of qubits. Defaults to None. """ self.__num_qubits_update(qubits_idx) - self.append(GeneralizedAmplitudeDamping(gamma, prob, qubits_idx, - self.num_qubits if num_qubits is None else num_qubits)) - + self.append(GeneralizedAmplitudeDamping(gamma, qubits_idx, + self.num_qubits if num_qubits is None else num_qubits)) + def phase_damping( self, gamma: Union[paddle.Tensor, float], qubits_idx: Union[Iterable[int], int, str] = 'full', num_qubits: int = None ) -> None: @@ -1182,8 +1225,8 @@ def phase_damping( num_qubits: Total number of qubits. Defaults to None. """ self.__num_qubits_update(qubits_idx) - self.append(PhaseDamping(gamma, qubits_idx, - self.num_qubits if num_qubits is None else num_qubits)) + self.append(PhaseDamping(gamma, qubits_idx, + self.num_qubits if num_qubits is None else num_qubits)) def depolarizing( self, prob: Union[paddle.Tensor, float], qubits_idx: Union[Iterable[int], int, str] = 'full', num_qubits: int = None @@ -1196,8 +1239,8 @@ def depolarizing( num_qubits: Total number of qubits. Defaults to None. """ self.__num_qubits_update(qubits_idx) - self.append(Depolarizing(prob, qubits_idx, - self.num_qubits if num_qubits is None else num_qubits)) + self.append(Depolarizing(prob, qubits_idx, + self.num_qubits if num_qubits is None else num_qubits)) def pauli_channel( self, prob: Union[paddle.Tensor, float], qubits_idx: Union[Iterable[int], int, str] = 'full', num_qubits: int = None @@ -1210,9 +1253,9 @@ def pauli_channel( num_qubits: Total number of qubits. Defaults to None. """ self.__num_qubits_update(qubits_idx) - self.append(PauliChannel(prob, qubits_idx, - self.num_qubits if num_qubits is None else num_qubits)) - + self.append(PauliChannel(prob, qubits_idx, + self.num_qubits if num_qubits is None else num_qubits)) + def reset_channel( self, prob: Union[paddle.Tensor, float], qubits_idx: Union[Iterable[int], int, str] = 'full', num_qubits: int = None ) -> None: @@ -1224,11 +1267,11 @@ def reset_channel( num_qubits: Total number of qubits. Defaults to None. """ self.__num_qubits_update(qubits_idx) - self.append(ResetChannel(prob, qubits_idx, - self.num_qubits if num_qubits is None else num_qubits)) + self.append(ResetChannel(prob, qubits_idx, + self.num_qubits if num_qubits is None else num_qubits)) def thermal_relaxation( - self, const_t: Union[paddle.Tensor, Iterable[float]], exec_time: Union[paddle.Tensor, float], + self, const_t: Union[paddle.Tensor, Iterable[float]], exec_time: Union[paddle.Tensor, float], qubits_idx: Union[Iterable[int], int, str] = 'full', num_qubits: int = None ) -> None: r"""Add thermal relaxation channels. @@ -1240,8 +1283,8 @@ def thermal_relaxation( num_qubits: Total number of qubits. Defaults to None. """ self.__num_qubits_update(qubits_idx) - self.append(ThermalRelaxation(const_t, exec_time, qubits_idx, - self.num_qubits if num_qubits is None else num_qubits)) + self.append(ThermalRelaxation(const_t, exec_time, qubits_idx, + self.num_qubits if num_qubits is None else num_qubits)) def kraus_repr( self, kraus_oper: Iterable[paddle.Tensor], @@ -1256,16 +1299,16 @@ def kraus_repr( num_qubits: Total number of qubits. Defaults to None. """ self.__num_qubits_update(qubits_idx) - self.append(KrausRepr(kraus_oper, qubits_idx, - self.num_qubits if num_qubits is None else num_qubits)) + self.append(KrausRepr(kraus_oper, qubits_idx, + self.num_qubits if num_qubits is None else num_qubits)) - def qaoa_layer(self, edges: Iterable, nodes: Iterable, depth: Optional[int] = 1) -> None: + def qaoa_layer(self, edges: Iterable, nodes: Iterable, depth: Optional[int] = 1) -> None: # TODO: see qaoa layer in layer.py self.__num_qubits_update(edges) self.__num_qubits_update(nodes) self.append(QAOALayer(edges, nodes, depth)) - def unitary_matrix(self, num_qubits: Optional[int] = None) -> paddle.Tensor: + def unitary_matrix(self, num_qubits: Optional[int] = None) -> paddle.Tensor: r"""Get the unitary matrix form of the circuit. Args: @@ -1278,11 +1321,11 @@ def unitary_matrix(self, num_qubits: Optional[int] = None) -> paddle.Tensor: num_qubits = self.__num_qubits else: assert num_qubits >= self.__num_qubits - + backend = get_backend() self.to(backend=Backend.UnitaryMatrix) - unitary = State(paddle.eye( - 2 ** num_qubits).cast(get_dtype()), backend=Backend.UnitaryMatrix) + unitary = State(paddle.eye(2 ** num_qubits).cast(get_dtype()), + backend=Backend.UnitaryMatrix) for layer in self._sub_layers.values(): unitary = layer(unitary) self.to(backend=backend) @@ -1294,188 +1337,15 @@ def gate_history(self) -> List[Dict[str, Union[str, List[int], paddle.Tensor]]]: Returns: history of quantum gates of circuit - + """ - def get_gate_name(gate: Gate) -> str: - r""" return the corresponding string of gate - - Returns: - gate name - - """ - if isinstance(gate, H): - return 'h' - if isinstance(gate, S): - return 's' - if isinstance(gate, T): - return 't' - if isinstance(gate, X): - return 'x' - if isinstance(gate, Y): - return 'y' - if isinstance(gate, Z): - return 'z' - if isinstance(gate, U3): - return 'u' - if isinstance(gate, P): - return 'p' - if isinstance(gate, RX): - return 'rx' - if isinstance(gate, RY): - return 'ry' - if isinstance(gate, RZ): - return 'rz' - if isinstance(gate, CNOT): - return 'cnot' - if isinstance(gate, SWAP): - return 'swap' - if isinstance(gate, CX): - return 'cnot' - if isinstance(gate, CY): - return 'cy' - if isinstance(gate, CZ): - return 'cz' - if isinstance(gate, RXX): - return 'rxx' - if isinstance(gate, RYY): - return 'ryy' - if isinstance(gate, RZZ): - return 'rzz' - if isinstance(gate, CP): - return 'cp' - if isinstance(gate, CRX): - return 'crx' - if isinstance(gate, CRY): - return 'cry' - if isinstance(gate, CRZ): - return 'crz' - if isinstance(gate, CU): - return 'cu' - if isinstance(gate, MS): - return 'ms' - if isinstance(gate, CSWAP): - return 'cswap' - if isinstance(gate, Toffoli): - return 'ccx' - raise ValueError("The gate is not a simple quantum gate.") - - not_implemented_gate = [ - AmplitudeEncoding, - Oracle, - ControlOracle, - UniversalTwoQubits, - UniversalThreeQubits, - RealBlockLayer, - ComplexBlockLayer, - ] - single_qubit_gate = {'h', 's', 't', 'x', - 'y', 'z', 'p', 'rx', 'ry', 'rz', 'u'} gate_history = [] - qubit_max_idx = 0 for gate in self.sublayers(): - if any((isinstance(gate, gate_class) for gate_class in not_implemented_gate)): - raise NotImplementedError( - f"Not support to print the {type(gate)}.") - if isinstance(gate, SuperpositionLayer): - for depth_idx in range(0, gate.depth): - for qubit_idx in gate.qubits_idx: - gate_info = { - 'gate': 'h', 'which_qubits': [qubit_idx], 'theta': None - } - gate_history.append(gate_info) - elif isinstance(gate, WeakSuperpositionLayer): - for depth_idx in range(0, gate.depth): - for qubit_idx in gate.qubits_idx: - gate_info = { - 'gate': 'ry', 'which_qubits': [qubit_idx], - 'theta': paddle.to_tensor([pi / 4]) - } - gate_history.append(gate_info) - elif isinstance(gate, LinearEntangledLayer): - for depth_idx in range(0, gate.depth): - for idx, qubit_idx in enumerate(gate.qubits_idx): - gate_info = { - 'gate': 'ry', 'which_qubits': [qubit_idx], - 'theta': gate.parameters()[0][depth_idx][idx][0] - } - gate_history.append(gate_info) - for idx in range(0, len(gate.qubits_idx) - 1): - gate_info = { - 'gate': 'cnot', 'which_qubits': [gate.qubits_idx[idx], gate.qubits_idx[idx + 1]], - 'theta': None - } - gate_history.append(gate_info) - for idx, qubit_idx in enumerate(gate.qubits_idx): - gate_info = { - 'gate': 'rz', 'which_qubits': [qubit_idx], - 'theta': gate.parameters()[0][depth_idx][idx][1] - } - gate_history.append(gate_info) - for idx in range(0, len(gate.qubits_idx) - 1): - gate_info = { - 'gate': 'cnot', 'which_qubits': [gate.qubits_idx[idx], gate.qubits_idx[idx + 1]], - 'theta': None - } - gate_history.append(gate_info) - elif isinstance(gate, RealEntangledLayer): - for depth_idx in range(0, gate.depth): - for idx, qubit_idx in enumerate(gate.qubits_idx): - gate_info = { - 'gate': 'ry', 'which_qubits': [qubit_idx], - 'theta': gate.parameters()[0][depth_idx][idx] - } - gate_history.append(gate_info) - for idx in range(0, len(gate.qubits_idx)): - gate_info = { - 'gate': 'cnot', - 'which_qubits': [ - gate.qubits_idx[idx], - gate.qubits_idx[(idx + 1) % - len(gate.qubits_idx)] - ], - 'theta': None - } - gate_history.append(gate_info) - elif isinstance(gate, ComplexEntangledLayer): - for depth_idx in range(0, gate.depth): - for idx, qubit_idx in enumerate(gate.qubits_idx): - gate_info = { - 'gate': 'u', 'which_qubits': [qubit_idx], 'theta': None - } - gate_history.append(gate_info) - for idx in range(0, len(gate.qubits_idx)): - gate_info = { - 'gate': 'cnot', - 'which_qubits': [ - gate.qubits_idx[idx], - gate.qubits_idx[(idx + 1) % - len(gate.qubits_idx)] - ], - 'theta': None - } - gate_history.append(gate_info) + if gate.gate_name is None: + raise NotImplementedError(f"Gate {type(gate)} has no gate name and hence cannot be recorded into history.") else: - gate_name = get_gate_name(gate) - if gate_name in single_qubit_gate: - for depth_idx in range(0, gate.depth): - for idx, qubit_idx in enumerate(gate.qubits_idx): - gate_info = {'gate': gate_name, - 'which_qubits': [qubit_idx]} - param = None - if gate_name in {'rx', 'ry', 'rz'}: - param = gate.theta[depth_idx][idx] - gate_info['theta'] = param - gate_history.append(gate_info) - else: - for depth_idx in range(0, gate.depth): - for idx, qubit_idx in enumerate(gate.qubits_idx): - gate_info = {'gate': gate_name, - 'which_qubits': qubit_idx} - param = None - if gate_name in {'cp', 'crx', 'cry', 'crz', 'rxx', 'ryy', 'rzz'}: - param = gate.parameters()[0][depth_idx][idx] - gate_info['theta'] = param - gate_history.append(gate_info) + gate.gate_history_generation() + gate_history.extend(gate.gate_history) return gate_history def __count_history(self, history): @@ -1492,7 +1362,7 @@ def __count_history(self, history): for current_gate in history: # Single-qubit gates with no params to print if current_gate['gate'] in {'h', 's', 't', 'x', 'y', 'z', 'u', 'sdg', 'tdg'}: - curr_qubit = current_gate['which_qubits'][0] + curr_qubit = current_gate['which_qubits'] gate.append(qubit[curr_qubit]) qubit[curr_qubit] = qubit[curr_qubit] + 1 # A new section is added @@ -1501,7 +1371,7 @@ def __count_history(self, history): qubit_max = qubit[curr_qubit] # Gates with params to print elif current_gate['gate'] in {'p', 'rx', 'ry', 'rz'}: - curr_qubit = current_gate['which_qubits'][0] + curr_qubit = current_gate['which_qubits'] gate.append(qubit[curr_qubit]) if length[qubit[curr_qubit]] == 5: length[qubit[curr_qubit]] = 13 @@ -1512,8 +1382,8 @@ def __count_history(self, history): # Two-qubit gates or Three-qubit gates elif ( current_gate['gate'] in { - 'cnot', 'swap', 'rxx', 'ryy', 'rzz', 'ms', - 'cy', 'cz', 'cu', 'cp', 'crx', 'cry', 'crz'} or + 'cnot', 'swap', 'rxx', 'ryy', 'rzz', 'ms', + 'cy', 'cz', 'cu', 'cp', 'crx', 'cry', 'crz'} or current_gate['gate'] in {'cswap', 'ccx'} ): a = max(current_gate['which_qubits']) @@ -1530,14 +1400,14 @@ def __count_history(self, history): qubit_max = ind + 1 return length, gate - + @property def qubit_history(self) -> List[List[Tuple[Dict[str, Union[str, List[int], paddle.Tensor]], int]]]: r""" gate information on each qubit - + Returns: list of gate history on each qubit - + Note: The entry ``qubit_history[i][j][0/1]`` returns the gate information / gate index of the j-th gate applied on the i-th qubit. @@ -1548,8 +1418,11 @@ def qubit_history(self) -> List[List[Tuple[Dict[str, Union[str, List[int], paddl history_qubit.append(history_i) for idx, i in enumerate(self.gate_history): qubits = i["which_qubits"] - for j in qubits: - history_qubit[j].append([i, idx]) + if not isinstance(qubits, Iterable): + history_qubit[qubits].append([i, idx]) + else: + for j in qubits: + history_qubit[j].append([i, idx]) return history_qubit def __str__(self) -> str: @@ -1559,7 +1432,7 @@ def __str__(self) -> str: # Ignore the unused section total_length = sum(length) - 5 - print_list = [['-' if i % 2 == 0 else ' '] * + print_list = [['-' if i % 2 == 0 else ' '] * total_length for i in range(num_qubits * 2)] for i, current_gate in enumerate(history): @@ -1567,22 +1440,21 @@ def __str__(self) -> str: # Calculate starting position ind of current gate sec = gate[i] ind = sum(length[:sec]) - print_list[current_gate['which_qubits'][0] * 2][ind + - length[sec] // 2] = current_gate['gate'].upper() + print_list[current_gate['which_qubits'] * 2][ind + length[sec] // 2] = current_gate['gate'].upper() elif current_gate['gate'] in {'sdg'}: sec = gate[i] ind = sum(length[:sec]) - print_list[current_gate['which_qubits'][0] * 2][ + print_list[current_gate['which_qubits'] * 2][ ind + length[sec] // 2 - 1: ind + length[sec] // 2 + 2] = current_gate['gate'].upper() elif current_gate['gate'] in {'tdg'}: sec = gate[i] ind = sum(length[:sec]) - print_list[current_gate['which_qubits'][0] * 2][ + print_list[current_gate['which_qubits'] * 2][ ind + length[sec] // 2 - 1: ind + length[sec] // 2 + 2] = current_gate['gate'].upper() elif current_gate['gate'] in {'p', 'rx', 'ry', 'rz'}: sec = gate[i] ind = sum(length[:sec]) - line = current_gate['which_qubits'][0] * 2 + line = current_gate['which_qubits'] * 2 # param = self.__param[current_gate['theta'][2 if current_gate['gate'] == 'rz' else 0]] param = current_gate['theta'] if current_gate['gate'] == 'p': @@ -1592,8 +1464,7 @@ def __str__(self) -> str: print_list[line][ind + 2] = 'R' print_list[line][ind + 3] = current_gate['gate'][1] print_list[line][ind + 4] = '(' - print_list[line][ind + 5: ind + - 10] = format(float(param.numpy()), '.3f')[:5] + print_list[line][ind + 5: ind + 10] = format(float(param.numpy()), '.3f')[:5] print_list[line][ind + 10] = ')' # Two-qubit gates elif current_gate['gate'] in {'cnot', 'swap', 'rxx', 'ryy', 'rzz', 'ms', 'cz', 'cy', @@ -1604,11 +1475,9 @@ def __str__(self) -> str: tqubit = current_gate['which_qubits'][1] if current_gate['gate'] in {'cnot', 'swap', 'cy', 'cz', 'cu'}: print_list[cqubit * 2][ind + length[sec] // 2] = \ - '*' if current_gate['gate'] in {'cnot', - 'cy', 'cz', 'cu'} else 'x' + '*' if current_gate['gate'] in {'cnot', 'cy', 'cz', 'cu'} else 'x' print_list[tqubit * 2][ind + length[sec] // 2] = \ - 'x' if current_gate['gate'] in { - 'swap', 'cnot'} else current_gate['gate'][1] + 'x' if current_gate['gate'] in {'swap', 'cnot'} else current_gate['gate'][1] elif current_gate['gate'] == 'ms': for qubit in {cqubit, tqubit}: print_list[qubit * 2][ind + length[sec] // 2 - 1] = 'M' @@ -1619,11 +1488,9 @@ def __str__(self) -> str: param = current_gate['theta'] for line in {cqubit * 2, tqubit * 2}: print_list[line][ind + 2] = 'R' - print_list[line][ind + 3: ind + - 5] = current_gate['gate'][1:3].lower() + print_list[line][ind + 3: ind + 5] = current_gate['gate'][1:3].lower() print_list[line][ind + 5] = '(' - print_list[line][ind + 6: ind + - 10] = format(float(param.numpy()), '.2f')[:4] + print_list[line][ind + 6: ind + 10] = format(float(param.numpy()), '.2f')[:4] print_list[line][ind + 10] = ')' elif current_gate['gate'] in {'crx', 'cry', 'crz'}: # param = self.__param[current_gate['theta'][2 if current_gate['gate'] == 'crz' else 0]] @@ -1632,8 +1499,7 @@ def __str__(self) -> str: print_list[tqubit * 2][ind + 2] = 'R' print_list[tqubit * 2][ind + 3] = current_gate['gate'][2] print_list[tqubit * 2][ind + 4] = '(' - print_list[tqubit * 2][ind + 5: ind + - 10] = format(float(param.numpy()), '.3f')[:5] + print_list[tqubit * 2][ind + 5: ind + 10] = format(float(param.numpy()), '.3f')[:5] print_list[tqubit * 2][ind + 10] = ')' start_line = min(cqubit, tqubit) end_line = max(cqubit, tqubit) @@ -1668,24 +1534,30 @@ def __str__(self) -> str: print_list[cqubit1 * 2][ind + length[sec] // 2] = '*' print_list[cqubit2 * 2][ind + length[sec] // 2] = '*' print_list[tqubit * 2][ind + length[sec] // 2] = 'X' + else: + raise NotImplementedError(f"Not support to print the gate {current_gate['gate']}.") print_list = list(map(''.join, print_list)) return_str = '\n'.join(print_list) return return_str - - def forward(self, state: Optional[State] = None) -> State: + + def forward(self, state: Optional[State] = None) -> State: r""" forward the input - + Args: state: initial state - + Returns: output quantum state - + """ + assert self.__num_qubits is not None, "Information about num_qubits is required before running the circuit" + if state is None: - assert self.__num_qubits is not None, "Information about num_qubits is required before running the circuit" state = zero_state(self.__num_qubits, self.backend, self.dtype) - + else: + assert self.__num_qubits == state.num_qubits, \ + f"num_qubits does not agree: expected {self.__num_qubits}, received {state.num_qubits}" + return super().forward(state) diff --git a/paddle_quantum/ansatz/container.py b/paddle_quantum/ansatz/container.py index 172ac97..28f138d 100644 --- a/paddle_quantum/ansatz/container.py +++ b/paddle_quantum/ansatz/container.py @@ -18,7 +18,6 @@ """ import collections -from sre_parse import State from paddle_quantum import Operator from typing import Optional, Union, Iterable, Any, List diff --git a/paddle_quantum/ansatz/vans.py b/paddle_quantum/ansatz/vans.py index ec5ebe0..9ec0f6e 100644 --- a/paddle_quantum/ansatz/vans.py +++ b/paddle_quantum/ansatz/vans.py @@ -82,7 +82,7 @@ def __place_identity_block(cls, cir: Circuit, theta: paddle.Tensor) -> Circuit: # add one qubit gates rz_rx_rz to the circuit cir.insert(insert_ind, RZ([index], param=theta[0])) cir.insert(insert_ind + 1, RX([index], param=theta[1])) - cir.insert(insert_ind + 2, RX([index], param=theta[2])) + cir.insert(insert_ind + 2, RZ([index], param=theta[2])) else: # add two qubit gates to the circuit # obtain which two qubits to insert @@ -121,9 +121,9 @@ def __place_identity_block(cls, cir: Circuit, theta: paddle.Tensor) -> Circuit: cir.insert(insert_ind + 1, RZ([qubit_i], param=theta[0])) cir.insert(insert_ind + 2, RX([qubit_i], param=theta[1])) cir.insert(insert_ind + 3, RZ([qubit_i], param=theta[2])) - cir.insert(insert_ind + 4, RX([qubit_i], param=theta[3])) - cir.insert(insert_ind + 5, RZ([qubit_i], param=theta[4])) - cir.insert(insert_ind + 6, RX([qubit_i], param=theta[5])) + cir.insert(insert_ind + 4, RX([qubit_j], param=theta[3])) + cir.insert(insert_ind + 5, RZ([qubit_j], param=theta[4])) + cir.insert(insert_ind + 6, RX([qubit_j], param=theta[5])) cir.insert(insert_ind + 7, CNOT([qubit_i, qubit_j])) return cir @@ -145,7 +145,7 @@ def __count_qubit_gates(cls, cir: Circuit) -> np.ndarray: for gate_info in history: qubits_idx = gate_info["which_qubits"] if gate_info["gate"] == "rz" or gate_info["gate"] == "rx": - qubit_ind = qubits_idx[0] + qubit_ind = qubits_idx count_gates[qubit_ind] += 1 elif gate_info["gate"] == "cnot": qubit_i = min(qubits_idx[0], qubits_idx[1]) @@ -712,11 +712,12 @@ def simplify_circuit(cls, cir: Circuit, zero_init_state: Optional[bool] = True) return cir -def cir_decompose(cir: Circuit) -> Circuit: - r"""Decompose all layers of circuit into gates. +def cir_decompose(cir: Circuit, trainable: Optional[bool] = False) -> Circuit: + r"""Decompose all layers of circuit into gates, and make all parameterized gates trainable if needed Args: cir: Target quantum circuit. + trainable: whether the decomposed parameterized gates are trainable Returns: A quantum circuit with same structure and parameters but all layers are decomposed into Gates. @@ -730,13 +731,17 @@ def cir_decompose(cir: Circuit) -> Circuit: gate_name = gate_info['gate'] qubits_idx = gate_info['which_qubits'] param = gate_info['theta'] - # get gate function if param is None: getattr(new_cir, gate_name)(qubits_idx) - else: - getattr(new_cir, gate_name)(qubits_idx, param=param) - + continue + + if trainable: + param = param.reshape([1] + param.shape) + param = paddle.create_parameter( + shape=param.shape, dtype=param.dtype, + default_initializer=paddle.nn.initializer.Assign(param)) + getattr(new_cir, gate_name)(qubits_idx, param=param) return new_cir @@ -831,7 +836,7 @@ def train(self) -> Circuit: self.loss = itr_loss else: # insert + simplification # Insert - new_cir = cir_decompose(self.cir) + new_cir = cir_decompose(self.cir, trainable=True) new_cir = Inserter.insert_identities( new_cir, self.insert_rate, self.epsilon) @@ -874,6 +879,7 @@ def optimization(self, cir: Circuit) -> float: Returns: Optimized loss. """ + cir = cir_decompose(cir, trainable=True) opt = paddle.optimizer.Adam( learning_rate=self.LR, parameters=cir.parameters()) diff --git a/paddle_quantum/backend/__init__.py b/paddle_quantum/backend/__init__.py index 4de55dd..b1c3ade 100644 --- a/paddle_quantum/backend/__init__.py +++ b/paddle_quantum/backend/__init__.py @@ -20,6 +20,7 @@ from . import state_vector from . import density_matrix from . import quleaf +from . import unitary_matrix import enum diff --git a/paddle_quantum/backend/quleaf.py b/paddle_quantum/backend/quleaf.py index 3ac3f4f..8430d4c 100644 --- a/paddle_quantum/backend/quleaf.py +++ b/paddle_quantum/backend/quleaf.py @@ -63,6 +63,7 @@ def set_quleaf_token(token: str) -> None: """ global TOKEN TOKEN = token + QCompute.Define.hubToken = token def get_quleaf_token() -> str: diff --git a/paddle_quantum/channel/common.py b/paddle_quantum/channel/common.py index 2fa3435..389357c 100644 --- a/paddle_quantum/channel/common.py +++ b/paddle_quantum/channel/common.py @@ -46,7 +46,7 @@ def __init__( qubits_idx: Union[Iterable[int], int, str] = 'full', num_qubits: int = None ): super().__init__() - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=True) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits) if isinstance(prob, float): self.prob = paddle.to_tensor(prob) else: @@ -80,7 +80,7 @@ def __init__( qubits_idx: Union[Iterable[int], int, str] = 'full', num_qubits: int = None ): super().__init__() - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=True) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits) if isinstance(prob, float): self.prob = paddle.to_tensor(prob) else: @@ -114,7 +114,7 @@ def __init__( qubits_idx: Union[Iterable[int], int, str] = 'full', num_qubits: int = None ): super().__init__() - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=True) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits) if isinstance(prob, float): self.prob = paddle.to_tensor(prob) else: @@ -156,7 +156,7 @@ def __init__( qubits_idx: Union[Iterable[int], int, str] = 'full', num_qubits: int = None ): super().__init__() - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=True) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits) if isinstance(gamma, float): self.gamma = paddle.to_tensor(gamma) else: @@ -197,7 +197,7 @@ def __init__( qubits_idx: Union[Iterable[int], int, str] = 'full', num_qubits: int = None ): super().__init__() - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=True) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits) if isinstance(prob, float): self.prob = paddle.to_tensor(prob) else: @@ -244,7 +244,7 @@ def __init__( qubits_idx: Union[Iterable[int], int, str] = 'full', num_qubits: int = None ): super().__init__() - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=True) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits) if isinstance(gamma, float): self.gamma = paddle.to_tensor(gamma) else: @@ -280,7 +280,7 @@ def __init__( qubits_idx: Union[Iterable[int], int, str] = 'full', num_qubits: int = None ): super().__init__() - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=True) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits) if isinstance(prob, float): self.prob = paddle.to_tensor(prob) else: @@ -311,7 +311,7 @@ def __init__( qubits_idx: Union[Iterable[int], int, str] = 'full', num_qubits: int = None ): super().__init__() - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=True) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits) if isinstance(prob, Iterable): self.prob = paddle.to_tensor(prob) else: @@ -369,7 +369,7 @@ def __init__( qubits_idx: Union[Iterable[int], int, str] = 'full', num_qubits: int = None ): super().__init__() - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=True) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits) if isinstance(prob, Iterable): self.prob = paddle.to_tensor(prob) else: @@ -402,7 +402,7 @@ def __init__( qubits_idx: Union[Iterable[int], int, str] = 'full', num_qubits: int = None ): super().__init__() - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=True) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits) if isinstance(const_t, float): self.const_t = paddle.to_tensor(const_t) else: diff --git a/paddle_quantum/channel/custom.py b/paddle_quantum/channel/custom.py index f2ca0ec..7da5d0a 100644 --- a/paddle_quantum/channel/custom.py +++ b/paddle_quantum/channel/custom.py @@ -45,7 +45,7 @@ def __init__( assert 2 ** num_acted_qubits == kraus_oper[0].shape[0], "The length of oracle should be integer power of 2." self.kraus_oper = kraus_oper is_single_qubit = True if num_acted_qubits == 1 else False - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit, num_acted_qubits) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, num_acted_qubits) def forward(self, state: 'paddle_quantum.State') -> 'paddle_quantum.State': for qubits_idx in self.qubits_idx: diff --git a/paddle_quantum/dataset.py b/paddle_quantum/dataset.py index 0c9ab96..3ab15d5 100644 --- a/paddle_quantum/dataset.py +++ b/paddle_quantum/dataset.py @@ -27,6 +27,8 @@ from sklearn.model_selection import train_test_split from sklearn import datasets from paddle_quantum.gate import RY, RZ, U3, CNOT, IQPEncoding, AmplitudeEncoding +from .base import get_dtype +from .intrinsic import _get_float_dtype __all__ = [ "Dataset", @@ -117,14 +119,15 @@ def data2circuit( """ quantum_states = classical_data.copy() quantum_circuits = classical_data.copy() + float_dtype = _get_float_dtype(get_dtype()) if encoding == AMPLITUDE_ENCODING: # Not support to return circuit in amplitude encoding if return_state is False or split_circuit is True: raise Exception("Not support to return circuit in amplitude encoding") for i in range(len(classical_data)): - x = paddle.to_tensor(_normalize(classical_data[i])) + x = paddle.to_tensor(_normalize(classical_data[i]), dtype=float_dtype) if is_image: - x = paddle.to_tensor(_normalize_image(classical_data[i])) + x = paddle.to_tensor(_normalize_image(classical_data[i]), dtype=float_dtype) circuit = AmplitudeEncoding(qubits_idx='full', num_qubits=num_qubits) state = circuit(x) quantum_states[i] = state.data.numpy() @@ -133,9 +136,9 @@ def data2circuit( for i in range(len(classical_data)): one_block_param = 1 * num_qubits depth = int(can_describe_dimension / one_block_param) - param = paddle.to_tensor(_normalize(classical_data[i])) + param = paddle.to_tensor(_normalize(classical_data[i]), dtype=float_dtype) if is_image: - param = paddle.to_tensor(_normalize_image(classical_data[i])) + param = paddle.to_tensor(_normalize_image(classical_data[i]), dtype=float_dtype) param = paddle.reshape(param, (depth, num_qubits, 1)) which_qubits = list(range(num_qubits)) if split_circuit: @@ -158,9 +161,9 @@ def data2circuit( for i in range(len(classical_data)): one_block_param = 1 * num_qubits depth = int(can_describe_dimension / one_block_param) - param = paddle.to_tensor(_normalize(classical_data[i])) + param = paddle.to_tensor(_normalize(classical_data[i]), dtype=float_dtype) if is_image: - param = paddle.to_tensor(_normalize_image(classical_data[i])) + param = paddle.to_tensor(_normalize_image(classical_data[i]), dtype=float_dtype) param = paddle.reshape(param, (depth, num_qubits)) if split_circuit: quantum_circuits[i] = [] @@ -190,9 +193,9 @@ def data2circuit( for i in range(len(classical_data)): one_block_param = 3 * num_qubits depth = int(can_describe_dimension / one_block_param) - param = paddle.to_tensor(_normalize(classical_data[i])) + param = paddle.to_tensor(_normalize(classical_data[i]), dtype=float_dtype) if is_image: - param = paddle.to_tensor(_normalize_image(classical_data[i])) + param = paddle.to_tensor(_normalize_image(classical_data[i]), dtype=float_dtype) param = paddle.reshape(param, (depth, num_qubits, 3)) which_qubits = list(range(num_qubits)) if split_circuit: @@ -219,9 +222,9 @@ def data2circuit( for i in range(len(classical_data)): one_block_param = 2 * num_qubits depth = int(can_describe_dimension / one_block_param) - param = paddle.to_tensor(_normalize(classical_data[i])) + param = paddle.to_tensor(_normalize(classical_data[i]), dtype=float_dtype) if is_image: - param = paddle.to_tensor(_normalize_image(classical_data[i])) + param = paddle.to_tensor(_normalize_image(classical_data[i]), dtype=float_dtype) param = paddle.reshape(param, (depth, num_qubits, 2)) which_qubits = [k for k in range(num_qubits)] if split_circuit: @@ -256,9 +259,9 @@ def data2circuit( for i in range(len(classical_data)): one_block_param = 1 * num_qubits depth = int(can_describe_dimension / one_block_param) - param = paddle.to_tensor(_normalize(classical_data[i])) + param = paddle.to_tensor(_normalize(classical_data[i]), dtype=float_dtype) if is_image: - param = paddle.to_tensor(_normalize_image(classical_data[i])) + param = paddle.to_tensor(_normalize_image(classical_data[i]), dtype=float_dtype) param = paddle.reshape(param, (depth, num_qubits, 1)) which_qubits = [k for k in range(num_qubits)] if split_circuit: @@ -287,9 +290,9 @@ def data2circuit( for i in range(len(classical_data)): one_block_param = 3 * num_qubits depth = int(can_describe_dimension / one_block_param) - param = paddle.to_tensor(_normalize(classical_data[i])) + param = paddle.to_tensor(_normalize(classical_data[i]), dtype=float_dtype) if is_image: - param = paddle.to_tensor(_normalize_image(classical_data[i])) + param = paddle.to_tensor(_normalize_image(classical_data[i]), dtype=float_dtype) param = paddle.reshape(param, (depth, num_qubits, 3)) which_qubits = [k for k in range(num_qubits)] if split_circuit: @@ -463,7 +466,7 @@ def encode(self, feature: Union[list, np.ndarray], encoding: str, num_qubits: in new_size = int(np.sqrt(self.dimension)) cur_image = transform.resize(cur_image.reshape((self.figure_size, self.figure_size)), (new_size, new_size)) - self.classical_image_vectors[i] = cur_image.reshape(-1).astype(np.float64) # now it is one-dimension + self.classical_image_vectors[i] = cur_image.reshape(-1) # now it is one-dimension if self.can_describe_dimension < len(self.classical_image_vectors[i]): self.classical_image_vectors[i] = self.classical_image_vectors[i][:self.can_describe_dimension] @@ -476,14 +479,14 @@ def encode(self, feature: Union[list, np.ndarray], encoding: str, num_qubits: in elif downscaling_method == DOWNSCALINGMETHOD_PCA: for i in range(len(self.classical_image_vectors)): _, s, _ = np.linalg.svd(self.classical_image_vectors[i].reshape((self.figure_size, self.figure_size))) - s = s[:self.dimension].astype(np.float64) + s = s[:self.dimension] if self.can_describe_dimension > self.dimension: self.classical_image_vectors[i] = np.append(s, np.array( [0.0] * (self.can_describe_dimension - self.dimension))) else: self.classical_image_vectors[i] = s[:self.can_describe_dimension] - # Step 4: Encode the data, which must be of float64 type(needed in paddle quantum) + # Step 4: Encode the data self.quantum_image_states, self.quantum_image_circuits = self.data2circuit( self.classical_image_vectors, encoding, num_qubits, self.can_describe_dimension, split_circuit, return_state, is_image=True) @@ -714,14 +717,13 @@ def encode(self, feature: Union[list, np.ndarray], encoding: str, num_qubits: in # The second step: fill the vector to ``can_describe_dimension`` using zero for i in range(len(self.feature)): - self.feature[i] = self.feature[i].reshape(-1).astype( - np.float64) # now self.images[i] is a numpy with (new_size*new_size,1) shape + self.feature[i] = self.feature[i].reshape(-1) # now self.images[i] is a numpy with (new_size*new_size,1) shape self.feature[i] = np.append( self.feature[i], np.array([0.0] * (self.can_describe_dimension - self.dimension)) ) # now self.images[i] is filled to ``self.can_describe_dimension`` - # Step 3: Encode the data, which must be of float64 type(needed in paddle quantum) + # Step 3: Encode the data self.quantum_states, self.quantum_circuits = self.data2circuit( self.feature, encoding, num_qubits, self.can_describe_dimension, False, # split_circuit=False return_state @@ -800,7 +802,7 @@ class BreastCancer(SimpleDataset): """ - def __init__(self, encoding: str, num_qubits: int, test_rate: Optional[float] =0.2, + def __init__(self, encoding: str, num_qubits: int, test_rate: Optional[float] = 0.2, return_state: Optional[bool] = True, seed: Optional[int] = 0) -> None: SimpleDataset.__init__(self, dimension=30) # The dimension is 30 self.dimension = 30 diff --git a/paddle_quantum/gate/__init__.py b/paddle_quantum/gate/__init__.py index 396e934..bcc6e98 100644 --- a/paddle_quantum/gate/__init__.py +++ b/paddle_quantum/gate/__init__.py @@ -18,7 +18,7 @@ """ from . import functional -from .base import Gate +from .base import Gate, ParamGate from .clifford import Clifford, compose_clifford_circuit from .single_qubit_gate import H, S, T, X, Y, Z, P, RX, RY, RZ, U3 from .multi_qubit_gate import CNOT, CX, CY, CZ, SWAP diff --git a/paddle_quantum/gate/base.py b/paddle_quantum/gate/base.py index 8499607..576f26e 100644 --- a/paddle_quantum/gate/base.py +++ b/paddle_quantum/gate/base.py @@ -17,11 +17,15 @@ The source file of the basic class for the quantum gates. """ +import paddle import paddle_quantum +from typing import Union, List, Iterable +from ..intrinsic import _get_float_dtype +from math import pi class Gate(paddle_quantum.Operator): - r"""Basis class for quantum gates. + r"""Base class for quantum gates. Args: depth: Number of layers. Defaults to 1. @@ -36,6 +40,7 @@ def __init__( ): super().__init__(backend, dtype, name_scope) self.depth = depth + self.gate_name = None def forward(self, *inputs, **kwargs): raise NotImplementedError @@ -47,3 +52,75 @@ def __setattr__(self, name, value): value.backend = paddle_quantum.get_backend() if self.backend is None else self.backend if value.dtype is None: value.dtype = paddle_quantum.get_dtype() if self.dtype is None else self.dtype + + def gate_history_generation(self) -> None: + r""" determine self.gate_history + + """ + gate_history = [] + for _ in range(0, self.depth): + for qubit_idx in self.qubits_idx: + gate_info = {'gate': self.gate_name, 'which_qubits': qubit_idx, 'theta': None} + gate_history.append(gate_info) + self.gate_history = gate_history + + +class ParamGate(Gate): + r""" Base class for quantum parameterized gates + + """ + def theta_generation(self, param: Union[paddle.Tensor, float, List[float]], param_shape: List[int]) -> None: + """ determine self.theta, and create parameter if necessary + + Args: + param: input theta + param_shape: shape for theta + + Note: + in the following cases ``param`` will be transformed to a parameter: + - ``param`` is None + in the following cases ``param`` will be added to the parameter list: + - ``param`` is ParamBase + in the following cases ``param`` will keep unchange: + - ``param`` is a Tensor but not a parameter + - ``param`` is a float or list of floats + + """ + + float_dtype = _get_float_dtype(self.dtype) + + if param is None: + theta = self.create_parameter( + shape=param_shape, dtype=float_dtype, + default_initializer=paddle.nn.initializer.Uniform(low=0, high=2 * pi) + ) + self.add_parameter('theta', theta) + + elif isinstance(param, paddle.fluid.framework.ParamBase): + assert param.shape == param_shape, "received: " + str(param.shape) + " expect: " + str(param_shape) + self.add_parameter('theta', param) + + elif isinstance(param, paddle.Tensor): + param = param.reshape(param_shape) + self.theta = param + + elif isinstance(param, float): + self.theta = paddle.ones(param_shape, dtype=float_dtype) * param + + else: # when param is a list of float + self.theta = paddle.to_tensor(param, dtype=float_dtype).reshape(param_shape) + + def gate_history_generation(self) -> None: + r""" determine self.gate_history when gate is parameterized + + """ + gate_history = [] + for depth_idx in range(0, self.depth): + for idx, qubit_idx in enumerate(self.qubits_idx): + if self.param_sharing: + param = self.theta[depth_idx] + else: + param = self.theta[depth_idx][idx] + gate_info = {'gate': self.gate_name, 'which_qubits': qubit_idx, 'theta': param} + gate_history.append(gate_info) + self.gate_history = gate_history diff --git a/paddle_quantum/gate/custom.py b/paddle_quantum/gate/custom.py index 0bdcea5..31dfc87 100644 --- a/paddle_quantum/gate/custom.py +++ b/paddle_quantum/gate/custom.py @@ -38,13 +38,16 @@ class Oracle(Gate): """ def __init__( self, oracle: paddle.Tensor, qubits_idx: Union[Iterable[Iterable[int]], Iterable[int], int], - num_qubits: int = None, depth: int = 1 + num_qubits: int = None, depth: int = 1, gate_name: str = 'O' ): super().__init__(depth) + oracle = oracle.cast(paddle_quantum.get_dtype()) assert is_unitary(oracle), "the input oracle must be a unitary matrix" num_acted_qubits = int(math.log2(oracle.shape[0])) self.oracle = paddle.cast(oracle, paddle_quantum.get_dtype()) - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, False, num_acted_qubits) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, num_acted_qubits) + + self.gate_name = gate_name def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: for _ in range(0, self.depth): @@ -63,23 +66,25 @@ class ControlOracle(Gate): depth: Number of layers. Defaults to ``1``. """ def __init__( - self, oracle: paddle.Tensor, - # num_control_qubits: int, controlled_value: 'str', - qubits_idx: Union[Iterable[Iterable[int]], Iterable[int]], - num_qubits: int = None, depth: int = 1 - ): + self, oracle: paddle.Tensor, qubits_idx: Union[Iterable[Iterable[int]], Iterable[int]], + num_qubits: int = None, depth: int = 1, gate_name: str = 'cO' + ) -> None: super().__init__(depth) + complex_dtype = paddle_quantum.get_dtype() + oracle = oracle.cast(complex_dtype) assert is_unitary(oracle), "the input oracle must be a unitary matrix" num_acted_qubits = int(math.log2(oracle.shape[0])) # 暂时只支持单控制位 oracle = ( - paddle.kron(paddle.to_tensor([[1.0, 0], [0, 0]]), paddle.eye(2 ** num_acted_qubits)) + - paddle.kron(paddle.to_tensor([[0.0, 0], [0, 1]]), oracle) + paddle.kron(paddle.to_tensor([[1.0, 0], [0, 0]], dtype=complex_dtype), paddle.eye(2 ** num_acted_qubits)) + + paddle.kron(paddle.to_tensor([[0.0, 0], [0, 1]], dtype=complex_dtype), oracle) ) num_acted_qubits = num_acted_qubits + 1 - self.oracle = paddle.cast(oracle, paddle_quantum.get_dtype()) - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, False, num_acted_qubits) + self.oracle = oracle + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, num_acted_qubits) + + self.gate_name = gate_name def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: for _ in range(0, self.depth): diff --git a/paddle_quantum/gate/encoding.py b/paddle_quantum/gate/encoding.py index f09531c..8d3e9b5 100644 --- a/paddle_quantum/gate/encoding.py +++ b/paddle_quantum/gate/encoding.py @@ -41,17 +41,26 @@ def __init__( ) -> None: super().__init__() self.num_qubits = num_qubits - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=True) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits) + self.gate_name = 'BasisEnc' - def forward(self, feature: paddle.Tensor, - state: Optional[paddle_quantum.State] = None, inverse: Optional[bool] = False) -> paddle_quantum.State: - feature = paddle.cast(feature, 'int32') + def forward(self, feature: paddle.Tensor, state: 'paddle_quantum.State' = None) -> 'paddle_quantum.State': if state is None: state = paddle_quantum.state.zero_state(self.num_qubits) + feature = paddle.cast(feature, 'int32') + gate_history = [] for idx, element in enumerate(feature): if element: state = functional.x(state, self.qubits_idx[idx], self.dtype, self.backend) + gate_history.append({'gate': 'x', 'which_qubits': self.qubits_idx[idx], 'theta': None}) + self.gate_history = gate_history return state + + def gate_history_generation(self) -> None: + if self.gate_history is None: + raise RuntimeError("you must forward the encoding to receive the gate history") + pass + class AmplitudeEncoding(Gate): @@ -68,9 +77,10 @@ def __init__( num_qubits = max(qubits_idx) super().__init__() self.num_qubits = num_qubits - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=True) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits) + self.gate_name = 'AmpEnc' - def forward(self, feature: paddle.Tensor) -> paddle_quantum.State: + def forward(self, feature: paddle.Tensor) -> 'paddle_quantum.State': def calc_location(location_of_bits_list): if len(location_of_bits_list) <= 1: result_list = [0, location_of_bits_list[0]] @@ -133,21 +143,28 @@ def __init__( num_qubits = max(qubits_idx) super().__init__() self.num_qubits = num_qubits - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=True) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits) + if encoding_gate == 'rx': self.encoding_gate = functional.rx elif encoding_gate == 'ry': self.encoding_gate = functional.ry elif encoding_gate == 'rz': self.encoding_gate = functional.rz + self.encoding_gate_name = encoding_gate + feature = paddle.cast(feature, _get_float_dtype(paddle_quantum.get_dtype())) feature = paddle.flatten(feature) self.feature = feature + + self.gate_name = 'AngleEnc' def forward( - self, state: paddle_quantum.State, invert: Optional[bool] = False - ) -> paddle_quantum.State: - + self, state: 'paddle_quantum.State' = None, invert: bool = False + ) -> 'paddle_quantum.State': + gate_history = [] + if state is None: + state = paddle_quantum.state.zero_state(self.num_qubits) if invert: feature = -1 * self.feature else: @@ -157,7 +174,14 @@ def forward( state, element[0], self.qubits_idx[idx], dtype=self.dtype, backend=self.backend ) + gate_history.append({'gate': self.encoding_gate_name, 'which_qubits': self.qubits_idx[idx], 'theta': element[0]}) + self.gate_history = gate_history return state + + def gate_history_generation(self) -> None: + if self.gate_history is None: + raise RuntimeError("you must forward the encoding to receive the gate history") + pass class IQPEncoding(Gate): @@ -181,10 +205,15 @@ def __init__( feature = paddle.cast(feature, _get_float_dtype(paddle_quantum.get_dtype())) feature = paddle.flatten(feature) self.feature = feature + + self.gate_name = 'IQPEnc' def forward( - self, state: paddle_quantum.State, invert: Optional[bool] = False + self, state: paddle_quantum.State = None, invert: Optional[bool] = False ) -> paddle_quantum.State: + gate_history = [] + if state is None: + state = paddle_quantum.state.zero_state(self.num_qubits) for _ in range(0, self.num_repeat): if invert: for qubits_idx in self.qubits_idx: @@ -194,15 +223,28 @@ def forward( dtype=self.dtype, backend=self.backend ) state = functional.cnot(state, qubits_idx, dtype=self.dtype, backend=self.backend) + + gate_history.append({'gate': 'cnot', 'which_qubits': qubits_idx, 'theta': None}) + gate_history.append({'gate': 'rz', 'which_qubits': qubits_idx[1], + 'theta': -self.feature[qubits_idx[0]] * self.feature[qubits_idx[1]]}) + gate_history.append({'gate': 'cnot', 'which_qubits': qubits_idx, 'theta': None}) + for idx in range(0, self.feature.size): state = functional.rz(state, -self.feature[idx], idx, dtype=self.dtype, backend=self.backend) + gate_history.append({'gate': 'rz', 'which_qubits': idx, 'theta': -self.feature[idx]}) + for idx in range(0, self.feature.size): state = functional.h(state, idx, dtype=self.dtype, backend=self.backend) + gate_history.append({'gate': 'h', 'which_qubits': idx, 'theta': None}) else: for idx in range(0, self.feature.size): state = functional.h(state, idx, dtype=self.dtype, backend=self.backend) + gate_history.append({'gate': 'h', 'which_qubits': idx, 'theta': None}) + for idx in range(0, self.feature.size): state = functional.rz(state, self.feature[idx], idx, dtype=self.dtype, backend=self.backend) + gate_history.append({'gate': 'rz', 'which_qubits': idx, 'theta': self.feature[idx]}) + for qubits_idx in self.qubits_idx: state = functional.cnot(state, qubits_idx, dtype=self.dtype, backend=self.backend) state = functional.rz( @@ -210,4 +252,16 @@ def forward( dtype=self.dtype, backend=self.backend ) state = functional.cnot(state, qubits_idx, dtype=self.dtype, backend=self.backend) + + gate_history.append({'gate': 'cnot', 'which_qubits': qubits_idx, 'theta': None}) + gate_history.append({'gate': 'rz', 'which_qubits': qubits_idx[1], + 'theta': self.feature[qubits_idx[0]] * self.feature[qubits_idx[1]]}) + gate_history.append({'gate': 'cnot', 'which_qubits': qubits_idx, 'theta': None}) + + self.gate_history = gate_history return state + + def gate_history_generation(self) -> None: + if self.gate_history is None: + raise RuntimeError("you must forward the encoding to receive the gate history") + pass \ No newline at end of file diff --git a/paddle_quantum/gate/functional/single_qubit_gate.py b/paddle_quantum/gate/functional/single_qubit_gate.py index 581fda7..857eb6d 100644 --- a/paddle_quantum/gate/functional/single_qubit_gate.py +++ b/paddle_quantum/gate/functional/single_qubit_gate.py @@ -85,7 +85,7 @@ def t(state: paddle_quantum.State, qubit_idx: int, dtype: str, backend: paddle_q """ gate = [ [1, 0], - [0, math.cos(math.pi / 4) - math.sin(math.pi / 4) * 1j], + [0, math.cos(math.pi / 4) + math.sin(math.pi / 4) * 1j], ] gate = paddle.to_tensor(gate, dtype=dtype) state_data = simulation(state, gate, qubit_idx, state.num_qubits, backend) diff --git a/paddle_quantum/gate/layer.py b/paddle_quantum/gate/layer.py index cc1134f..456843c 100644 --- a/paddle_quantum/gate/layer.py +++ b/paddle_quantum/gate/layer.py @@ -24,7 +24,7 @@ from paddle_quantum.gate import functional from paddle_quantum.intrinsic import _get_float_dtype from .base import Gate -from typing import Iterable, List, Optional, Union +from typing import Iterable, List, Union, Tuple, Dict def qubits_idx_filter(qubits_idx: Union[Iterable[int], str], num_qubits: int) -> List[Iterable[int]]: @@ -64,12 +64,22 @@ def __init__( ): super().__init__(depth) self.qubits_idx = qubits_idx_filter(qubits_idx, num_qubits) + self.gate_name = 'SupLayer' def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: for _ in range(0, self.depth): for qubit_idx in self.qubits_idx: state = functional.h(state, qubit_idx, self.dtype, self.backend) return state + + def gate_history_generation(self): + gate_history = [] + for _ in range(0, self.depth): + for qubit_idx in self.qubits_idx: + gate_info = {'gate': 'h', 'which_qubits': qubit_idx, 'theta': None} + gate_history.append(gate_info) + self.gate_history = gate_history + class WeakSuperpositionLayer(Gate): @@ -85,6 +95,7 @@ def __init__( ): super().__init__(depth) self.qubits_idx = qubits_idx_filter(qubits_idx, num_qubits) + self.gate_name = 'WSupLayer' def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: theta = paddle.to_tensor([np.pi / 4]) @@ -92,6 +103,14 @@ def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: for qubit_idx in self.qubits_idx: state = functional.ry(state, theta, qubit_idx, self.dtype, self.backend) return state + + def gate_history_generation(self): + gate_history = [] + for _ in range(0, self.depth): + for qubit_idx in self.qubits_idx: + gate_info = {'gate': 'ry', 'which_qubits': qubit_idx, 'theta': paddle.to_tensor([np.pi / 4])} + gate_history.append(gate_info) + self.gate_history = gate_history class LinearEntangledLayer(Gate): @@ -117,6 +136,7 @@ def __init__( default_initializer=initializer ) self.add_parameter('theta', theta) + self.gate_name = 'LEntLayer' def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: for depth_idx in range(0, self.depth): @@ -133,6 +153,23 @@ def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: state = functional.cnot( state, [self.qubits_idx[idx], self.qubits_idx[idx + 1]], self.dtype, self.backend) return state + + def gate_history_generation(self): + gate_history = [] + qubits_idx = self.qubits_idx + for depth_idx in range(0, self.depth): + for idx, qubit_idx in enumerate(qubits_idx): + gate_history.append({'gate': 'ry', 'which_qubits': qubit_idx, 'theta': self.theta[depth_idx][idx][0]}) + + for idx in range(0, len(qubits_idx) - 1): + gate_history.append({'gate': 'cnot', 'which_qubits': [qubits_idx[idx], qubits_idx[idx + 1]], 'theta': None}) + + for idx, qubit_idx in enumerate(qubits_idx): + gate_history.append({'gate': 'rz', 'which_qubits': qubit_idx, 'theta': self.theta[depth_idx][idx][1]}) + + for idx in range(0, len(qubits_idx) - 1): + gate_history.append({'gate': 'cnot', 'which_qubits': [qubits_idx[idx], qubits_idx[idx + 1]], 'theta': None}) + self.gate_history = gate_history class RealEntangledLayer(Gate): @@ -163,6 +200,7 @@ def __init__( default_initializer=initializer ) self.add_parameter('theta', theta) + self.gate_name = 'REntLayer' def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: for depth_idx in range(0, self.depth): @@ -174,6 +212,21 @@ def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: state, [self.qubits_idx[qubit_idx], self.qubits_idx[(qubit_idx + 1) % len(self.qubits_idx)]], self.dtype, self.backend) return state + + def gate_history_generation(self): + gate_history = [] + qubits_idx = self.qubits_idx + for depth_idx in range(0, self.depth): + for idx, qubit_idx in enumerate(qubits_idx): + gate_history.append({'gate': 'ry', 'which_qubits': qubit_idx, 'theta': self.theta[depth_idx][idx]}) + + for idx in range(0, len(qubits_idx)): + gate_history.append({ + 'gate': 'cnot', + 'which_qubits': [qubits_idx[idx], qubits_idx[(idx + 1) % len(qubits_idx)]], + 'theta': None + }) + self.gate_history = gate_history class ComplexEntangledLayer(Gate): @@ -203,6 +256,7 @@ def __init__( default_initializer=initializer ) self.add_parameter('theta', theta) + self.gate_name = 'CEntLayer' def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: for depth_idx in range(0, self.depth): @@ -214,6 +268,21 @@ def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: state, [self.qubits_idx[qubit_idx], self.qubits_idx[(qubit_idx + 1) % len(self.qubits_idx)]], self.dtype, self.backend) return state + + def gate_history_generation(self): + gate_history = [] + qubits_idx = self.qubits_idx + for depth_idx in range(0, self.depth): + for idx, qubit_idx in enumerate(qubits_idx): + gate_history.append({'gate': 'u', 'which_qubits': qubit_idx, 'theta': self.theta[depth_idx][idx]}) + + for idx in range(0, len(qubits_idx)): + gate_history.append({ + 'gate': 'cnot', + 'which_qubits': [qubits_idx[idx], qubits_idx[(idx + 1) % len(qubits_idx)]], + 'theta': None + }) + self.gate_history = gate_history class RealBlockLayer(Gate): @@ -244,22 +313,34 @@ def __init__( default_initializer=initializer ) self.add_parameter('theta', theta) + self.gate_name = 'RBLayer' def __add_real_block(self, theta: paddle.Tensor, position: List[int]) -> None: assert len(theta) == 4, 'the length of theta is not right' position[0] = self.qubits_idx[position[0]] position[1] = self.qubits_idx[position[1]] + + if self.count_history: + gate_history = [] + gate_history.append({'gate': 'ry', 'which_qubits': position[0], 'theta': theta[0]}) + gate_history.append({'gate': 'ry', 'which_qubits': position[1], 'theta': theta[1]}) + + gate_history.append({'gate': 'cnot', 'which_qubits': [position[0], position[1]], 'theta': None}) + + gate_history.append({'gate': 'ry', 'which_qubits': position[0], 'theta': theta[2]}) + gate_history.append({'gate': 'ry', 'which_qubits': position[1], 'theta': theta[3]}) + self.gate_history.extend(gate_history) + else: + state = functional.ry(self.state, theta[0], position[0], self.dtype, self.backend) + state = functional.ry(state, theta[1], position[1], self.dtype, self.backend) - state = functional.ry(self.state, theta[0], position[0], self.dtype, self.backend) - state = functional.ry(state, theta[1], position[1], self.dtype, self.backend) - - state = functional.cnot(state, [position[0], position[1]], self.dtype, self.backend) + state = functional.cnot(state, [position[0], position[1]], self.dtype, self.backend) - state = functional.ry(state, theta[2], position[0], self.dtype, self.backend) - state = functional.ry(state, theta[3], position[1], self.dtype, self.backend) + state = functional.ry(state, theta[2], position[0], self.dtype, self.backend) + state = functional.ry(state, theta[3], position[1], self.dtype, self.backend) - self.state = state + self.state = state def __add_real_layer(self, theta: paddle.Tensor, position: List) -> None: assert theta.shape[1] == 4 and theta.shape[0] == (position[1] - position[0] + 1) / 2, \ @@ -282,6 +363,21 @@ def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: self.__add_real_layer(self.theta[depth_idx, int((n - 1) / 2):], [1, n - 1]) return self.state + + def gate_history_generation(self): + self.gate_history = [] + self.count_history = True + n = len(self.qubits_idx) + + if n % 2 == 0: + for depth_idx in range(self.depth): + self.__add_real_layer(self.theta[depth_idx, :int(n / 2)], [0, n - 1]) + self.__add_real_layer(self.theta[depth_idx, int(n / 2):], [1, n - 2]) if n > 2 else None + else: + for depth_idx in range(self.depth): + self.__add_real_layer(self.theta[depth_idx, :int((n - 1) / 2)], [0, n - 2]) + self.__add_real_layer(self.theta[depth_idx, int((n - 1) / 2):], [1, n - 1]) + self.count_history = False class ComplexBlockLayer(Gate): @@ -295,8 +391,7 @@ class ComplexBlockLayer(Gate): num_qubits: Total number of qubits. Defaults to ``None``. depth: Number of layers. Defaults to ``1``. """ - def __init__(self, qubits_idx: Optional[Union[Iterable[int], str]] = 'full', - num_qubits: Optional[int] = None, depth: Optional[int] = 1) -> None: + def __init__(self, qubits_idx: Union[Iterable[int], str] = 'full', num_qubits: int = None, depth: int = 1): super().__init__(depth) self.qubits_idx = qubits_idx_filter(qubits_idx, num_qubits) assert len(self.qubits_idx) > 1, 'you need at least 2 qubits' @@ -311,23 +406,36 @@ def __init__(self, qubits_idx: Optional[Union[Iterable[int], str]] = 'full', default_initializer=initializer ) self.add_parameter('theta', theta) - self.state = None + self.gate_name = 'CBLayer' def __add_complex_block(self, theta: paddle.Tensor, position: List[int]) -> None: assert len(theta) == 12, 'the length of theta is not right' position[0] = self.qubits_idx[position[0]] position[1] = self.qubits_idx[position[1]] + + + if self.count_history: + gate_history = [] + gate_history.append({'gate': 'u', 'which_qubits': position[0], 'theta': theta[0:3]}) + gate_history.append({'gate': 'u', 'which_qubits': position[1], 'theta': theta[3:6]}) + + gate_history.append({'gate': 'cnot', 'which_qubits': [position[0], position[1]], 'theta': None}) + + gate_history.append({'gate': 'u', 'which_qubits': position[0], 'theta': theta[6:9]}) + gate_history.append({'gate': 'u', 'which_qubits': position[1], 'theta': theta[9:12]}) + self.gate_history.extend(gate_history) + else: - state = functional.u3(self.state, [theta[0], theta[1], theta[2]], position[0], self.dtype, self.backend) - state = functional.u3(state, [theta[3], theta[4], theta[5]], position[1], self.dtype, self.backend) - - state = functional.cnot(state, [position[0], position[1]], self.dtype, self.backend) + state = functional.u3(self.state, theta[0:3], position[0], self.dtype, self.backend) + state = functional.u3(state, theta[3:6], position[1], self.dtype, self.backend) - state = functional.u3(state, [theta[6], theta[7], theta[8]], position[0], self.dtype, self.backend) - state = functional.u3(state, [theta[9], theta[10], theta[11]], position[1], self.dtype, self.backend) + state = functional.cnot(state, [position[0], position[1]], self.dtype, self.backend) - self.state = state + state = functional.u3(state, theta[6:9], position[0], self.dtype, self.backend) + state = functional.u3(state, theta[9:12], position[1], self.dtype, self.backend) + + self.state = state def __add_complex_layer(self, theta: paddle.Tensor, position: List[int]) -> None: assert theta.shape[1] == 12 and theta.shape[0] == (position[1] - position[0] + 1) / 2, \ @@ -351,37 +459,126 @@ def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: self.__add_complex_layer(self.theta[depth_idx, (num_acted_qubits - 1) // 2:], [1, num_acted_qubits - 1]) return self.state + + def gate_history_generation(self): + self.gate_history = [] + self.count_history = True + n = len(self.qubits_idx) + + if n % 2 == 0: + for depth_idx in range(self.depth): + self.__add_complex_layer(self.theta[depth_idx, :int(n / 2)], [0, n - 1]) + self.__add_complex_layer(self.theta[depth_idx, int(n / 2):], [1, n - 2]) if n > 2 else None + else: + for depth_idx in range(self.depth): + self.__add_complex_layer(self.theta[depth_idx, :int((n - 1) / 2)], [0, n - 2]) + self.__add_complex_layer(self.theta[depth_idx, int((n - 1) / 2):], [1, n - 1]) + self.count_history = False class QAOALayer(Gate): + r""" QAOA driving layers + + Note: + this layer only works for MAXCUT problem + + Args: + edges: edges of the graph + nodes: nodes of the graph + depth: depth of layer + + """ # TODO: only maxcut now def __init__( self, edges: Iterable, nodes: Iterable, depth: int = 1 ): super().__init__(depth) float_dtype = _get_float_dtype(self.dtype) - initializer = paddle.nn.initializer.Uniform(low=0, high=2 * math.pi) self.edges = edges self.nodes = nodes - gamma = self.create_parameter( - shape=[self.depth], + + theta = self.create_parameter( + shape=[self.depth * 2], dtype=float_dtype, - default_initializer=initializer + default_initializer=paddle.nn.initializer.Uniform(low=0, high=2 * math.pi) ) - beta = self.create_parameter( - shape=[self.depth], + self.add_parameter('theta', theta) + self.gate_name = 'QAOALayer' + + def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: + gamma = self.theta[:self.depth] + beta = self.theta[self.depth:] + for depth_idx in range(0, self.depth): + for node0, node1 in self.edges: + state = functional.cnot(state, [node0, node1], self.dtype, self.backend) + state = functional.rz(state, gamma[depth_idx], node1, self.dtype, self.backend) + state = functional.cnot(state, [node0, node1], self.dtype, self.backend) + for node in self.nodes: + state = functional.rx(state, beta[depth_idx], node, self.dtype, self.backend) + return state + + def gate_history_generation(self): + gate_history = [] + gamma = self.theta[:self.depth] + beta = self.theta[self.depth:] + for depth_idx in range(0, self.depth): + for node0, node1 in self.edges: + gate_history.append({'gate': 'cnot', 'which_qubits': [node0, node1], 'theta': None}) + gate_history.append({'gate': 'rz', 'which_qubits': node1, 'theta': gamma[depth_idx]}) + gate_history.append({'gate': 'cnot', 'which_qubits': [node0, node1], 'theta': None}) + for node in self.nodes: + gate_history.append({'gate': 'rx', 'which_qubits': node, 'theta': beta[depth_idx]}) + + +class QAOALayerWeighted(Gate): + r""" QAOA driving layers with weights + + Args: + edges: edges of the graph with weights + nodes: nodes of the graph with weights + depth: depth of layer + + """ + def __init__( + self, edges: Dict[Tuple[int, int], float], nodes: Dict[int, float], depth: int = 1 + ) -> None: + super().__init__(depth) + float_dtype = _get_float_dtype(self.dtype) + self.edges = edges.keys() + self.nodes = nodes.keys() + self.edges_full = edges + self.nodes_full = nodes + + theta = self.create_parameter( + shape=[self.depth * 2], dtype=float_dtype, - default_initializer=initializer + default_initializer=paddle.nn.initializer.Uniform(low=0, high=2 * math.pi) ) - self.add_parameter('gamma', gamma) - self.add_parameter('beta', beta) + self.add_parameter('theta', theta) + self.gate_history_generation() def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: + gamma = self.theta[:self.depth] + beta = self.theta[self.depth:] for depth_idx in range(0, self.depth): for node0, node1 in self.edges: state = functional.cnot(state, [node0, node1], self.dtype, self.backend) - state = functional.rz(state, self.gamma[depth_idx], node1, self.dtype, self.backend) + state = functional.rz(state, gamma[depth_idx] * self.edges_full[(node0, node1)], node1, self.dtype, self.backend) state = functional.cnot(state, [node0, node1], self.dtype, self.backend) for node in self.nodes: - state = functional.rx(state, self.beta[depth_idx], node, self.dtype, self.backend) + state = functional.rz(state, gamma[depth_idx] * self.nodes_full[node], node, self.dtype, self.backend) + state = functional.rx(state, beta[depth_idx], node, self.dtype, self.backend) return state + + def gate_history_generation(self): + gate_history = [] + gamma = self.theta[:self.depth] + beta = self.theta[self.depth:] + for depth_idx in range(0, self.depth): + for node0, node1 in self.edges: + gate_history.append({'gate': 'cnot', 'which_qubits': [node0, node1], 'theta': None}) + gate_history.append({'gate': 'rz', 'which_qubits': node1, 'theta': gamma[depth_idx]}) + gate_history.append({'gate': 'cnot', 'which_qubits': [node0, node1], 'theta': None}) + for node in self.nodes: + gate_history.append({'gate': 'rx', 'which_qubits': node, 'theta': beta[depth_idx]}) + \ No newline at end of file diff --git a/paddle_quantum/gate/multi_qubit_gate.py b/paddle_quantum/gate/multi_qubit_gate.py index 6e9ef2f..6161389 100644 --- a/paddle_quantum/gate/multi_qubit_gate.py +++ b/paddle_quantum/gate/multi_qubit_gate.py @@ -22,7 +22,7 @@ import paddle import paddle_quantum from . import functional -from .base import Gate +from .base import Gate, ParamGate from ..backend import Backend from ..intrinsic import _format_qubits_idx, _get_float_dtype from typing import Optional, Union, Iterable @@ -53,7 +53,8 @@ class CNOT(Gate): """ def __init__(self, qubits_idx: Optional[Union[Iterable, str]] = 'cycle', num_qubits: Optional[int] = None, depth: Optional[int] = 1): super().__init__(depth) - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=False, num_acted_qubits=2) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, num_acted_qubits=2) + self.gate_name = 'cnot' def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: if self.backend == Backend.QuLeaf and state.backend == Backend.QuLeaf: @@ -80,7 +81,8 @@ class CX(Gate): def __init__(self, qubits_idx: Optional[Union[Iterable, str]] = 'cycle', num_qubits: Optional[int] = None, depth: Optional[int] = 1): super().__init__(depth) - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=False, num_acted_qubits=2) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, num_acted_qubits=2) + self.gate_name = 'cnot' def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: if self.backend == Backend.QuLeaf and state.backend == Backend.QuLeaf: @@ -122,7 +124,8 @@ class CY(Gate): def __init__(self, qubits_idx: Optional[Union[Iterable, str]] = 'cycle', num_qubits: Optional[int] = None, depth: Optional[int] = 1): super().__init__(depth) - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=False, num_acted_qubits=2) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, num_acted_qubits=2) + self.gate_name = 'cy' def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: if self.backend == Backend.QuLeaf and state.backend == Backend.QuLeaf: @@ -164,7 +167,8 @@ class CZ(Gate): def __init__(self, qubits_idx: Optional[Union[Iterable, str]] = 'cycle', num_qubits: Optional[int] = None, depth: Optional[int] = 1): super().__init__(depth) - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=False, num_acted_qubits=2) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, num_acted_qubits=2) + self.gate_name = 'cz' def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: if self.backend == Backend.QuLeaf and state.backend == Backend.QuLeaf: @@ -176,7 +180,7 @@ def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: return state for _ in range(0, self.depth): for qubits_idx in self.qubits_idx: - state = functional.cz(state, qubits_idx, self.dtype, self.backend) + state = functional.cz(state, qubits_idx, self.dtype, self.backend) return state @@ -205,7 +209,8 @@ class SWAP(Gate): def __init__(self, qubits_idx: Optional[Union[Iterable, str]] = 'cycle', num_qubits: Optional[int] = None, depth: Optional[int] = 1): super().__init__(depth) - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=False, num_acted_qubits=2) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, num_acted_qubits=2) + self.gate_name = 'swap' def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: if self.backend == Backend.QuLeaf and state.backend == Backend.QuLeaf: @@ -221,7 +226,7 @@ def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: return state -class CP(Gate): +class CP(ParamGate): r"""A collection of controlled P gates. For a 2-qubit quantum circuit, when ``qubits_idx`` is ``[0, 1]``, the matrix form of such a gate is: @@ -250,29 +255,12 @@ def __init__( param: Optional[Union[paddle.Tensor, float]] = None, param_sharing: Optional[bool] = False ): super().__init__(depth) - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=False, num_acted_qubits=2) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, num_acted_qubits=2) self.param_sharing = param_sharing - float_dtype = _get_float_dtype(self.dtype) - - if param_sharing: - param_shape = [1] - else: - param_shape = len(self.qubits_idx) - param_shape = [self.depth, param_shape] - if param is None: - initializer = paddle.nn.initializer.Uniform(low=0, high=2 * math.pi) - else: - if isinstance(param, float): - initializer = paddle.nn.initializer.Constant(param) - elif isinstance(param, paddle.Tensor): - initializer = paddle.nn.initializer.Assign(param.reshape(param_shape)) - else: - raise ValueError("The param must be paddle.Tensor or float.") - theta = self.create_parameter( - shape=param_shape, dtype=float_dtype, - default_initializer=initializer - ) - self.add_parameter('theta', theta) + + param_shape = [depth, 1 if param_sharing else len(self.qubits_idx)] + self.theta_generation(param, param_shape) + self.gate_name = 'cp' def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: if self.backend == Backend.QuLeaf and state.backend == Backend.QuLeaf: @@ -288,7 +276,7 @@ def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: return state -class CRX(Gate): +class CRX(ParamGate): r"""A collection of controlled rotation gates about the x-axis. For a 2-qubit quantum circuit, when ``qubits_idx`` is ``[0, 1]``, the matrix form of such a gate is: @@ -321,29 +309,12 @@ def __init__( param: Optional[Union[paddle.Tensor, float]] = None, param_sharing: Optional[bool] = False ): super().__init__(depth) - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=False, num_acted_qubits=2) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, num_acted_qubits=2) self.param_sharing = param_sharing - float_dtype = _get_float_dtype(self.dtype) - - if param_sharing: - param_shape = [1] - else: - param_shape = len(self.qubits_idx) - param_shape = [self.depth, param_shape] - if param is None: - initializer = paddle.nn.initializer.Uniform(low=0, high=2 * math.pi) - else: - if isinstance(param, float): - initializer = paddle.nn.initializer.Constant(param) - elif isinstance(param, paddle.Tensor): - initializer = paddle.nn.initializer.Assign(param.reshape(param_shape)) - else: - raise ValueError("The param must be paddle.Tensor or float.") - theta = self.create_parameter( - shape=param_shape, dtype=float_dtype, - default_initializer=initializer - ) - self.add_parameter('theta', theta) + + param_shape = [depth, 1 if param_sharing else len(self.qubits_idx)] + self.theta_generation(param, param_shape) + self.gate_name = 'crx' def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: if self.backend == Backend.QuLeaf and state.backend == Backend.QuLeaf: @@ -375,7 +346,7 @@ def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: return state -class CRY(Gate): +class CRY(ParamGate): r"""A collection of controlled rotation gates about the y-axis. For a 2-qubit quantum circuit, when ``qubits_idx`` is ``[0, 1]``, the matrix form of such a gate is: @@ -408,29 +379,12 @@ def __init__( param: Optional[Union[paddle.Tensor, float]] = None, param_sharing: Optional[bool] = False ): super().__init__(depth) - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=False, num_acted_qubits=2) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, num_acted_qubits=2) self.param_sharing = param_sharing - float_dtype = _get_float_dtype(self.dtype) - - if param_sharing: - param_shape = [1] - else: - param_shape = len(self.qubits_idx) - param_shape = [self.depth, param_shape] - if param is None: - initializer = paddle.nn.initializer.Uniform(low=0, high=2 * math.pi) - else: - if isinstance(param, float): - initializer = paddle.nn.initializer.Constant(param) - elif isinstance(param, paddle.Tensor): - initializer = paddle.nn.initializer.Assign(param.reshape(param_shape)) - else: - raise ValueError("The param must be paddle.Tensor or float.") - theta = self.create_parameter( - shape=param_shape, dtype=float_dtype, - default_initializer=initializer - ) - self.add_parameter('theta', theta) + + param_shape = [depth, 1 if param_sharing else len(self.qubits_idx)] + self.theta_generation(param, param_shape) + self.gate_name = 'cry' def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: if self.backend == Backend.QuLeaf and state.backend == Backend.QuLeaf: @@ -462,7 +416,7 @@ def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: return state -class CRZ(Gate): +class CRZ(ParamGate): r"""A collection of controlled rotation gates about the z-axis. For a 2-qubit quantum circuit, when ``qubits_idx`` is ``[0, 1]``, the matrix form of such a gate is: @@ -495,29 +449,12 @@ def __init__( param: Optional[Union[paddle.Tensor, float]] = None, param_sharing: Optional[bool] = False ): super().__init__(depth) - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=False, num_acted_qubits=2) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, num_acted_qubits=2) self.param_sharing = param_sharing - float_dtype = _get_float_dtype(self.dtype) - - if param_sharing: - param_shape = [1] - else: - param_shape = len(self.qubits_idx) - param_shape = [self.depth, param_shape] - if param is None: - initializer = paddle.nn.initializer.Uniform(low=0, high=2 * math.pi) - else: - if isinstance(param, float): - initializer = paddle.nn.initializer.Constant(param) - elif isinstance(param, paddle.Tensor): - initializer = paddle.nn.initializer.Assign(param.reshape(param_shape)) - else: - raise ValueError("The param must be paddle.Tensor or float.") - theta = self.create_parameter( - shape=param_shape, dtype=float_dtype, - default_initializer=initializer - ) - self.add_parameter('theta', theta) + + param_shape = [depth, 1 if param_sharing else len(self.qubits_idx)] + self.theta_generation(param, param_shape) + self.gate_name = 'crz' def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: if self.backend == Backend.QuLeaf and state.backend == Backend.QuLeaf: @@ -549,7 +486,7 @@ def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: return state -class CU(Gate): +class CU(ParamGate): r"""A collection of controlled single-qubit rotation gates. For a 2-qubit quantum circuit, when ``qubits_idx`` is ``[0, 1]``, the matrix form of such a gate is: @@ -582,29 +519,15 @@ def __init__( param: Optional[Union[paddle.Tensor, float]] = None, param_sharing: Optional[bool] = False ): super().__init__(depth) - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=False, num_acted_qubits=2) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, num_acted_qubits=2) self.param_sharing = param_sharing - float_dtype = _get_float_dtype(self.dtype) - + if param_sharing: - param_shape = [3] - else: - param_shape = [len(self.qubits_idx), 3] - param_shape = [self.depth] + param_shape - if param is None: - initializer = paddle.nn.initializer.Uniform(low=0, high=2 * math.pi) + param_shape = [depth, 3] else: - if isinstance(param, float): - initializer = paddle.nn.initializer.Constant(param) - elif isinstance(param, paddle.Tensor): - initializer = paddle.nn.initializer.Assign(param.reshape(param_shape)) - else: - raise ValueError("The param must be paddle.Tensor or float.") - theta = self.create_parameter( - shape=param_shape, dtype=float_dtype, - default_initializer=initializer - ) - self.add_parameter('theta', theta) + param_shape = [depth, len(self.qubits_idx), 3] + self.theta_generation(param, param_shape) + self.gate_name = 'cu' def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: if self.backend == Backend.QuLeaf and state.backend == Backend.QuLeaf: @@ -636,7 +559,7 @@ def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: return state -class RXX(Gate): +class RXX(ParamGate): r"""A collection of RXX gates. The matrix form of such a gate is: @@ -668,29 +591,12 @@ def __init__( param: Optional[Union[paddle.Tensor, float]] = None, param_sharing: Optional[bool] = False ): super().__init__(depth) - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=False, num_acted_qubits=2) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, num_acted_qubits=2) self.param_sharing = param_sharing - float_dtype = _get_float_dtype(self.dtype) - - if param_sharing: - param_shape = [1] - else: - param_shape = len(self.qubits_idx) - param_shape = [self.depth, param_shape] - if param is None: - initializer = paddle.nn.initializer.Uniform(low=0, high=2 * math.pi) - else: - if isinstance(param, float): - initializer = paddle.nn.initializer.Constant(param) - elif isinstance(param, paddle.Tensor): - initializer = paddle.nn.initializer.Assign(param.reshape(param_shape)) - else: - raise ValueError("The param must be paddle.Tensor or float.") - theta = self.create_parameter( - shape=param_shape, dtype=float_dtype, - default_initializer=initializer - ) - self.add_parameter('theta', theta) + + param_shape = [depth, 1 if param_sharing else len(self.qubits_idx)] + self.theta_generation(param, param_shape) + self.gate_name = 'rxx' def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: if self.backend == Backend.QuLeaf and state.backend == Backend.QuLeaf: @@ -706,7 +612,7 @@ def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: return state -class RYY(Gate): +class RYY(ParamGate): r"""A collection of RYY gates. The matrix form of such a gate is: @@ -738,29 +644,12 @@ def __init__( param: Optional[Union[paddle.Tensor, float]] = None, param_sharing: Optional[bool] = False ): super().__init__(depth) - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=False, num_acted_qubits=2) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, num_acted_qubits=2) self.param_sharing = param_sharing - float_dtype = _get_float_dtype(self.dtype) - - if param_sharing: - param_shape = [1] - else: - param_shape = len(self.qubits_idx) - param_shape = [self.depth, param_shape] - if param is None: - initializer = paddle.nn.initializer.Uniform(low=0, high=2 * math.pi) - else: - if isinstance(param, float): - initializer = paddle.nn.initializer.Constant(param) - elif isinstance(param, paddle.Tensor): - initializer = paddle.nn.initializer.Assign(param.reshape(param_shape)) - else: - raise ValueError("The param must be paddle.Tensor or float.") - theta = self.create_parameter( - shape=param_shape, dtype=float_dtype, - default_initializer=initializer - ) - self.add_parameter('theta', theta) + + param_shape = [depth, 1 if param_sharing else len(self.qubits_idx)] + self.theta_generation(param, param_shape) + self.gate_name = 'ryy' def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: if self.backend == Backend.QuLeaf and state.backend == Backend.QuLeaf: @@ -776,7 +665,7 @@ def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: return state -class RZZ(Gate): +class RZZ(ParamGate): r"""A collection of RZZ gates. The matrix form of such a gate is: @@ -808,29 +697,12 @@ def __init__( param: Optional[Union[paddle.Tensor, float]] = None, param_sharing: Optional[bool] = False ): super().__init__(depth) - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=False, num_acted_qubits=2) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, num_acted_qubits=2) self.param_sharing = param_sharing - float_dtype = _get_float_dtype(self.dtype) - - if param_sharing: - param_shape = [1] - else: - param_shape = len(self.qubits_idx) - param_shape = [self.depth, param_shape] - if param is None: - initializer = paddle.nn.initializer.Uniform(low=0, high=2 * math.pi) - else: - if isinstance(param, float): - initializer = paddle.nn.initializer.Constant(param) - elif isinstance(param, paddle.Tensor): - initializer = paddle.nn.initializer.Assign(param.reshape(param_shape)) - else: - raise ValueError("The param must be paddle.Tensor or float.") - theta = self.create_parameter( - shape=param_shape, dtype=float_dtype, - default_initializer=initializer - ) - self.add_parameter('theta', theta) + + param_shape = [depth, 1 if param_sharing else len(self.qubits_idx)] + self.theta_generation(param, param_shape) + self.gate_name = 'rzz' def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: if self.backend == Backend.QuLeaf and state.backend == Backend.QuLeaf: @@ -870,14 +742,15 @@ class MS(Gate): """ def __init__(self, qubits_idx: Optional[Union[Iterable, str]] = 'cycle', num_qubits: Optional[int] = None, depth: Optional[int] = 1): super().__init__(depth) - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=False, num_acted_qubits=2) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, num_acted_qubits=2) + self.gate_name = 'ms' def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: if self.backend == Backend.QuLeaf and state.backend == Backend.QuLeaf: raise NotImplementedError for _ in range(0, self.depth): for qubits_idx in self.qubits_idx: - functional.ms(state, qubits_idx, self.dtype, self.backend) + state = functional.ms(state, qubits_idx, self.dtype, self.backend) return state @@ -909,7 +782,8 @@ class CSWAP(Gate): """ def __init__(self, qubits_idx: Optional[Union[Iterable, str]] = 'cycle', num_qubits: Optional[int] = None, depth: Optional[int] = 1): super().__init__(depth) - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=False, num_acted_qubits=3) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, num_acted_qubits=3) + self.gate_name = 'cswap' def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: if self.backend == Backend.QuLeaf and state.backend == Backend.QuLeaf: @@ -921,7 +795,7 @@ def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: return state for _ in range(0, self.depth): for qubits_idx in self.qubits_idx: - functional.cswap(state, qubits_idx, self.dtype, self.backend) + state = functional.cswap(state, qubits_idx, self.dtype, self.backend) return state @@ -952,7 +826,8 @@ class Toffoli(Gate): """ def __init__(self, qubits_idx: Optional[Union[Iterable, str]] = 'cycle', num_qubits: Optional[int] = None, depth: Optional[int] = 1): super().__init__(depth) - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=False, num_acted_qubits=3) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, num_acted_qubits=3) + self.gate_name = 'ccx' def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: if self.backend == Backend.QuLeaf and state.backend == Backend.QuLeaf: @@ -964,11 +839,11 @@ def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: return state for _ in range(0, self.depth): for qubits_idx in self.qubits_idx: - functional.toffoli(state, qubits_idx, self.dtype, self.backend) + state = functional.toffoli(state, qubits_idx, self.dtype, self.backend) return state -class UniversalTwoQubits(Gate): +class UniversalTwoQubits(ParamGate): r"""A collection of universal two-qubit gates. One of such a gate requires 15 parameters. Args: @@ -986,29 +861,15 @@ def __init__( param: Optional[Union[paddle.Tensor, float]] = None, param_sharing: Optional[bool] = False ): super().__init__(depth) - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=False, num_acted_qubits=2) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, num_acted_qubits=2) self.param_sharing = param_sharing - float_dtype = _get_float_dtype(self.dtype) - + if param_sharing: - param_shape = [15] + param_shape = [depth, 15] else: - param_shape = [len(self.qubits_idx), 15] - param_shape = [self.depth] + param_shape - if param is None: - initializer = paddle.nn.initializer.Uniform(low=0, high=2 * math.pi) - else: - if isinstance(param, float): - initializer = paddle.nn.initializer.Constant(param) - elif isinstance(param, paddle.Tensor): - initializer = paddle.nn.initializer.Assign(param.reshape(param_shape)) - else: - raise ValueError("The param must be paddle.Tensor or float.") - theta = self.create_parameter( - shape=param_shape, dtype=float_dtype, - default_initializer=initializer - ) - self.add_parameter('theta', theta) + param_shape = [depth, len(self.qubits_idx), 15] + self.theta_generation(param, param_shape) + self.gate_name = 'uni2' def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: if self.backend == Backend.QuLeaf and state.backend == Backend.QuLeaf: @@ -1025,7 +886,7 @@ def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: return state -class UniversalThreeQubits(Gate): +class UniversalThreeQubits(ParamGate): r"""A collection of universal three-qubit gates. One of such a gate requires 81 parameters. Args: @@ -1043,29 +904,15 @@ def __init__( param: Optional[Union[paddle.Tensor, float]] = None, param_sharing: Optional[bool] = False ): super().__init__(depth) - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=False, num_acted_qubits=3) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, num_acted_qubits=3) self.param_sharing = param_sharing - float_dtype = _get_float_dtype(self.dtype) - + if param_sharing: - param_shape = [81] + param_shape = [depth, 81] else: - param_shape = [len(self.qubits_idx), 81] - param_shape = [self.depth] + param_shape - if param is None: - initializer = paddle.nn.initializer.Uniform(low=0, high=2 * math.pi) - else: - if isinstance(param, float): - initializer = paddle.nn.initializer.Constant(param) - elif isinstance(param, paddle.Tensor): - initializer = paddle.nn.initializer.Assign(param.reshape(param_shape)) - else: - raise ValueError("The param must be paddle.Tensor or float.") - theta = self.create_parameter( - shape=param_shape, dtype=float_dtype, - default_initializer=initializer - ) - self.add_parameter('theta', theta) + param_shape = [depth, len(self.qubits_idx), 81] + self.theta_generation(param, param_shape) + self.gate_name = 'uni3' def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: if self.backend == Backend.QuLeaf and state.backend == Backend.QuLeaf: diff --git a/paddle_quantum/gate/single_qubit_gate.py b/paddle_quantum/gate/single_qubit_gate.py index 1bc3e78..6dc02fd 100644 --- a/paddle_quantum/gate/single_qubit_gate.py +++ b/paddle_quantum/gate/single_qubit_gate.py @@ -23,10 +23,10 @@ import paddle.nn import paddle_quantum from . import functional -from .base import Gate +from .base import Gate, ParamGate from ..backend import Backend from paddle_quantum.intrinsic import _format_qubits_idx, _get_float_dtype -from typing import Optional, Union, Iterable +from typing import Optional, List, Union, Iterable class H(Gate): @@ -49,7 +49,8 @@ class H(Gate): """ def __init__(self, qubits_idx: Optional[Union[Iterable, int, str]] = 'full', num_qubits: Optional[int] = None, depth: Optional[int] = 1): super().__init__(depth) - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=True) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits) + self.gate_name = 'h' def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: if self.backend == Backend.QuLeaf and state.backend == Backend.QuLeaf: @@ -85,7 +86,8 @@ class S(Gate): """ def __init__(self, qubits_idx: Optional[Union[Iterable, int, str]] = 'full', num_qubits: Optional[int] = None, depth: Optional[int] = 1): super().__init__(depth) - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=True) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits) + self.gate_name = 's' def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: if self.backend == Backend.QuLeaf and state.backend == Backend.QuLeaf: @@ -121,7 +123,8 @@ class T(Gate): """ def __init__(self, qubits_idx: Optional[Union[Iterable, int, str]] = 'full', num_qubits: Optional[int] = None, depth: Optional[int] = 1): super().__init__(depth) - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=True) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits) + self.gate_name = 't' def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: if self.backend == Backend.QuLeaf and state.backend == Backend.QuLeaf: @@ -156,8 +159,9 @@ class X(Gate): """ def __init__(self, qubits_idx: Optional[Union[Iterable, int, str]] = 'full', num_qubits: Optional[int] = None, depth: Optional[int] = 1): super().__init__(depth) - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=True) - + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits) + self.gate_name = 'x' + def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: if self.backend == Backend.QuLeaf and state.backend == Backend.QuLeaf: state.gate_history.append({ @@ -191,7 +195,8 @@ class Y(Gate): """ def __init__(self, qubits_idx: Optional[Union[Iterable, int, str]] = 'full', num_qubits: Optional[int] = None, depth: Optional[int] = 1): super().__init__(depth) - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=True) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits) + self.gate_name = 'y' def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: if self.backend == Backend.QuLeaf and state.backend == Backend.QuLeaf: @@ -226,7 +231,8 @@ class Z(Gate): """ def __init__(self, qubits_idx: Optional[Union[Iterable, int, str]] = 'full', num_qubits: Optional[int] = None, depth: Optional[int] = 1): super().__init__(depth) - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=True) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits) + self.gate_name = 'z' def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: if self.backend == Backend.QuLeaf and state.backend == Backend.QuLeaf: @@ -242,7 +248,7 @@ def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: return state -class P(Gate): +class P(ParamGate): r"""A collection of single-qubit P gates. The matrix form of such a gate is: @@ -269,29 +275,12 @@ def __init__( param: Optional[Union[paddle.Tensor, float]] = None, param_sharing: Optional[bool] = False ): super().__init__(depth) - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=True) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits) self.param_sharing = param_sharing - float_dtype = _get_float_dtype(self.dtype) - - if param_sharing: - param_shape = [1] - else: - param_shape = list(np.shape(self.qubits_idx)) - param_shape = [self.depth] + param_shape - if param is None: - initializer = paddle.nn.initializer.Uniform(low=0, high=2 * math.pi) - else: - if isinstance(param, float): - initializer = paddle.nn.initializer.Constant(param) - elif isinstance(param, paddle.Tensor): - initializer = paddle.nn.initializer.Assign(param.reshape(param_shape)) - else: - raise ValueError("The param must be paddle.Tensor or float.") - theta = self.create_parameter( - shape=param_shape, dtype=float_dtype, - default_initializer=initializer - ) - self.add_parameter('theta', theta) + + param_shape = [depth, 1 if param_sharing else len(self.qubits_idx)] + self.theta_generation(param, param_shape) + self.gate_name = 'p' def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: if self.backend == Backend.QuLeaf and state.backend == Backend.QuLeaf: @@ -307,7 +296,7 @@ def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: return state -class RX(Gate): +class RX(ParamGate): r"""A collection of single-qubit rotation gates about the x-axis. The matrix form of such a gate is: @@ -334,31 +323,12 @@ def __init__( param: Optional[Union[paddle.Tensor, float]] = None, param_sharing: Optional[bool] = False ): super().__init__(depth) - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=True) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits) self.param_sharing = param_sharing - float_dtype = _get_float_dtype(self.dtype) - - if param_sharing: - param_shape = [1] - else: - param_shape = list(np.shape(self.qubits_idx)) - param_shape = [self.depth] + param_shape - if param is None: - theta = self.create_parameter( - shape=param_shape, dtype=float_dtype, - default_initializer=paddle.nn.initializer.Uniform(low=0, high=2 * math.pi) - ) - self.add_parameter('theta', theta) - elif isinstance(param, paddle.Tensor): - theta = self.create_parameter( - shape=param_shape, dtype=float_dtype, - default_initializer=paddle.nn.initializer.Assign(param.reshape(param_shape)) - ) - self.add_parameter('theta', theta) - elif isinstance(param, float): - self.theta = paddle.ones(param_shape, dtype=float_dtype) * param - else: - raise ValueError("The param must be paddle.Tensor or float.") + + param_shape = [depth, 1 if param_sharing else len(self.qubits_idx)] + self.theta_generation(param, param_shape) + self.gate_name = 'rx' def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: if self.backend == Backend.QuLeaf and state.backend == Backend.QuLeaf: @@ -390,7 +360,7 @@ def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: return state -class RY(Gate): +class RY(ParamGate): r"""A collection of single-qubit rotation gates about the y-axis. The matrix form of such a gate is: @@ -417,31 +387,12 @@ def __init__( param: Optional[Union[paddle.Tensor, float]] = None, param_sharing: Optional[bool] = False ): super().__init__(depth) - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=True) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits) self.param_sharing = param_sharing - float_dtype = _get_float_dtype(self.dtype) - - if param_sharing: - param_shape = [1] - else: - param_shape = list(np.shape(self.qubits_idx)) - param_shape = [self.depth] + param_shape - if param is None: - theta = self.create_parameter( - shape=param_shape, dtype=float_dtype, - default_initializer=paddle.nn.initializer.Uniform(low=0, high=2 * math.pi) - ) - self.add_parameter('theta', theta) - elif isinstance(param, paddle.Tensor): - theta = self.create_parameter( - shape=param_shape, dtype=float_dtype, - default_initializer=paddle.nn.initializer.Assign(param.reshape(param_shape)) - ) - self.add_parameter('theta', theta) - elif isinstance(param, float): - self.theta = paddle.ones(param_shape, dtype=float_dtype) * param - else: - raise ValueError("The param must be paddle.Tensor or float.") + + param_shape = [depth, 1 if param_sharing else len(self.qubits_idx)] + self.theta_generation(param, param_shape) + self.gate_name = 'ry' def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: if self.backend == Backend.QuLeaf and state.backend == Backend.QuLeaf: @@ -473,7 +424,7 @@ def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: return state -class RZ(Gate): +class RZ(ParamGate): r"""A collection of single-qubit rotation gates about the z-axis. The matrix form of such a gate is: @@ -500,31 +451,12 @@ def __init__( param: Optional[Union[paddle.Tensor, float]] = None, param_sharing: Optional[bool] = False ): super().__init__(depth) - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=True) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits) self.param_sharing = param_sharing - float_dtype = _get_float_dtype(self.dtype) - - if param_sharing: - param_shape = [1] - else: - param_shape = list(np.shape(self.qubits_idx)) - param_shape = [self.depth] + param_shape - if param is None: - theta = self.create_parameter( - shape=param_shape, dtype=float_dtype, - default_initializer=paddle.nn.initializer.Uniform(low=0, high=2 * math.pi) - ) - self.add_parameter('theta', theta) - elif isinstance(param, paddle.Tensor): - theta = self.create_parameter( - shape=param_shape, dtype=float_dtype, - default_initializer=paddle.nn.initializer.Assign(param.reshape(param_shape)) - ) - self.add_parameter('theta', theta) - elif isinstance(param, float): - self.theta = paddle.ones(param_shape, dtype=float_dtype) * param - else: - raise ValueError("The param must be paddle.Tensor or float.") + + param_shape = [depth, 1 if param_sharing else len(self.qubits_idx)] + self.theta_generation(param, param_shape) + self.gate_name = 'rz' def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: if self.backend == Backend.QuLeaf and state.backend == Backend.QuLeaf: @@ -556,7 +488,7 @@ def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: return state -class U3(Gate): +class U3(ParamGate): r"""A collection of single-qubit rotation gates. The matrix form of such a gate is: @@ -586,29 +518,15 @@ def __init__( param: Optional[Union[paddle.Tensor, Iterable[float]]] = None, param_sharing: Optional[bool] = False ): super().__init__(depth) - self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits, is_single_qubit_gate=True) + self.qubits_idx = _format_qubits_idx(qubits_idx, num_qubits) self.param_sharing = param_sharing - float_dtype = _get_float_dtype(self.dtype) - + if param_sharing: - param_shape = [3] + param_shape = [depth, 3] else: - param_shape = list(np.shape(self.qubits_idx)) + [3] - param_shape = [self.depth] + param_shape - if param is None: - initializer = paddle.nn.initializer.Uniform(low=0, high=2 * math.pi) - else: - if isinstance(param, float): - initializer = paddle.nn.initializer.Constant(param) - elif isinstance(param, paddle.Tensor): - initializer = paddle.nn.initializer.Assign(param.reshape(param_shape)) - else: - raise ValueError("The param must be paddle.Tensor or float.") - theta = self.create_parameter( - shape=param_shape, dtype=float_dtype, - default_initializer=initializer - ) - self.add_parameter('theta', theta) + param_shape = [depth, len(self.qubits_idx), 3] + self.theta_generation(param, param_shape) + self.gate_name = 'u' def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: if self.backend == Backend.QuLeaf and state.backend == Backend.QuLeaf: diff --git a/paddle_quantum/gradtool.py b/paddle_quantum/gradtool.py index 93b7022..da67e78 100644 --- a/paddle_quantum/gradtool.py +++ b/paddle_quantum/gradtool.py @@ -238,6 +238,9 @@ def plot_supervised_loss_grad(circuit: Circuit, loss_func: Callable[[Circuit, An TRAIN_Y: Label set. *args: Parameters for ``loss_func`` other than ``circuit``. + Raises: + Exception: Training data should be paddle.Tensor type + Returns: Contains the following two elements. - loss_list: A list of losses for each iteration. @@ -304,6 +307,9 @@ def random_sample_supervised(circuit: Circuit, loss_func: Callable[[Circuit, Any - In single mode, we calculate the mean and variance of gradients of every trainable parameters. - In max mode, we calculate the mean and variance of maximum gradients of for every trainable parameters. - In random mode, we calculate the mean and variance of data randomly extracted from gradients of every trainable parameters. + + Raises: + Exception: Training data should be paddle.Tensor type Returns: Contains the following two elements. diff --git a/paddle_quantum/hamiltonian.py b/paddle_quantum/hamiltonian.py index 95d7e3b..d6f647c 100644 --- a/paddle_quantum/hamiltonian.py +++ b/paddle_quantum/hamiltonian.py @@ -136,7 +136,7 @@ def coefficients(self) -> list: @property def pauli_words(self) -> list: - r"""The Pauli word of each term,i.e. ``['ZIZ', 'IIX']``. + r"""The Pauli word of each term, i.e. ``['ZIZ', 'IIX']``. """ if self.__update_flag: self.__decompose() @@ -146,7 +146,7 @@ def pauli_words(self) -> list: @property def pauli_words_r(self) -> list: - r"""A list of Pauli word (exclude I),i.e. ``['ZXZZ', 'Z', 'X']``. + r"""A list of Pauli word (exclude I), i.e. ``['ZXZZ', 'Z', 'X']``. """ if self.__update_flag: self.__decompose() @@ -209,11 +209,14 @@ def n_qubits(self) -> int: return self.__nqubits def __decompose(self): - r"""将哈密顿量分解为不同的形式 + r"""decompose the Hamiltonian into vairious forms + + Raises: + Exception: Operators should be defined with a string composed of Pauli operators followed by qubit index on which it act, separated with ",". i.e. "Z0, X1" Notes: - 这是一个内部函数,你不需要直接使用它 - 这是一个比较基础的函数,它负责将输入的 Pauli string 拆分为不同的形式并存储在内部变量中 + This is an intrinsic function, user do not need to call this directly + This is a fundamental function, it decomposes the input Pauli string into different forms and stores them into private variables. """ self.__pauli_words = [] self.__pauli_words_r = [] @@ -261,10 +264,10 @@ def __decompose(self): self.__update_flag = False def __compress(self): - r""" 对同类项进行合并。 + r"""combine like terms Notes: - 这是一个内部函数,你不需要直接使用它 + This is an intrinsic function, user do not need to call this directly """ if self.__update_flag: self.__decompose() @@ -388,14 +391,14 @@ def sigx_p(self) -> list: return self.__sigx_p def __direct_prod_op(self, spin_op, spin_index): - r"""直积,得到第 n 个自旋(量子比特)上的自旋算符 + r"""get spin operators on n-th spin (qubit) with direct product Args: - spin_op: 单体自旋算符 - spin_index: 标记第 n 个自旋(量子比特) + spin_op: single body spin operator + spin_index: on which spin (qubit) Returns: - scipy.sparse or np.ndarray: 直积后的自旋算符,其数据类型取决于 self.__use_sparse + scipy.sparse or np.ndarray: spin operator with direct product form. The data type is specified by self.__use_sparse """ s_p = copy.copy(spin_op) for i in range(self.size): diff --git a/paddle_quantum/intrinsic.py b/paddle_quantum/intrinsic.py index 5f07a68..0a5b040 100644 --- a/paddle_quantum/intrinsic.py +++ b/paddle_quantum/intrinsic.py @@ -32,9 +32,9 @@ def _one(dtype): def _format_qubits_idx( qubits_idx: Union[Iterable[Iterable[int]], Iterable[int], int, str], - num_qubits: int, is_single_qubit_gate: bool, num_acted_qubits: int = 0 + num_qubits: int, num_acted_qubits: int = 1 ) -> Union[List[List[int]], List[int]]: - if is_single_qubit_gate: + if num_acted_qubits == 1: if qubits_idx == 'full': qubits_idx = list(range(0, num_qubits)) elif qubits_idx == 'even': @@ -48,13 +48,15 @@ def _format_qubits_idx( else: if qubits_idx == 'cycle': qubits_idx = [] - for idx in range(0, num_qubits - 1): - qubits_idx.append([idx, idx + 1]) - qubits_idx.append([num_qubits - 1, 0]) + for idx in range(0, num_qubits - num_acted_qubits): + qubits_idx.append([i for i in range(idx, idx + num_acted_qubits)]) + for idx in range(num_qubits - num_acted_qubits, num_qubits): + qubits_idx.append([i for i in range(idx, num_qubits)] + + [i for i in range(idx + num_acted_qubits - num_qubits)]) elif qubits_idx == 'linear': qubits_idx = [] - for idx in range(0, num_qubits - 1): - qubits_idx.append([idx, idx + 1]) + for idx in range(0, num_qubits - num_acted_qubits): + qubits_idx.append([i for i in range(idx, idx + num_acted_qubits)]) elif len(np.shape(qubits_idx)) == 1 and len(qubits_idx) == num_acted_qubits: qubits_idx = [list(qubits_idx)] elif len(np.shape(qubits_idx)) == 2 and all((len(indices) == num_acted_qubits for indices in qubits_idx)): diff --git a/paddle_quantum/linalg.py b/paddle_quantum/linalg.py index e545e40..7c678bb 100644 --- a/paddle_quantum/linalg.py +++ b/paddle_quantum/linalg.py @@ -17,14 +17,16 @@ The common linear algorithm in paddle quantum. """ -from typing import Optional +from typing import Optional, Union import paddle import math import numpy as np import scipy +from scipy.stats import unitary_group from functools import reduce import paddle_quantum +from paddle_quantum.intrinsic import _get_float_dtype def abs_norm(mat: paddle.Tensor) -> float: @@ -37,7 +39,7 @@ def abs_norm(mat: paddle.Tensor) -> float: norm of mat """ - mat = mat.cast('complex64') + mat = mat.cast(paddle_quantum.get_dtype()) return paddle.norm(paddle.abs(mat)).item() @@ -58,7 +60,7 @@ def is_hermitian(mat: paddle.Tensor, eps: Optional[float] = 1e-6) -> bool: r""" verify whether mat ``P`` is Hermitian Args: - mat: matrix + mat: hermitian candidate eps: tolerance of error Returns: @@ -67,7 +69,7 @@ def is_hermitian(mat: paddle.Tensor, eps: Optional[float] = 1e-6) -> bool: """ shape = mat.shape if len(shape) != 2 or shape[0] != shape[1] or math.log2(shape[0]) != math.ceil(math.log2(shape[0])): - # not a mat / not a square mat / shape is not in form 2^n x 2^n + # not a mat / not a square mat / shape is not in form 2^num_qubits x 2^num_qubits return False return abs_norm(mat - dagger(mat)) < eps @@ -76,7 +78,7 @@ def is_projector(mat: paddle.Tensor, eps: Optional[float] = 1e-6) -> bool: r""" verify whether mat ``P`` is a projector Args: - mat: matrix + mat: projector candidate eps: tolerance of error Returns: @@ -85,7 +87,7 @@ def is_projector(mat: paddle.Tensor, eps: Optional[float] = 1e-6) -> bool: """ shape = mat.shape if len(shape) != 2 or shape[0] != shape[1] or math.log2(shape[0]) != math.ceil(math.log2(shape[0])): - # not a mat / not a square mat / shape is not in form 2^n x 2^n + # not a mat / not a square mat / shape is not in form 2^num_qubits x 2^num_qubits return False return abs_norm(mat @ mat - mat) < eps @@ -94,7 +96,7 @@ def is_unitary(mat: paddle.Tensor, eps: Optional[float] = 1e-5) -> bool: r""" verify whether mat ``P`` is a unitary Args: - mat: matrix + mat: unitary candidate eps: tolerance of error Returns: @@ -103,52 +105,87 @@ def is_unitary(mat: paddle.Tensor, eps: Optional[float] = 1e-5) -> bool: """ shape = mat.shape if len(shape) != 2 or shape[0] != shape[1] or math.log2(shape[0]) != math.ceil(math.log2(shape[0])): - # not a mat / not a square mat / shape is not in form 2^n x 2^n + # not a mat / not a square mat / shape is not in form 2^num_qubits x 2^num_qubits return False return abs_norm(mat @ dagger(mat) - paddle.cast(paddle.eye(shape[0]), mat.dtype)) < eps def hermitian_random(num_qubits: int) -> paddle.Tensor: - r"""randomly generate a :math:`2^n \times 2^n` hermitian matrix + r"""randomly generate a :math:`2^num_qubits \times 2^num_qubits` hermitian matrix Args: num_qubits: log2(dimension) Returns: - a :math:`2^n \times 2^n` hermitian matrix + a :math:`2^num_qubits \times 2^num_qubits` hermitian matrix """ assert num_qubits > 0 n = 2 ** num_qubits - vec = paddle.randn([n, n]) + 1j * paddle.randn([n, n]) + + float_dtype = _get_float_dtype(paddle_quantum.get_dtype()) + vec = paddle.randn([n, n], dtype=float_dtype) + 1j * paddle.randn([n, n], dtype=float_dtype) mat = vec @ dagger(vec) return mat / paddle.trace(mat) def orthogonal_projection_random(num_qubits: int) -> paddle.Tensor: - r"""randomly generate a :math:`2^n \times 2^n` rank-1 orthogonal projector + r"""randomly generate a :math:`2^num_qubits \times 2^num_qubits` rank-1 orthogonal projector Args: num_qubits: log2(dimension) Returns: - a :math:`2^n \times 2^n` orthogonal projector and its eigenstate + a 2^num_qubits x 2^num_qubits orthogonal projector """ assert num_qubits > 0 n = 2 ** num_qubits - vec = paddle.randn([n, 1]) + 1j * paddle.randn([n, 1]) + float_dtype = _get_float_dtype(paddle_quantum.get_dtype()) + vec = paddle.randn([n, 1], dtype=float_dtype) + 1j * paddle.randn([n, 1], dtype=float_dtype) mat = vec @ dagger(vec) return mat / paddle.trace(mat) +def density_matrix_random(num_qubits: int) -> paddle.Tensor: + r""" randomly generate an num_qubits-qubit state in density matrix form + + Args: + num_qubits: number of qubits + + Returns: + a 2^num_qubits x 2^num_qubits density matrix + + """ + float_dtype = _get_float_dtype(paddle_quantum.get_dtype()) + real = paddle.rand([2 ** num_qubits, 2 ** num_qubits], dtype=float_dtype) + imag = paddle.rand([2 ** num_qubits, 2 ** num_qubits], dtype=float_dtype) + M = real + 1j * imag + M = M @ dagger(M) + rho = M / paddle.trace(M) + return rho + + +def unitary_random(num_qubits: int) -> paddle.Tensor: + r"""randomly generate a :math:`2^num_qubits \times 2^num_qubits` unitary + + Args: + num_qubits: :math:`\log_{2}(dimension)` + + Returns: + a :math:`2^num_qubits \times 2^num_qubits` unitary matrix + + """ + return paddle.to_tensor(unitary_group.rvs(2 ** num_qubits), dtype=paddle_quantum.get_dtype()) + + def unitary_hermitian_random(num_qubits: int) -> paddle.Tensor: - r"""randomly generate a :math:`2^n \times 2^n` hermitian unitary + r"""randomly generate a :math:`2^num_qubits \times 2^num_qubits` hermitian unitary Args: num_qubits: :math:`\log_{2}(dimension)` Returns: - a :math:`2^n \times 2^n` hermitian unitary matrix + a :math:`2^num_qubits \times 2^num_qubits` hermitian unitary matrix """ proj_mat = orthogonal_projection_random(num_qubits) @@ -156,39 +193,29 @@ def unitary_hermitian_random(num_qubits: int) -> paddle.Tensor: return (2 + 0j) * proj_mat - id_mat -def unitary_random_with_hermitian_block(num_qubits: int) -> paddle.Tensor: - r"""randomly generate a unitary :math:`2^n \times 2^n` matrix that is a block encoding of a :math:`2^{n/2} \times 2^{n/2}` Hermitian matrix +def unitary_random_with_hermitian_block(num_qubits: int, is_unitary: bool = False) -> paddle.Tensor: + r"""randomly generate a unitary :math:`2^num_qubits \times 2^num_qubits` matrix that is a block encoding of a :math:`2^{num_qubits/2} \times 2^{num_qubits/2}` Hermitian matrix Args: num_qubits: :math:`\log_{2}(dimension)` + is_unitary: whether the hermitian block is a unitary divided by 2 (for tutorial only) Returns: - a :math:`2^n \times 2^n` unitary matrix that its upper-left block is a Hermitian matrix + a :math:`2^num_qubits \times 2^num_qubits` unitary matrix that its upper-left block is a Hermitian matrix """ assert num_qubits > 0 - dtype = paddle_quantum.get_dtype() - mat0 = hermitian_random(num_qubits - 1).numpy() + + if is_unitary: + mat0 = unitary_hermitian_random(num_qubits - 1).numpy() / 2 + else: + mat0 = hermitian_random(num_qubits - 1).numpy() id_mat = np.eye(2 ** (num_qubits - 1)) mat1 = 1j * scipy.linalg.sqrtm(id_mat - np.matmul(mat0, mat0)) mat = np.block([[mat0, mat1], [mat1, mat0]]) - return paddle.to_tensor(mat, dtype=dtype) - - -def unitary_random(num_qubits: int) -> paddle.Tensor: - r"""randomly generate a :math:`2^n \times 2^n` unitary - - Args: - num_qubits: :math:`\log_{2}(dimension)` - - Returns: - a :math:`2^n \times 2^n` unitary matrix - - """ - unitary = scipy.stats.unitary_group(2 ** num_qubits) - return paddle.to_tensor(unitary) + return paddle.to_tensor(mat, dtype=paddle_quantum.get_dtype()) def haar_orthogonal(num_qubits: int) -> paddle.Tensor: @@ -198,7 +225,7 @@ def haar_orthogonal(num_qubits: int) -> paddle.Tensor: num_qubits: number of qubits Returns: - a :math:`2^n \times 2^n` orthogonal matrix + a :math:`2^num_qubits \times 2^num_qubits` orthogonal matrix """ # Matrix dimension @@ -210,7 +237,7 @@ def haar_orthogonal(num_qubits: int) -> paddle.Tensor: # Step 3: make the decomposition unique mat_lambda = np.diag(mat_r) / abs(np.diag(mat_r)) mat_u = mat_q @ np.diag(mat_lambda) - return paddle.to_tensor(mat_u) + return paddle.to_tensor(mat_u, dtype=paddle_quantum.get_dtype()) def haar_unitary(num_qubits: int) -> paddle.Tensor: @@ -220,7 +247,7 @@ def haar_unitary(num_qubits: int) -> paddle.Tensor: num_qubits: number of qubits Returns: - a :math:`2^n \times 2^n` unitary + a :math:`2^num_qubits \times 2^num_qubits` unitary """ # Matrix dimension @@ -232,7 +259,7 @@ def haar_unitary(num_qubits: int) -> paddle.Tensor: # Step 3: make the decomposition unique mat_lambda = np.diag(mat_r) / np.abs(np.diag(mat_r)) mat_u = mat_q @ np.diag(mat_lambda) - return paddle.to_tensor(mat_u) + return paddle.to_tensor(mat_u, dtype=paddle_quantum.get_dtype()) def haar_state_vector(num_qubits: int, is_real: Optional[bool] = False) -> paddle.Tensor: @@ -243,7 +270,7 @@ def haar_state_vector(num_qubits: int, is_real: Optional[bool] = False) -> paddl is_real: whether the vector is real, default to be False Returns: - a :math:`2^n \times 1` state vector + a :math:`2^num_qubits \times 1` state vector """ # Vector dimension @@ -259,7 +286,7 @@ def haar_state_vector(num_qubits: int, is_real: Optional[bool] = False) -> paddl # Perform u onto |0>, i.e., the first column of u phi = unitary[:, 0] - return paddle.to_tensor(phi) + return paddle.to_tensor(phi, dtype=paddle_quantum.get_dtype()) def haar_density_operator(num_qubits: int, rank: Optional[int] = None, is_real: Optional[bool] = False) -> paddle.Tensor: @@ -271,7 +298,7 @@ def haar_density_operator(num_qubits: int, rank: Optional[int] = None, is_real: is_real: whether the density matrix is real, default to be False Returns: - a :math:`2^n \times 2^n` density matrix + a :math:`2^num_qubits \times 2^num_qubits` density matrix """ dim = 2 ** num_qubits rank = rank if rank is not None else dim @@ -283,18 +310,40 @@ def haar_density_operator(num_qubits: int, rank: Optional[int] = None, is_real: ginibre_matrix = np.random.randn(dim, rank) + 1j * np.random.randn(dim, rank) rho = ginibre_matrix @ ginibre_matrix.conj().T rho = rho / np.trace(rho) - return paddle.to_tensor(rho / np.trace(rho)) + return paddle.to_tensor(rho / np.trace(rho), dtype=paddle_quantum.get_dtype()) -def NKron(matrix_A: np.ndarray, matrix_B: np.ndarray, *args: np.ndarray) -> np.ndarray: +def NKron( + matrix_A: Union[paddle.Tensor, np.ndarray], + matrix_B: Union[paddle.Tensor, np.ndarray], + *args: Union[paddle.Tensor, np.ndarray] + ) -> Union[paddle.Tensor, np.ndarray]: r""" calculate Kronecker product of at least two matrices Args: - matrix_A: matrix - matrix_B: matrix - *args: other matrices + matrix_A: matrix, as paddle.Tensor or numpy.ndarray + matrix_B: matrix, as paddle.Tensor or numpy.ndarray + *args: other matrices, as paddle.Tensor or numpy.ndarray Returns: - Kronecker product of matrices + Kronecker product of matrices, determined by input type of matrix_A + + .. code-block:: python + + from paddle_quantum.state import density_op_random + from paddle_quantum.utils import NKron + A = density_op_random(2) + B = density_op_random(2) + C = density_op_random(2) + result = NKron(A, B, C) + + Note: + result should be A \otimes B \otimes C """ - return reduce(lambda result, index: np.kron(result, index), args, np.kron(matrix_A, matrix_B), ) \ No newline at end of file + is_ndarray = False + if isinstance(matrix_A, np.ndarray): + is_ndarray = True + if not is_ndarray: + return reduce(lambda result, index: paddle.kron(result, index), args, paddle.kron(matrix_A, matrix_B), ) + else: + return reduce(lambda result, index: np.kron(result, index), args, np.kron(matrix_A, matrix_B), ) diff --git a/paddle_quantum/locc/locc_ansatz.py b/paddle_quantum/locc/locc_ansatz.py index 78904e2..5a76b60 100644 --- a/paddle_quantum/locc/locc_ansatz.py +++ b/paddle_quantum/locc/locc_ansatz.py @@ -18,6 +18,7 @@ """ import collections +from matplotlib import docstring import paddle import paddle_quantum from ..gate import H, S, T, X, Y, Z, P, RX, RY, RZ, U3 @@ -40,26 +41,29 @@ class LoccAnsatz(paddle_quantum.ansatz.Circuit): Args: party: The owner of this circuit. """ + def __init__(self, party: LoccParty): super().__init__() self.party = party self.num_local_qubits = len(self.party) def __transform_qubits_idx(self, oper): - if hasattr(oper, 'qubits_idx'): + if hasattr(oper, "qubits_idx"): if isinstance(oper.qubits_idx[0], Iterable): oper.qubits_idx = [ - [self.party[qubit_idx] for qubit_idx in qubits_idx] for qubits_idx in oper.qubits_idx + [self.party[qubit_idx] for qubit_idx in qubits_idx] + for qubits_idx in oper.qubits_idx ] else: - oper.qubits_idx = [self.party[qubit_idx] for qubit_idx in oper.qubits_idx] + oper.qubits_idx = [ + self.party[qubit_idx] for qubit_idx in oper.qubits_idx + ] + + def append(self, operator: Union[Iterable, paddle_quantum.Operator]) -> None: + r"""Append an operator. - def append(self, operator: Union[Iterable, paddle_quantum.Operator]): - r""" Append an operator. - Args: operator: operator with a name or just an operator. - """ if isinstance(operator, Iterable): name, oper = operator @@ -70,12 +74,11 @@ def append(self, operator: Union[Iterable, paddle_quantum.Operator]): idx = len(self._sub_layers) self.add_sublayer(str(idx), operator) - def extend(self, operators: List[Operator]): - r""" Append a list of operators. - + def extend(self, operators: List[Operator]) -> None: + r"""Append a list of operators. + Args: - operator: List of operators. - + operators: List of operators. """ if len(operators) > 0 and isinstance(operators[0], (list, tuple)): for name, oper in operators: @@ -87,13 +90,12 @@ def extend(self, operators: List[Operator]): self.__transform_qubits_idx(oper) self.add_sublayer(str(idx + origin_len), oper) - def insert(self, index: int, operator: Operator): - r""" Insert an operator at index ``index``. - + def insert(self, index: int, operator: Operator) -> None: + r"""Insert an operator at index ``index``. + Args: index: Index to be inserted. operator: An operator. - """ new_operators = collections.OrderedDict() for idx, name in enumerate(self._sub_layers): @@ -113,7 +115,7 @@ def insert(self, index: int, operator: Operator): new_operators[name] = self._sub_layers[name] self._sub_layers = new_operators - def pop(self, operator: Operator): + def pop(self, operator: Operator) -> None: r"""Remove the matched operator. Args: @@ -133,21 +135,23 @@ def pop(self, operator: Operator): self._sub_layers = new_operators def forward(self, state: LoccState) -> LoccState: - r""" Forward the input. - + r"""Forward the input. + Args: state: Initial state. - + Returns: Output state. - - """ + """ for layer in self._sub_layers.values(): state = layer(state) return state def h( - self, qubits_idx: Union[Iterable, int, str] = 'full', num_qubits: Optional[int] = None, depth: int = 1 + self, + qubits_idx: Union[Iterable, int, str] = "full", + num_qubits: Optional[int] = None, + depth: int = 1, ) -> None: r"""Add single-qubit Hadamard gates. @@ -162,7 +166,10 @@ def h( self.append(oper) def s( - self, qubits_idx: Union[Iterable, int, str] = 'full', num_qubits: Optional[int] = None, depth: int = 1 + self, + qubits_idx: Union[Iterable, int, str] = "full", + num_qubits: Optional[int] = None, + depth: int = 1, ) -> None: r"""Add single-qubit S gates. @@ -177,7 +184,10 @@ def s( self.append(oper) def t( - self, qubits_idx: Union[Iterable, int, str] = 'full', num_qubits: Optional[int] = None, depth: int = 1 + self, + qubits_idx: Union[Iterable, int, str] = "full", + num_qubits: Optional[int] = None, + depth: int = 1, ) -> None: r"""Add single-qubit T gates. @@ -192,7 +202,10 @@ def t( self.append(oper) def x( - self, qubits_idx: Union[Iterable, int, str] = 'full', num_qubits: Optional[int] = None, depth: int = 1 + self, + qubits_idx: Union[Iterable, int, str] = "full", + num_qubits: Optional[int] = None, + depth: int = 1, ) -> None: r"""Add single-qubit X gates. @@ -207,7 +220,10 @@ def x( self.append(oper) def y( - self, qubits_idx: Union[Iterable, int, str] = 'full', num_qubits: Optional[int] = None, depth: int = 1 + self, + qubits_idx: Union[Iterable, int, str] = "full", + num_qubits: Optional[int] = None, + depth: int = 1, ) -> None: r"""Add single-qubit Y gates. @@ -222,7 +238,10 @@ def y( self.append(oper) def z( - self, qubits_idx: Union[Iterable, int, str] = 'full', num_qubits: Optional[int] = None, depth: int = 1 + self, + qubits_idx: Union[Iterable, int, str] = "full", + num_qubits: Optional[int] = None, + depth: int = 1, ) -> None: r"""Add single-qubit Z gates. @@ -237,8 +256,12 @@ def z( self.append(oper) def p( - self, qubits_idx: Union[Iterable, int, str] = 'full', num_qubits: Optional[int] = None, depth: int = 1, - param: Union[paddle.Tensor, float] = None, param_sharing: bool = False + self, + qubits_idx: Union[Iterable, int, str] = "full", + num_qubits: Optional[int] = None, + depth: int = 1, + param: Union[paddle.Tensor, float] = None, + param_sharing: bool = False, ) -> None: r"""Add single-qubit P gates. @@ -255,8 +278,12 @@ def p( self.append(oper) def rx( - self, qubits_idx: Union[Iterable, int, str] = 'full', num_qubits: Optional[int] = None, depth: int = 1, - param: Union[paddle.Tensor, float] = None, param_sharing: bool = False + self, + qubits_idx: Union[Iterable, int, str] = "full", + num_qubits: Optional[int] = None, + depth: int = 1, + param: Union[paddle.Tensor, float] = None, + param_sharing: bool = False, ) -> None: r"""Add single-qubit rotation gates about the x-axis. @@ -273,8 +300,12 @@ def rx( self.append(oper) def ry( - self, qubits_idx: Union[Iterable, int, str] = 'full', num_qubits: Optional[int] = None, depth: int = 1, - param: Union[paddle.Tensor, float] = None, param_sharing: bool = False + self, + qubits_idx: Union[Iterable, int, str] = "full", + num_qubits: Optional[int] = None, + depth: int = 1, + param: Union[paddle.Tensor, float] = None, + param_sharing: bool = False, ) -> None: r"""Add single-qubit rotation gates about the y-axis. @@ -291,8 +322,12 @@ def ry( self.append(oper) def rz( - self, qubits_idx: Union[Iterable, int, str] = 'full', num_qubits: Optional[int] = None, depth: int = 1, - param: Union[paddle.Tensor, float] = None, param_sharing: bool = False + self, + qubits_idx: Union[Iterable, int, str] = "full", + num_qubits: Optional[int] = None, + depth: int = 1, + param: Union[paddle.Tensor, float] = None, + param_sharing: bool = False, ) -> None: r"""Add single-qubit rotation gates about the z-axis. @@ -309,8 +344,12 @@ def rz( self.append(oper) def u3( - self, qubits_idx: Union[Iterable, int, str] = 'full', num_qubits: int = None, depth: int = 1, - param: Union[paddle.Tensor, Iterable[float]] = None, param_sharing: bool = False + self, + qubits_idx: Union[Iterable, int, str] = "full", + num_qubits: int = None, + depth: int = 1, + param: Union[paddle.Tensor, Iterable[float]] = None, + param_sharing: bool = False, ) -> None: r"""Add single-qubit rotation gates. @@ -327,7 +366,10 @@ def u3( self.append(oper) def cnot( - self, qubits_idx: Union[Iterable, str] = 'cycle', num_qubits: int = None, depth: int = 1 + self, + qubits_idx: Union[Iterable, str] = "cycle", + num_qubits: int = None, + depth: int = 1, ) -> None: r"""Add CNOT gates. @@ -342,7 +384,10 @@ def cnot( self.append(oper) def cx( - self, qubits_idx: Union[Iterable, str] = 'cycle', num_qubits: int = None, depth: int = 1 + self, + qubits_idx: Union[Iterable, str] = "cycle", + num_qubits: int = None, + depth: int = 1, ) -> None: r"""Same as cnot. @@ -357,7 +402,10 @@ def cx( self.append(oper) def cy( - self, qubits_idx: Union[Iterable, str] = 'cycle', num_qubits: int = None, depth: int = 1 + self, + qubits_idx: Union[Iterable, str] = "cycle", + num_qubits: int = None, + depth: int = 1, ) -> None: r"""Add controlled Y gates. @@ -372,7 +420,10 @@ def cy( self.append(oper) def cz( - self, qubits_idx: Union[Iterable, str] = 'cycle', num_qubits: int = None, depth: int = 1 + self, + qubits_idx: Union[Iterable, str] = "cycle", + num_qubits: int = None, + depth: int = 1, ) -> None: r"""Add controlled Z gates. @@ -387,7 +438,10 @@ def cz( self.append(oper) def swap( - self, qubits_idx: Union[Iterable, str] = 'cycle', num_qubits: int = None, depth: int = 1 + self, + qubits_idx: Union[Iterable, str] = "cycle", + num_qubits: int = None, + depth: int = 1, ) -> None: r"""Add SWAP gates. @@ -402,8 +456,12 @@ def swap( self.append(oper) def cp( - self, qubits_idx: Union[Iterable, str] = 'cycle', num_qubits: int = None, depth: int = 1, - param: Union[paddle.Tensor, float] = None, param_sharing: bool = False + self, + qubits_idx: Union[Iterable, str] = "cycle", + num_qubits: int = None, + depth: int = 1, + param: Union[paddle.Tensor, float] = None, + param_sharing: bool = False, ) -> None: r"""Add controlled P gates. @@ -420,8 +478,12 @@ def cp( self.append(oper) def crx( - self, qubits_idx: Union[Iterable, str] = 'cycle', num_qubits: int = None, depth: int = 1, - param: Union[paddle.Tensor, float] = None, param_sharing: bool = False + self, + qubits_idx: Union[Iterable, str] = "cycle", + num_qubits: int = None, + depth: int = 1, + param: Union[paddle.Tensor, float] = None, + param_sharing: bool = False, ) -> None: r"""Add controlled rotation gates about the x-axis. @@ -438,8 +500,12 @@ def crx( self.append(oper) def cry( - self, qubits_idx: Union[Iterable, str] = 'cycle', num_qubits: int = None, depth: int = 1, - param: Union[paddle.Tensor, float] = None, param_sharing: bool = False + self, + qubits_idx: Union[Iterable, str] = "cycle", + num_qubits: int = None, + depth: int = 1, + param: Union[paddle.Tensor, float] = None, + param_sharing: bool = False, ) -> None: r"""Add controlled rotation gates about the y-axis. @@ -456,8 +522,12 @@ def cry( self.append(oper) def crz( - self, qubits_idx: Union[Iterable, str] = 'cycle', num_qubits: int = None, depth: int = 1, - param: Union[paddle.Tensor, float] = None, param_sharing: bool = False + self, + qubits_idx: Union[Iterable, str] = "cycle", + num_qubits: int = None, + depth: int = 1, + param: Union[paddle.Tensor, float] = None, + param_sharing: bool = False, ) -> None: r"""Add controlled rotation gates about the z-axis. @@ -474,8 +544,12 @@ def crz( self.append(oper) def cu( - self, qubits_idx: Union[Iterable, str] = 'cycle', num_qubits: int = None, depth: int = 1, - param: Union[paddle.Tensor, float] = None, param_sharing: bool = False + self, + qubits_idx: Union[Iterable, str] = "cycle", + num_qubits: int = None, + depth: int = 1, + param: Union[paddle.Tensor, float] = None, + param_sharing: bool = False, ) -> None: r"""Add controlled single-qubit rotation gates. @@ -492,8 +566,12 @@ def cu( self.append(oper) def rxx( - self, qubits_idx: Union[Iterable, str] = 'cycle', num_qubits: int = None, depth: int = 1, - param: Union[paddle.Tensor, float] = None, param_sharing: bool = False + self, + qubits_idx: Union[Iterable, str] = "cycle", + num_qubits: int = None, + depth: int = 1, + param: Union[paddle.Tensor, float] = None, + param_sharing: bool = False, ) -> None: r"""Add RXX gates. @@ -510,8 +588,12 @@ def rxx( self.append(oper) def ryy( - self, qubits_idx: Union[Iterable, str] = 'cycle', num_qubits: int = None, depth: int = 1, - param: Union[paddle.Tensor, float] = None, param_sharing: bool = False + self, + qubits_idx: Union[Iterable, str] = "cycle", + num_qubits: int = None, + depth: int = 1, + param: Union[paddle.Tensor, float] = None, + param_sharing: bool = False, ) -> None: r"""Add RYY gates. @@ -528,8 +610,12 @@ def ryy( self.append(oper) def rzz( - self, qubits_idx: Union[Iterable, str] = 'cycle', num_qubits: int = None, depth: int = 1, - param: Union[paddle.Tensor, float] = None, param_sharing: bool = False + self, + qubits_idx: Union[Iterable, str] = "cycle", + num_qubits: int = None, + depth: int = 1, + param: Union[paddle.Tensor, float] = None, + param_sharing: bool = False, ) -> None: r"""Add RZZ gates. @@ -546,7 +632,10 @@ def rzz( self.append(oper) def ms( - self, qubits_idx: Union[Iterable, str] = 'cycle', num_qubits: int = None, depth: int = 1 + self, + qubits_idx: Union[Iterable, str] = "cycle", + num_qubits: int = None, + depth: int = 1, ) -> None: r"""Add Mølmer-Sørensen (MS) gates. @@ -561,7 +650,10 @@ def ms( self.append(oper) def cswap( - self, qubits_idx: Union[Iterable, str] = 'cycle', num_qubits: int = None, depth: int = 1 + self, + qubits_idx: Union[Iterable, str] = "cycle", + num_qubits: int = None, + depth: int = 1, ) -> None: r"""Add CSWAP (Fredkin) gates. @@ -576,7 +668,10 @@ def cswap( self.append(oper) def ccx( - self, qubits_idx: Union[Iterable, str] = 'cycle', num_qubits: int = None, depth: int = 1 + self, + qubits_idx: Union[Iterable, str] = "cycle", + num_qubits: int = None, + depth: int = 1, ) -> None: r"""Add CCX gates. @@ -595,8 +690,12 @@ def ccx( self.append(oper) def universal_two_qubits( - self, qubits_idx: Union[Iterable, str] = 'cycle', num_qubits: int = None, depth: int = 1, - param: Union[paddle.Tensor, float] = None, param_sharing: bool = False + self, + qubits_idx: Union[Iterable, str] = "cycle", + num_qubits: int = None, + depth: int = 1, + param: Union[paddle.Tensor, float] = None, + param_sharing: bool = False, ) -> None: r"""Add universal two-qubit gates. One of such a gate requires 15 parameters. @@ -613,8 +712,12 @@ def universal_two_qubits( self.append(oper) def universal_three_qubits( - self, qubits_idx: Union[Iterable, str] = 'cycle', num_qubits: int = None, depth: int = 1, - param: Union[paddle.Tensor, float] = None, param_sharing: bool = False + self, + qubits_idx: Union[Iterable, str] = "cycle", + num_qubits: int = None, + depth: int = 1, + param: Union[paddle.Tensor, float] = None, + param_sharing: bool = False, ) -> None: r"""Add universal three-qubit gates. One of such a gate requires 81 parameters. @@ -631,8 +734,11 @@ def universal_three_qubits( self.append(oper) def oracle( - self, oracle: paddle.Tensor, qubits_idx: Union[Iterable[Iterable[int]], Iterable[int], int], - num_qubits: int = None, depth: int = 1 + self, + oracle: paddle.Tensor, + qubits_idx: Union[Iterable[Iterable[int]], Iterable[int], int], + num_qubits: int = None, + depth: int = 1, ) -> None: r"""Add an oracle gate. @@ -648,10 +754,12 @@ def oracle( self.append(oper) def control_oracle( - self, oracle: paddle.Tensor, - # num_control_qubits: int, controlled_value: 'str', - qubits_idx: Union[Iterable[Iterable[int]], Iterable[int]], - num_qubits: int = None, depth: int = 1 + self, + oracle: paddle.Tensor, + # num_control_qubits: int, controlled_value: 'str', + qubits_idx: Union[Iterable[Iterable[int]], Iterable[int]], + num_qubits: int = None, + depth: int = 1, ) -> None: """Add a controlled oracle gate. diff --git a/paddle_quantum/locc/locc_net.py b/paddle_quantum/locc/locc_net.py index 364a5c7..5ed26c8 100644 --- a/paddle_quantum/locc/locc_net.py +++ b/paddle_quantum/locc/locc_net.py @@ -29,8 +29,8 @@ class LoccNet(paddle.nn.Layer): - r"""Used to design LOCC protocols and perform training or verification. - """ + r"""Used to design LOCC protocols and perform training or verification.""" + def __init__(self): super().__init__() self.parties_by_number = [] @@ -54,15 +54,14 @@ def set_init_state(self, state: paddle_quantum.State, qubits_idx: Iterable): self.init_status.num_qubits += state.num_qubits for idx, (party_id, qubit_id) in enumerate(qubits_idx): if isinstance(party_id, str): - self.parties_by_name[party_id][qubit_id] = (temp_len + idx) + self.parties_by_name[party_id][qubit_id] = temp_len + idx elif isinstance(party_id, int): - self.parties_by_number[party_id][qubit_id] = (temp_len + idx) + self.parties_by_number[party_id][qubit_id] = temp_len + idx else: raise ValueError - def __partial_trace(self, rho_AB, dim1, dim2, A_or_B): - r"""TODO To be checked. - """ + def __partial_trace(self, rho_AB: paddle.Tensor, dim1: int, dim2: int, A_or_B: int): + r"""TODO To be checked.""" if A_or_B == 2: dim1, dim2 = dim2, dim1 idty_np = np.identity(dim2) @@ -82,8 +81,8 @@ def __partial_trace(self, rho_AB, dim1, dim2, A_or_B): res, paddle.matmul( paddle.matmul(row_tmp, rho_AB), - paddle.transpose(row_tmp_conj, perm=[1, 0]) - ) + paddle.transpose(row_tmp_conj, perm=[1, 0]), + ), ) if A_or_B == 2: row_tmp = paddle.kron(idty_B, bra_j) @@ -93,13 +92,16 @@ def __partial_trace(self, rho_AB, dim1, dim2, A_or_B): res, paddle.matmul( paddle.matmul(row_tmp, rho_AB), - paddle.transpose(row_tmp_conj, perm=[1, 0]) - ) + paddle.transpose(row_tmp_conj, perm=[1, 0]), + ), ) return res def partial_state( - self, state: Union[List[LoccState], LoccState], qubits_idx: Iterable, is_desired: bool = True + self, + state: Union[List[LoccState], LoccState], + qubits_idx: Iterable, + is_desired: bool = True, ) -> Union[List[LoccState], LoccState]: r"""Get the quantum state of the qubits of interest. @@ -149,17 +151,17 @@ def partial_state( swapped[target_seq[next_idx]] = True swap_list.append((next_idx, target_seq[next_idx])) next_idx = target_seq[next_idx] - - cir = paddle_quantum.ansatz.Circuit() + + cir = paddle_quantum.ansatz.Circuit(n) for a, b in swap_list: cir.swap([a, b]) if isinstance(state, LoccState): state = cir(state) if is_desired: - state_data = self.__partial_trace(state.data, 2 ** m, 2 ** (n - m), 2) + state_data = self.__partial_trace(state.data, 2**m, 2 ** (n - m), 2) else: - state_data = self.__partial_trace(state.data, 2 ** m, 2 ** (n - m), 1) + state_data = self.__partial_trace(state.data, 2**m, 2 ** (n - m), 1) new_state = state.clone() new_state.data = state_data new_state.num_qubits = int(math.log2(state_data.shape[-1])) @@ -168,9 +170,13 @@ def partial_state( for each_state in state: each_state = cir(each_state) if is_desired: - state_data = self.__partial_trace(each_state.data, 2 ** m, 2 ** (n - m), 2) + state_data = self.__partial_trace( + each_state.data, 2**m, 2 ** (n - m), 2 + ) else: - state_data = self.__partial_trace(each_state.data, 2 ** m, 2 ** (n - m), 1) + state_data = self.__partial_trace( + each_state.data, 2**m, 2 ** (n - m), 1 + ) _state = each_state.clone() _state.data = state_data _state.num_qubits = int(math.log2(state_data.shape[-1])) @@ -182,7 +188,10 @@ def partial_state( return new_state def reset_state( - self, status: Union[List[LoccState], LoccState], state: paddle_quantum.State, which_qubits: Iterable + self, + status: Union[List[LoccState], LoccState], + state: paddle_quantum.State, + which_qubits: Iterable, ) -> Union[List[LoccState], LoccState]: r"""Reset the quantum state of the qubits of interest. @@ -234,18 +243,18 @@ def reset_state( swap_list.append((next_idx, target_seq[next_idx])) next_idx = target_seq[next_idx] - cir0 = paddle_quantum.ansatz.Circuit() + cir0 = paddle_quantum.ansatz.Circuit(n) for a, b in swap_list: cir0.swap([a, b]) - cir1 = paddle_quantum.ansatz.Circuit() + cir1 = paddle_quantum.ansatz.Circuit(n) swap_list.reverse() for a, b in swap_list: cir1.swap([a, b]) if isinstance(status, LoccState): _state = cir0(status) - state_data = self.__partial_trace(_state.data, 2 ** m, 2 ** (n - m), 1) + state_data = self.__partial_trace(_state.data, 2**m, 2 ** (n - m), 1) state_data = paddle.kron(state.data, state_data) _state = _state.clone() _state.data = state_data @@ -255,7 +264,7 @@ def reset_state( new_status = [] for each_status in status: _state = cir0(each_status) - state_data = self.__partial_trace(_state.data, 2 ** m, 2 ** (n - m), 1) + state_data = self.__partial_trace(_state.data, 2**m, 2 ** (n - m), 1) state_data = paddle.kron(state.data, state_data) _state = _state.clone() _state.data = state_data @@ -268,7 +277,9 @@ def reset_state( return new_status - def add_new_party(self, qubits_number: int, party_name: Optional[str] = None) -> Union[int, str]: + def add_new_party( + self, qubits_number: int, party_name: Optional[str] = None + ) -> Union[int, str]: r"""Add a new LOCC party. Args: @@ -320,30 +331,41 @@ def create_ansatz(self, party_id: Union[int, str]) -> LoccAnsatz: else: raise ValueError - def __measure_parameterized(self, state_data, which_qubits, result_desired, theta): - r"""TODO 进行参数化的测量。 + def __measure_parameterized( + self, + state_data: paddle.Tensor, + which_qubits: Iterable, + result_desired: str, + theta: paddle.Tensor, + ): + r"""TODO Do parameterized measurement。 Args: - state_data (Tensor): 输入的量子态 - which_qubits (list): 测量作用的量子比特编号 - result_desired (str): 期望得到的测量结果 - theta (Tensor): 测量运算的参数 + state_data (Tensor): The input quantum state + which_qubits (list): The indices of qubits to be measured + result_desired (str): The desired result + theta (Tensor): The parameters of measurement Returns: - Tensor: 测量坍塌后的量子态 - Tensor: 测量坍塌得到的概率 - str: 测量得到的结果 + Tensor: The quantum state collapsed after measurement + Tensor: The probability of collapsing to the resulting state + str: The result of measurement """ n = self.get_num_qubits() - assert len(which_qubits) == len(result_desired), \ - "the length of qubits wanted to be measured and the result desired should be same" + assert len(which_qubits) == len( + result_desired + ), "the length of qubits wanted to be measured and the result desired should be same" op_list = [paddle.to_tensor(np.eye(2), dtype=paddle_quantum.get_dtype())] * n for idx in range(0, len(which_qubits)): i = which_qubits[idx] ele = result_desired[idx] if int(ele) == 0: - basis0 = paddle.to_tensor(np.array([[1, 0], [0, 0]]), dtype=paddle_quantum.get_dtype()) - basis1 = paddle.to_tensor(np.array([[0, 0], [0, 1]]), dtype=paddle_quantum.get_dtype()) + basis0 = paddle.to_tensor( + np.array([[1, 0], [0, 0]]), dtype=paddle_quantum.get_dtype() + ) + basis1 = paddle.to_tensor( + np.array([[0, 0], [0, 1]]), dtype=paddle_quantum.get_dtype() + ) rho0 = paddle.multiply(basis0, paddle.cos(theta[idx])) rho1 = paddle.multiply(basis1, paddle.sin(theta[idx])) rho = paddle.add(rho0, rho1) @@ -351,8 +373,12 @@ def __measure_parameterized(self, state_data, which_qubits, result_desired, thet elif int(ele) == 1: # rho = diag(concat([cos(theta[idx]), sin(theta[idx])])) # rho = paddle.to_tensor(rho, zeros((2, 2), dtype="float64")) - basis0 = paddle.to_tensor(np.array([[1, 0], [0, 0]]), dtype=paddle_quantum.get_dtype()) - basis1 = paddle.to_tensor(np.array([[0, 0], [0, 1]]), dtype=paddle_quantum.get_dtype()) + basis0 = paddle.to_tensor( + np.array([[1, 0], [0, 0]]), dtype=paddle_quantum.get_dtype() + ) + basis1 = paddle.to_tensor( + np.array([[0, 0], [0, 1]]), dtype=paddle_quantum.get_dtype() + ) rho0 = paddle.multiply(basis0, paddle.sin(theta[idx])) rho1 = paddle.multiply(basis1, paddle.cos(theta[idx])) rho = paddle.add(rho0, rho1) @@ -366,31 +392,40 @@ def __measure_parameterized(self, state_data, which_qubits, result_desired, thet measure_operator = paddle.kron(measure_operator, op_list[idx]) state_measured = paddle.matmul( paddle.matmul(measure_operator, state_data), - paddle_quantum.linalg.dagger(measure_operator) + paddle_quantum.linalg.dagger(measure_operator), + ) + prob = paddle.real( + paddle.trace( + paddle.matmul( + paddle.matmul( + paddle_quantum.linalg.dagger(measure_operator), measure_operator + ), + state_data, + ) + ) ) - prob = paddle.real(paddle.trace(paddle.matmul( - paddle.matmul(paddle_quantum.linalg.dagger(measure_operator), measure_operator), - state_data - ))) state_measured = paddle.divide(state_measured, prob) return state_measured, prob, result_desired - def __measure_parameterless(self, state, which_qubits, result_desired): - r"""TODO 进行 01 测量。 + def __measure_parameterless( + self, state: paddle.Tensor, which_qubits: Iterable, result_desired: str + ): + r"""TODO Do 01 measurement。 Args: - state (Tensor): 输入的量子态 - which_qubits (list): 测量作用的量子比特编号 - result_desired (str): 期望得到的测量结果 + state (Tensor): The input quantum state + which_qubits (list): The indices of qubits to be measured + result_desired (str): The desired result Returns: - Tensor: 测量坍塌后的量子态 - Tensor: 测量坍塌得到的概率 - str: 测量得到的结果 + Tensor: The quantum state after measurement + Tensor: The probability of collapsing to the resulting state + str: The result of measurement """ n = self.get_num_qubits() - assert len(which_qubits) == len(result_desired), \ - "the length of qubits wanted to be measured and the result desired should be same" + assert len(which_qubits) == len( + result_desired + ), "the length of qubits wanted to be measured and the result desired should be same" op_list = [np.eye(2)] * n for i, ele in zip(which_qubits, result_desired): k = int(ele) @@ -398,23 +433,36 @@ def __measure_parameterless(self, state, which_qubits, result_desired): rho[int(k), int(k)] = 1 op_list[i] = rho if n > 1: - measure_operator = paddle.to_tensor(functools.reduce(np.kron, op_list), dtype=paddle_quantum.get_dtype()) + measure_operator = paddle.to_tensor( + functools.reduce(np.kron, op_list), dtype=paddle_quantum.get_dtype() + ) else: - measure_operator = paddle.to_tensor(op_list[0], dtype=paddle_quantum.get_dtype()) + measure_operator = paddle.to_tensor( + op_list[0], dtype=paddle_quantum.get_dtype() + ) state_measured = paddle.matmul( paddle.matmul(measure_operator, state), - paddle_quantum.linalg.dagger(measure_operator) + paddle_quantum.linalg.dagger(measure_operator), + ) + prob = paddle.real( + paddle.trace( + paddle.matmul( + paddle.matmul( + paddle_quantum.linalg.dagger(measure_operator), measure_operator + ), + state, + ) + ) ) - prob = paddle.real(paddle.trace(paddle.matmul( - paddle.matmul(paddle_quantum.linalg.dagger(measure_operator), measure_operator), - state - ))) state_measured = paddle.divide(state_measured, prob) return state_measured, prob, result_desired def measure( - self, status: Union[List[LoccState], LoccState], which_qubits: Iterable, - results_desired: Union[List[str], str], theta: paddle.Tensor = None + self, + status: Union[List[LoccState], LoccState], + which_qubits: Iterable, + results_desired: Union[List[str], str], + theta: paddle.Tensor = None, ) -> Union[List[LoccState], LoccState]: r"""Perform 0-1 measurement or parameterized measurement on an LOCC state. @@ -455,9 +503,13 @@ def measure( new_status = [] for result_desired in results_desired: if theta is None: - result_measured = self.__measure_parameterless(status.data, qubits_list, result_desired) + result_measured = self.__measure_parameterless( + status.data, qubits_list, result_desired + ) else: - result_measured = self.__measure_parameterized(status.data, qubits_list, result_desired, theta) + result_measured = self.__measure_parameterized( + status.data, qubits_list, result_desired, theta + ) state_data, prob, res = result_measured _state = status.clone() _state.data = state_data @@ -474,9 +526,13 @@ def measure( prior_prob = each_status.prob for result_desired in results_desired: if theta is None: - result_measured = self.__measure_parameterless(each_status.state, qubits_list, result_desired) + result_measured = self.__measure_parameterless( + each_status.state, qubits_list, result_desired + ) else: - result_measured = self.__measure_parameterized(each_status.state, qubits_list, result_desired, theta) + result_measured = self.__measure_parameterized( + each_status.state, qubits_list, result_desired, theta + ) state_data, prob, res = result_measured _state = each_status.clone() _state.data = state_data @@ -493,7 +549,7 @@ def get_num_qubits(self) -> int: r"""Get the number of the qubits in this LOCCNet. Returns: - Number of qubits in LOCCNet. + The number of qubits in LOCCNet. """ num_qubits = 0 for party in self.parties_by_number: diff --git a/paddle_quantum/locc/locc_state.py b/paddle_quantum/locc/locc_state.py index 4e84c3b..e4a2723 100644 --- a/paddle_quantum/locc/locc_state.py +++ b/paddle_quantum/locc/locc_state.py @@ -40,14 +40,22 @@ class LoccState(paddle_quantum.State): backend: Backend of Paddle Quantum. Defaults to ``None``. dtype: Type of data. Defaults to ``None``. """ + def __init__( - self, data: paddle.Tensor = None, prob: paddle.Tensor = None, measured_result: str = None, num_qubits: Optional[int] = None, - backend: Optional[paddle_quantum.Backend] = None, dtype: Optional[str] = None + self, + data: paddle.Tensor = None, + prob: paddle.Tensor = None, + measured_result: str = None, + num_qubits: Optional[int] = None, + backend: Optional[paddle_quantum.Backend] = None, + dtype: Optional[str] = None, ): if data is None and prob is None and measured_result is None: self.data = paddle.to_tensor([1], dtype=paddle_quantum.get_dtype()) - self.prob = paddle.to_tensor([1], dtype=_get_float_dtype(paddle_quantum.get_dtype())) - self.measured_result = '' + self.prob = paddle.to_tensor( + [1], dtype=_get_float_dtype(paddle_quantum.get_dtype()) + ) + self.measured_result = "" self.num_qubits = 0 else: self.data = data @@ -62,13 +70,20 @@ def __init__( self.backend = backend self.dtype = dtype if dtype is not None else paddle_quantum.get_dtype() - def clone(self) -> 'LoccState': + def clone(self) -> "LoccState": r"""Create a copy of the current object. Returns: A copy of the current object. """ - return LoccState(self.data, self.prob, self.measured_result, self.num_qubits, self.backend, self.dtype) + return LoccState( + self.data, + self.prob, + self.measured_result, + self.num_qubits, + self.backend, + self.dtype, + ) def __getitem__(self, item): if item == 0: @@ -83,13 +98,15 @@ def __repr__(self): return ( f"state: {self.data.numpy()}\n" f"prob: {self.prob.numpy()[0]}\n" - f"measured_result: {self.measured_result}") + f"measured_result: {self.measured_result}" + ) def __str__(self): return ( f"state: {self.data.numpy()}\n" f"prob: {self.prob.numpy()[0]}\n" - f"measured_result: {self.measured_result}") + f"measured_result: {self.measured_result}" + ) LoccStatus = LoccState diff --git a/paddle_quantum/loss/measure.py b/paddle_quantum/loss/measure.py index da736c2..8ae1706 100644 --- a/paddle_quantum/loss/measure.py +++ b/paddle_quantum/loss/measure.py @@ -21,6 +21,7 @@ import paddle_quantum from ..backend import quleaf from ..backend import Backend +from ..intrinsic import _get_float_dtype from typing import Optional, Union, Iterable @@ -90,8 +91,12 @@ def forward(self, state: paddle_quantum.State) -> paddle.Tensor: The expectation value. If the backend is QuLeaf, it is computed by sampling. """ if self.backend == Backend.QuLeaf: + if len(state.param_list) > 0: + param = paddle.concat(state.param_list) + else: + param = paddle.to_tensor(state.param_list) expec_val = quleaf.ExpecValOp.apply( - paddle.concat(state.param_list), + param, state, self.hamiltonian, self.shots ) return expec_val @@ -154,26 +159,32 @@ def forward( qubits_idx: The index of the qubits to be measured. Defaults to ``'full'`` which means measure all the qubits. desired_result: Specify the results of the measurement to return. Defaults to ``None`` which means return the probability of all the results. + Raises: + NotImplementedError: The backend is wrong or not supported. + NotImplementedError: The qubit index is wrong or not supported. + NotImplementedError: Currently we just support the z basis. + Returns: The probability of the measurement. """ + float_dtype = _get_float_dtype(paddle_quantum.get_dtype()) num_qubits = state.num_qubits if self.measure_basis == 'z': if state.backend == paddle_quantum.Backend.StateVector: prob_amplitude = paddle.multiply(paddle.conj(state.data), state.data).real() elif state.backend == paddle_quantum.Backend.DensityMatrix: - prob_amplitude = paddle.zeros([2 ** num_qubits]) + prob_amplitude = paddle.zeros([2 ** num_qubits], dtype=float_dtype) for idx in range(0, 2 ** num_qubits): prob_amplitude[idx] += state.data[idx, idx].real() else: - raise NotImplementedError + raise NotImplementedError("The backend is wrong or not supported.") if isinstance(qubits_idx, int): qubits_idx = [qubits_idx] if isinstance(qubits_idx, Iterable) and all((isinstance(idx, int) for idx in qubits_idx)): qubits_idx = list(qubits_idx) measured_num = len(qubits_idx) - prob_array = paddle.zeros([2 ** measured_num]) + prob_array = paddle.zeros([2 ** measured_num], dtype=float_dtype) for idx in range(0, 2 ** num_qubits): binary = bin(idx)[2:] binary = '0' * (num_qubits - len(binary)) + binary @@ -184,11 +195,14 @@ def forward( elif qubits_idx == 'full': prob_array = prob_amplitude else: - raise NotImplementedError + raise NotImplementedError("The qubit index is wrong or not supported.") + + prob_array = prob_array / paddle.sum(prob_array) # normalize calculation error if desired_result is None: return prob_array if isinstance(desired_result, str): desired_result = [desired_result] - return paddle.concat([prob_array[int(res, base=2)] for res in desired_result]) + prob_array = paddle.concat([prob_array[int(res, base=2)] for res in desired_result]) + return prob_array else: - raise NotImplementedError + raise NotImplementedError("Currently we just support the z basis.") diff --git a/paddle_quantum/operator/operator.py b/paddle_quantum/operator/operator.py index 7e500be..a730a1b 100644 --- a/paddle_quantum/operator/operator.py +++ b/paddle_quantum/operator/operator.py @@ -17,11 +17,17 @@ The source file of the class for the special quantum operator. """ -import random +import numpy as np import paddle import paddle_quantum +import warnings from ..base import Operator from typing import Union, Iterable +from ..intrinsic import _format_qubits_idx, _get_float_dtype +from ..backend import Backend +from ..backend import state_vector, density_matrix, unitary_matrix +from ..linalg import abs_norm +from ..qinfo import partial_trace_discontiguous class ResetState(Operator): @@ -58,70 +64,108 @@ class Collapse(Operator): r"""The class to compute the collapse of the quantum state. Args: + qubits_idx: list of qubits to be collapsed. Defaults to ``'full'``. + num_qubits: Total number of qubits. Defaults to ``None``. + desired_result: The desired result you want to collapse. Defaults to ``None`` meaning randomly choose one. + if_print: whether print the information about the collapsed state. Defaults to ``False``. measure_basis: The basis of the measurement. The quantum state will collapse to the corresponding eigenstate. Raises: - NotImplementedError: If the basis of measurement is not z. Other bases will be implemented soon. + NotImplementedError: If the basis of measurement is not z. Other bases will be implemented in future. + + Note: + When desired_result is `None`, Collapse does not support gradient calculation """ - def __init__(self, measure_basis: Union[Iterable[paddle.Tensor], str]): + def __init__(self, qubits_idx: Union[Iterable[int], int, str] = 'full', num_qubits: int = None, + desired_result: Union[int, str] = None, if_print: bool = False, + measure_basis: Union[Iterable[paddle.Tensor], str] = 'z'): super().__init__() self.measure_basis = [] + + # the qubit indices must be sorted + self.qubits_idx = sorted(_format_qubits_idx(qubits_idx, num_qubits)) + + self.desired_result = desired_result + self.if_print = if_print + if measure_basis == 'z' or measure_basis == 'computational_basis': - basis0 = paddle.to_tensor([[1.0, 0], [0, 0]]) - basis1 = paddle.to_tensor([[0.0, 0], [0, 1]]) - self.measure_basis.append(basis0) - self.measure_basis.append(basis1) + self.measure_basis = 'z' else: raise NotImplementedError - - def forward(self, state: paddle_quantum.State, desired_result: Union[int, str]) -> paddle_quantum.State: + + def forward(self, state: paddle_quantum.State) -> paddle_quantum.State: r"""Compute the collapse of the input state. Args: - state: The input state, which will be collapsed. - desired_result: The desired result you want to collapse. - - Raises: - NotImplementedError: Currently we just support the z basis. + state: The input state, which will be collapsed Returns: The collapsed quantum state. """ - if self.backend == paddle_quantum.Backend.StateVector: - if desired_result == 'random': - prob_list = [] - idx_list = list(range(0, len(self.measure_basis))) - for idx in idx_list: - measure_op = self.measure_basis[idx] - state = paddle.unsqueeze(state.data, axis=1) - _prob = paddle.matmul(measure_op, state.data) - prob = paddle.matmul(paddle.conj(paddle.t(_prob)), _prob).item() - prob_list.append(prob) - desired_result = random.choices(idx_list, prob_list) - measure_op = self.measure_basis[desired_result] - state = paddle.unsqueeze(state.data, axis=1) - _prob = paddle.matmul(measure_op, state.data) - prob = paddle.matmul(paddle.conj(paddle.t(_prob)), _prob) - prob = paddle.reshape(prob, [1]) - state = paddle.matmul(measure_op, state) / paddle.sqrt(prob) - measured_state = paddle_quantum.State(state, backend=self.backend) - elif self.backend == paddle_quantum.Backend.DensityMatrix: - if desired_result == 'random': - prob_list = [] - idx_list = list(range(0, len(self.measure_basis))) - for idx in idx_list: - measure_op = self.measure_basis[idx] - state = paddle.unsqueeze(state.data, axis=1) - measure_op_dagger = paddle.conj(paddle.t(measure_op)) - prob = paddle.trace(paddle.matmul(paddle.matmul(measure_op_dagger, measure_op), state)).item() - prob_list.append(prob) - desired_result = random.choices(idx_list, prob_list) - measure_op = self.measure_basis[desired_result] - state = state.data - measure_op_dagger = paddle.conj(paddle.t(measure_op)) - prob = paddle.trace(paddle.matmul(paddle.matmul(measure_op_dagger, measure_op), state)) - state = paddle.matmul(paddle.matmul(measure_op, state), measure_op_dagger) / prob - measured_state = paddle_quantum.State(state) + complex_dtype = paddle_quantum.get_dtype() + float_dtype = _get_float_dtype(complex_dtype) + + num_qubits = state.num_qubits + backend = state.backend + num_acted_qubits = len(self.qubits_idx) + desired_result = self.desired_result + desired_result = int(desired_result, 2) if isinstance(desired_result, str) else desired_result + + # when backend is unitary + if backend == Backend.UnitaryMatrix: + assert isinstance(desired_result, int), "desired_result cannot be None in unitary_matrix backend" + warnings.warn( + "the unitary_matrix of a circuit containing Collapse operator is no longer a unitary" + ) + + # determine local projector + local_projector = paddle.zeros([2 ** num_acted_qubits, 2 ** num_acted_qubits]) + local_projector[desired_result, desired_result] += 1 + local_projector = local_projector.cast(complex_dtype) + + projected_state = unitary_matrix.unitary_transformation(state.data, local_projector, self.qubits_idx, num_qubits) + return paddle_quantum.State(projected_state, backend=Backend.UnitaryMatrix) + + # retrieve prob_amplitude + if backend == Backend.StateVector: + rho = state.ket @ state.bra else: - raise NotImplementedError - return measured_state + rho = state.data + rho = partial_trace_discontiguous(rho, self.qubits_idx) + prob_amplitude = paddle.zeros([2 ** num_acted_qubits], dtype=float_dtype) + for idx in range(0, 2 ** num_acted_qubits): + prob_amplitude[idx] += rho[idx, idx].real() + prob_amplitude /= paddle.sum(prob_amplitude) + + if desired_result is None: + # randomly choose desired_result + desired_result = np.random.choice([i for i in range(2 ** num_acted_qubits)], p=prob_amplitude) + else: + # check whether the state can collapse to desired_result + assert prob_amplitude[desired_result] > 1e-20, ("it is infeasible for the state in qubits " + + f"{self.qubits_idx} to collapse to state |{desired_result_str}>") + + # retrieve the binary version of desired result + desired_result_str = bin(desired_result)[2:] + assert num_acted_qubits >= len(desired_result_str), "the desired_result is too large" + for _ in range(num_acted_qubits - len(desired_result_str)): + desired_result_str = '0' + desired_result_str + + # whether print the collapsed result + if self.if_print: + # retrieve binary representation + prob = prob_amplitude[desired_result].item() + print(f"qubits {self.qubits_idx} collapse to the state |{desired_result_str}> with probability {prob}") + + # determine projector according to the desired result + local_projector = paddle.zeros([2 ** num_acted_qubits, 2 ** num_acted_qubits]) + local_projector[desired_result, desired_result] += 1 + local_projector = local_projector.cast(complex_dtype) + + # apply the local projector and normalize it + if backend == Backend.StateVector: + projected_state = state_vector.unitary_transformation(state.data, local_projector, self.qubits_idx, num_qubits) + return paddle_quantum.State(projected_state / (abs_norm(projected_state) + 0j)) + else: + projected_state = density_matrix.unitary_transformation(state.data, local_projector, self.qubits_idx, num_qubits) + return paddle_quantum.State(projected_state / paddle.trace(projected_state)) diff --git a/paddle_quantum/qchem/loss.py b/paddle_quantum/qchem/loss.py index cf21dc7..0b9ce14 100644 --- a/paddle_quantum/qchem/loss.py +++ b/paddle_quantum/qchem/loss.py @@ -65,6 +65,9 @@ class RHFEnergyLoss(pq.Operator): basis: chemical basis, e.g. "sto-3g". multiplicity: spin multiplicity. charge: charge of the molecule. + + Raises: + ModuleNotFoundError: `hartree fock` method needs pyscf being installed, please run `pip install -U pyscf`. """ def __init__( diff --git a/paddle_quantum/qchem/slater_determinant.py b/paddle_quantum/qchem/slater_determinant.py index 5ce6a6b..5124076 100644 --- a/paddle_quantum/qchem/slater_determinant.py +++ b/paddle_quantum/qchem/slater_determinant.py @@ -31,11 +31,8 @@ class GivensRotationBlock(pq.gate.Gate): r"""This is a two-qubit gate performs the Givens rotation. - .. math:: - - \begin{align} - U(\theta)=e^{-i\frac{\theta}{2}(Y\otimes X-X\otimes Y)} - \end{align} + .. math: + U(\theta)=e^{-i\frac{\theta}{2}(Y\otimes X-X\otimes Y)} Args: pindex, qindex qubits where Givens rotation gate acts on. diff --git a/paddle_quantum/qinfo.py b/paddle_quantum/qinfo.py index dd54692..ebeb717 100644 --- a/paddle_quantum/qinfo.py +++ b/paddle_quantum/qinfo.py @@ -23,20 +23,22 @@ import numpy as np from scipy.linalg import logm, sqrtm import paddle -from paddle import kron -from paddle import matmul -from paddle import transpose -from paddle_quantum.intrinsic import _get_float_dtype +from paddle import kron, matmul, transpose +from .state import State +from .base import get_dtype +from .intrinsic import _get_float_dtype +from .linalg import dagger, is_unitary, NKron +from .backend import Backend import matplotlib.image -from paddle_quantum.linalg import dagger, is_unitary, NKron -from typing import Optional, Tuple, List +from typing import Optional, Tuple, List, Union -def partial_trace(rho_AB: paddle_quantum.State, dim1: int, dim2: int, A_or_B: int) -> paddle.Tensor: +def partial_trace(state: Union[State, paddle.Tensor], + dim1: int, dim2: int, A_or_B: int) -> Union[State, paddle.Tensor]: r"""Calculate the partial trace of the quantum state. Args: - rho_AB: Input quantum state. + state: Input quantum state. dim1: The dimension of system A. dim2: The dimension of system B. A_or_B: 1 or 2. 1 means to calculate partial trace on system A; 2 means to calculate partial trace on system B. @@ -47,15 +49,17 @@ def partial_trace(rho_AB: paddle_quantum.State, dim1: int, dim2: int, A_or_B: in if A_or_B == 2: dim1, dim2 = dim2, dim1 - rho_AB = rho_AB.data - complex_dtype = paddle_quantum.get_dtype() - float_dtype = _get_float_dtype(complex_dtype) - - idty_np = np.identity(dim2).astype(complex_dtype) - idty_B = paddle.to_tensor(idty_np) + is_State = False + if isinstance(state, State): + is_State = True + backend = state.backend + rho_AB = state.data if backend != Backend.StateVector else state.ket @ state.bra + else: + rho_AB = state + complex_dtype = rho_AB.dtype - zero_np = np.zeros([dim2, dim2], complex_dtype) - res = paddle.to_tensor(zero_np) + idty_B = paddle.eye(dim2).cast(complex_dtype) + res = paddle.zeros([dim2, dim2]).cast(complex_dtype) for dim_j in range(dim1): row_top = paddle.zeros([1, dim_j]) @@ -67,73 +71,87 @@ def partial_trace(rho_AB: paddle_quantum.State, dim1: int, dim2: int, A_or_B: in if A_or_B == 1: row_tmp = kron(bra_j, idty_B) row_tmp_conj = paddle.conj(row_tmp) - res = paddle.add(res, paddle.matmul(paddle.matmul(row_tmp, rho_AB), paddle.transpose(row_tmp_conj, perm=[1, 0]), ), ) + res += (row_tmp @ rho_AB) @ paddle.transpose(row_tmp_conj, perm=[1, 0]) if A_or_B == 2: row_tmp = kron(idty_B, bra_j) row_tmp_conj = paddle.conj(row_tmp) - res = paddle.add(res, paddle.matmul(paddle.matmul(row_tmp, rho_AB), paddle.transpose(row_tmp_conj, perm=[1, 0]), ), ) - + res += (row_tmp @ rho_AB) @ paddle.transpose(row_tmp_conj, perm=[1, 0]) + + if is_State: + if backend == Backend.StateVector: + eigval, eigvec = paddle.linalg.eig(res) + res = eigvec[:, paddle.argmax(paddle.real(eigval))] + return State(res, backend=backend) return res -def partial_trace_discontiguous(rho: paddle_quantum.State, preserve_qubits: Optional[list] = None) -> paddle.Tensor: +def partial_trace_discontiguous(state: Union[State, paddle.Tensor], + preserve_qubits: list=None) -> Union[State, paddle.Tensor]: r"""Calculate the partial trace of the quantum state with arbitrarily selected subsystem Args: - rho: Input quantum state. + state: Input quantum state. preserve_qubits: Remaining qubits, default is None, indicate all qubits remain. Returns: Partial trace of the quantum state with arbitrarily selected subsystem. """ - if type(rho) == paddle_quantum.State: - rho = rho.data - complex_dtype = paddle_quantum.get_dtype() - float_dtype = _get_float_dtype(complex_dtype) + is_State = False + if isinstance(state, State): + is_State = True + backend = rho.backend + rho = state.data if backend != Backend.StateVector else state.ket @ state.bra + else: + rho = state + complex_dtype = rho.dtype if preserve_qubits is None: return rho - else: - n = int(math.log2(rho.size) // 2) - num_preserve = len(preserve_qubits) - - shape = paddle.ones((n + 1,)) - shape = 2 * shape - shape[n] = 2 ** n - shape = paddle.cast(shape, "int32") - identity = paddle.eye(2 ** n) - identity = paddle.reshape(identity, shape=shape) - discard = list() - for idx in range(0, n): - if idx not in preserve_qubits: - discard.append(idx) - addition = [n] - preserve_qubits.sort() - - preserve_qubits = paddle.to_tensor(preserve_qubits) - discard = paddle.to_tensor(discard) - addition = paddle.to_tensor(addition) - permute = paddle.concat([discard, preserve_qubits, addition]) - - identity = paddle.transpose(identity, perm=permute) - identity = paddle.reshape(identity, (2 ** n, 2 ** n)) - - result = np.zeros((2 ** num_preserve, 2 ** num_preserve), dtype=complex_dtype) - result = paddle.to_tensor(result) - - for i in range(0, 2 ** (n - num_preserve)): - bra = identity[i * 2 ** num_preserve:(i + 1) * 2 ** num_preserve, :] - result = result + matmul(matmul(bra, rho), transpose(bra, perm=[1, 0])) - - return result - - -def trace_distance(rho: paddle_quantum.State, sigma: paddle_quantum.State) -> paddle.Tensor: - r"""Calculate the trace distance between two quantum states. + + n = int(math.log2(rho.size) // 2) + num_preserve = len(preserve_qubits) + + shape = paddle.ones((n + 1,)) + shape = 2 * shape + shape[n] = 2 ** n + shape = paddle.cast(shape, "int32") + identity = paddle.eye(2 ** n) + identity = paddle.reshape(identity, shape=shape) + discard = list() + for idx in range(0, n): + if idx not in preserve_qubits: + discard.append(idx) + addition = [n] + preserve_qubits.sort() + + preserve_qubits = paddle.to_tensor(preserve_qubits) + discard = paddle.to_tensor(discard) + addition = paddle.to_tensor(addition) + permute = paddle.concat([discard, preserve_qubits, addition]) + + identity = paddle.transpose(identity, perm=permute) + identity = paddle.reshape(identity, (2 ** n, 2 ** n)) + + result = np.zeros((2 ** num_preserve, 2 ** num_preserve)) + result = paddle.to_tensor(result, dtype=complex_dtype) + + for i in range(0, 2 ** (n - num_preserve)): + bra = identity[i * 2 ** num_preserve:(i + 1) * 2 ** num_preserve, :] + result = result + matmul(matmul(bra, rho), transpose(bra, perm=[1, 0])) + + if is_State: + if backend == Backend.StateVector: + eigval, eigvec = paddle.linalg.eig(result) + result = eigvec[:, paddle.argmax(paddle.real(eigval))] + return State(result, backend=backend) + return result + + +def trace_distance(rho: Union[State, paddle.Tensor], sigma: Union[State, paddle.Tensor]) -> paddle.Tensor: + r"""Calculate the fidelity of two quantum states. .. math:: - D(\rho, \sigma) = 1 / 2 * \text{tr}|\rho-\sigma| Args: @@ -141,16 +159,17 @@ def trace_distance(rho: paddle_quantum.State, sigma: paddle_quantum.State) -> pa sigma: Density matrix form of the quantum state. Returns: - Trace distance between the input quantum states. + The fidelity between the input quantum states. """ - assert rho.data.shape == sigma.data.shape, 'The shape of two quantum states are different' - A = rho.data.numpy() - sigma.data.numpy() - distance = 1 / 2 * np.sum(np.abs(np.linalg.eigvals(A))) - - return paddle.to_tensor(distance) + if isinstance(rho, State): + rho = rho.data + sigma = sigma.data + assert rho.shape == sigma.shape, 'The shape of two quantum states are different' + eigval, eigvec = paddle.linalg.eig(rho - sigma) + return 0.5 * paddle.sum(paddle.abs(eigval)) -def state_fidelity(rho: paddle_quantum.State, sigma: paddle_quantum.State) -> paddle.Tensor: +def state_fidelity(rho: Union[State, paddle.Tensor], sigma: Union[State, paddle.Tensor]) -> paddle.Tensor: r"""Calculate the fidelity of two quantum states. .. math:: @@ -163,8 +182,12 @@ def state_fidelity(rho: paddle_quantum.State, sigma: paddle_quantum.State) -> pa Returns: The fidelity between the input quantum states. """ - rho = rho.data.numpy() - sigma = sigma.data.numpy() + + if isinstance(rho, State): + rho = rho.data + sigma = sigma.data + rho = rho.numpy() + sigma = sigma.numpy() assert rho.shape == sigma.shape, 'The shape of two quantum states are different' fidelity = np.trace(sqrtm(sqrtm(rho) @ sigma @ sqrtm(rho))).real @@ -188,18 +211,15 @@ def gate_fidelity(U: paddle.Tensor, V: paddle.Tensor) -> paddle.Tensor: fidelity between gates """ - complex_dtype = paddle_quantum.get_dtype() - U = paddle.to_tensor(U, dtype=complex_dtype) - V = paddle.to_tensor(V, dtype=complex_dtype) - # assert is_unitary(U), "U is not a unitary" - # assert is_unitary(V), "V is not a unitary" - assert U.shape == V.shape, 'The shape of two unitary matrices are different' + complex_dtype = U.dtype + V = paddle.cast(V, dtype=complex_dtype) + assert U.shape == V.shape, 'The shape of two matrices are different' fidelity = paddle.abs(paddle.trace(U @ dagger(V))) / U.shape[0] return fidelity -def purity(rho: paddle_quantum.State) -> paddle.Tensor: +def purity(rho: Union[State, paddle.Tensor]) -> paddle.Tensor: r"""Calculate the purity of a quantum state. .. math:: @@ -212,13 +232,14 @@ def purity(rho: paddle_quantum.State) -> paddle.Tensor: Returns: The purity of the input quantum state. """ - rho = rho.data + if isinstance(rho, State): + rho = rho.data gamma = paddle.trace(paddle.matmul(rho, rho)) return gamma.real() -def von_neumann_entropy(rho: paddle_quantum.State) -> paddle.Tensor: +def von_neumann_entropy(rho: Union[State, paddle.Tensor]) -> paddle.Tensor: r"""Calculate the von Neumann entropy of a quantum state. .. math:: @@ -231,7 +252,9 @@ def von_neumann_entropy(rho: paddle_quantum.State) -> paddle.Tensor: Returns: The von Neumann entropy of the input quantum state. """ - rho = rho.data.numpy() + if isinstance(rho, State): + rho = rho.data + rho = rho.numpy() rho_eigenvalues = np.real(np.linalg.eigvals(rho)) entropy = 0 for eigenvalue in rho_eigenvalues: @@ -242,7 +265,7 @@ def von_neumann_entropy(rho: paddle_quantum.State) -> paddle.Tensor: return paddle.to_tensor(entropy) -def relative_entropy(rho: paddle_quantum.State, sig: paddle_quantum.State) -> paddle.Tensor: +def relative_entropy(rho: Union[State, paddle.Tensor], sig: Union[State, paddle.Tensor]) -> paddle.Tensor: r"""Calculate the relative entropy of two quantum states. .. math:: @@ -257,8 +280,11 @@ def relative_entropy(rho: paddle_quantum.State, sig: paddle_quantum.State) -> pa Returns: Relative entropy between input quantum states. """ - rho = rho.data.numpy() - sig = sig.data.numpy() + if isinstance(rho, State): + rho = rho.data + sig = sig.data + rho = rho.numpy() + sig = sig.numpy() assert rho.shape == sig.shape, 'The shape of two quantum states are different' res = np.trace(rho @ logm(rho) - rho @ logm(sig)) return paddle.to_tensor(res.real) @@ -276,7 +302,7 @@ def random_pauli_str_generator(n: int, terms: Optional[int] = 3) -> List: terms: Number of terms in the observable. Defaults to 3. Returns: - The randomly generated observable’s list form. + The randomly generated observable's list form. """ pauli_str = [] for sublen in np.random.randint(1, high=n + 1, size=terms): @@ -332,12 +358,15 @@ def pauli_str_to_matrix(pauli_str: list, n: int) -> paddle.Tensor: if len(op_str) == 1: matrices.append(coeff * sub_matrices[0]) else: - matrices.append(coeff * NKron(sub_matrices[0], sub_matrices[1], *sub_matrices[2:])) + mat = sub_matrices[0] + for idx in range(1, len(sub_matrices)): + mat = np.kron(mat, sub_matrices[idx]) + matrices.append(coeff * mat) - return paddle.to_tensor(sum(matrices), dtype=paddle_quantum.get_dtype()) + return paddle.to_tensor(sum(matrices), dtype=get_dtype()) -def partial_transpose_2(density_op: paddle_quantum.State, sub_system: Optional[int] = None) -> paddle.Tensor: +def partial_transpose_2(density_op: Union[State, paddle.Tensor], sub_system: int = None) -> paddle.Tensor: r"""Calculate the partial transpose :math:`\rho^{T_A}` of the input quantum state. Args: @@ -350,7 +379,10 @@ def partial_transpose_2(density_op: paddle_quantum.State, sub_system: Optional[i sys_idx = 2 if sub_system is None else 1 # Copy the density matrix and not corrupt the original one - density_op = density_op.data.numpy() + if isinstance(density_op, State): + density_op = density_op.data + complex_dtype = density_op.dtype + density_op = density_op.numpy() transposed_density_op = np.copy(density_op) if sys_idx == 2: for j in [0, 2]: @@ -360,10 +392,10 @@ def partial_transpose_2(density_op: paddle_quantum.State, sub_system: Optional[i transposed_density_op[2:4, 0:2] = density_op[0:2, 2:4] transposed_density_op[0:2, 2:4] = density_op[2:4, 0:2] - return paddle.to_tensor(transposed_density_op) + return paddle.to_tensor(transposed_density_op, dtype=complex_dtype) -def partial_transpose(density_op: paddle_quantum.State, n: int) -> paddle.Tensor: +def partial_transpose(density_op: Union[State, paddle.Tensor], n: int) -> paddle.Tensor: r"""Calculate the partial transpose :math:`\rho^{T_A}` of the input quantum state. Args: @@ -374,16 +406,19 @@ def partial_transpose(density_op: paddle_quantum.State, n: int) -> paddle.Tensor The partial transpose of the input quantum state. """ # Copy the density matrix and not corrupt the original one - density_op = density_op.data.numpy() + if isinstance(density_op, State): + density_op = density_op.data + complex_dtype = density_op.dtype + density_op = density_op.numpy() transposed_density_op = np.copy(density_op) for j in range(0, 2 ** n, 2): for i in range(0, 2 ** n, 2): transposed_density_op[i:i + 2, j:j + 2] = density_op[i:i + 2, j:j + 2].transpose() - return paddle.to_tensor(transposed_density_op) + return paddle.to_tensor(transposed_density_op, dtype=complex_dtype) -def negativity(density_op: paddle_quantum.State) -> paddle.Tensor: +def negativity(density_op: Union[State, paddle.Tensor]) -> paddle.Tensor: r"""Compute the Negativity :math:`N = ||\frac{\rho^{T_A}-1}{2}||` of the input quantum state. Args: @@ -394,6 +429,9 @@ def negativity(density_op: paddle_quantum.State) -> paddle.Tensor: """ # Implement the partial transpose density_op_T = partial_transpose_2(density_op) + if isinstance(density_op_T, State): + density_op_T = density_op_T.data + density_op_T = density_op_T.numpy() # Calculate through the equivalent expression N = sum(abs(\lambda_i)) when \lambda_i<0 n = 0.0 @@ -401,10 +439,10 @@ def negativity(density_op: paddle_quantum.State) -> paddle.Tensor: for val in eigen_val: if val < 0: n = n + np.abs(val) - return paddle.to_tensor(n) + return paddle.to_tensor(n, dtype=_get_float_dtype(paddle_quantum.get_dtype())) -def logarithmic_negativity(density_op: paddle_quantum.State) -> paddle.Tensor: +def logarithmic_negativity(density_op: Union[State, paddle.Tensor]) -> paddle.Tensor: r"""Calculate the Logarithmic Negativity :math:`E_N = ||\rho^{T_A}||` of the input quantum state. Args: @@ -421,7 +459,7 @@ def logarithmic_negativity(density_op: paddle_quantum.State) -> paddle.Tensor: return log2_n -def is_ppt(density_op: paddle_quantum.State) -> bool: +def is_ppt(density_op: Union[State, paddle.Tensor]) -> bool: r"""Check if the input quantum state is PPT. Args: @@ -439,11 +477,12 @@ def is_ppt(density_op: paddle_quantum.State) -> bool: return ppt -def schmidt_decompose(psi: paddle_quantum.State, sys_A: Optional[List[int]] = None) -> Tuple[paddle.Tensor]: +def schmidt_decompose(psi: Union[State, paddle.Tensor], + sys_A: List[int]=None) -> Tuple[paddle.Tensor, paddle.Tensor, paddle.Tensor]: r"""Calculate the Schmidt decomposition of a quantum state :math:`\lvert\psi\rangle=\sum_ic_i\lvert i_A\rangle\otimes\lvert i_B \rangle`. Args: - psi: State vector form of the quantum state, with shape(2**n) + psi: State vector form of the quantum state, with shape (2**n) sys_A: Qubit indices to be included in subsystem A (other qubits are included in subsystem B), default are the first half qubits of :math:`\lvert \psi\rangle` Returns: @@ -453,7 +492,10 @@ def schmidt_decompose(psi: paddle_quantum.State, sys_A: Optional[List[int]] = No * A high dimensional array composed of bases for subsystem A :math:`\lvert i_A\rangle`, with shape ``(k, 2**m, 1)`` * A high dimensional array composed of bases for subsystem B :math:`\lvert i_B\rangle` , with shape ``(k, 2**m, 1)`` """ - psi = psi.data.numpy() + if isinstance(psi, State): + psi = psi.data + psi = psi.numpy() + complex_dtype = psi.dtype assert psi.ndim == 1, 'Psi must be a one dimensional vector.' assert np.log2(psi.size).is_integer(), 'The number of amplitutes must be an integral power of 2.' @@ -465,20 +507,20 @@ def schmidt_decompose(psi: paddle_quantum.State, sys_A: Optional[List[int]] = No # Permute qubit indices psi = psi.reshape([2] * tot_qu).transpose(sys_A + sys_B) - # construct amplitute matrix + # construct amplitude matrix amp_mtr = psi.reshape([2**len(sys_A), 2**len(sys_B)]) # Standard process to obtain schmidt decomposition u, c, v = np.linalg.svd(amp_mtr) k = np.count_nonzero(c > 1e-13) - c = c[:k] - u = u.T[:k].reshape([k, -1, 1]) - v = v[:k].reshape([k, -1, 1]) - return paddle.to_tensor(c), paddle.to_tensor(u), paddle.to_tensor(v) + c = paddle.to_tensor(c[:k], dtype=complex_dtype) + u = paddle.to_tensor(u.T[:k].reshape([k, -1, 1]), dtype=complex_dtype) + v = paddle.to_tensor(v[:k].reshape([k, -1, 1]), dtype=complex_dtype) + return c, u, v -def image_to_density_matrix(image_filepath: str) -> paddle_quantum.State: +def image_to_density_matrix(image_filepath: str) -> State: r"""Encode image to density matrix Args: @@ -498,10 +540,10 @@ def image_to_density_matrix(image_filepath: str) -> paddle_quantum.State: # Density matrix whose trace is 1 rho = image_matrix@image_matrix.T rho = rho/np.trace(rho) - return paddle_quantum.State(paddle.to_tensor(rho), backend=paddle_quantum.Backend.DensityMatrix) + return State(paddle.to_tensor(rho), backend=paddle_quantum.Backend.DensityMatrix) -def shadow_trace(state: 'paddle_quantum.State', hamiltonian: paddle_quantum.Hamiltonian, +def shadow_trace(state: 'State', hamiltonian: paddle_quantum.Hamiltonian, sample_shots: int, method: Optional[str] = 'CS') -> float: r"""Estimate the expectation value :math:`\text{trace}(H\rho)` of an observable :math:`H`. @@ -510,6 +552,9 @@ def shadow_trace(state: 'paddle_quantum.State', hamiltonian: paddle_quantum.Hami hamiltonian: Observable. sample_shots: Number of samples. method: Method used to , which should be one of “CS”, “LBCS”, and “APS”. Default is “CS”. + + Raises: + ValueError: Hamiltonian has a bad form Returns: The estimated expectation value for the hamiltonian. @@ -639,3 +684,47 @@ def update_pauli_estimator(hamiltonian, pauli_estimator, pauli_str, measurement_ trace_estimation = estimation return trace_estimation + + +def tensor_product(state_a: Union[State, paddle.Tensor], state_b: Union[State, paddle.Tensor], *args: Union[State, paddle.Tensor]) -> State: + r"""calculate tensor product (kronecker product) between at least two state. This function automatically returns State instance + + Args: + state_a: State + state_b: State + *args: other states + + Raises: + NotImplementedError: only accept state or tensor instances + + Returns: + tensor product of input states + + Note: + the backend should be density matrix. + """ + if isinstance(state_a, State): + data_a = state_a.data + elif isinstance(state_a, paddle.Tensor): + data_a = state_a + else: + raise NotImplementedError + + if isinstance(state_b, State): + data_b = state_b.data + elif isinstance(state_b, paddle.Tensor): + data_b = state_b + else: + raise NotImplementedError + + addis = [] + for st in args: + if isinstance(st, State): + addis.append(st.data) + elif isinstance(st, paddle.Tensor): + addis.append(st) + else: + raise NotImplementedError + + res = paddle_quantum.linalg.NKron(data_a, data_b, *addis) + return State(res) \ No newline at end of file diff --git a/paddle_quantum/qsvt/__init__.py b/paddle_quantum/qsvt/__init__.py new file mode 100644 index 0000000..09358b6 --- /dev/null +++ b/paddle_quantum/qsvt/__init__.py @@ -0,0 +1,22 @@ +# !/usr/bin/env python3 +# Copyright (c) 2021 Institute for Quantum Computing, Baidu Inc. All Rights Reserved. +# +# 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 +# +# http://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. + +r""" +The module of quantum singular value transformation. +""" + +from .qsp_utils import poly_matrix +from .qsp import ScalarQSP, Phi_verification, reflection_based_quantum_signal_processing +from .qsvt import QSVT diff --git a/paddle_quantum/qsvt/qsp.py b/paddle_quantum/qsvt/qsp.py new file mode 100644 index 0000000..c9b230d --- /dev/null +++ b/paddle_quantum/qsvt/qsp.py @@ -0,0 +1,484 @@ +# !/usr/bin/env python3 +# Copyright (c) 2022 Institute for Quantum Computing, Baidu Inc. All Rights Reserved. +# +# 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 +# +# http://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 +from math import pi, acos +import paddle +from paddle_quantum.ansatz.circuit import Circuit +from numpy.polynomial.polynomial import Polynomial, polytrim +from typing import List, Tuple +from .qsp_utils import clean_small_error + + +""" + Libraries for Quantum Signal Processing + referring to paper https://arxiv.org/abs/1806.01838 + +""" + + +# ----------------------------- belows are support and test functions ------------------------ + + +def signal_unitary(signal_x: float) -> np.ndarray: + r"""signal unitary :math:`W(x)` in paper https://arxiv.org/abs/1806.01838 + + Args: + signal_x: variable x in [-1, 1] + + Returns: + matrix W(x = x) + + """ + assert -1 <= signal_x <= 1, "x must be in domain [-1, 1]" + return np.array([[signal_x, 1j * np.sqrt(1 - signal_x ** 2)], + [1j * np.sqrt(1 - signal_x ** 2), signal_x]]) + + +def poly_parity_verification(poly_p: Polynomial, k: int, error: float = 1e-6) -> bool: + r"""verify whether :math:`P` has parity-(k mod 2), i.e., the second condition of theorem 3 holds + + Args: + poly_p: polynomial :math:`P(x)` + k: parameter that determine parity + error: tolerated error, defaults to `1e-6` + + Returns: + determine whether :math:`P` has parity-(k mod 2) + + """ + parity = k % 2 + P_coef = poly_p.coef + + for i in range(poly_p.degree()): + if i % 2 != parity: + if np.abs(P_coef[i]) > error: + return False + P_coef[i] = 0 # this element should be 0 + return True + + +def normalization_verification(poly_p: Polynomial, poly_q: Polynomial, trials: int = 10, error: float = 1e-2) -> bool: + r"""verify whether polynomials :math:`P(x)` and :math:`Q(x)` satisfy the normalization condition :math:`|P|^2 + (1 - x^2)|Q|^2 = 1`, i.e., the third condition of Theorem 3. + + Args: + poly_p: polynomial :math:`P(x)` + poly_q: polynomial :math:`Q(x)` + trials: number of tests, defaults to `10` + error: tolerated error, defaults to `1e-2` + + Returns: + determine whether :math:`|P|^2 + (1 - x^2)|Q|^2 = 1` + + """ + P_conj = Polynomial(np.conj(poly_p.coef)) + Q_conj = Polynomial(np.conj(poly_q.coef)) + + test_poly = poly_p * P_conj + Polynomial([1, 0, -1]) * poly_q * Q_conj + for _ in range(trials): + x = np.random.rand() * 2 - 1 # sample x from [-1, 1] + y = test_poly(x) + if abs(np.real(y) - 1) > error or abs(np.imag(y)) > error: + print(np.real(y) - 1) + return False + return True + + +def angle_phi_verification(phi: float, poly_p: Polynomial, poly_p_hat: Polynomial, poly_q_hat: Polynomial, + trials: int = 10, error: float = 0.01) -> bool: + r"""verify :math:`\phi` during the iteration of finding :math:`\Phi` + + Args: + phi: rotation angle :math:`\phi` + poly_p: polynomial :math:`P(x)` + poly_p_hat: updated polynomial :math:`\hat{P}` + poly_q_hat: updated polynomial :math:`\hat{Q}` + trials: number of tests, defaults to `10` + error: tolerated error, defaults to `0.01` + + Returns: + determine whether the equation (6) in paper https://arxiv.org/abs/1806.01838 holds + + """ + + def block_encoding_new_p(x): + return np.array([[poly_p_hat(x), 1j * poly_q_hat(x) * np.sqrt(1 - x ** 2)], + [1j * np.conj(poly_q_hat(x)) * np.sqrt(1 - x ** 2), np.conj(poly_p_hat(x))]]) + + rz = np.array([[np.exp(1j * phi), 0], + [0, np.exp(-1j * phi)]]) + + for _ in range(trials): + x = np.random.rand() * 2 - 1 # sample x from [-1, 1] + matrix = np.matmul(np.matmul(block_encoding_new_p(x), signal_unitary(x)), rz) + y = matrix[0, 0] + if np.abs(y - poly_p(x)) > error: + print(y) + print(poly_p(x)) + return False + return True + + +def processing_unitary(list_matrices: List[np.ndarray], signal_x: float) -> np.ndarray: + r"""processing unitary :math:`W_\Phi(x)`, see equation 1 in paper https://arxiv.org/abs/1806.01838 + + Args: + list_matrices: array of phi's matrices + signal_x: input signal x in [-1, 1] + + Returns: + matrix :math:`W_\Phi(x)` + + """ + assert -1 <= signal_x <= 1, "x must be in domain [-1, 1]" + + W = signal_unitary(signal_x) + M = list_matrices[0] + for i in range(1, len(list_matrices)): + M = np.matmul(M, np.matmul(W, list_matrices[i])) + return M + + +def Phi_verification(list_phi: np.ndarray, poly_p: Polynomial, trials: int = 100, error: float = 1e-6) -> bool: + r"""verify the final :math:`\Phi` + + Args: + list_phi: array of :math:`\phi`'s + poly_p: polynomial :math:`P(x)` + trials: number of tests, defaults to `100` + error: tolerated error, defaults to `1e-6` + + Returns: + determine whether :math:`W_\Phi(x)` is a block encoding of :math:`P(x)` + + """ + def rz(theta: float) -> np.ndarray: + return np.array([[np.exp(1j * theta), 0], + [0, np.exp(-1j * theta)]]) + + matrix_phi = list(map(rz, list_phi)) + + for _ in range(trials): + x = np.random.rand() * 2 - 1 # sample x from [-1, 1] + y = processing_unitary(matrix_phi, x)[0, 0] + if np.abs(y - poly_p(x)) > error: + print(y - poly_p(x), error) + return False + return True + + +# ----------------------------- belows are Phi-generation functions ------------------------ + + + +def update_polynomial(poly_p: Polynomial, poly_q: Polynomial, phi: float) -> Tuple[Polynomial, Polynomial]: + r"""update :math:`P, Q` by given phi according to proof in theorem 3 + + Args: + poly_p: polynomial :math:`P(x)` + poly_q: polynomial :math:`Q(x)` + phi: derived :math:`phi` + + Returns: + updated :math:`P(x), Q(x)` + + """ + poly_1 = Polynomial([0, 1]) # x + poly_2 = Polynomial([1, 0, -1]) # 1 - x^2 + + # P = e^{-i phi} x P + e^{i phi} (1 - x^2) Q + # Q = e^{i phi} x Q - e^{-i phi} P + P_new = np.exp(-1j * phi) * poly_1 * poly_p + np.exp(1j * phi) * poly_2 * poly_q + Q_new = np.exp(1j * phi) * poly_1 * poly_q - np.exp(-1j * phi) * poly_p + + # clean the error that is lower than 0.001 + P_new = Polynomial(polytrim(P_new.coef, 0.001)) + Q_new = Polynomial(polytrim(Q_new.coef, 0.001)) + + # clean the error further, + P_new.coef = clean_small_error(P_new.coef) + Q_new.coef = clean_small_error(Q_new.coef) + + if P_new.degree() >= poly_p.degree() and np.abs(P_new.coef[-1]) < np.abs(P_new.coef[-3]): + P_new.coef = np.delete(np.delete(P_new.coef, -1), -1) + if Q_new.degree() >= poly_q.degree() > 0 and np.abs(Q_new.coef[-1]) < np.abs(Q_new.coef[-3]): + Q_new.coef = np.delete(np.delete(Q_new.coef, -1), -1) + + # used for debug, can be removed in formal version + assert P_new.degree() < poly_p.degree(), print(P_new, '\n', poly_p) + assert Q_new.degree() < poly_q.degree() or poly_q.degree() == 0, print(Q_new, '\n', poly_q) + assert poly_parity_verification(P_new, poly_p.degree() - 1) + assert poly_parity_verification(Q_new, poly_q.degree() - 1) + assert normalization_verification(P_new, Q_new), print(P_new, '\n', Q_new) + assert angle_phi_verification(phi, poly_p, P_new, Q_new) + + return P_new, Q_new + + +def alg_find_Phi(poly_p: Polynomial, poly_q: Polynomial, length: int) -> np.ndarray: + r"""The algorithm of finding phi's by theorem 3 + + Args: + poly_p: polynomial :math:`P(x)` + poly_q: polynomial :math:`Q(x)` + length: length of returned array + + Returns: + array of phi's + + """ + n = poly_p.degree() + m = poly_q.degree() + + # condition check for theorem 3 + assert n <= length, "the condition for P's degree is not satisfied" + assert m <= max(0, length - 1), "the condition for Q's degree is not satisfied" + assert poly_parity_verification(poly_p, length), "the condition for P's parity is not satisfied" + assert poly_parity_verification(poly_q, length - 1), "the condition for Q's parity is not satisfied" + assert normalization_verification(poly_p, poly_q), "the third equation for P, Q is not satisfied" + + i = length + Phi = np.zeros([length + 1]) + + while n > 0: + # assign phi + Phi[i] = np.log(poly_p.coef[n] / poly_q.coef[m]) * -1j / 2 + + if Phi[i] == 0: + Phi[i] = np.pi + + # update step + poly_p, poly_q = update_polynomial(poly_p, poly_q, Phi[i]) + + n = poly_p.degree() + m = poly_q.degree() + i = i - 1 + + for j in range(1, i): + Phi[j] = (-1) ** (j - 1) * pi / 2 + Phi[0] = -1j * np.log(poly_p.coef[0]) + + return Phi + + +# ----------------------------- belows are Q-generation functions ------------------------ + + +def poly_A_hat_generation(poly_p: Polynomial) -> Polynomial: + r"""function for :math:`\hat{A}` generation + + Args: + poly_p: polynomial :math:`P(x)` + + Returns: + polynomial :math:`\hat{A}(y) = 1 - P(x)P^*(x)`, with :math:`y = x^2` + + """ + P_conj = Polynomial(np.conj(poly_p.coef)) + A = 1 - poly_p * P_conj + A_coef = A.coef + coef = [A_coef[0]] + for i in range(1, poly_p.degree() + 1): + coef.append(A_coef[2 * i]) + + return Polynomial(np.array(coef)) + + +def poly_A_hat_decomposition(A_hat: Polynomial, error: float = 0.001) -> Tuple[float, List[float]]: + r"""function for :math:`\hat{A}` decomposition + + Args: + A_hat: polynomial :math:`\hat{A}(x)` + error: tolerated error, defaults to `0.001` + + Returns: + Tuple: including the following elements + - leading coefficient of :math:`\hat{A}` + - list of roots of :math:`\hat{A}` such that there exist no two roots that are complex conjugates + + """ + leading_coef = A_hat.coef[A_hat.degree()] + + # remove one 1 and 0 (if k is even) from this list + roots = [i for i in A_hat.roots() if not ((np.abs(np.real(i) - 1) < error + and np.abs(np.imag(i)) < error) or np.abs(i) < error)] + + # Note that root function in numpy return roots in complex conjugate pairs + # Now elements in roots are all in pairs + output_roots = [roots[i] for i in range(len(roots)) if i % 2 == 0] + + return leading_coef, output_roots + + +def poly_Q_generation(leading_coef: float, roots: List[float], parity: int) -> Polynomial: + r"""function for polynomial :math:`Q` generation + + Args: + leading_coef: leading coefficient of :math:`\hat{A}` + roots: filtered list of roots of :math:`\hat{A}` + parity: parity that affects decomposition + + Returns: + polynomial :math:`Q` + + """ + a = np.sqrt(-1 * leading_coef) + + poly_q = Polynomial([a]) + for i in range(len(roots)): + poly_q = poly_q * Polynomial([-1 * roots[i], 0, 1]) + + if parity % 2 == 0: + poly_q = poly_q * Polynomial([0, 1]) + return poly_q + return poly_q + + +def alg_find_Q(poly_p: Polynomial, k: int) -> Polynomial: + r"""The algorithm of finding :math:`Q` by theorem 4 in paper https://arxiv.org/abs/1806.01838 + + Args: + poly_p: polynomial :math:`P(x)` + k: length of returned array + + Returns: + polynomial :math:`Q(x)` + + """ + n = poly_p.degree() + + # condition check for theorem 3 + assert n <= k, "the condition for P's degree is not satisfied" + assert poly_parity_verification(poly_p, k), "the condition for P's parity is not satisfied" + + A_hat = poly_A_hat_generation(poly_p) + leading_coef, roots = poly_A_hat_decomposition(A_hat) + poly_q = poly_Q_generation(leading_coef, roots, k) + + return poly_q + + +# ----------------------------- belows are final functions ------------------------ + + +def quantum_signal_processing(poly_p: Polynomial, length: int = None) -> np.ndarray: + r""" Compute :math:`\Phi` that transfer a block encoding of x to a block encoding of :math:`P(x)` by :math:`W_\Phi(x)` + + Args: + poly_p: polynomial :math:`P(x)` + length: length of returned array, defaults to `None` to be the degree of :math:`P(x)` + + Returns: + array of :math:`\phi`'s + + """ + if length is None: + length = poly_p.degree() + + Q = alg_find_Q(poly_p, length) + Phi = alg_find_Phi(poly_p, Q, length) + + assert Phi_verification(Phi, poly_p) + + return Phi + +def reflection_based_quantum_signal_processing(P: Polynomial) -> np.ndarray: + r""" Reflection-based quantum signal processing, compute Phi that transfer a block encoding of x to a block encoding of :math:`P(x)` with :math:`R_\Phi(x)`. Refer to Corollary 8 in the paper. + + Args: + P: polynomial :math:`P(x)` + + Returns: + array of :math:`phi`'s + + """ + Phi = quantum_signal_processing(P) + k = P.degree() + Phi_new = np.zeros([k]) + + Phi_new[0] = Phi[0] + Phi[k] + (k - 1) * np.pi / 2 + for i in range(1, k): + Phi_new[i] = Phi[i] - np.pi / 2 + + # assertion + phi_sum = 0 + phi_alternate = 0 + for i in range(k): + phi_sum += Phi_new[i] + phi_alternate += ((-1) ** i) * Phi_new[i] + + assert np.abs(P(1) - np.exp(1j * phi_sum)) < 10 ** (-8) + assert np.abs(P(-1) - ((-1) ** k) * np.exp(1j * phi_sum)) < 10 ** (-8) + if k % 2 == 0: + assert np.abs(P(0) - np.exp(-1j * phi_alternate)) < 10 ** (-8) + + return Phi_new + + +# ----------------------------- below is the class for QSP ------------------------ + + +class ScalarQSP(object): + def __init__(self, poly_p: Polynomial, length: int = None) -> None: + r"""Initialize a class that is used for QSP in single qubit + + Args: + poly_p: Polynomial P(x) + k: length of array Phi - 1 + + """ + if length is None: + length = poly_p.degree() + + self.poly_p = poly_p + self.poly_q = alg_find_Q(poly_p, length) + self.Phi = paddle.to_tensor(quantum_signal_processing(poly_p, length)) + + def block_encoding(self, signal_x: float) -> Circuit: + r"""generate a block encoding of :math:`P(x)` by quantum circuit + + Args: + x: input parameter + + Returns: + a quantum circuit of unitary that is the block encoding of :math:`P(x)` + + """ + assert -1 <= signal_x <= 1, "x must be in domain [-1, 1]" + + Phi = self.Phi + cir = Circuit() + signal_x = -2 * acos(signal_x) + + for i in range(1, len(Phi)): + cir.rz(0, param=-2 * Phi[-i]) + cir.rx(0, param=signal_x) + cir.rz(0, param=-2 * Phi[0]) + return cir + + def block_encoding_matrix(self, signal_x: float) -> paddle.Tensor: + r"""generate a block encoding of :math:`P(x)` for verification + + Args: + x: input parameter + + Returns: + a block encoding unitary of :math:`P(x)` + + """ + assert -1 <= signal_x <= 1, "x must be in domain [-1, 1]" + matrix = np.array([[self.poly_p(signal_x), 1j * self.poly_q(signal_x) * np.sqrt(1 - signal_x ** 2)], + [1j * np.conj(self.poly_q(signal_x)) * np.sqrt(1 - signal_x ** 2), np.conj(self.poly_p(signal_x))]]) + return paddle.to_tensor(matrix) + \ No newline at end of file diff --git a/paddle_quantum/qsvt/qsp_utils.py b/paddle_quantum/qsvt/qsp_utils.py new file mode 100644 index 0000000..de41e06 --- /dev/null +++ b/paddle_quantum/qsvt/qsp_utils.py @@ -0,0 +1,155 @@ +# !/usr/bin/env python3 +# Copyright (c) 2022 Institute for Quantum Computing, Baidu Inc. All Rights Reserved. +# +# 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 +# +# http://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 numpy.polynomial.polynomial import Polynomial +import numpy as np +from typing import Tuple, Optional +import paddle +from scipy.linalg import expm + + +r""" + Tools for Polynomial & Tensor in qsvt Modules +""" + + +# ----------------------------- belows are polynomial tools ------------------------ + + +def clean_small_error(array: np.ndarray) -> np.ndarray: + r"""clean relatively small quantity + + Args: + array: target array + + Returns: + cleaned array + + """ + + def compare_and_clean(a: float, b: float) -> Tuple[float, float]: + r"""discard tiny or relatively tiny real or imaginary parts of elements""" + if a == 0 or b == 0: + return a, b + + a_abs = np.abs(a) + b_abs = np.abs(b) + + abs_error = 10 ** (-2) + rel_error = 10 ** (-4) + + if a_abs < abs_error and a_abs / b_abs < rel_error: + return 0, b + + if b_abs < abs_error and b_abs / a_abs < rel_error: + return a, 0 + + return a, b + + for i in range(len(array)): + real, imag = compare_and_clean(np.real(array[i]), np.imag(array[i])) + array[i] = real + 1j * imag + + return array + + +def poly_norm(poly: Polynomial, p: Optional[int] = 1) -> float: + r"""calculate the p-norm of a polynomial + + Args: + poly: the target polynomial + p: order of norm, 0 means to be infinity + + Returns: + p-norm of the target polynomial + + """ + coef = poly.coef + if p == 0: + return np.max(coef) + + coef_pow = list(map(lambda x: np.abs(x) ** p, coef)) + return np.power(np.sum(coef_pow), 1 / p) + + +def poly_real(poly: Polynomial) -> Polynomial: + r"""return the real part of a polynomial + + Args: + poly: the target polynomial + + Returns: + the real part of poly + + """ + return Polynomial(np.real(poly.coef)) + + +def poly_imag(poly: Polynomial) -> Polynomial: + r"""return the imaginary part of the polynomial + + Args: + poly: the target polynomial + + Returns: + the imaginary part of poly + + """ + return Polynomial(np.imag(poly.coef)) + + +# ----------------------------- belows are polynomial and tensor tools ------------------------ + + +def poly_matrix(poly: Polynomial, matrix_A: paddle.Tensor) -> paddle.Tensor: + r"""calculate the polynomial of a matrix, poly(matrix_A) + + Args: + poly: the polynomial + matrix_A: the matrix + + Returns: + poly(matrix_A) + + """ + coef = paddle.to_tensor(poly.coef) + k = poly.degree() + + N = matrix_A.shape[0] + I = paddle.eye(N) + + matrix = paddle.cast(paddle.zeros([N, N]), "complex128") + matrix_temp = I + for i in range(0, k + 1): + for j in range(i): + matrix_temp = matrix_temp @ matrix_A + matrix += coef[i] * matrix_temp + matrix_temp = I + + return matrix + + +def exp_matrix(t: float, matrix_A: paddle.Tensor) -> paddle.Tensor: + r"""calculate :math:`e^{itA}` + + Args: + t: time constant + A: the target matrix + + Returns: + :math:`e^{itA}` + + """ + matrix_A = matrix_A.cast("complex128").numpy() + return paddle.to_tensor(expm(1j * t * matrix_A), dtype="complex128") diff --git a/paddle_quantum/qsvt/qsvt.py b/paddle_quantum/qsvt/qsvt.py new file mode 100644 index 0000000..2402a47 --- /dev/null +++ b/paddle_quantum/qsvt/qsvt.py @@ -0,0 +1,189 @@ +# !/usr/bin/env python3 +# Copyright (c) 2022 Institute for Quantum Computing, Baidu Inc. All Rights Reserved. +# +# 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 +# +# http://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 paddle_quantum.ansatz.circuit import Circuit +from paddle_quantum import set_dtype +from paddle_quantum.linalg import is_hermitian, is_projector, is_unitary, dagger +from paddle_quantum.qinfo import partial_trace +import paddle +from math import log2, ceil +from scipy.linalg import expm +from numpy.polynomial.polynomial import Polynomial +from .qsp import reflection_based_quantum_signal_processing + + +""" + Libraries for Quantum Singular Value Transformations + referring to paper https://arxiv.org/abs/1806.01838 + +""" + + +def block_encoding_projector( + num_qubits: int, num_projected_qubits: int = None +) -> paddle.Tensor: + r"""Generate a projector that is used for simple block encoding + + Args: + num_qubits: number of total qubits + num_projected_qubits: number of projected qubits, default to be `num_qubits - 1` + + Returns: + :math:`\ket{0}\bra{0} \otimes I` + + """ + if num_projected_qubits is None: + num_projected_qubits = num_qubits - 1 + + m, n = num_projected_qubits, num_qubits + small_I = paddle.eye(2**m) + ket_0 = paddle.zeros([2 ** (n - m), 2 ** (n - m)]) + ket_0[0, 0] += 1 + return paddle.kron(ket_0, small_I) + + +def qubitization(proj: paddle.Tensor, phi: paddle.Tensor) -> Circuit: + r"""generate quantum circuit that is equivalent to :math:`e^{i \phi (2P - I)}` + + Args: + proj: orthogonal projector + phi: angle parameter + + Returns: + a quantum circuit that is equivalent to e^{i \phi (2P - I)}. + + """ + assert is_hermitian(proj) and is_projector(proj) + + # preparation + n = ceil(log2(proj.shape[0])) + dtype = "complex64" + X = paddle.to_tensor([[0.0, 1.0], [1.0, 0.0]], dtype=dtype) + + # define registers + system_register = [i for i in range(n)] + aux_register = [n] + + CPX_matrix = paddle.kron(proj, X) + paddle.kron( + paddle.eye(2**n) - proj, paddle.eye(2) + ) + + cir = Circuit() + cir.oracle(CPX_matrix, system_register + aux_register) + + cir.rz(aux_register, param=2 * phi) + + cir.oracle(CPX_matrix, system_register + aux_register) + + return cir + + +class QSVT(object): + def __init__( + self, poly_p: Polynomial, oracle: paddle.Tensor, m: int = None + ) -> None: + r"""Initialize a class that is used for QSP in multiple qubits + + Args: + poly_p: polynomial :math:`P(x)` + oracle: unitary :math:`U` which is a block encoding of a Hermitian :math:`X` + m: log2(dimension of :math:`X`), default to be n - 1 + + """ + + shape = oracle.shape + N = shape[0] + n = int(log2(N)) + + if m is None: + m = n - 1 + + assert is_unitary(oracle) # assert U is a 2^n x 2^n unitary + assert is_hermitian( + oracle[0 : 2**m, 0 : 2**m] + ) # assert the matrix block encoded by U is Hermitian + + self.I = paddle.eye(N) + self.n = n + self.U = oracle + + # find A + assert m <= N # make sure A is a block encoding of U + + # determine V + + self.V = block_encoding_projector(n, m) + + # determine phi + self.Phi = paddle.to_tensor( + reflection_based_quantum_signal_processing(poly_p), dtype="float32" + ) + + def block_encoding_matrix(self) -> paddle.Tensor: + r"""provide the matrix of a block encoding of :math:`P(X)` + + Returns: + block encoding of :math:`P(X)` in matrix form + + """ + k = len(self.Phi) + + matrix = self.I + Vz = 2 * self.V - self.I + + for i in range(k): + VRz = paddle.to_tensor(expm((Vz * 1j * self.Phi[i]).numpy())) + + if i % 2 != k % 2: + matrix = matrix @ VRz @ self.U + else: + matrix = matrix @ VRz @ dagger(self.U) + + return matrix + + def block_encoding_circuit(self) -> Circuit: + r"""generate a block encoding of :math:`P(X)` by quantum circuit + + Returns: + a quantum circuit of unitary that is the block encoding of :math:`P(X)` + + """ + set_dtype("complex64") + Phi = paddle.cast(self.Phi, dtype="float32") + U = self.U + system_register = [i for i in range(self.n)] + + cir = Circuit(self.n + 1) + k = len(self.Phi) + for i in range(k): + if i % 2 == 0: + cir.oracle(U, system_register) + else: + cir.oracle(dagger(U), system_register) + cir.extend(qubitization(self.V, Phi[-i - 1])) + + return cir + + def block_encoding_unitary(self) -> paddle.Tensor: + r"""generate the unitary of above circuit for verification + + Returns: + a block encoding unitary of :math:`P(X)` + + """ + U = self.block_encoding_circuit().unitary_matrix() + zero_state = paddle.zeros([2**1, 2**1]) + zero_state[0, 0] += 1 + U = U @ paddle.kron(paddle.eye(2**self.n), zero_state) + return partial_trace(U, 2**self.n, 2**1, A_or_B=2) diff --git a/paddle_quantum/shadow.py b/paddle_quantum/shadow.py index 182d9c1..f00f422 100644 --- a/paddle_quantum/shadow.py +++ b/paddle_quantum/shadow.py @@ -43,6 +43,11 @@ def shadow_sample( mode: Representation form of the input quantum state. hamiltonian: A ``Hamiltonian`` object representing the observable to be measured. Defaults to ``None``. method: Method for sampling random Pauli operators, which should be one of ``'CS'``, ``'LBCS'``, and ``'APS'``. Defaults to ``'CS'``. + + Raises: + ValueError: Hamiltonian has a bad form + NotImplementedError: The backend of ``state`` should be ``StateVector`` or ``DensityMatrix`` + Returns: Randomly chosen Pauli operators and their corresponding measurement result in a list of shape ``(sample_shots, 2)``. diff --git a/paddle_quantum/state/common.py b/paddle_quantum/state/common.py index ca408cf..19f1044 100644 --- a/paddle_quantum/state/common.py +++ b/paddle_quantum/state/common.py @@ -172,7 +172,7 @@ def bell_diagonal_state(prob: List[float]) -> State: prob: The prob of each bell state. Raises: - Exception: The state should bu a pure state if the backend is state_vector. + Exception: The state should be a pure state if the backend is state_vector. NotImplementedError: If the backend is wrong or not implemented. Returns: @@ -325,7 +325,7 @@ def completely_mixed_computational(num_qubits: int) -> State: num_qubits: The number of qubits contained in the quantum state. Raises: - Exception: The state should bu a pure state if the backend is state_vector. + Exception: The state should be a pure state if the backend is state_vector. NotImplementedError: If the backend is wrong or not implemented. Returns: @@ -365,7 +365,7 @@ def r_state(prob: float) -> State: prob: The parameter of the R-state to be generated. It should be in :math:`[0,1]` . Raises: - Exception: The state should bu a pure state if the backend is state_vector. + Exception: The state should be a pure state if the backend is state_vector. NotImplementedError: If the backend is wrong or not implemented. Returns: @@ -410,7 +410,7 @@ def s_state(prob: float) -> State: prob: The parameter of the S-state to be generated. It should be in :math:`[0,1]` . Raises: - Exception: The state should bu a pure state if the backend is state_vector. + Exception: The state should be a pure state if the backend is state_vector. NotImplementedError: If the backend is wrong or not implemented. Returns: @@ -455,7 +455,7 @@ def isotropic_state(num_qubits: int, prob: float) -> State: prob: The parameter of the isotropic state to be generated. It should be in :math:`[0,1]` . Raises: - Exception: The state should bu a pure state if the backend is state_vector. + Exception: The state should be a pure state if the backend is state_vector. NotImplementedError: If the backend is wrong or not implemented. Returns: diff --git a/paddle_quantum/state/state.py b/paddle_quantum/state/state.py index 10dd262..daeffa1 100644 --- a/paddle_quantum/state/state.py +++ b/paddle_quantum/state/state.py @@ -39,7 +39,10 @@ class State(object): num_qubits: The number of qubits contained in the quantum state. Defaults to ``None``, which means it will be inferred by the data. backend: Used to specify the backend used. Defaults to ``None``, which means to use the default backend. dtype: Used to specify the data dtype of the data. Defaults to ``None``, which means to use the default data type. - + + Raises: + Exception: The shape of the data is not correct. + NotImplementedError: If the backend is wrong or not implemented. """ def __init__( self, data: Union[paddle.Tensor, np.ndarray, QCompute.QEnv], num_qubits: Optional[int] = None, @@ -55,6 +58,8 @@ def __init__( data = paddle.cast(data, dtype) self.dtype = dtype if dtype is not None else paddle_quantum.get_dtype() if self.backend == Backend.StateVector: + if data.shape[-1] == 1: + data = paddle.squeeze(data) self.data = data if num_qubits is not None: if data.shape[-1] != 2 ** num_qubits: @@ -91,6 +96,33 @@ def __init__( raise NotImplementedError self.num_qubits = num_qubits + @property + def ket(self) -> paddle.Tensor: + r""" return the ket form in state_vector backend + + Returns: + ket form of the state + + """ + if self.backend != Backend.StateVector: + raise Exception("the backend must be in state_vector to raise the ket form of state") + + return self.data.reshape([2 ** self.num_qubits, 1]) + + @property + def bra(self) -> paddle.Tensor: + r""" return the bra form in state_vector backend + + Returns: + bra form of the state + + """ + if self.backend != Backend.StateVector: + raise Exception("the backend must be in state_vector to raise the bra form of state") + + return paddle.conj(self.data.reshape([1, 2 ** self.num_qubits])) + + def numpy(self) -> np.ndarray: r"""get the data in numpy. @@ -132,6 +164,9 @@ def expec_val(self, hamiltonian: Hamiltonian, shots: Optional[int] = 0) -> float hamiltonian: Input observable. shots: Number of measurement shots. + Raises: + NotImplementedError: If the backend is wrong or not implemented. + Returns: The expectation value of the input observable for the quantum state. """ @@ -148,14 +183,6 @@ def expec_val(self, hamiltonian: Hamiltonian, shots: Optional[int] = 0) -> float [1 / math.sqrt(2), -1j / math.sqrt(2)], [1 / math.sqrt(2), 1j / math.sqrt(2)], ], dtype=self.dtype) - # gate_for_x = paddle.to_tensor([ - # [1 / math.sqrt(2), 1j / math.sqrt(2)], - # [1j / math.sqrt(2), 1 / math.sqrt(2)], - # ], dtype=self.dtype) - # gate_for_y = paddle.to_tensor([ - # [1 / math.sqrt(2), -1j / math.sqrt(2)], - # [-1j / math.sqrt(2), 1 / math.sqrt(2)], - # ], dtype=self.dtype) if self.backend == Backend.StateVector: simulator = state_vector.unitary_transformation elif self.backend == Backend.DensityMatrix: @@ -210,11 +237,26 @@ def measure(self, shots: Optional[int] = 0, qubits_idx: Optional[Union[Iterable[ qubits_idx: The index of the qubit to be measured. Defaults to ``None``, which means all qubits are measured. plot: Whether to draw the measurement result plot. Defaults to ``False`` which means no plot. + Raises: + Exception: The number of shots should be greater than 0. + NotImplementedError: If the backend is wrong or not implemented. + NotImplementedError: The qubit index is wrong or not supported. Returns: Measurement results """ - if self.backend == paddle_quantum.Backend.StateVector: + if self.backend == paddle_quantum.Backend.QuLeaf: + if shots == 0: + raise Exception("The quleaf server requires the number of shots to be greater than 0.") + state_data = self.data + QCompute.MeasureZ(*state_data.Q.toListPair()) + result = state_data.commit(shots, fetchMeasure=True)['counts'] + result = {''.join(reversed(key)): value for key, value in result.items()} + if qubits_idx is not None: + # new_result = [(self.__process_string(key, qubits_idx), value) for key, value in result.items()] + # result = self.__process_similiar(new_result) + pass + elif self.backend == paddle_quantum.Backend.StateVector: prob_amplitude = paddle.multiply(paddle.conj(self.data), self.data).real() elif self.backend == paddle_quantum.Backend.DensityMatrix: prob_amplitude = paddle.zeros([2 ** self.num_qubits]) diff --git a/paddle_quantum/trotter.py b/paddle_quantum/trotter.py index 088fc43..e4d6fe3 100644 --- a/paddle_quantum/trotter.py +++ b/paddle_quantum/trotter.py @@ -63,6 +63,12 @@ def construct_trotter_circuit( and ``'even_odd'`` grouping methods. Defaults to None. coefficient: Custom coefficients corresponding to terms of the Hamiltonian. Only works for ``method='custom'``. Defaults to None. permutation: Custom permutation of the Hamiltonian. Only works for ``method='custom'``. Defaults to None. + + Raises: + ValueError: The order of the trotter-suzuki decomposition should be either 1, 2 or 2k (k an integer) + ValueError: Shape of the permutation and coefficient array don\'t match + ValueError: Grouping method ``grouping`` is not supported, valid key words: 'xyz', 'even_odd' + ValueError: The method ``method`` is not supported, valid method keywords: 'suzuki', 'custom' Hint: For a more detailed explanation of how this function works, users may refer to the tutorials on Paddle Quantum's website: https://qml.baidu.com/tutorials/overview.html. @@ -155,7 +161,7 @@ def check_input_legitimacy(arg_in): def __get_suzuki_num(order): - r"""计算阶数为 order 的 suzuki product formula 的 trotter 数。 + r"""compute the Trotter number of the suzuki product formula with the order of ``order`` """ if order == 1 or order == 2: n_suzuki = order @@ -169,32 +175,34 @@ def __get_suzuki_num(order): def __sort_pauli_word(pauli_word, site): - r"""将 pauli_word 按照 site 的大小进行排列,并同时返回排序后的 pauli_word 和 site。 + r"""reordering the ``pauli_word`` by the value of ``site``, return the new pauli_word and site after sort. Note: - 这是一个内部函数,一般你不需要直接使用它。 + This is an intrinsic function, user do not need to call this directly """ sort_index = np.argsort(np.array(site)) return ''.join(np.array(list(pauli_word))[sort_index].tolist()), np.array(site)[sort_index] def _add_trotter_block(circuit, tau, grouped_hamiltonian, order): - r"""添加一个 trotter 块,i.e. :math:`e^{-iH\tau}`,并使用 Trotter-Suzuki 分解对其进行展开。 + r"""add a Trotter block, i.e. :math:`e^{-iH\tau}`, use Trotter-Suzuki decomposition to expand it. Args: - circuit (Circuit): 需要添加 trotter 块的电路 - tau (float or tensor): 该 trotter 块的演化时间 - grouped_hamiltonian (list): 一个由 Hamiltonian 对象组成的列表,该函数会默认该列表中的哈密顿量为 Trotter-Suzuki 展开的基本项 - order (int): Trotter-Suzuki 展开的阶数 + circuit: target circuit to add the Trotter block + tau: evolution time of this Trotter block + grouped_hamiltonian: list of Hamiltonian objects, this function uses these as the basic terms of Trotter-Suzuki expansion by default + order: The order of Trotter-Suzuki expansing - Note (关于 grouped_hamiltonian 的使用方法): - 以二阶的 trotter-suzki 分解 S2(t) 为例,若 grouped_hamiltonian = [H_1, H_2],则会按照 - (H_1, t/2)(H_2, t/2)(H_2, t/2)(H_1, t/2) 的方法进行添加 trotter 电路 - 特别地,若用户没有预先对 Hamiltonian 进行 grouping 的话,传入一个单个的 Hamiltonian 对象,则该函数会按照该 Hamiltonian - 的顺序进行正则(canonical)的分解:依然以二阶 trotter 为例,若传入单个 H,则添加 (H[0:-1:1], t/2)(H[-1:0:-1], t/2) 的电路 + Note: + About how to use grouped_hamiltonian: + For example, consider Trotter-Suzuki decomposition of the second order S2(t), if grouped_hamiltonian = [H_1, H_2], it will add Trotter circuit + with (H_1, t/2)(H_2, t/2)(H_2, t/2)(H_1, t/2). Specifically, if user does not pre-grouping the Hamiltonians and put a single Hamiltonian object, + this function will make canonical decomposition according to the order of this Hamiltonian: for second order, if put a single Hamiltonian H, + the circuit will be added with (H[0:-1:1], t/2)(H[-1:0:-1], t/2) Warning: - 本函数一般情况下为内部函数,不会对输入的合法性进行检测和尝试修正。推荐使用 construct_trotter_circuit() 来构建时间演化电路 + This function is usually an intrinsic function, it does not check or correct the input. + To build time evolution circuit, function ``construct_trotter_circuit()`` is recommanded """ if order == 1: __add_first_order_trotter_block(circuit, tau, grouped_hamiltonian) @@ -206,18 +214,19 @@ def _add_trotter_block(circuit, tau, grouped_hamiltonian, order): def _add_custom_block(circuit, tau, grouped_hamiltonian, custom_coefficients, permutation): - r""" 添加一个自定义形式的 trotter 块 + r"""Add a custom Trotter block Args: - circuit (Circuit)): 需要添加 trotter 块的电路 - tau (float or tensor): 该 trotter 块的演化时间 - grouped_hamiltonian (list): 一个由 Hamiltonian 对象组成的列表,该函数会默认该列表中的哈密顿量为 trotter-suzuki 展开的基本项 - order (int): trotter-suzuki 展开的阶数 - permutation (np.ndarray): 自定义置换 - custom_coefficients (np.ndarray or Tensor): 自定义系数 + circuit: target circuit to add the Trotter block + tau: evolution time of this Trotter block + grouped_hamiltonian: list of Hamiltonian objects, this function uses these as the basic terms of Trotter-Suzuki expansion by default + order: The order of Trotter-Suzuki expansing + permutation: custom permutation + custom_coefficients: custom coefficients Warning: - 本函数一般情况下为内部函数,不会对输入的合法性进行检测和尝试修正。推荐使用 construct_trotter_circuit() 来构建时间演化电路 + This function is usually an intrinsic function, it does not check or correct the input. + To build time evolution circuit, function ``construct_trotter_circuit()`` is recommanded """ # combine the grouped hamiltonian into one single hamiltonian @@ -235,10 +244,10 @@ def _add_custom_block(circuit, tau, grouped_hamiltonian, custom_coefficients, pe def __add_first_order_trotter_block(circuit, tau, grouped_hamiltonian, reverse=False, optimization=False): - r""" 添加一阶 trotter-suzuki 分解的时间演化块 + r"""Add a time evolution block of the first order Trotter-Suzuki decompositon Notes: - 这是一个内部函数,你不需要使用它 + This is an intrinsic function, user do not need to call this directly """ if not reverse: for hamiltonian in grouped_hamiltonian: @@ -338,20 +347,20 @@ def optimal_circuit(circuit: paddle_quantum.ansatz.Circuit, theta: Union[paddle. def __add_second_order_trotter_block(circuit, tau, grouped_hamiltonian): - r""" 添加二阶 trotter-suzuki 分解的时间演化块 + r"""Add a time evolution block of the second order Trotter-Suzuki decompositon Notes: - 这是一个内部函数,你不需要使用它 + This is an intrinsic function, user do not need to call this directly """ __add_first_order_trotter_block(circuit, tau / 2, grouped_hamiltonian) __add_first_order_trotter_block(circuit, tau / 2, grouped_hamiltonian, reverse=True) def __add_higher_order_trotter_block(circuit, tau, grouped_hamiltonian, order): - r""" 添加高阶(2k 阶) trotter-suzuki 分解的时间演化块 + r"""Add a time evolution block of the higher order (2k) Trotter-Suzuki decompositon Notes: - 这是一个内部函数,你不需要使用它 + This is an intrinsic function, user do not need to call this directly """ assert order % 2 == 0 p_values = get_suzuki_p_values(order) @@ -423,13 +432,13 @@ def add_n_pauli_gate( def __group_hamiltonian_xyz(hamiltonian): - r""" 将哈密顿量拆分成 X、Y、Z 以及剩余项四个部分,并返回由他们组成的列表 + r"""Decompose the Hamiltonian as X, Y, Z, and remainder term, return the list of them. Args: - hamiltonian (Hamiltonian): Paddle Quantum 中的 Hamiltonian 类 + hamiltonian: Hamiltonian class in Paddle Quantum Notes: - X、Y、Z 项分别指的是该项的 Pauli word 只含有 X、Y、Z,例如 'XXXY' 就会被分类到剩余项 + X, (Y, Z) means the terms whose Pauli word only include X, (Y, Z). For example, 'XXXY' would be a remainder term. """ grouped_hamiltonian = [] coeffs, pauli_words, sites = hamiltonian.decompose_with_sites() @@ -455,14 +464,16 @@ def __group_hamiltonian_xyz(hamiltonian): def __group_hamiltonian_even_odd(hamiltonian): - r""" 将哈密顿量拆分为奇数项和偶数项两部分 + r"""Decompose the Hamiltonian into odd and even parts. Args: - hamiltonian (Hamiltonian): + hamiltonian (Hamiltonian): Hamiltonian class in Paddle Quantum Warning: - 注意该分解方法并不能保证拆分后的奇数项和偶数项内部一定相互对易,因此不正确的使用该方法反而会增加 trotter 误差。 - 请在使用该方法前检查哈密顿量是否为可以进行奇偶分解:例如一维最近邻相互作用系统的哈密顿量可以进行奇偶分解 + Note this decomposition cannot make sure the mutual commutativity among odd terms or even terms. Use this method incorrectly would cause larger + Trotter error. + Please check whether Hamiltonian could be odd-even decomposed before you call this method. For example, 1-D Hamiltonian with nearest neighbor + interaction could be odd-even decomposed. """ grouped_hamiltonian = [] coeffs, pauli_words, sites = hamiltonian.decompose_with_sites() diff --git a/paddle_quantum/visual.py b/paddle_quantum/visual.py index e58841a..4496c9f 100644 --- a/paddle_quantum/visual.py +++ b/paddle_quantum/visual.py @@ -497,6 +497,10 @@ def plot_density_matrix_graph(density_matrix: paddle_quantum.State, size: Option Args: density_matrix: The state vector or density matrix of quantum state with multi qubits, requiring the number of qubits greater than 1 size: Bar width, between 0 and 1, default is ``0.3``. + + Raises: + TypeError: Expected density_matrix to be np.ndarray or paddle.Tensor or paddle_quantum.State + ValueError: Expected density matrix dim0 equal to dim1 """ if not isinstance( density_matrix, (np.ndarray, paddle.Tensor, paddle_quantum.State) diff --git a/requirements.txt b/requirements.txt index 26e4707..59a1e12 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,8 @@ -qcompute -paddlepaddle>=2.2.0 -scipy +paddlepaddle<=2.3.0 +scipy>=1.8.1 +protobuf<=3.20.1 networkx>=2.5 +qcompute matplotlib tqdm openfermion diff --git a/setup.py b/setup.py index f329267..43569ad 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ setuptools.setup( name='paddle-quantum', - version='2.2.0', + version='2.2.1', author='Institute for Quantum Computing, Baidu INC.', author_email='qml@baidu.com', description='Paddle Quantum is a quantum machine learning (QML) toolkit developed based on Baidu PaddlePaddle.', @@ -44,6 +44,7 @@ 'paddle_quantum.operator', 'paddle_quantum.state', 'paddle_quantum.qchem', + 'paddle_quantum.qsvt', 'paddle_quantum.mbqc', 'paddle_quantum.GIBBS', 'paddle_quantum.GIBBS.example', @@ -78,10 +79,11 @@ 'paddle_quantum.mbqc.VQSVD.example': ['*.txt'], }, install_requires=[ - 'paddlepaddle>=2.2.0', - 'qcompute', - 'scipy', + 'paddlepaddle<=2.3.0', + 'scipy>=1.8.1', + 'protobuf<=3.20.1', 'networkx>=2.5', + 'qcompute', 'matplotlib>=3.3.0', 'tqdm', 'openfermion', diff --git a/tutorials/combinatorial_optimization/TSP_CN.ipynb b/tutorials/combinatorial_optimization/TSP_CN.ipynb index 6b1e602..368dfe6 100644 --- a/tutorials/combinatorial_optimization/TSP_CN.ipynb +++ b/tutorials/combinatorial_optimization/TSP_CN.ipynb @@ -565,7 +565,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3.8.13 ('new_pq')", "language": "python", "name": "python3" }, @@ -593,6 +593,11 @@ "toc_position": {}, "toc_section_display": true, "toc_window_display": false + }, + "vscode": { + "interpreter": { + "hash": "58b83104798ee1b81625bc6249d4d66f2bacd4a7d411a9b7a27bac0b4765adf2" + } } }, "nbformat": 4, diff --git a/tutorials/combinatorial_optimization/TSP_EN.ipynb b/tutorials/combinatorial_optimization/TSP_EN.ipynb index bcfb344..4b1adad 100644 --- a/tutorials/combinatorial_optimization/TSP_EN.ipynb +++ b/tutorials/combinatorial_optimization/TSP_EN.ipynb @@ -574,7 +574,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3.8.13 ('new_pq')", "language": "python", "name": "python3" }, @@ -602,6 +602,11 @@ "toc_position": {}, "toc_section_display": true, "toc_window_display": false + }, + "vscode": { + "interpreter": { + "hash": "58b83104798ee1b81625bc6249d4d66f2bacd4a7d411a9b7a27bac0b4765adf2" + } } }, "nbformat": 4, diff --git a/tutorials/locc/EntanglementDistillation_DEJMPS_CN.ipynb b/tutorials/locc/EntanglementDistillation_DEJMPS_CN.ipynb index 657868e..aabb816 100644 --- a/tutorials/locc/EntanglementDistillation_DEJMPS_CN.ipynb +++ b/tutorials/locc/EntanglementDistillation_DEJMPS_CN.ipynb @@ -28,7 +28,7 @@ "\\end{align*}\n", "$$\n", "\n", - "$A$ 和 $B$ 代表的是共享纠缠对的双方 Alice 和 Bob。根据贝尔对角态(Bell-digonal state)的定义,在贝尔态作为基底的密度矩阵可以表示为如下对角形式,\n", + "$A$ 和 $B$ 代表的是共享纠缠对的双方 Alice 和 Bob。根据贝尔对角态(Bell-diagonal state)的定义,在贝尔态作为基底的密度矩阵可以表示为如下对角形式,\n", "\n", "$$\n", "\\rho_{\\text{diag}} = p_1 | \\Phi^+\\rangle \\langle \\Phi^+ | + p_2 | \\Psi^+\\rangle \\langle \\Psi^+ | + \n", @@ -324,7 +324,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "Python 3", "language": "python", "name": "python3" }, @@ -338,7 +338,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.13" + "version": "3.8.3" }, "toc": { "base_numbering": 1, diff --git a/tutorials/machine_learning/EncodingAnalysis_CN.ipynb b/tutorials/machine_learning/EncodingAnalysis_CN.ipynb new file mode 100644 index 0000000..6a408e8 --- /dev/null +++ b/tutorials/machine_learning/EncodingAnalysis_CN.ipynb @@ -0,0 +1,643 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 数据编码分析\n", + "\n", + "*Copyright (c) 2022 Institute for Quantum Computing, Baidu Inc. All Rights Reserved.*" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 概览\n", + "\n", + "目前很多研究表明变分量子算法(Variational Quantum Algorithms, VQA)有望在近期量子设备上展现出量子优势,其中备受关注的是用其解决监督学习任务。VQA 通常也被称为参数化量子电路 (Parameterized Quantum Circuit, PQC),在本教程中将其分为数据编码电路和量子神经网络两部分。数据编码电路将经典信息编码为量子态。编码后量子态的质量直接影响了后续的分类效果。从核函数的角度分析数据编码,不同的编码方式就对应不同的核函数,其对量子态的分类起着决定性作用 [1,2]。从统计学习理论的角度考察数据编码,它通常决定了算法的表达能力和泛化能力 [3,4]。 因此,我们有必要对数据编码方式进行系统的分析。最近文献 [5] 得到了一些进展,从量子信息的角度严格的分析了数据编码电路的宽度和深度对编码后量子态的影响。\n", + "\n", + "接下来本教程围绕文献 [5] 展开,主要分为原理和 Paddle Quantum 实现两部分:先介绍基本概念与主要结论,然后给出具体的实现方案。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 原理" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 基本概念\n", + "\n", + "图 1 是将经典数据编码为量子态的流程图。假设经典数据是分布 $D$ 的独立同分布采样,每个经典数据为 $\\pmb x$, 通过数据编码电路后变成了量子态 $\\rho(\\pmb x)$。这里引入分布 $D$ 上的**平均量子态**的概念,即 \n", + "\n", + "$$\n", + "\\bar{\\rho}:=\\mathbf{E}[\\rho(\\pmb x)]. \\tag{1}\n", + "$$\n", + "\n", + "在给定 $M$ 条数据组成的经典数据集 $S$ 时,我们通常使用平均值 \n", + "\n", + "$$\n", + "\\bar{\\rho}:=\\frac{1}{M}\\sum_{j=1}^M\\rho(\\pmb x_j) \\tag{2}\n", + "$$ \n", + "\n", + "近似 $S$ 的平均量子态。\n", + "\n", + "![illustration](figures/EncodingAnalysis-fig-illustration.png \"图 1:经典数据编码为量子态的流程图。\")\n", + "\n", + "有很多方法可以衡量量子态之间的距离,常用的有迹距离、保真度、**Petz-Rényi 散度**等等。在本教程中使用 Petz-Rényi 散度作为不同量子态之间距离的度量。具体的,量子态 $\\rho_0$ 和 $\\rho_1$ 的Petz-Rényi 散度定义为\n", + "\n", + "$$\n", + "D_2(\\rho_0||\\rho_1)=logTr[\\rho_0^2\\rho_1^{-1}]. \\tag{3}\n", + "$$\n", + "\n", + "$D_2$ 的取值越小表示两个量子态越接近。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 主要结论\n", + "\n", + "接下来我们借助 Petz-Rényi 散度这一度量工具考察编码后的平均量子态与最大混合态之间的距离。不难想到这个距离应该与经典数据集本身的性质和数据编码方式有关。具体的,假设经典数据集的每个特征维度满足一定的独立性且标准差至少为 $\\sigma$, 那么对于如图 2 所示的编码电路(宽度和深度分别为 $n$ 和 $D$)有如下不等式成立\n", + "\n", + "$$\n", + "D_2(\\bar{\\rho}||I/2^n)\\leq log(1+(2^n-1)2^{-D\\sigma^2}), \\tag{4}\n", + "$$\n", + "\n", + "其中 $I/2^n$ 为 $n$ 比特的最大混合态,$I$ 为单位矩阵。\n", + "\n", + "![encoding-u3](figures/EncodingAnalysis-fig-u3_circuit.png \"图 2:更一般的数据编码电路。其中 Etg 表示控制非门和 CZ 门的任意组合。\")\n", + "\n", + "更严格的定理描述和证明可以参考文献 [5],在本教程中我们主要体会这一结论意味着什么以及有那些启发:\n", + "\n", + "* 上述结论意味着随着电路深度增加,编码后的平均量子态以指数的速度趋向于最大混合态。例如一个二分类的数据集,0类和1类的平均量子态最终都趋向于最大混合态,那么从量子信息的角度看将无法区分这两类的平均量子态,也即从平均意义下无法区分经典数据特征。\n", + "\n", + "* 当经典数据特征维度很高时(例如图片数据)使用角度编码可能不是一个合适的选择,因为这很容易导致很深的数据编码电路,而加宽电路又会遇到贫瘠高原问题,这严重限制了 VQA 的能力。因此经典数据在输入给电路之前需要对其做一些降维操作。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Paddle Quantum 实现 \n", + "\n", + "这一节中主要使用 Paddle Quantum 在 MNIST 数据集上完成两个实验:\n", + "- 考察平均量子态和最大混合态之间的 Petz-Rényi 散度随着数据编码电路的深度的变化趋势;\n", + "- 考察随着数据编码电路的加深分类准确率的变化。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 首先导入相关的包" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# 导入 numpy、paddle 和 paddle_quantum\n", + "import numpy as np\n", + "import paddle\n", + "import paddle_quantum\n", + "\n", + "# 构建量子电路\n", + "from paddle_quantum.ansatz import Circuit\n", + "\n", + "# 一些用到的函数\n", + "from numpy import pi as PI\n", + "from paddle import matmul, transpose, reshape, real, argmax, cast, mean, concat, real\n", + "from paddle_quantum.qinfo import pauli_str_to_matrix \n", + "from paddle_quantum.linalg import dagger\n", + "import paddle.nn.functional as F\n", + "\n", + "# 数据集工具包\n", + "from paddle_quantum.dataset import MNIST\n", + "\n", + "# 作图与计算时间\n", + "from matplotlib import pyplot as plt\n", + "from pylab import axes\n", + "import time" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 参数化量子电路\n", + "\n", + "![encoding-ry](figures/EncodingAnalysis-fig-ry_circuit.png \"图 3:参数化量子电路。\")\n", + "\n", + "图 3 中左侧红色框为数据编码电路,是图 2 的一个特例,右侧蓝色框为量子神经网络。其中数据编码电路由 $R_y$ 旋转门和控制非门组成,具体的电路深度 $D$ 由数据的特征维度决定。例如在本教程中将使用 MNIST 数据集,由于图片被降采样为 16 维的特征向量,因此在设计实验时选择量子比特数为 8、6、4、3、2 ,则默认情况下对应的电路深度为 2、3、4、6、8, 大于 16 维的位置用 0 填充,即作用 $R_y(0)$. 量子神经网络部分由单比特的通用门 $U3$ 和控制非门构成。具体的电路深度 $L$ 可以自由设置。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 将经典数据编码为量子态\n", + "\n", + "这里需要量桨提供的数据集处理工具 `dataset`。使用图 2 所示的数据编码电路将 MNIST 数据中的经典数据编码为量子态并保存起来,以便将其输入给量子神经网络进行训练。" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "train_data, test_data = [], []\n", + "\n", + "# 二分类的手写数字类别\n", + "classes = [3,6]\n", + "\n", + "training_data_num = 1000\n", + "testing_data_num = 200\n", + "qubit_num_list = [8, 6, 4, 3, 2]\n", + "\n", + "# 使用不同的宽度和深度的电路编码经典数据并保存下来\n", + "for qubit_num in qubit_num_list:\n", + " \n", + " # 训练数据集\n", + " train_dataset = MNIST(mode='train', encoding='real_entangled_encoding', num_qubits=qubit_num,\n", + " classes=classes,\n", + " data_num=training_data_num,\n", + " downscaling_method='resize', target_dimension=16,\n", + " need_relabel=True, return_state=True)\n", + "\n", + " # 验证数据集\n", + " val_dataset = MNIST(mode='test', encoding='real_entangled_encoding', num_qubits=qubit_num,\n", + " classes=classes,\n", + " data_num=testing_data_num,\n", + " downscaling_method='resize', target_dimension=16,\n", + " need_relabel=True, return_state=True)\n", + "\n", + " # 获取数据集的输入和标签\n", + " train_x, train_y = train_dataset.quantum_image_states, train_dataset.labels\n", + " test_x, test_y = val_dataset.quantum_image_states, val_dataset.labels\n", + " train_data.append((train_x, train_y))\n", + " test_data.append((test_x, test_y))" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[1000, 256]\n", + "(1000,)\n", + "[200, 256]\n", + "(200,)\n" + ] + } + ], + "source": [ + "print(train_data[0][0].shape)\n", + "print(train_data[0][1].shape)\n", + "print(test_data[0][0].shape)\n", + "print(test_data[0][1].shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 构建量子神经网络" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# 搭建整个优化流程图\n", + "class Net(paddle.nn.Layer):\n", + " \"\"\"\n", + " 创建模型训练网络\n", + " \"\"\"\n", + " def __init__(self, n, depth):\n", + " # 初始化部分,通过n, depth给出初始电路\n", + " super(Net, self).__init__()\n", + " self.n = n\n", + " self.depth = depth\n", + " \n", + " self.circuit = Circuit(n)\n", + " # 先搭建广义的旋转层\n", + " for i in range(n):\n", + " self.circuit.rz(qubits_idx=i)\n", + " self.circuit.ry(qubits_idx=i)\n", + " self.circuit.rz(qubits_idx=i)\n", + "\n", + " # 默认深度为 depth = 1\n", + " # 对每一层搭建电路\n", + " for d in range(3, depth + 3):\n", + " # 搭建纠缠层\n", + " for i in range(n-1):\n", + " self.circuit.cnot(qubits_idx=[i, i + 1])\n", + " self.circuit.cnot(qubits_idx=[n-1, 0])\n", + " # 对每一个量子比特搭建Ry\n", + " for i in range(n):\n", + " self.circuit.ry(qubits_idx=i)\n", + "\n", + " # 定义前向传播机制、计算损失函数 和交叉验证正确率\n", + " def forward(self, state_in, label):\n", + " \"\"\"\n", + " 输入:state_in:输入量子态,shape: [-1, 1, 2^n] -- 此教程中为[BATCH, 1, 2^n]\n", + " label:输入量子态对应标签,shape: [-1, 1]\n", + " 计算损失函数:交叉熵损失函数\n", + " \"\"\"\n", + " # 按照随机初始化的参数 theta \n", + " Utheta = self.circuit.unitary_matrix()\n", + "\n", + " # 因为 Utheta是学习到的,我们这里用行向量运算来提速而不会影响训练效果\n", + " state_out = matmul(state_in, Utheta) # 维度 [-1, 1, 2 ** n]\n", + "\n", + " # 测量得到泡利 Z 算符的期望值 \n", + " Ob1 = paddle.to_tensor(pauli_str_to_matrix([[1.0, 'z0']], self.n))\n", + " E_Ob1 = matmul(matmul(state_out, Ob1), transpose(paddle.conj(state_out), perm=[0, 2, 1]))\n", + " E_Ob1_re = reshape(real(E_Ob1), [-1, 1])\n", + "\n", + " Ob2 = paddle.to_tensor(pauli_str_to_matrix([[1.0, 'x0']], self.n))\n", + " E_Ob2 = matmul(matmul(state_out, Ob2), transpose(paddle.conj(state_out), perm=[0, 2, 1]))\n", + " E_Ob2_re = reshape(real(E_Ob2), [-1, 1])\n", + "\n", + " outputs = concat([E_Ob1_re, E_Ob2_re], axis=-1)\n", + "\n", + " # 计算损失函数和准确率\n", + " loss = F.cross_entropy(outputs, label)\n", + " # validation accuracy\n", + " acc = mean(cast(argmax(outputs, axis=-1) == label, \"float32\"))\n", + " \n", + " return loss, acc" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# 定义一个分类器\n", + "def QClassifier(train_x, train_y, test_x, test_y, N, D, EPOCH, LR, BATCH, seed=0):\n", + " \"\"\"\n", + " 量子二分类器\n", + " \"\"\"\n", + " train_y = paddle.to_tensor(train_y, dtype=\"int64\")\n", + " test_y = paddle.to_tensor(test_y, dtype=\"int64\")\n", + "\n", + " N_train, in_dim = train_x.shape\n", + " \n", + " # 定义优化图,Net是用户定义的量子神经网络\n", + " paddle.seed(0)\n", + " net = Net(n=N, depth=D)\n", + "\n", + " # 一般来说,我们利用Adam优化器来获得相对好的收敛\n", + " # 当然你可以改成SGD或者是RMSprop\n", + " opt = paddle.optimizer.Adam(learning_rate=LR, parameters=net.parameters())\n", + "\n", + " # 优化循环\n", + " for ep in range(EPOCH):\n", + " for itr in range(N_train // BATCH):\n", + " input_state = train_x[itr * BATCH:(itr + 1) * BATCH]\n", + " input_state = reshape(input_state, [-1, 1, 2 ** N])\n", + " label = train_y[itr * BATCH:(itr + 1) * BATCH]\n", + "\n", + " test_input_state = reshape(test_x, [-1, 1, 2 ** N])\n", + "\n", + " # 前向传播计算损失函数\n", + " train_loss, train_acc = net(state_in=input_state, label=label)\n", + "\n", + " if itr % 3 == 0:\n", + " # 计算测试集上的正确率\n", + " loss_useless, test_acc = net(state_in=test_input_state, label=test_y)\n", + " print(\"epoch:\", ep, \"iter:\", itr,\n", + " \"train loss: %.4f\" % train_loss.numpy(),\n", + " \"train acc: %.4f\" % train_acc,\n", + " \"test acc: %.4f\" % test_acc)\n", + "\n", + " # 反向传播极小化损失函数\n", + " train_loss.backward()\n", + " opt.minimize(train_loss)\n", + " opt.clear_grad()\n", + " \n", + " # 返回测试集的分类准确率\n", + " _, test_acc = net(state_in=test_input_state, label=test_y) \n", + " \n", + " return test_acc.numpy()" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "***************************** qubit num : 8 *****************************\n", + "epoch: 0 iter: 0 train loss: 0.6913 train acc: 0.5200 test acc: 0.4250\n", + "epoch: 0 iter: 3 train loss: 0.6836 train acc: 0.6500 test acc: 0.5200\n", + "epoch: 1 iter: 0 train loss: 0.6759 train acc: 0.6850 test acc: 0.5550\n", + "epoch: 1 iter: 3 train loss: 0.6709 train acc: 0.6950 test acc: 0.5650\n", + "epoch: 2 iter: 0 train loss: 0.6651 train acc: 0.6650 test acc: 0.5700\n", + "epoch: 2 iter: 3 train loss: 0.6621 train acc: 0.7050 test acc: 0.6050\n", + "epoch: 3 iter: 0 train loss: 0.6589 train acc: 0.6900 test acc: 0.6000\n", + "epoch: 3 iter: 3 train loss: 0.6597 train acc: 0.7050 test acc: 0.6250\n", + "epoch: 4 iter: 0 train loss: 0.6563 train acc: 0.7150 test acc: 0.6550\n", + "epoch: 4 iter: 3 train loss: 0.6566 train acc: 0.7250 test acc: 0.6700\n", + "***************************** qubit num : 6 *****************************\n", + "epoch: 0 iter: 0 train loss: 0.6966 train acc: 0.4900 test acc: 0.5250\n", + "epoch: 0 iter: 3 train loss: 0.6938 train acc: 0.4850 test acc: 0.5450\n", + "epoch: 1 iter: 0 train loss: 0.6884 train acc: 0.5350 test acc: 0.5450\n", + "epoch: 1 iter: 3 train loss: 0.6862 train acc: 0.5400 test acc: 0.5700\n", + "epoch: 2 iter: 0 train loss: 0.6775 train acc: 0.5750 test acc: 0.5850\n", + "epoch: 2 iter: 3 train loss: 0.6744 train acc: 0.6100 test acc: 0.6000\n", + "epoch: 3 iter: 0 train loss: 0.6642 train acc: 0.6350 test acc: 0.5950\n", + "epoch: 3 iter: 3 train loss: 0.6615 train acc: 0.6450 test acc: 0.6200\n", + "epoch: 4 iter: 0 train loss: 0.6526 train acc: 0.6900 test acc: 0.6300\n", + "epoch: 4 iter: 3 train loss: 0.6560 train acc: 0.6250 test acc: 0.6350\n", + "***************************** qubit num : 4 *****************************\n", + "epoch: 0 iter: 0 train loss: 0.7081 train acc: 0.4650 test acc: 0.5350\n", + "epoch: 0 iter: 3 train loss: 0.6994 train acc: 0.4950 test acc: 0.5900\n", + "epoch: 1 iter: 0 train loss: 0.6902 train acc: 0.5450 test acc: 0.5750\n", + "epoch: 1 iter: 3 train loss: 0.6942 train acc: 0.5150 test acc: 0.5800\n", + "epoch: 2 iter: 0 train loss: 0.6869 train acc: 0.6100 test acc: 0.5850\n", + "epoch: 2 iter: 3 train loss: 0.6923 train acc: 0.5150 test acc: 0.6000\n", + "epoch: 3 iter: 0 train loss: 0.6825 train acc: 0.5700 test acc: 0.6050\n", + "epoch: 3 iter: 3 train loss: 0.6917 train acc: 0.5200 test acc: 0.5950\n", + "epoch: 4 iter: 0 train loss: 0.6776 train acc: 0.5850 test acc: 0.5800\n", + "epoch: 4 iter: 3 train loss: 0.6901 train acc: 0.5450 test acc: 0.6050\n", + "***************************** qubit num : 3 *****************************\n", + "epoch: 0 iter: 0 train loss: 0.7104 train acc: 0.4550 test acc: 0.5000\n", + "epoch: 0 iter: 3 train loss: 0.6931 train acc: 0.4950 test acc: 0.4900\n", + "epoch: 1 iter: 0 train loss: 0.6928 train acc: 0.4600 test acc: 0.4700\n", + "epoch: 1 iter: 3 train loss: 0.6968 train acc: 0.4800 test acc: 0.4800\n", + "epoch: 2 iter: 0 train loss: 0.6964 train acc: 0.5100 test acc: 0.4750\n", + "epoch: 2 iter: 3 train loss: 0.7004 train acc: 0.5150 test acc: 0.4600\n", + "epoch: 3 iter: 0 train loss: 0.6961 train acc: 0.5050 test acc: 0.4800\n", + "epoch: 3 iter: 3 train loss: 0.6957 train acc: 0.5300 test acc: 0.4850\n", + "epoch: 4 iter: 0 train loss: 0.6938 train acc: 0.5250 test acc: 0.5150\n", + "epoch: 4 iter: 3 train loss: 0.6919 train acc: 0.5150 test acc: 0.5250\n", + "***************************** qubit num : 2 *****************************\n", + "epoch: 0 iter: 0 train loss: 0.7031 train acc: 0.5450 test acc: 0.4800\n", + "epoch: 0 iter: 3 train loss: 0.6960 train acc: 0.5350 test acc: 0.4550\n", + "epoch: 1 iter: 0 train loss: 0.6932 train acc: 0.5100 test acc: 0.5100\n", + "epoch: 1 iter: 3 train loss: 0.6930 train acc: 0.5250 test acc: 0.5200\n", + "epoch: 2 iter: 0 train loss: 0.6930 train acc: 0.5150 test acc: 0.4750\n", + "epoch: 2 iter: 3 train loss: 0.6933 train acc: 0.5050 test acc: 0.4600\n", + "epoch: 3 iter: 0 train loss: 0.6925 train acc: 0.5400 test acc: 0.4600\n", + "epoch: 3 iter: 3 train loss: 0.6909 train acc: 0.5350 test acc: 0.4700\n", + "epoch: 4 iter: 0 train loss: 0.6939 train acc: 0.5450 test acc: 0.4750\n", + "epoch: 4 iter: 3 train loss: 0.6853 train acc: 0.5600 test acc: 0.4750\n", + "主程序段总共运行了 125.62448906898499 秒\n" + ] + } + ], + "source": [ + "time_start = time.time()\n", + "\n", + "acc_list = []\n", + "\n", + "for i in range(5):\n", + " print('***************************** qubit num : %s *****************************'%qubit_num_list[i])\n", + " train_x, train_y = train_data[i]\n", + " test_x, test_y = test_data[i]\n", + "\n", + " acc = QClassifier(\n", + " train_x,\n", + " train_y,\n", + " test_x,\n", + " test_y,\n", + " N=qubit_num_list[i], # 所需的量子比特数量\n", + " D=qubit_num_list[i] + 2, # 采用的电路深度\n", + " EPOCH=5, # 训练 epoch 轮数\n", + " LR=0.05, # 设置学习速率\n", + " BATCH=200, # 训练时 batch 的大小\n", + " seed=0\n", + " )\n", + " acc_list.append(acc) \n", + "\n", + "time_span = time.time() - time_start\n", + "print('主程序段总共运行了', time_span, '秒')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 考察平均量子态与最大混合态的 Petz-Rényi 散度" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[1.9842463368536747, 0.5256437464833253, 0.1964853652901484, 0.05162865627740749, 0.022790754263846934]\n", + "[1.8628793706046225, 0.6064395532199834, 0.1529884926031612, 0.04173701231534178, 0.009023512622560221]\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "

" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "fig = plt.figure(1)\n", + "ax = axes([0.15, 0.15, 0.8, 0.8])\n", + "\n", + "\n", + "def average_encode_state(train_x):\n", + " d1, d2 = train_x.shape\n", + " density_matrices = np.reshape(train_x, [d1, d2, 1]) @ np.reshape(np.conj(train_x), [d1, 1, d2])\n", + " return np.mean(density_matrices, axis=0)\n", + "\n", + "\n", + "depth_list = [2, 3, 4, 6, 8]\n", + "\n", + "Q_2Renyi_D3_list = []\n", + "Q_2Renyi_D6_list = []\n", + "\n", + "for i,q in enumerate(qubit_num_list):\n", + " train_x, train_y = train_data[i]\n", + " train_x = train_x.numpy()\n", + " \n", + " train_x0 = train_x[train_y == 0]\n", + " train_x1 = train_x[train_y == 1]\n", + " average_state0 = average_encode_state(train_x0.real)\n", + " average_state1 = average_encode_state(train_x1.real)\n", + "# print(average_state)\n", + " Q_2Renyi_D3 = np.log2(np.trace(average_state0 @ average_state0) * 2 ** q)\n", + " Q_2Renyi_D6 = np.log2(np.trace(average_state1 @ average_state1) * 2 ** q)\n", + "# bound = np.sum(np.sqrt(S0)) ** 2 / 2 ** num_qubits\n", + " Q_2Renyi_D3_list.append(Q_2Renyi_D3)\n", + " Q_2Renyi_D6_list.append(Q_2Renyi_D6)\n", + "\n", + "print(Q_2Renyi_D3_list)\n", + "print(Q_2Renyi_D6_list)\n", + "\n", + "\n", + "func3, = ax.plot(depth_list, Q_2Renyi_D3_list, linewidth=1.5,\n", + " marker=\"<\",\n", + " linestyle=\":\",\n", + " color=\"r\"\n", + " )\n", + "\n", + "func6, = ax.plot(depth_list, Q_2Renyi_D6_list, linewidth=1.5,\n", + " marker=\"o\",\n", + " linestyle=\"-.\",\n", + " color=\"g\"\n", + " )\n", + "\n", + "plt.xticks(fontsize=22)\n", + "plt.yticks(fontsize=22)\n", + "\n", + "plt.xlabel(\"Depth\", fontsize=22)\n", + "plt.ylabel(r\"$D_2(\\overline{\\rho} || I/2^n)$\", fontsize=22)\n", + "ax.semilogy()\n", + "ax.legend(handles=[func3, func6],\n", + " labels=[\"Digit 3\", \"Digit 6\"],\n", + " loc=\"best\",\n", + " fontsize=18)\n", + "ax.grid(axis=\"y\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 考察分类准确率受到的影响" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[array([0.675], dtype=float32), array([0.61], dtype=float32), array([0.585], dtype=float32), array([0.535], dtype=float32), array([0.475], dtype=float32)]\n" + ] + }, + { + "data": { + "image/png": "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1IjI+fJDmTJlOOuss45LbgADBw6kU6dOpKWl8emnJ9aSkS+88AJLliyhZ8+ePPHEE1mSG0DZsmU57bTTohKbJTgTs7ZsgU6dYGuw9dpjxIEDB3jkkUdo164d5cuXp2TJkjRs2JABAwbwwQcfhHSOP/74gyeffJLu3bvToEEDSpQoQVJSEm3btmXSpEmkpaX5fd2XX37JgAEDqFWrFgkJCSQlJdGoUSOGDBly3JftoUOHmDhxIq1bt6ZMmTIZ9f/atWvHnXfeyaFDWdcEzn4Pznev7O677wbg3nvvzWjTuXNnrrzySkSEiRMnBvycTz/9NCLCJZdcctx5s9+DW7hwYca5U1NTGT9+PE2aNKFEiRJUrVqVSy+9lN9++y3ge7355pu0b9+eMmXKUKFCBc4//3wWLVqU5byR4quAn5iYGPJrUlNTefXVVxk8eDCNGzembNmylCpVimbNmjF27Fh27tzp93WZ75V98skndO3alaSkJEqVKkXbtm2ZPXt2wPf89ddfueyyy6hWrRolS5akWbNmPPTQQwH/feXkmWeeAeDmm2/O1evzlaraI58ebdq00aJg1apVUXnfa65RjYtz21i2YcMGbdy4sQJapkwZ7d69uw4cOFDbt2+vpUuX1k6dOmVp36lTJwV0wYIFWfa/+uqrCmjt2rW1S5cuOmjQIO3cubMmJiYqoBdddJGmp6dnec3HH3+sCQkJCmirVq10wIAB2qdPH01OTtaEhAS9+uqrM9qmpaXpueeeq4AmJSXphRdeqIMHD9Zzzz1Xa9WqpYBu2bIly/kBdV8TzqJFi3T48OHaokULBbRFixY6fPhwHT58uE6YMEGXL1+ugNavX1/T0tL8/nk1adJEAV24cGHGvpdeekkBHT58eJa2CxYsUEDbt2+vXbt21TJlymiPHj20T58+Wq1aNQW0Tp06umvXruPeZ/z48QqoiGiHDh108ODBevrpp2t8fLzedNNNChz3d5NbH3zwgSYkJGipUqX0999/D/l1GzduVEArVKig7dq100suuUS7d++ulStXVkAbNmyof/zxx3Gvq1evngJ65513qojomWeeqYMGDdJWrVplfOaZM2ce97offvgh49x16tTRgQMH6vnnn6/FixfXiy++OOO869evDyn+zZs3K6Dx8fF68OBBXbNmjd533306cuRIHTdunM6dO/e4f7OhCOc7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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "print(acc_list)\n", + "\n", + "fig = plt.figure(1)\n", + "ax = axes([0.15, 0.15, 0.8, 0.8])\n", + "\n", + "func36_acc, = ax.plot(depth_list, acc_list, linewidth=1.5,\n", + " marker=\"*\",\n", + " linestyle=\"--\",\n", + " color=\"b\"\n", + " )\n", + "\n", + "plt.xticks(fontsize=22)\n", + "plt.yticks(fontsize=22)\n", + "plt.ylim(0.48, 0.75)\n", + "plt.xlabel(\"Depth\", fontsize=22)\n", + "plt.ylabel(r\"Test Accuracy\", fontsize=22)\n", + "ax.legend(handles=[func36_acc,],\n", + " labels=[\"classifying 3 and 6\"],\n", + " loc=\"best\", fontsize=22)\n", + "ax.grid(axis=\"y\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 总结\n", + "\n", + "从实验结果上看每个类别的平均量子态确实随着数据编码电路的加深以指数的速度趋向于最大混合态,从而导致了最终量子神经网络分类准确率下降。在文献 [5] 的帮助下我们逐渐清楚了角度编码存在的一些局限性,同时也体会到了设计一种能够解决实际问题的(通常数据特征维度很高)数据编码策略是迫切需要的,当然也极富挑战性。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 参考文献\n", + "\n", + "[1] Schuld, M. \"Quantum machine learning models are kernel methods.\" [arXiv preprint arXiv:2101.11020.(2021)](https://arxiv.org/abs/2101.11020)\n", + "\n", + "[2] Lloyd, Seth, et al. \"Quantum embeddings for machine learning.\" [arXiv preprint arXiv:2001.03622 (2020).](https://arxiv.org/pdf/2001.03622.pdf)\n", + "\n", + "[3] Caro, Matthias C., et al. \"Encoding-dependent generalization bounds for parametrized quantum circuits.\" [Quantum 5 (2021): 582.](https://quantum-journal.org/papers/q-2021-11-17-582/)\n", + "\n", + "[4] Leonardo Banchi, et al. \"Generalization in quantum machine learning: A quantum information standpoint.\" [PRX Quantum 2.4 (2021): 040321.](https://journals.aps.org/prxquantum/abstract/10.1103/PRXQuantum.2.040321)\n", + "\n", + "[5] Li, Guangxi, et al. \"Concentration of Data Encoding in Parameterized Quantum Circuits.\" [arXiv preprint arXiv:2206.08273 (2022).](https://arxiv.org/pdf/2206.08273.pdf)\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.8.13 ('new_pq_dev')", + "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.8.13" + }, + "vscode": { + "interpreter": { + "hash": "ea8ffbee42045ec282b7cb9811c8c332764b47a134b1aefa35055ac6f62b46f4" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tutorials/machine_learning/EncodingAnalysis_EN.ipynb b/tutorials/machine_learning/EncodingAnalysis_EN.ipynb new file mode 100644 index 0000000..6f62de1 --- /dev/null +++ b/tutorials/machine_learning/EncodingAnalysis_EN.ipynb @@ -0,0 +1,642 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Data Encoding Analysis\n", + "\n", + "*Copyright (c) 2022 Institute for Quantum Computing, Baidu Inc. All Rights Reserved.*" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Overview\n", + "\n", + "Many studies are now exhibiting the promise of Variational Quantum Algorithms (VQA) in demonstrating quantum advantages on near-term quantum devices, with particular emphasis on using them to solve supervised learning tasks. VQA is also known as a Parameterized Quantum Circuit (PQC), which is divided into two main parts in this tutorial: the data encoding circuit and the quantum neural network. The data encoding circuit converts classical data into quantum states, and the quality of the quantum states has a direct impact on the classification results. When data encoding is analyzed through the perspective of kernel functions, different encoding methods correspond to different kernel functions, which play a critical role in the classification of quantum states [1,2]. It often determines the expressiveness and generalization ability of the method by analyzing data encoding from the standpoint of statistical learning theory [3,4]. Therefore, it is necessary to conduct a systematic analysis of data encoding. From the standpoint of quantum information, literature [5] critically analyzes the effect of the width and depth of the data encoding circuit on the quantum state.\n", + "\n", + "The next part of the tutorial is organized around the literature [5] and is divided into two parts: theory and Paddle Quantum implementation. The fundamental concepts and main conclusions are introduced first, followed by specific implementation solutions." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Theory" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Fundamental Concepts\n", + "\n", + "Figure 1 shows the flowchart for encoding classical data into quantum states. Assume that the classical data are independent identically distributed samples of distribution $D$, each classical data is $\\pmb x$, which becomes a quantum state $\\rho(\\pmb x)$ after passing through the data encoding circuit. Here the concept of **average quantum state** over the distribution $D$ is introduced, i.e. \n", + "\n", + "$$\n", + "\\bar{\\rho}:=\\mathbf{E}[\\rho(\\pmb x)]. \\tag{1}\n", + "$$\n", + "\n", + "Given a classical data set $S$ consisting of $M$ data, we usually use the average value to approximate the average quantum state of $S$\n", + "\n", + "$$\n", + "\\bar{\\rho}:=\\frac{1}{M}\\sum_{j=1}^M\\rho(\\pmb x_j). \\tag{2}\n", + "$$\n", + "\n", + "\n", + "![illustration](figures/EncodingAnalysis-fig-illustration.png \"Figure 1: Flowchart for encoding classical data into quantum states.\")\n", + "\n", + "There are various methods for measuring the distance between quantum states, including trace distance, fidelity, **Petz-Rényi divergence**, and others. In this tutorial, the Petz-Rényi divergence is used as a measure of the distance between different quantum states. Specifically, the Petz-Rényi divergence of the quantum states $\\rho_0$ and $\\rho_1$ is defined as\n", + "\n", + "$$\n", + "D_2(\\rho_0||\\rho_1)=logTr[\\rho_0^2\\rho_1^{-1}], \\tag{3}\n", + "$$\n", + "\n", + "the closer the two quantum states are, the smaller the value of $D_2$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Main Result\n", + "\n", + "The Petz-Rényi divergence metric is then used to calculate the distance between the encoding average quantum state and the maximum mixed state. It is not difficult to believe that this distance is related to the property of the classical dataset and how the data are encoded. Assume that each feature dimension of the classical dataset satisfies certain independence and has a standard deviation of at least $\\sigma$. The following inequality then holds for the encoding circuit depicted in figure 2 (with width and depth $n$ and $D$, respectively)\n", + "\n", + "$$\n", + "D_2(\\bar{\\rho}||I/2^n)\\leq log(1+(2^n-1)2^{-D\\sigma^2}), \\tag{4}\n", + "$$\n", + "\n", + "where $I/2^n$ is the maximum mixed state of $n$ bits and $I$ is the unit matrix.\n", + "\n", + "![encoding-u3](figures/EncodingAnalysis-fig-u3_circuit.png \" Figure 2: General data encoding circuits. Etg denotes any combination of control non-gates and CZ gates.\")\n", + "\n", + "A more rigorous description and proof of the theorem can be found in [5], and the focus of this tutorial will be on what this conclusion implies and illuminates.\n", + "\n", + "* This means that the average quantum state converges to the maximum mixed state at an exponential rate as the circuit depth increases. For example, if the average quantum states of both classes 0 and 1 eventually converge to the maximum mixed state, it will be impossible to distinguish the average quantum states of these two classes from the standpoint of quantum information, i.e., it will be impossible to distinguish classical data features in an average sense.\n", + "\n", + "* When the classical data features have a high dimensionality (e.g., picture data), angle encoding may not be the best option. This is due to the fact that it can easily lead to very deep encoding circuits, and widening circuits may run into the barren plateau problem, which severely limits VQA's capability. As a result, some dimensionality reduction operations must be performed on the traditional data before it is fed into the circuit." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Paddle Quantum Implementation\n", + "\n", + "This section focuses on two experiments using Paddle Quantum on the MNIST dataset: \n", + "- Exploring the trend of the Petz-Rényi divergence between the average quantum state and the maximum mixed state with the depth of the data encoding circuit\n", + "- Investigate changes in classification accuracy as the data encoding circuit becomes deeper." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### First import the relevant packages" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# import numpy, paddle, and paddle_quantum\n", + "import numpy as np\n", + "import paddle\n", + "import paddle_quantum\n", + "\n", + "# import circuit module\n", + "from paddle_quantum.ansatz import Circuit\n", + "\n", + "# import some function\n", + "from numpy import pi as PI\n", + "from paddle import matmul, transpose, reshape, real, argmax, cast, mean, concat, real\n", + "from paddle_quantum.qinfo import pauli_str_to_matrix \n", + "from paddle_quantum.linalg import dagger\n", + "import paddle.nn.functional as F\n", + "\n", + "# dataset tool\n", + "from paddle_quantum.dataset import MNIST\n", + "\n", + "# plot and time module\n", + "from matplotlib import pyplot as plt\n", + "from pylab import axes\n", + "import time" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Parameterized Quantum Circuit\n", + "\n", + "![encoding-ry](figures/EncodingAnalysis-fig-ry_circuit.png \"Figure 3: Parameterized quantum circuit.\")\n", + "\n", + "The red box on the left in Figure 3 is the data encoding circuit, a special case of Figure 2, and the blue box on the right is the quantum neural network. The data encoding circuit here is made up of $R_y$ and CNOT, and the specific circuit depth $D$ is determined by the data feature dimension. The MNIST dataset will be used in this tutorial and the images are downscaled to 16-dimensional feature vectors. The number of quantum bits is chosen to be 8, 6, 4, 3, 2, and the corresponding circuit depths are 2, 3, 4, 6, 8. The positions larger than 16 dimensions will be filled with 0, i.e., $R_y(0)$. The quantum neural network part consists of the single-bit universal gate $U3$ and CNOT.\n", + "The specific circuit depth $L$ can be set freely." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Encoding classical Data into quantum states\n", + "\n", + "Here you need the dataset tool `dataset` provided by Paddle Quantum. The MNIST data are encoded into quantum states using the data encoding circuit shown in Figure 2 before being fed into the quantum neural network for training." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "train_data, test_data = [], []\n", + "\n", + "# Binary classification task \n", + "classes = [3,6]\n", + "\n", + "training_data_num = 1000\n", + "testing_data_num = 200\n", + "qubit_num_list = [8, 6, 4, 3, 2]\n", + "\n", + "# Encode classical data using circuits of different widths and depths and save them\n", + "for qubit_num in qubit_num_list:\n", + " \n", + " # training dataset\n", + " train_dataset = MNIST(mode='train', encoding='real_entangled_encoding', num_qubits=qubit_num,\n", + " classes=classes,\n", + " data_num=training_data_num,\n", + " downscaling_method='resize', target_dimension=16,\n", + " need_relabel=True, return_state=True)\n", + "\n", + " # validation dataset\n", + " val_dataset = MNIST(mode='test', encoding='real_entangled_encoding', num_qubits=qubit_num,\n", + " classes=classes,\n", + " data_num=testing_data_num,\n", + " downscaling_method='resize', target_dimension=16,\n", + " need_relabel=True, return_state=True)\n", + "\n", + " # x and y\n", + " train_x, train_y = train_dataset.quantum_image_states, train_dataset.labels\n", + " test_x, test_y = val_dataset.quantum_image_states, val_dataset.labels\n", + " train_data.append((train_x, train_y))\n", + " test_data.append((test_x, test_y))" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[1000, 256]\n", + "(1000,)\n", + "[200, 256]\n", + "(200,)\n" + ] + } + ], + "source": [ + "print(train_data[0][0].shape)\n", + "print(train_data[0][1].shape)\n", + "print(test_data[0][0].shape)\n", + "print(test_data[0][1].shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Building Quantum Neural Network" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# Construct model\n", + "class Net(paddle.nn.Layer):\n", + " \"\"\"\n", + " construct network\n", + " \"\"\"\n", + " def __init__(self, n, depth):\n", + " # Initialize the circuit: n, depth\n", + " super(Net, self).__init__()\n", + " self.n = n\n", + " self.depth = depth\n", + " \n", + " self.circuit = Circuit(n)\n", + " # Add layers of rotation gates\n", + " for i in range(n):\n", + " self.circuit.rz(qubits_idx=i)\n", + " self.circuit.ry(qubits_idx=i)\n", + " self.circuit.rz(qubits_idx=i)\n", + "\n", + " # default depth = 1\n", + " # Add layers of entanglement\n", + " for d in range(3, depth + 3):\n", + " for i in range(n-1):\n", + " self.circuit.cnot(qubits_idx=[i, i + 1])\n", + " self.circuit.cnot(qubits_idx=[n-1, 0])\n", + " for i in range(n):\n", + " self.circuit.ry(qubits_idx=i)\n", + "\n", + " # Define forward propagation mechanism, and then calculate loss function and cross-validation accuracy\n", + " def forward(self, state_in, label):\n", + " \"\"\"\n", + " Input: \n", + " state_in: input quantum state, shape: [-1, 1, 2^n] -- Here is [BATCH, 1, 2^n]\n", + " label: labels of input quantum state, shape: [-1, 1]\n", + " Loss function:\n", + " The cross entropy loss \n", + " \"\"\"\n", + " # Initialize theta \n", + " Utheta = self.circuit.unitary_matrix()\n", + "\n", + " # row vector operations here to speed up \n", + " state_out = matmul(state_in, Utheta) # shape [-1, 1, 2 ** n]\n", + "\n", + " # Measure the expected value of the pauli Z operator \n", + " Ob1 = paddle.to_tensor(pauli_str_to_matrix([[1.0, 'z0']], self.n))\n", + " E_Ob1 = matmul(matmul(state_out, Ob1), transpose(paddle.conj(state_out), perm=[0, 2, 1]))\n", + " E_Ob1_re = reshape(real(E_Ob1), [-1, 1])\n", + "\n", + " Ob2 = paddle.to_tensor(pauli_str_to_matrix([[1.0, 'x0']], self.n))\n", + " E_Ob2 = matmul(matmul(state_out, Ob2), transpose(paddle.conj(state_out), perm=[0, 2, 1]))\n", + " E_Ob2_re = reshape(real(E_Ob2), [-1, 1])\n", + "\n", + " outputs = concat([E_Ob1_re, E_Ob2_re], axis=-1)\n", + " \n", + " # Calculate loss and accuracy\n", + " loss = F.cross_entropy(outputs, label)\n", + " acc = mean(cast(argmax(outputs, axis=-1) == label, \"float32\"))\n", + " \n", + " return loss, acc" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# Define a classifier\n", + "def QClassifier(train_x, train_y, test_x, test_y, N, D, EPOCH, LR, BATCH, seed=0):\n", + " \"\"\"\n", + " Quantum binary classifier\n", + " \"\"\"\n", + " train_y = paddle.to_tensor(train_y, dtype=\"int64\")\n", + " test_y = paddle.to_tensor(test_y, dtype=\"int64\")\n", + "\n", + " N_train, in_dim = train_x.shape\n", + " \n", + " # Initialize the neural network\n", + " paddle.seed(0)\n", + " net = Net(n=N, depth=D)\n", + "\n", + " # Generally speaking, we use Adam optimizer to obtain relatively good convergence,\n", + " # You can change it to SGD or RMS prop.\n", + " opt = paddle.optimizer.Adam(learning_rate=LR, parameters=net.parameters())\n", + "\n", + " # Optimization loop\n", + " for ep in range(EPOCH):\n", + " for itr in range(N_train // BATCH):\n", + " input_state = train_x[itr * BATCH:(itr + 1) * BATCH]\n", + " input_state = reshape(input_state, [-1, 1, 2 ** N])\n", + " label = train_y[itr * BATCH:(itr + 1) * BATCH]\n", + "\n", + " test_input_state = reshape(test_x, [-1, 1, 2 ** N])\n", + "\n", + " # Forward propagation to calculate loss and accuracy\n", + " train_loss, train_acc = net(state_in=input_state, label=label)\n", + "\n", + " if itr % 3 == 0:\n", + " # Compute test accuracy and loss\n", + " loss_useless, test_acc = net(state_in=test_input_state, label=test_y)\n", + " print(\"epoch:\", ep, \"iter:\", itr,\n", + " \"train loss: %.4f\" % train_loss.numpy(),\n", + " \"train acc: %.4f\" % train_acc,\n", + " \"test acc: %.4f\" % test_acc)\n", + "\n", + " # Use back propagation to minimize the loss function\n", + " train_loss.backward()\n", + " opt.minimize(train_loss)\n", + " opt.clear_grad()\n", + " \n", + " # Compute test accuracy and loss\n", + " _, test_acc = net(state_in=test_input_state, label=test_y) \n", + " \n", + " return test_acc.numpy()" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "***************************** qubit num : 8 *****************************\n", + "epoch: 0 iter: 0 train loss: 0.6913 train acc: 0.5200 test acc: 0.4250\n", + "epoch: 0 iter: 3 train loss: 0.6836 train acc: 0.6500 test acc: 0.5200\n", + "epoch: 1 iter: 0 train loss: 0.6759 train acc: 0.6850 test acc: 0.5550\n", + "epoch: 1 iter: 3 train loss: 0.6709 train acc: 0.6950 test acc: 0.5650\n", + "epoch: 2 iter: 0 train loss: 0.6651 train acc: 0.6650 test acc: 0.5700\n", + "epoch: 2 iter: 3 train loss: 0.6621 train acc: 0.7050 test acc: 0.6050\n", + "epoch: 3 iter: 0 train loss: 0.6589 train acc: 0.6900 test acc: 0.6000\n", + "epoch: 3 iter: 3 train loss: 0.6597 train acc: 0.7050 test acc: 0.6250\n", + "epoch: 4 iter: 0 train loss: 0.6563 train acc: 0.7150 test acc: 0.6550\n", + "epoch: 4 iter: 3 train loss: 0.6566 train acc: 0.7250 test acc: 0.6700\n", + "***************************** qubit num : 6 *****************************\n", + "epoch: 0 iter: 0 train loss: 0.6966 train acc: 0.4900 test acc: 0.5250\n", + "epoch: 0 iter: 3 train loss: 0.6938 train acc: 0.4850 test acc: 0.5450\n", + "epoch: 1 iter: 0 train loss: 0.6884 train acc: 0.5350 test acc: 0.5450\n", + "epoch: 1 iter: 3 train loss: 0.6862 train acc: 0.5400 test acc: 0.5700\n", + "epoch: 2 iter: 0 train loss: 0.6775 train acc: 0.5750 test acc: 0.5850\n", + "epoch: 2 iter: 3 train loss: 0.6744 train acc: 0.6100 test acc: 0.6000\n", + "epoch: 3 iter: 0 train loss: 0.6642 train acc: 0.6350 test acc: 0.5950\n", + "epoch: 3 iter: 3 train loss: 0.6615 train acc: 0.6450 test acc: 0.6200\n", + "epoch: 4 iter: 0 train loss: 0.6526 train acc: 0.6900 test acc: 0.6300\n", + "epoch: 4 iter: 3 train loss: 0.6560 train acc: 0.6250 test acc: 0.6350\n", + "***************************** qubit num : 4 *****************************\n", + "epoch: 0 iter: 0 train loss: 0.7081 train acc: 0.4650 test acc: 0.5350\n", + "epoch: 0 iter: 3 train loss: 0.6994 train acc: 0.4950 test acc: 0.5900\n", + "epoch: 1 iter: 0 train loss: 0.6902 train acc: 0.5450 test acc: 0.5750\n", + "epoch: 1 iter: 3 train loss: 0.6942 train acc: 0.5150 test acc: 0.5800\n", + "epoch: 2 iter: 0 train loss: 0.6869 train acc: 0.6100 test acc: 0.5850\n", + "epoch: 2 iter: 3 train loss: 0.6923 train acc: 0.5150 test acc: 0.6000\n", + "epoch: 3 iter: 0 train loss: 0.6825 train acc: 0.5700 test acc: 0.6050\n", + "epoch: 3 iter: 3 train loss: 0.6917 train acc: 0.5200 test acc: 0.5950\n", + "epoch: 4 iter: 0 train loss: 0.6776 train acc: 0.5850 test acc: 0.5800\n", + "epoch: 4 iter: 3 train loss: 0.6901 train acc: 0.5450 test acc: 0.6050\n", + "***************************** qubit num : 3 *****************************\n", + "epoch: 0 iter: 0 train loss: 0.7104 train acc: 0.4550 test acc: 0.5000\n", + "epoch: 0 iter: 3 train loss: 0.6931 train acc: 0.4950 test acc: 0.4900\n", + "epoch: 1 iter: 0 train loss: 0.6928 train acc: 0.4600 test acc: 0.4700\n", + "epoch: 1 iter: 3 train loss: 0.6968 train acc: 0.4800 test acc: 0.4800\n", + "epoch: 2 iter: 0 train loss: 0.6964 train acc: 0.5100 test acc: 0.4750\n", + "epoch: 2 iter: 3 train loss: 0.7004 train acc: 0.5150 test acc: 0.4600\n", + "epoch: 3 iter: 0 train loss: 0.6961 train acc: 0.5050 test acc: 0.4800\n", + "epoch: 3 iter: 3 train loss: 0.6957 train acc: 0.5300 test acc: 0.4850\n", + "epoch: 4 iter: 0 train loss: 0.6938 train acc: 0.5250 test acc: 0.5150\n", + "epoch: 4 iter: 3 train loss: 0.6919 train acc: 0.5150 test acc: 0.5250\n", + "***************************** qubit num : 2 *****************************\n", + "epoch: 0 iter: 0 train loss: 0.7031 train acc: 0.5450 test acc: 0.4800\n", + "epoch: 0 iter: 3 train loss: 0.6960 train acc: 0.5350 test acc: 0.4550\n", + "epoch: 1 iter: 0 train loss: 0.6932 train acc: 0.5100 test acc: 0.5100\n", + "epoch: 1 iter: 3 train loss: 0.6930 train acc: 0.5250 test acc: 0.5200\n", + "epoch: 2 iter: 0 train loss: 0.6930 train acc: 0.5150 test acc: 0.4750\n", + "epoch: 2 iter: 3 train loss: 0.6933 train acc: 0.5050 test acc: 0.4600\n", + "epoch: 3 iter: 0 train loss: 0.6925 train acc: 0.5400 test acc: 0.4600\n", + "epoch: 3 iter: 3 train loss: 0.6909 train acc: 0.5350 test acc: 0.4700\n", + "epoch: 4 iter: 0 train loss: 0.6939 train acc: 0.5450 test acc: 0.4750\n", + "epoch: 4 iter: 3 train loss: 0.6853 train acc: 0.5600 test acc: 0.4750\n", + "time used: 184.6011986732483 s\n" + ] + } + ], + "source": [ + "time_start = time.time()\n", + "\n", + "acc_list = []\n", + "\n", + "for i in range(5):\n", + " print('***************************** qubit num : %s *****************************'%qubit_num_list[i])\n", + " train_x, train_y = train_data[i]\n", + " test_x, test_y = test_data[i]\n", + "\n", + " acc = QClassifier(\n", + " train_x, # training data x\n", + " train_y, # training data label\n", + " test_x, # test data x\n", + " test_y, # test data label\n", + " N=qubit_num_list[i], # Number of qubits\n", + " D=qubit_num_list[i] + 2, # Circuit depth\n", + " EPOCH=5, # Number of training epochs\n", + " LR=0.05, # Learning rate\n", + " BATCH=200, # Batch size\n", + " seed=0\n", + " )\n", + " acc_list.append(acc) \n", + "\n", + "time_span = time.time() - time_start\n", + "print('time used:', time_span, 's')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### The Petz-Rényi divergence of the average quantum state and the maximum mixed state" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[1.9842463368536747, 0.5256437464833253, 0.1964853652901484, 0.05162865627740749, 0.022790754263846934]\n", + "[1.8628793706046225, 0.6064395532199834, 0.1529884926031612, 0.04173701231534178, 0.009023512622560221]\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "fig = plt.figure(1)\n", + "ax = axes([0.15, 0.15, 0.8, 0.8])\n", + "\n", + "\n", + "def average_encode_state(train_x):\n", + " d1, d2 = train_x.shape\n", + " density_matrices = np.reshape(train_x, [d1, d2, 1]) @ np.reshape(np.conj(train_x), [d1, 1, d2])\n", + " return np.mean(density_matrices, axis=0)\n", + "\n", + "\n", + "depth_list = [2, 3, 4, 6, 8]\n", + "\n", + "Q_2Renyi_D3_list = []\n", + "Q_2Renyi_D6_list = []\n", + "\n", + "for i,q in enumerate(qubit_num_list):\n", + " train_x, train_y = train_data[i]\n", + " train_x = train_x.numpy()\n", + " \n", + " train_x0 = train_x[train_y == 0]\n", + " train_x1 = train_x[train_y == 1]\n", + " average_state0 = average_encode_state(train_x0.real)\n", + " average_state1 = average_encode_state(train_x1.real)\n", + "# print(average_state)\n", + " Q_2Renyi_D3 = np.log2(np.trace(average_state0 @ average_state0) * 2 ** q)\n", + " Q_2Renyi_D6 = np.log2(np.trace(average_state1 @ average_state1) * 2 ** q)\n", + "# bound = np.sum(np.sqrt(S0)) ** 2 / 2 ** num_qubits\n", + " Q_2Renyi_D3_list.append(Q_2Renyi_D3)\n", + " Q_2Renyi_D6_list.append(Q_2Renyi_D6)\n", + "\n", + "print(Q_2Renyi_D3_list)\n", + "print(Q_2Renyi_D6_list)\n", + "\n", + "\n", + "func3, = ax.plot(depth_list, Q_2Renyi_D3_list, linewidth=1.5,\n", + " marker=\"<\",\n", + " linestyle=\":\",\n", + " color=\"r\"\n", + " )\n", + "\n", + "func6, = ax.plot(depth_list, Q_2Renyi_D6_list, linewidth=1.5,\n", + " marker=\"o\",\n", + " linestyle=\"-.\",\n", + " color=\"g\"\n", + " )\n", + "\n", + "plt.xticks(fontsize=22)\n", + "plt.yticks(fontsize=22)\n", + "\n", + "plt.xlabel(\"Depth\", fontsize=22)\n", + "plt.ylabel(r\"$D_2(\\overline{\\rho} || I/2^n)$\", fontsize=22)\n", + "ax.semilogy()\n", + "ax.legend(handles=[func3, func6],\n", + " labels=[\"Digit 3\", \"Digit 6\"],\n", + " loc=\"best\",\n", + " fontsize=18)\n", + "ax.grid(axis=\"y\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Investigating the effect on classification accuracy" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[array([0.675], dtype=float32), array([0.61], dtype=float32), array([0.585], dtype=float32), array([0.535], dtype=float32), array([0.475], dtype=float32)]\n" + ] + }, + { + "data": { + "image/png": "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1IjI+fJDmTJlOOuss45LbgADBw6kU6dOpKWl8emnJ9aSkS+88AJLliyhZ8+ePPHEE1mSG0DZsmU57bTTohKbJTgTs7ZsgU6dYGuw9dpjxIEDB3jkkUdo164d5cuXp2TJkjRs2JABAwbwwQcfhHSOP/74gyeffJLu3bvToEEDSpQoQVJSEm3btmXSpEmkpaX5fd2XX37JgAEDqFWrFgkJCSQlJdGoUSOGDBly3JftoUOHmDhxIq1bt6ZMmTIZ9f/atWvHnXfeyaFDWdcEzn4Pznev7O677wbg3nvvzWjTuXNnrrzySkSEiRMnBvycTz/9NCLCJZdcctx5s9+DW7hwYca5U1NTGT9+PE2aNKFEiRJUrVqVSy+9lN9++y3ge7355pu0b9+eMmXKUKFCBc4//3wWLVqU5byR4quAn5iYGPJrUlNTefXVVxk8eDCNGzembNmylCpVimbNmjF27Fh27tzp93WZ75V98skndO3alaSkJEqVKkXbtm2ZPXt2wPf89ddfueyyy6hWrRolS5akWbNmPPTQQwH/feXkmWeeAeDmm2/O1evzlaraI58ebdq00aJg1apVUXnfa65RjYtz21i2YcMGbdy4sQJapkwZ7d69uw4cOFDbt2+vpUuX1k6dOmVp36lTJwV0wYIFWfa/+uqrCmjt2rW1S5cuOmjQIO3cubMmJiYqoBdddJGmp6dnec3HH3+sCQkJCmirVq10wIAB2qdPH01OTtaEhAS9+uqrM9qmpaXpueeeq4AmJSXphRdeqIMHD9Zzzz1Xa9WqpYBu2bIly/kBdV8TzqJFi3T48OHaokULBbRFixY6fPhwHT58uE6YMEGXL1+ugNavX1/T0tL8/nk1adJEAV24cGHGvpdeekkBHT58eJa2CxYsUEDbt2+vXbt21TJlymiPHj20T58+Wq1aNQW0Tp06umvXruPeZ/z48QqoiGiHDh108ODBevrpp2t8fLzedNNNChz3d5NbH3zwgSYkJGipUqX0999/D/l1GzduVEArVKig7dq100suuUS7d++ulStXVkAbNmyof/zxx3Gvq1evngJ65513qojomWeeqYMGDdJWrVplfOaZM2ce97offvgh49x16tTRgQMH6vnnn6/FixfXiy++OOO869evDyn+zZs3K6Dx8fF68OBBXbNmjd533306cuRIHTdunM6dO/e4f7OhCOc7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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "print(acc_list)\n", + "\n", + "fig = plt.figure(1)\n", + "ax = axes([0.15, 0.15, 0.8, 0.8])\n", + "\n", + "func36_acc, = ax.plot(depth_list, acc_list, linewidth=1.5,\n", + " marker=\"*\",\n", + " linestyle=\"--\",\n", + " color=\"b\"\n", + " )\n", + "\n", + "plt.xticks(fontsize=22)\n", + "plt.yticks(fontsize=22)\n", + "plt.ylim(0.48, 0.75)\n", + "plt.xlabel(\"Depth\", fontsize=22)\n", + "plt.ylabel(r\"Test Accuracy\", fontsize=22)\n", + "ax.legend(handles=[func36_acc,],\n", + " labels=[\"classifying 3 and 6\"],\n", + " loc=\"best\", fontsize=22)\n", + "ax.grid(axis=\"y\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Conclusion\n", + "\n", + "According to the experimental results, the average quantum state of each category tends to the maximum mixed state at an exponential rate as the data encoding circuit deepens. As a result, the final quantum neural network's classification accuracy decreases. With the help of the literature [5], we have come to understand some of the limitations of angle coding. It is also acknowledged that designing a data coding strategy capable of solving real-world problems (often with high dimensionality of data features) is both urgent and difficult." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## References\n", + "\n", + "[1] Schuld, M. \"Quantum machine learning models are kernel methods.\" [arXiv preprint arXiv:2101.11020.(2021)](https://arxiv.org/abs/2101.11020)\n", + "\n", + "[2] Lloyd, Seth, et al. \"Quantum embeddings for machine learning.\" [arXiv preprint arXiv:2001.03622 (2020).](https://arxiv.org/pdf/2001.03622.pdf)\n", + "\n", + "[3] Caro, Matthias C., et al. \"Encoding-dependent generalization bounds for parametrized quantum circuits.\" [Quantum 5 (2021): 582.](https://quantum-journal.org/papers/q-2021-11-17-582/)\n", + "\n", + "[4] Leonardo Banchi, et al. \"Generalization in quantum machine learning: A quantum information standpoint.\" [PRX Quantum 2.4 (2021): 040321.](https://journals.aps.org/prxquantum/abstract/10.1103/PRXQuantum.2.040321)\n", + "\n", + "[5] Li, Guangxi, et al. \"Concentration of Data Encoding in Parameterized Quantum Circuits.\" [arXiv preprint arXiv:2206.08273 (2022).](https://arxiv.org/pdf/2206.08273.pdf)\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.8.13 ('new_pq')", + "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.8.13" + }, + "vscode": { + "interpreter": { + "hash": "58b83104798ee1b81625bc6249d4d66f2bacd4a7d411a9b7a27bac0b4765adf2" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tutorials/machine_learning/QApproximating_CN.ipynb b/tutorials/machine_learning/QApproximating_CN.ipynb new file mode 100644 index 0000000..df63e2a --- /dev/null +++ b/tutorials/machine_learning/QApproximating_CN.ipynb @@ -0,0 +1,614 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 量子神经网络模拟函数\n", + "\n", + "*Copyright (c) 2022 Institute for Quantum Computing, Baidu Inc. All Rights Reserved.*" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 概览\n", + "量子神经网络(Quantum Neural Network, QNN)由参数化量子电路组成,通过训练电路中的参数从而最小化目标损失函数,是一种常用的量子机器学习模型。和机器学习中的神经网络(Neural Network, NN)模型类似,QNN 的表达能力可以通过其能近似的函数来刻画。经典神经网络中的通用近似定理(Universal Approximation Theorem, UAT)描述了多层人工神经网络近似任意函数的能力。一些研究将 QNN 与傅里叶级数联系起来,发现多比特的 QNN 也有类似的通用近似性质(Universal Approximation Property, UAP)[1],然而单比特 QNN 的表达能力仍旧是一个开放问题。我们从量子信号处理(Quantum Signal Processing, QSP)中获取灵感,建立了单比特 QNN 和单元傅里叶级数的一一对应关系,从而证明了单比特 QNN 的表达能力,解决了这个开放问题。在这篇教程中,我们将展示使用单比特 QNN 模拟任意目标函数。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 单比特 QNN 模拟任意偶函数\n", + "我们使用 data re-uploading 形式的单比特 QNN,由数据编码门和可训练门交替组成,其中数据编码门和可训练门均从三种泡利旋转门$R_X,R_Y,R_Z$ 中选择。令 QNN 作用的初始态为 $|0\\rangle$,我们定义 QNN 的输出为对某个可观测量 $M$ 的测量,即\n", + "\n", + "$$\n", + "f_U(x) = \\langle 0| U^\\dagger M U |0\\rangle, \\tag{1}\n", + "$$\n", + "\n", + "其中 $x$ 为数据输入,$U$ 表示该 QNN。\n", + "\n", + "首先我们考虑最简单的情况,选择 $R_Z$ 为编码门,$R_Y$ 为可训练门,定义一种如下的 QNN:\n", + "\n", + "$$\n", + "U^{\\mathit{YZY}}_{\\mathbf{\\theta}, L}(x) = R_Y(\\theta_0) \\sum_{j=1}^LR_Z(x)R_Y(\\theta_j), \\tag{2}\n", + "$$\n", + "\n", + "其中 $\\mathbf{\\theta} := (\\theta_0, \\ldots, \\theta_L)$ 为可训练参数,$L$ 为 QNN 的层数。\n", + "\n", + "我们证明了这种单比特 QNN 可以表示傅里叶级数\n", + "\n", + "$$\n", + "\\langle 0|U^{\\mathit{YZY}}_{\\mathbf{\\theta}, L}(x) |0\\rangle = a_0 + \\sum_{j=1}^{L}a_j\\cos(nx), \\tag{3}\n", + "$$\n", + "\n", + "当选取可观测量为泡利算子 $Z$ 时,该 QNN 的输出也就能近似模拟任意平方可积的偶函数 $f: [-\\pi, \\pi] \\to [-1, 1]$。\n", + "\n", + "接下来我们使用量桨实现单比特 QNN 模拟偶函数的实验来验证以上结果。我们先通过下面几行代码引入必要的包。" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import paddle\n", + "import numpy as np\n", + "import paddle_quantum\n", + "from paddle_quantum.ansatz import Circuit\n", + "from paddle_quantum.hamiltonian import Hamiltonian\n", + "from paddle_quantum.loss import ExpecVal\n", + "import matplotlib.pyplot as plt\n", + "import brewer2mpl\n", + "import matplotlib\n", + "# 设置后端为态矢量模式\n", + "paddle_quantum.set_backend(\"state_vector\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "我们首先构造 QNN 对应的参数化量子电路,由数据编码门 $R_Z$ 和可训练门 $R_Y$ 组成。 " + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# 构造 YZY 结构的参数化量子电路\n", + "def U_YZY(train_block, w_theta, x):\n", + " cir = Circuit(1)\n", + " for i in range(train_block):\n", + " cir.ry(0, param=w_theta[i])\n", + " cir.rz(0, param=x) # 输入数据\n", + " cir.ry(0, param=w_theta[-1])\n", + " return cir" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "选定模拟的目标函数为阻尼振荡函数 $f(x) = \\sin(5x)/5x$,我们需要从目标函数中采样获得数据点用于 QNN 的训练。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# 定义目标函数\n", + "def target_func(x):\n", + " return np.sin(5 * x) / (5 * x)\n", + "\n", + "# 随机采样获取关于目标函数的数据点\n", + "def get_data():\n", + " x_plot = np.arange(0, np.pi, np.pi/1000)\n", + " y_plot = np.sin(5*x_plot) / (5*x_plot)\n", + " \n", + " np.random.seed(0)\n", + " x_all = np.random.uniform(0, np.pi, 300)\n", + " \n", + " y_all = np.sin(5*x_all) / (5*x_all)\n", + "\n", + " x_train, y_train = x_all[:200], y_all[:200]\n", + " x_test, y_test = x_all[200:], y_all[200:]\n", + "\n", + " return x_train, y_train, x_test, y_test, x_plot, y_plot\n", + " \n", + "# 采样获取训练集和测试集\n", + "x_train, y_train, x_test, y_test, x_plot, y_plot = get_data()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "接下来定义 QNN 训练模型以及训练相关的函数。" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "class QNN(paddle.nn.Layer):\n", + " def __init__(self, \n", + " train_block, # 电路的层数为 L\n", + " SEED=0,\n", + " dtype='float64'):\n", + " super(QNN, self).__init__()\n", + " self.train_block = train_block\n", + " paddle.seed(SEED)\n", + " # 初始化可训练参数\n", + " self.w_theta = self.create_parameter(\n", + " shape=[(train_block+1)],\n", + " default_initializer=paddle.nn.initializer.Uniform(0.0, 2 * np.pi),\n", + " dtype=dtype,\n", + " is_bias=False)\n", + "\n", + "\n", + " def forward(self, x):\n", + " \"\"\"\n", + " Forward propagation\n", + " \"\"\"\n", + " predict = []\n", + " H = Hamiltonian([(1.0, \"z0\")])\n", + " out_func = ExpecVal(H)\n", + " x = paddle.to_tensor(x, dtype='float64')\n", + " if len(x.shape) == 1: # 对于 1 维数据进行处理\n", + " x = x.reshape((-1, 1))\n", + " for i in range(x.shape[0]):\n", + " cir = U_YZY(self.train_block, self.w_theta, x[i])\n", + " # 运行量子电路\n", + " out_state = cir()\n", + " predict.append(out_func(out_state))\n", + " return paddle.concat(predict).reshape((-1,)), cir\n", + "\n", + "\n", + "# 训练函数\n", + "def train_qnn(x, y, train_block, LR, ITR, SEED, BATCHSIZE=20):\n", + " model = QNN(train_block, SEED)\n", + " opt = paddle.optimizer.Adam(learning_rate=LR, parameters=model.parameters())\n", + " loss_list = []\n", + " x = paddle.to_tensor(x, dtype='float64')\n", + " y = paddle.to_tensor(y, dtype='float64')\n", + " for ep in range(1, ITR + 1):\n", + " # 对数据进行分批训练\n", + " for itr in range(len(x) // BATCHSIZE):\n", + " x_batch = x[itr * BATCHSIZE:(itr + 1) * BATCHSIZE]\n", + " y_batch = y[itr * BATCHSIZE:(itr + 1) * BATCHSIZE]\n", + " # 运行定义的量子神经网络\n", + " predict, cir = model(x_batch)\n", + " avg_loss = paddle.mean((predict - y_batch) ** 2)\n", + " loss_list.append(avg_loss.numpy())\n", + " # 计算梯度并进行优化\n", + " avg_loss.backward()\n", + " opt.minimize(avg_loss)\n", + " opt.clear_grad()\n", + " if (itr+1) % 5 == 0:\n", + " print(\"qnn:epoch:\", ep,\"qnn:iter:\", (itr+1), \" train loss:\", \"%.8f\" % avg_loss.numpy())\n", + "\n", + " return model, loss_list" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "我们使用层数为 10 的 QNN 来模拟目标函数,在训练前还需要设定一些有关优化器的超参数。" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\Users\\yuzhan01\\Miniconda3\\envs\\pq_new\\lib\\site-packages\\paddle\\tensor\\creation.py:125: DeprecationWarning: `np.object` is a deprecated alias for the builtin `object`. To silence this warning, use `object` by itself. Doing this will not modify any behavior and is safe. \n", + "Deprecated in NumPy 1.20; for more details and guidance: https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations\n", + " if data.dtype == np.object:\n", + "c:\\Users\\yuzhan01\\Miniconda3\\envs\\pq_new\\lib\\site-packages\\paddle\\fluid\\framework.py:1104: DeprecationWarning: `np.bool` is a deprecated alias for the builtin `bool`. To silence this warning, use `bool` by itself. Doing this will not modify any behavior and is safe. If you specifically wanted the numpy scalar type, use `np.bool_` here.\n", + "Deprecated in NumPy 1.20; for more details and guidance: https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations\n", + " elif dtype == np.bool:\n", + "c:\\Users\\yuzhan01\\Miniconda3\\envs\\pq_new\\lib\\site-packages\\paddle\\fluid\\dygraph\\math_op_patch.py:276: UserWarning: The dtype of left and right variables are not the same, left dtype is paddle.float32, but right dtype is paddle.float64, the right dtype will convert to paddle.float32\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "qnn:epoch: 1 qnn:iter: 5 train loss: 0.12315345\n", + "qnn:epoch: 1 qnn:iter: 10 train loss: 0.06922857\n", + "qnn:epoch: 2 qnn:iter: 5 train loss: 0.02042443\n", + "qnn:epoch: 2 qnn:iter: 10 train loss: 0.04707706\n", + "qnn:epoch: 3 qnn:iter: 5 train loss: 0.01874223\n", + "qnn:epoch: 3 qnn:iter: 10 train loss: 0.01295448\n", + "qnn:epoch: 4 qnn:iter: 5 train loss: 0.00991240\n", + "qnn:epoch: 4 qnn:iter: 10 train loss: 0.00303511\n", + "qnn:epoch: 5 qnn:iter: 5 train loss: 0.00157935\n", + "qnn:epoch: 5 qnn:iter: 10 train loss: 0.00089821\n", + "qnn:epoch: 6 qnn:iter: 5 train loss: 0.00046386\n", + "qnn:epoch: 6 qnn:iter: 10 train loss: 0.00054655\n", + "qnn:epoch: 7 qnn:iter: 5 train loss: 0.00059435\n", + "qnn:epoch: 7 qnn:iter: 10 train loss: 0.00022313\n", + "qnn:epoch: 8 qnn:iter: 5 train loss: 0.00028409\n", + "qnn:epoch: 8 qnn:iter: 10 train loss: 0.00017835\n", + "qnn:epoch: 9 qnn:iter: 5 train loss: 0.00017996\n", + "qnn:epoch: 9 qnn:iter: 10 train loss: 0.00018871\n", + "qnn:epoch: 10 qnn:iter: 5 train loss: 0.00016455\n", + "qnn:epoch: 10 qnn:iter: 10 train loss: 0.00012700\n" + ] + } + ], + "source": [ + "SEED = 4096\n", + "QITR = 10\n", + "QLR = 0.1\n", + "train_block = 10\n", + "modelL10, loss_listL10 = train_qnn(x_train, y_train, train_block=train_block, LR=QLR, ITR=QITR, SEED=SEED)\n", + "predictL10 = modelL10(x_test)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "训练完成后,以图像形式展示函数模拟的结果。" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAbQAAAEZCAYAAAD/ttB2AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/YYfK9AAAACXBIWXMAAAsTAAALEwEAmpwYAABTo0lEQVR4nO3de3zO9fvA8de1s83MYQcMm/Mh51ZCMoqkIlLRmZBKopMOSgcdqUhRIkUq/XTyVUhOhcKcmcOcz2bYwc679/79ce++m9ls7HBv967n43E/Zu/P6bp33+7rfn/eJzHGoJRSSpV1Lo4OQCmllCoKmtCUUko5BU1oSimlnIImNKWUUk5BE5pSSimn4OboAMorf39/Exoa6ugwlFKqTNmwYUOMMSYgt22a0BwkNDSUiIgIR4ehlFJliogcymub3nJUSinlFDShKaWUcgqa0JRSSjkFp05oIlJDRBaJiM7vpZRSTs5pO4WISB/gIyD9Co51B14F7gIygHjgeWPMqlz2HQkMzdovA3jDGPPLFQeuVDmQmZnJ0aNHSUxMdHQoqpRxd3cnMDCQSpUqXfaxTpvQgBeAbsDLQIPLPHYy0BXoaIw5LSKDgSUi0t4Ys9m2k4i8ADwLtDPG7BORbsDvItLLGLOwSJ6FUk4oJiYGEaFx48a4uDj1jSJ1GYwxJCcnc+zYMYDLTmrO/E7qaIyJutyDRKQx1hrXu8aY0wDGmOnAfuCtbPtVBl4Bphhj9mXttwT4A5hQ6OiVcmKxsbEEBQVpMlMXEBG8vb0JDg4mOjr6so932neTMSbjCg/tAwiwPEf5MqC7iFTM+r0H4J3Hfs1EpMkVXj9fb02eRa9HRvPGpJlEbN2FLgGkyhqLxYK7u7ujw1ClVIUKFUhPv+zWIqe+5XilWgKZwOEc5Qew/r2aAeuy9rOV59zPdp5d2TeIyFCstT/q1KlzxQEeOnaCI8ejOXI8mp8X/UXbZg14cegAGjSud8XnVKqkiYijQ1Cl1JW+N5y2hlYI/kCSMcaSozw+62e1bPsBJOSzn50xZpoxJswYExYQkOvMLQVyfVhLOoa1wMPd+n1kY+ReBox4g3kz513xOZVSqqzThFZwBf3KUOxfOx/sdwufvPk0C2d9wG1+fogxZLgIb/3wGx9P+oqM89pzTKkr0aRJE8LDwwkPD6d69eoEBQXZf2/SpNhaEQpl3rx5tGrVirZt2/LKK6+U6LV79uzJihUrSvSal6IJ7WIxgLeIuOYo9836eSbbftnL89qv2HifPkf/7fuZMnoYFS2ZAMxctJK3wx8gZW/OO6ZKqfxUr16dFStWsGLFCnr06EG3bt3sv1evXr3E4wkNDc03YYwaNYqpU6eyfv36QjVl5Oe1117j4YcfvqDs+++/p3PnzsV2zculCe1iW7H+XWrnKK+LdZzZzmz7AYTmsl/27cUmetJs/B+5k+u6XMfH4R2oCogx+KWlc2Lc1OK+vFJO55133rmibY509OhRatasiaurK0OGDCnRa1eqVKlUtYWW+4QmIkEikv3v8DNggPAcu3YB/jDG2NrMFgFJeewXaYzZRTFKiTpEwop1+A/uB0DLJ+/nhRPneOzkObp7eJH471atpSl1mdq3b3/JbdOmTaNDhw507dqVG2+8kcjISADWrVtH69atCQ0NZfz48XTo0MH+QR8REUFYWBgdO3bkscce4/rrr6dJkybMnz8fgMWLF9O+fXvCw8O5/fbbOX78OAADBw7k5MmTjBw5kvDwcDZs2HBBPGlpaYSHhwPQv39/Bg4cyPPPP0/lypX56quvAHj00Ufx8vKy1/Ief/xxKleuzCuvvMJdd91F48aNeemlly447wcffMB1111Hly5d6NmzJxs3bmTu3Ll89dVXLFq0iPDwcN566y3Gjx9P9erVee211+zHLlq0iA4dOnDDDTdw8803s3fvXgCmTZtGaGgo/fv359FHH6Vt27b07NmTlJSUy3+RLqFc93IUkY7AX8A04DEAY8xuEZkGvCgiC4wxMSIyCKgP3G871hgTKyJvAs+IyCxjzH4RuQm4GehV3LHbameuvj4AuPr60HLgnYQs/AuP2jU4//cGToybSt2vSue3SqVymr9kFfOXXDQZzwUa16vDc8Putf++e99hxn/+bb7nnv7+C4WOD6wDf5cvX46npycrVqzg0Ucf5e+//+baa69l4sSJdO/enTZt2vDcc8/x7LPPkpaWRp8+fXj//fcZMGAAmzdvJiwsjOnTp9OrVy8OHDhAv379iIiIoHHjxnz66ac8+OCD/Pnnn8ycOZPly5czceJEe+LKzsPDgxUrViAifP/999jWV1y3bp19n88//5zFixfbf58yZQqRkZFs3LiRBQsWcPLkSerUqcPw4cOpWbMm3377LTNnzmTd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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "matplotlib.rcParams[\"font.family\"] = \"serif\"\n", + "matplotlib.rcParams[\"mathtext.fontset\"] = \"cm\"\n", + "bmap = brewer2mpl.get_map(\"Set1\", \"qualitative\", 7)\n", + "colors = bmap.mpl_colors\n", + "\n", + "plt.plot(x_plot, y_plot, color=\"#304860\", ls=\"--\", lw=2.5, label=\"Target function\")\n", + "\n", + "plt.scatter(\n", + " x_test,\n", + " predictL10[0].numpy(),\n", + " s=40,\n", + " marker=\"^\",\n", + " facecolor=\"white\",\n", + " color=\"#D1193E\",\n", + " label=\"QNN L=10\",\n", + " )\n", + "plt.xlabel(r\"$x$\", fontdict={\"size\": 22})\n", + "plt.ylabel(r\"$f(x)$\", fontdict={\"size\": 22})\n", + "plt.xticks(fontsize=10)\n", + "plt.yticks(fontsize=10)\n", + "plt.legend(prop={\"size\": 12})\n", + "plt.text(0, -0.2, r\"(a)\", fontsize=16)\n", + "plt.tick_params(labelsize=16)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "从结果可以看出,使用层数为 10 的 QNN 能以较高精度近似目标函数,验证了关于 QNN 表达能力的理论结果。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 单比特 QNN 模拟任意函数\n", + "既然 $U^{\\mathit{YZY}}_{\\mathbf{\\theta}, L}$ 可以模拟任意偶函数,那么有没有办法可以做出一些修改,使得 QNN 可以模拟任意的函数呢?实际上,我们只需要在 $U^{\\mathit{YZY}}_{\\mathbf{\\theta}, L}$ 对应的仅含 $\\cos$ 项的傅里叶级数的基础上,引入 $\\sin$ 项即可得到完整的傅里叶级数。具体的,我们需要加入额外的可训练门 $R_Z$,定义一种如下的 QNN:\n", + "\n", + "$$\n", + "U^{\\mathit{WZW}}_{\\mathbf{\\theta},\\mathbf{\\phi}, L}(x) = R_Y(\\theta_0)R_Z(\\phi_0) \\sum_{j=1}^L R_Z(x) R_Y(\\theta_j)R_Z(\\phi_j), \\tag{4}\n", + "$$\n", + "\n", + "其中 $\\mathbf{\\theta} := (\\theta_0, \\ldots, \\theta_L)$ 和 $\\mathbf{\\phi} := (\\phi_0, \\ldots, \\phi_L)$ 为可训练参数,$L$ 为 QNN 的层数。我们证明了这种单比特 QNN 可以表示傅里叶级数\n", + "\n", + "$$\n", + "\\langle 0|U^{\\mathit{WZW}}_{\\mathbf{\\theta},\\mathbf{\\phi}, L}(x) |0\\rangle = a_0 + \\sum_{j=1}^{L}(a_j\\cos(nx)+ b_j\\sin(nx)), \\tag{5}\n", + "$$\n", + "\n", + "从而能够近似模拟任意平方可积的函数 $f: [-\\pi, \\pi] \\to [-1, 1]$。\n", + "\n", + "接下来我们使用量桨实现单比特 QNN 模拟方波函数的实验来验证以上结果。首先构造 $U^{\\mathit{WZW}}_{\\mathbf{\\theta},\\mathbf{\\phi}, L}$ 形式的 QNN 对应的参数化量子电路。" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "def U_WZW(train_block, w_theta, x):\n", + " cir = Circuit(1)\n", + " for i in range(train_block):\n", + " cir.rz(0, param=w_theta[i][1])\n", + " cir.ry(0, param=w_theta[i][0])\n", + " cir.rz(0, param=x) # 输入数据\n", + " cir.rz(0, param=w_theta[-1][1])\n", + " cir.ry(0, param=w_theta[-1][0])\n", + " return cir" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "与前一章节类似,我们需要定义模拟的目标函数,并从目标函数中采样获得数据点用于 QNN 的训练。" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "def square_wave(trunk):\n", + " x_train = np.linspace(0, 20, 400)\n", + " x_test = np.linspace(0.02, 30, 150)\n", + "\n", + " def func(x):\n", + " cof = 0\n", + " for i in range(1, trunk+1, 2):\n", + " cof = cof + 4*np.sin(i*x)/(i*np.pi)\n", + " y_max = max(cof)\n", + " cof /= y_max\n", + " return cof\n", + " \n", + " y_train = func(x_train)\n", + " y_test = func(x_test)\n", + "\n", + " return x_train, y_train, x_test, y_test\n", + "\n", + "x_train, y_train, x_test, y_test = square_wave(10000)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "接下来根据 QNN 结构定义其训练模型以及训练相关的函数。" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "class QNN(paddle.nn.Layer):\n", + " def __init__(self, \n", + " train_block, # 电路的层数为 L\n", + " SEED=0,\n", + " dtype='float64'):\n", + " super(QNN, self).__init__()\n", + " self.train_block = train_block\n", + " paddle.seed(SEED)\n", + " # 初始化可训练参数\n", + " self.w_theta = self.create_parameter(\n", + " shape=[(train_block+1), 2],\n", + " default_initializer=paddle.nn.initializer.Uniform(0.0, 2*np.pi),\n", + " dtype=dtype,\n", + " is_bias=False)\n", + "\n", + "\n", + " def forward(self, x):\n", + " \"\"\"\n", + " Forward propagation\n", + " \"\"\"\n", + " predict = []\n", + " H = Hamiltonian([(1.0, \"z0\")])\n", + " out_func = ExpecVal(H)\n", + " x = paddle.to_tensor(x, dtype='float64')\n", + " if len(x.shape) == 1: # 对于 1 维数据进行处理\n", + " x = x.reshape((-1, 1))\n", + " for i in range(x.shape[0]):\n", + " cir = U_WZW(self.train_block, self.w_theta, x[i])\n", + " # 运行量子电路\n", + " out_state = cir()\n", + " predict.append(out_func(out_state))\n", + " return paddle.concat(predict).reshape((-1,)), cir\n", + "\n", + "\n", + "# 定义训练函数\n", + "def train_qnn(x, y, train_block, LR, ITR, SEED, BATCHSIZE=40):\n", + " model = QNN(train_block, SEED)\n", + " opt = paddle.optimizer.Adam(learning_rate=LR, parameters=model.parameters())\n", + " loss_list = []\n", + " x = paddle.to_tensor(x, dtype='float64')\n", + " y = paddle.to_tensor(y, dtype='float64')\n", + " for ep in range(1, ITR + 1):\n", + " # 对数据进行分批训练\n", + " for itr in range(len(x) // BATCHSIZE):\n", + " x_batch = x[itr * BATCHSIZE:(itr + 1) * BATCHSIZE]\n", + " y_batch = y[itr * BATCHSIZE:(itr + 1) * BATCHSIZE]\n", + " # 运行上面定义的量子神经网络\n", + " predict, cir = model(x_batch)\n", + " avg_loss = paddle.mean((predict - y_batch) ** 2)\n", + " loss_list.append(avg_loss.numpy())\n", + " # 计算梯度并进行优化\n", + " avg_loss.backward()\n", + " opt.minimize(avg_loss)\n", + " opt.clear_grad()\n", + " if (itr+1) % 5 == 0:\n", + " print(\"qnn:epoch:\", ep,\"qnn:iter:\", (itr+1), \" train loss:\", \"%.8f\" % avg_loss.numpy())\n", + "\n", + " return model, loss_list" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "我们使用层数为 45 的 QNN 来模拟该方波函数,注意这里使用的层数与目标函数的傅里叶截断误差有关,通常来说对于越复杂的函数使用的层数越多,使用的层数越多模拟的效果越好。" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\Users\\yuzhan01\\Miniconda3\\envs\\pq_new\\lib\\site-packages\\paddle\\tensor\\creation.py:125: DeprecationWarning: `np.object` is a deprecated alias for the builtin `object`. To silence this warning, use `object` by itself. Doing this will not modify any behavior and is safe. \n", + "Deprecated in NumPy 1.20; for more details and guidance: https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations\n", + " if data.dtype == np.object:\n", + "c:\\Users\\yuzhan01\\Miniconda3\\envs\\pq_new\\lib\\site-packages\\paddle\\fluid\\framework.py:1104: DeprecationWarning: `np.bool` is a deprecated alias for the builtin `bool`. To silence this warning, use `bool` by itself. Doing this will not modify any behavior and is safe. If you specifically wanted the numpy scalar type, use `np.bool_` here.\n", + "Deprecated in NumPy 1.20; for more details and guidance: https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations\n", + " elif dtype == np.bool:\n", + "c:\\Users\\yuzhan01\\Miniconda3\\envs\\pq_new\\lib\\site-packages\\paddle\\fluid\\dygraph\\math_op_patch.py:276: UserWarning: The dtype of left and right variables are not the same, left dtype is paddle.float32, but right dtype is paddle.float64, the right dtype will convert to paddle.float32\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "qnn:epoch: 1 qnn:iter: 5 train loss: 1.35621011\n", + "qnn:epoch: 1 qnn:iter: 10 train loss: 1.06700826\n", + "qnn:epoch: 2 qnn:iter: 5 train loss: 1.59428942\n", + "qnn:epoch: 2 qnn:iter: 10 train loss: 0.32698074\n", + "qnn:epoch: 3 qnn:iter: 5 train loss: 0.30190033\n", + "qnn:epoch: 3 qnn:iter: 10 train loss: 0.09284516\n", + "qnn:epoch: 4 qnn:iter: 5 train loss: 0.11605076\n", + "qnn:epoch: 4 qnn:iter: 10 train loss: 0.06084419\n", + "qnn:epoch: 5 qnn:iter: 5 train loss: 0.10283329\n", + "qnn:epoch: 5 qnn:iter: 10 train loss: 0.07899086\n", + "qnn:epoch: 6 qnn:iter: 5 train loss: 0.06403162\n", + "qnn:epoch: 6 qnn:iter: 10 train loss: 0.05624062\n", + "qnn:epoch: 7 qnn:iter: 5 train loss: 0.05701165\n", + "qnn:epoch: 7 qnn:iter: 10 train loss: 0.05501990\n", + "qnn:epoch: 8 qnn:iter: 5 train loss: 0.05415571\n", + "qnn:epoch: 8 qnn:iter: 10 train loss: 0.05919911\n", + "qnn:epoch: 9 qnn:iter: 5 train loss: 0.05508716\n", + "qnn:epoch: 9 qnn:iter: 10 train loss: 0.05666707\n", + "qnn:epoch: 10 qnn:iter: 5 train loss: 0.05592950\n", + "qnn:epoch: 10 qnn:iter: 10 train loss: 0.05661748\n" + ] + } + ], + "source": [ + "SEED = 2\n", + "QITR = 10\n", + "QLR = 0.1\n", + "train_block = 45\n", + "modelL45, loss_listL45 = train_qnn(x_train, y_train, train_block=train_block, LR=QLR, ITR=QITR, SEED=SEED)\n", + "predictL45 = modelL45(x_test)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "最后我们作图展示函数模拟的结果。" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax1 = plt.subplots(1, 1,figsize=(10,6))\n", + "ax1.plot(x_train, y_train, label='Target function', color='#000181', lw=2, linestyle='--')\n", + "ax1.plot(x_test, predictL45[0].numpy(), label='QNN L=45', color='#AE2D68', lw=2,linestyle='-')\n", + "ax1.axvline(20, alpha=0.7,ls='--',c='#280659')\n", + "ax1.set_xlabel(r'$x$', fontdict={'size':22})\n", + "ax1.set_ylabel(r'$f(x)$', fontdict={'size':22})\n", + "plt.tick_params(labelsize=16)\n", + "ax1.legend(prop={'size': 12})\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "由结果可以看出,即使是方波这种对于经典 NN 而言难以模拟的函数,使用 QNN 也可以达到良好的近似效果,验证了 QNN 的表达能力。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 参考文献\n", + "\n", + "[1] Schuld, Maria, Ryan Sweke, and Johannes Jakob Meyer. \"Effect of data encoding on the expressive power of variational quantum-machine-learning models.\" [Physical Review A 103.3 (2021): 032430.](https://doi.org/10.1103/PhysRevA.103.032430)\n", + "\n", + "[2] Yu, Zhan, et al. \"Power and limitations of single-qubit native quantum neural networks.\" [arXiv preprint arXiv:2205.07848 (2022).](https://doi.org/10.48550/arXiv.2205.07848)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.8.13 ('newpq')", + "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.8.13" + }, + "vscode": { + "interpreter": { + "hash": "de7cd855378eaab3f20deac830fabedba9ee866c40f7d3ccd0ebe0daf90b4a28" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tutorials/machine_learning/QApproximating_EN.ipynb b/tutorials/machine_learning/QApproximating_EN.ipynb new file mode 100644 index 0000000..a55a0b6 --- /dev/null +++ b/tutorials/machine_learning/QApproximating_EN.ipynb @@ -0,0 +1,633 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Quantum Neural Network Approximating Functions\n", + "\n", + "*Copyright (c) 2022 Institute for Quantum Computing, Baidu Inc. All Rights Reserved.*" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Overview\n", + "Quantum neural network (QNN) is a common quantum machine learning model that consists of parameterized quantum circuits. By tuning parameters of quantum circuits, a QNN is able to minimize an objective function of interest. Similar to the Neural Network (NN) model in machine learning, the expressivity of QNN is characterized by the function classes that it can approximate. The Universal Approximation Theorem (UAT) in machine learning theory describes the ability of multi-layer NNs to approximate any function. In recent times, the universal approximation property (UAP) of multi-qubit QNN models has been investigated by correlating QNN to Fourier series [1]. However, the expressivity of single-qubit QNNs remains an open problem. In our recent paper [2], we prove that single-qubit QNNs can approximate any univariate function, by exploring connections to quantum signal processing, which solve this open problem. In this tutorial, we demonstrate how to use single-qubit QNNs to approximate any target function." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Single-qubit QNN approximating any even function\n", + "We use data re-uploading single-qubit QNNs that consist of interleaved data encoding gates and trainable gates. Both the data encoding gates and trainable gates are selected from the Pauli rotation gates $\\{ R_X,R_Y,R_Z \\}$. Let the initial state be $|0\\rangle$,we define the output of the QNN be the measurement results of some observables $M$,\n", + "\n", + "$$\n", + "f_U(x) = \\langle 0| U^\\dagger M U |0\\rangle, \\tag{1}\n", + "$$\n", + "\n", + "where $x$ is the input data and $U$ denotes the QNN.\n", + "\n", + "First, let us consider the simplest case, i.e. choosing $R_Z$ as the data encoding gates and $R_Y$ as the trainable gate. We define the single-qubit QNN as follow,\n", + "\n", + "$$\n", + "U^{\\mathit{YZY}}_{\\mathbf{\\theta}, L}(x) = R_Y(\\theta_0) \\sum_{j=1}^LR_Z(x)R_Y(\\theta_j), \\tag{2}\n", + "$$\n", + "\n", + "where $\\mathbf{\\theta} := (\\theta_0, \\ldots, \\theta_L)$ is the set of trainable parameters and $L$ denotes the number of layers.\n", + "\n", + "We prove that a single-qubit QNN $U^{\\mathit{YZY}}_{\\mathbf{\\theta}, L}(x)$ can represent Fourier series\n", + "\n", + "$$\n", + "\\langle 0|U^{\\mathit{YZY}}_{\\mathbf{\\theta}, L}(x) |0\\rangle = a_0 + \\sum_{j=1}^{L}a_j\\cos(nx). \\tag{3}\n", + "$$\n", + "When choosing the observable as the Pauli operator $Z$,the output of this QNN can approximate any square-integrable even function $f: [-\\pi, \\pi] \\to [-1, 1]$.\n", + "\n", + "Now we numerically simulate the single-qubit QNN approximation on Paddle Quantum to verify the results. First we import the required packages." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "import paddle\n", + "import numpy as np\n", + "import paddle_quantum\n", + "from paddle_quantum.ansatz import Circuit\n", + "from paddle_quantum.hamiltonian import Hamiltonian\n", + "from paddle_quantum.loss import ExpecVal\n", + "import matplotlib.pyplot as plt\n", + "import brewer2mpl\n", + "import matplotlib\n", + "# set the backend to state vector mode\n", + "paddle_quantum.set_backend(\"state_vector\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We define a function to construct the corresponding QNN, consisting of interleaved data encoding gates $R_Z$ and trainable gates $R_Y$." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "# Construct the parameterized quantum circuit in YZY structure.\n", + "def U_YZY(train_block, w_theta, x):\n", + " cir = Circuit(1)\n", + " for i in range(train_block):\n", + " cir.ry(0, param=w_theta[i])\n", + " cir.rz(0, param=x) # input data\n", + " cir.ry(0, param=w_theta[-1])\n", + " return cir" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let a damping function $f(x) = \\sin(5x)/5x$ be the target function, and we need to sample data points used for training." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "C:\\Users\\yuzhan01\\AppData\\Local\\Temp\\ipykernel_26936\\3212922392.py:8: RuntimeWarning: invalid value encountered in true_divide\n", + " y_plot = np.sin(5*x_plot) / (5*x_plot)\n" + ] + } + ], + "source": [ + "# Define the target function\n", + "def target_func(x):\n", + " return np.sin(5 * x) / (5 * x)\n", + "\n", + "# Randomly sample data points from the target function.\n", + "def get_data():\n", + " x_plot = np.arange(0, np.pi, np.pi/1000)\n", + " y_plot = np.sin(5*x_plot) / (5*x_plot)\n", + " \n", + " np.random.seed(0)\n", + " x_all = np.random.uniform(0, np.pi, 300)\n", + " \n", + " y_all = np.sin(5*x_all) / (5*x_all)\n", + "\n", + " x_train, y_train = x_all[:200], y_all[:200]\n", + " x_test, y_test = x_all[200:], y_all[200:]\n", + "\n", + " return x_train, y_train, x_test, y_test, x_plot, y_plot\n", + " \n", + "# Get the training set and test set\n", + "x_train, y_train, x_test, y_test, x_plot, y_plot = get_data()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next we define the QNN training model and a training function." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [], + "source": [ + "class QNN(paddle.nn.Layer):\n", + " def __init__(self, \n", + " train_block, # L layer\n", + " SEED=0,\n", + " dtype='float64'):\n", + " super(QNN, self).__init__()\n", + " self.train_block = train_block\n", + " paddle.seed(SEED)\n", + " # initiate trainable parameter \n", + " self.w_theta = self.create_parameter(\n", + " shape=[(train_block+1)],\n", + " default_initializer=paddle.nn.initializer.Uniform(0.0, 2 * np.pi),\n", + " dtype=dtype,\n", + " is_bias=False)\n", + "\n", + "\n", + " def forward(self, x):\n", + " \"\"\"\n", + " Forward propagation\n", + " \"\"\"\n", + " predict = []\n", + " H = Hamiltonian([(1.0, \"z0\")])\n", + " out_func = ExpecVal(H)\n", + " x = paddle.to_tensor(x, dtype='float64')\n", + " if len(x.shape) == 1: # 1-dimension data\n", + " x = x.reshape((-1, 1))\n", + " for i in range(x.shape[0]):\n", + " cir = U_YZY(self.train_block, self.w_theta, x[i])\n", + " # Run the quantum circuit\n", + " out_state = cir()\n", + " predict.append(out_func(out_state))\n", + " return paddle.concat(predict).reshape((-1,)), cir\n", + "\n", + "\n", + "# Training\n", + "def train_qnn(x, y, train_block, LR, ITR, SEED, BATCHSIZE=20):\n", + " model = QNN(train_block, SEED)\n", + " opt = paddle.optimizer.Adam(learning_rate=LR, parameters=model.parameters())\n", + " loss_list = []\n", + " x = paddle.to_tensor(x, dtype='float64')\n", + " y = paddle.to_tensor(y, dtype='float64')\n", + " for ep in range(1, ITR + 1):\n", + " # Select batch of data\n", + " for itr in range(len(x) // BATCHSIZE):\n", + " x_batch = x[itr * BATCHSIZE:(itr + 1) * BATCHSIZE]\n", + " y_batch = y[itr * BATCHSIZE:(itr + 1) * BATCHSIZE]\n", + " # Run the network defined above\n", + " predict, cir = model(x_batch)\n", + " avg_loss = paddle.mean((predict - y_batch) ** 2)\n", + " loss_list.append(avg_loss.numpy())\n", + " # Calculate the gradient and optimize\n", + " avg_loss.backward()\n", + " opt.minimize(avg_loss)\n", + " opt.clear_grad()\n", + " if (itr+1) % 5 == 0:\n", + " print(\"qnn:epoch:\", ep,\"qnn:iter:\", (itr+1), \" train loss:\", \"%.8f\" % avg_loss.numpy())\n", + "\n", + " return model, loss_list" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We use a 10-layer QNN to approximate the target function. Before training, we need to set some hyper-parameters for the optimizer." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\Users\\yuzhan01\\Miniconda3\\envs\\pq_new\\lib\\site-packages\\paddle\\tensor\\creation.py:125: DeprecationWarning: `np.object` is a deprecated alias for the builtin `object`. To silence this warning, use `object` by itself. Doing this will not modify any behavior and is safe. \n", + "Deprecated in NumPy 1.20; for more details and guidance: https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations\n", + " if data.dtype == np.object:\n", + "c:\\Users\\yuzhan01\\Miniconda3\\envs\\pq_new\\lib\\site-packages\\paddle\\fluid\\framework.py:1104: DeprecationWarning: `np.bool` is a deprecated alias for the builtin `bool`. To silence this warning, use `bool` by itself. Doing this will not modify any behavior and is safe. If you specifically wanted the numpy scalar type, use `np.bool_` here.\n", + "Deprecated in NumPy 1.20; for more details and guidance: https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations\n", + " elif dtype == np.bool:\n", + "c:\\Users\\yuzhan01\\Miniconda3\\envs\\pq_new\\lib\\site-packages\\paddle\\fluid\\dygraph\\math_op_patch.py:276: UserWarning: The dtype of left and right variables are not the same, left dtype is paddle.float32, but right dtype is paddle.float64, the right dtype will convert to paddle.float32\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "qnn:epoch: 1 qnn:iter: 5 train loss: 0.12315345\n", + "qnn:epoch: 1 qnn:iter: 10 train loss: 0.06922857\n", + "qnn:epoch: 2 qnn:iter: 5 train loss: 0.02042443\n", + "qnn:epoch: 2 qnn:iter: 10 train loss: 0.04707706\n", + "qnn:epoch: 3 qnn:iter: 5 train loss: 0.01874223\n", + "qnn:epoch: 3 qnn:iter: 10 train loss: 0.01295448\n", + "qnn:epoch: 4 qnn:iter: 5 train loss: 0.00991240\n", + "qnn:epoch: 4 qnn:iter: 10 train loss: 0.00303511\n", + "qnn:epoch: 5 qnn:iter: 5 train loss: 0.00157935\n", + "qnn:epoch: 5 qnn:iter: 10 train loss: 0.00089821\n", + "qnn:epoch: 6 qnn:iter: 5 train loss: 0.00046386\n", + "qnn:epoch: 6 qnn:iter: 10 train loss: 0.00054655\n", + "qnn:epoch: 7 qnn:iter: 5 train loss: 0.00059435\n", + "qnn:epoch: 7 qnn:iter: 10 train loss: 0.00022313\n", + "qnn:epoch: 8 qnn:iter: 5 train loss: 0.00028409\n", + "qnn:epoch: 8 qnn:iter: 10 train loss: 0.00017835\n", + "qnn:epoch: 9 qnn:iter: 5 train loss: 0.00017996\n", + "qnn:epoch: 9 qnn:iter: 10 train loss: 0.00018871\n", + "qnn:epoch: 10 qnn:iter: 5 train loss: 0.00016455\n", + "qnn:epoch: 10 qnn:iter: 10 train loss: 0.00012700\n" + ] + } + ], + "source": [ + "SEED = 4096\n", + "QITR = 10\n", + "QLR = 0.1\n", + "train_block = 10\n", + "modelL10, loss_listL10 = train_qnn(x_train, y_train, train_block=train_block, LR=QLR, ITR=QITR, SEED=SEED)\n", + "predictL10 = modelL10(x_test)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After training, we plot the approximation result." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAbQAAAEZCAYAAAD/ttB2AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/YYfK9AAAACXBIWXMAAAsTAAALEwEAmpwYAABTo0lEQVR4nO3de3zO9fvA8de1s83MYQcMm/Mh51ZCMoqkIlLRmZBKopMOSgcdqUhRIkUq/XTyVUhOhcKcmcOcz2bYwc679/79ce++m9ls7HBv967n43E/Zu/P6bp33+7rfn/eJzHGoJRSSpV1Lo4OQCmllCoKmtCUUko5BU1oSimlnIImNKWUUk5BE5pSSimn4OboAMorf39/Exoa6ugwlFKqTNmwYUOMMSYgt22a0BwkNDSUiIgIR4ehlFJliogcymub3nJUSinlFDShKaWUcgqa0JRSSjkFp05oIlJDRBaJiM7vpZRSTs5pO4WISB/gIyD9Co51B14F7gIygHjgeWPMqlz2HQkMzdovA3jDGPPLFQeuVDmQmZnJ0aNHSUxMdHQoqpRxd3cnMDCQSpUqXfaxTpvQgBeAbsDLQIPLPHYy0BXoaIw5LSKDgSUi0t4Ys9m2k4i8ADwLtDPG7BORbsDvItLLGLOwSJ6FUk4oJiYGEaFx48a4uDj1jSJ1GYwxJCcnc+zYMYDLTmrO/E7qaIyJutyDRKQx1hrXu8aY0wDGmOnAfuCtbPtVBl4Bphhj9mXttwT4A5hQ6OiVcmKxsbEEBQVpMlMXEBG8vb0JDg4mOjr6so932neTMSbjCg/tAwiwPEf5MqC7iFTM+r0H4J3Hfs1EpMkVXj9fb02eRa9HRvPGpJlEbN2FLgGkyhqLxYK7u7ujw1ClVIUKFUhPv+zWIqe+5XilWgKZwOEc5Qew/r2aAeuy9rOV59zPdp5d2TeIyFCstT/q1KlzxQEeOnaCI8ejOXI8mp8X/UXbZg14cegAGjSud8XnVKqkiYijQ1Cl1JW+N5y2hlYI/kCSMcaSozw+62e1bPsBJOSzn50xZpoxJswYExYQkOvMLQVyfVhLOoa1wMPd+n1kY+ReBox4g3kz513xOZVSqqzThFZwBf3KUOxfOx/sdwufvPk0C2d9wG1+fogxZLgIb/3wGx9P+oqM89pzTKkr0aRJE8LDwwkPD6d69eoEBQXZf2/SpNhaEQpl3rx5tGrVirZt2/LKK6+U6LV79uzJihUrSvSal6IJ7WIxgLeIuOYo9836eSbbftnL89qv2HifPkf/7fuZMnoYFS2ZAMxctJK3wx8gZW/OO6ZKqfxUr16dFStWsGLFCnr06EG3bt3sv1evXr3E4wkNDc03YYwaNYqpU6eyfv36QjVl5Oe1117j4YcfvqDs+++/p3PnzsV2zculCe1iW7H+XWrnKK+LdZzZzmz7AYTmsl/27cUmetJs/B+5k+u6XMfH4R2oCogx+KWlc2Lc1OK+vFJO55133rmibY509OhRatasiaurK0OGDCnRa1eqVKlUtYWW+4QmIkEikv3v8DNggPAcu3YB/jDG2NrMFgFJeewXaYzZRTFKiTpEwop1+A/uB0DLJ+/nhRPneOzkObp7eJH471atpSl1mdq3b3/JbdOmTaNDhw507dqVG2+8kcjISADWrVtH69atCQ0NZfz48XTo0MH+QR8REUFYWBgdO3bkscce4/rrr6dJkybMnz8fgMWLF9O+fXvCw8O5/fbbOX78OAADBw7k5MmTjBw5kvDwcDZs2HBBPGlpaYSHhwPQv39/Bg4cyPPPP0/lypX56quvAHj00Ufx8vKy1/Ief/xxKleuzCuvvMJdd91F48aNeemlly447wcffMB1111Hly5d6NmzJxs3bmTu3Ll89dVXLFq0iPDwcN566y3Gjx9P9erVee211+zHLlq0iA4dOnDDDTdw8803s3fvXgCmTZtGaGgo/fv359FHH6Vt27b07NmTlJSUy3+RLqFc93IUkY7AX8A04DEAY8xuEZkGvCgiC4wxMSIyCKgP3G871hgTKyJvAs+IyCxjzH4RuQm4GehV3LHbameuvj4AuPr60HLgnYQs/AuP2jU4//cGToybSt2vSue3SqVymr9kFfOXXDQZzwUa16vDc8Putf++e99hxn/+bb7nnv7+C4WOD6wDf5cvX46npycrVqzg0Ucf5e+//+baa69l4sSJdO/enTZt2vDcc8/x7LPPkpaWRp8+fXj//fcZMGAAmzdvJiwsjOnTp9OrVy8OHDhAv379iIiIoHHjxnz66ac8+OCD/Pnnn8ycOZPly5czceJEe+LKzsPDgxUrViAifP/999jWV1y3bp19n88//5zFixfbf58yZQqRkZFs3LiRBQsWcPLkSerUqcPw4cOpWbMm3377LTNnzmTd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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "matplotlib.rcParams[\"font.family\"] = \"serif\"\n", + "matplotlib.rcParams[\"mathtext.fontset\"] = \"cm\"\n", + "bmap = brewer2mpl.get_map(\"Set1\", \"qualitative\", 7)\n", + "colors = bmap.mpl_colors\n", + "\n", + "plt.plot(x_plot, y_plot, color=\"#304860\", ls=\"--\", lw=2.5, label=\"Target function\")\n", + "\n", + "plt.scatter(\n", + " x_test,\n", + " predictL10[0].numpy(),\n", + " s=40,\n", + " marker=\"^\",\n", + " facecolor=\"white\",\n", + " color=\"#D1193E\",\n", + " label=\"QNN L=10\",\n", + " )\n", + "plt.xlabel(r\"$x$\", fontdict={\"size\": 22})\n", + "plt.ylabel(r\"$f(x)$\", fontdict={\"size\": 22})\n", + "plt.xticks(fontsize=10)\n", + "plt.yticks(fontsize=10)\n", + "plt.legend(prop={\"size\": 12})\n", + "plt.text(0, -0.2, r\"(a)\", fontsize=16)\n", + "plt.tick_params(labelsize=16)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "From the result, we can see that using a 10-layer QNN with YZY structure is able to approximate the target function in a high precision, which verifies the theoretical result on the expressivity of QNNs." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Single-qubit QNN approximating any function\n", + "\n", + "Now that $U^{\\mathit{YZY}}_{\\mathbf{\\theta}, L}$ can approximate any even function, can we make some changes so that it can approximate any function?\n", + "Actually, we just need to introduce $\\sin$ terms to complete the Fourier series, by adding an extra trainable gate $R_Z$ in each layer. We define a new QNN as follow:\n", + "\n", + "$$\n", + "U^{\\mathit{WZW}}_{\\mathbf{\\theta},\\mathbf{\\phi}, L}(x) = R_Y(\\theta_0)R_Z(\\phi_0) \\sum_{j=1}^L R_Z(x) R_Y(\\theta_j)R_Z(\\phi_j), \\tag{4}\n", + "$$\n", + "\n", + "where $\\mathbf{\\theta} := (\\theta_0, \\ldots, \\theta_L)$ and $\\mathbf{\\phi} := (\\phi_0, \\ldots, \\phi_L)$ are trainable parameters,$L$ denotes the number of layers. We proved that this QNN can represent the Fourier series,\n", + "\n", + "$$\n", + "\\langle 0|U^{\\mathit{WZW}}_{\\mathbf{\\theta},\\mathbf{\\phi}, L}(x) |0\\rangle = a_0 + \\sum_{j=1}^{L}(a_j\\cos(nx)+ b_j\\sin(nx)), \\tag{5}\n", + "$$\n", + "\n", + "and it can approximate any square-integrable function $f: [-\\pi, \\pi] \\to [-1, 1]$.\n", + "\n", + "Then we use the single-qubit QNN to approximate a square-wave function on Paddle Quantum to verify the results. First define a function to construct the corresponding QNN $U^{\\mathit{WZW}}_{\\mathbf{\\theta},\\mathbf{\\phi}, L}$." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "def U_WZW(train_block, w_theta, x):\n", + " cir = Circuit(1)\n", + " for i in range(train_block):\n", + " cir.rz(0, param=w_theta[i][1])\n", + " cir.ry(0, param=w_theta[i][0])\n", + " cir.rz(0, param=x) # input data\n", + " cir.rz(0, param=w_theta[-1][1])\n", + " cir.ry(0, param=w_theta[-1][0])\n", + " return cir" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Same as in previous section, we need to define the target function and sample data points used for training." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "def square_wave(trunk):\n", + " x_train = np.linspace(0, 20, 400)\n", + " x_test = np.linspace(0.02, 30, 150)\n", + "\n", + " def func(x):\n", + " cof = 0\n", + " for i in range(1, trunk+1, 2):\n", + " cof = cof + 4*np.sin(i*x)/(i*np.pi)\n", + " y_max = max(cof)\n", + " cof /= y_max\n", + " return cof\n", + " \n", + " y_train = func(x_train)\n", + " y_test = func(x_test)\n", + "\n", + " return x_train, y_train, x_test, y_test\n", + "\n", + "x_train, y_train, x_test, y_test = square_wave(10000)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next we define the QNN training model and a training function." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "class QNN(paddle.nn.Layer):\n", + " def __init__(self, \n", + " train_block, # L layer\n", + " SEED=0,\n", + " dtype='float64'):\n", + " super(QNN, self).__init__()\n", + " self.train_block = train_block\n", + " paddle.seed(SEED)\n", + " # initiate trainable parameter \n", + " self.w_theta = self.create_parameter(\n", + " shape=[(train_block+1), 2],\n", + " default_initializer=paddle.nn.initializer.Uniform(0.0, 2*np.pi),\n", + " dtype=dtype,\n", + " is_bias=False)\n", + "\n", + "\n", + " def forward(self, x):\n", + " \"\"\"\n", + " Forward propagation\n", + " \"\"\"\n", + " predict = []\n", + " H = Hamiltonian([(1.0, \"z0\")])\n", + " out_func = ExpecVal(H)\n", + " x = paddle.to_tensor(x, dtype='float64')\n", + " if len(x.shape) == 1: # 1-dimension data\n", + " x = x.reshape((-1, 1))\n", + " for i in range(x.shape[0]):\n", + " cir = U_WZW(self.train_block, self.w_theta, x[i])\n", + " # Run the quantum circuit\n", + " out_state = cir()\n", + " predict.append(out_func(out_state))\n", + " return paddle.concat(predict).reshape((-1,)), cir\n", + "\n", + "\n", + "# Training\n", + "def train_qnn(x, y, train_block, LR, ITR, SEED, BATCHSIZE=40):\n", + " model = QNN(train_block, SEED)\n", + " opt = paddle.optimizer.Adam(learning_rate=LR, parameters=model.parameters())\n", + " loss_list = []\n", + " x = paddle.to_tensor(x, dtype='float64')\n", + " y = paddle.to_tensor(y, dtype='float64')\n", + " for ep in range(1, ITR + 1):\n", + " # Select batch of data\n", + " for itr in range(len(x) // BATCHSIZE):\n", + " x_batch = x[itr * BATCHSIZE:(itr + 1) * BATCHSIZE]\n", + " y_batch = y[itr * BATCHSIZE:(itr + 1) * BATCHSIZE]\n", + " # Run the network defined above\n", + " predict, cir = model(x_batch)\n", + " avg_loss = paddle.mean((predict - y_batch) ** 2)\n", + " loss_list.append(avg_loss.numpy())\n", + " # Calculate the gradient and optimize\n", + " avg_loss.backward()\n", + " opt.minimize(avg_loss)\n", + " opt.clear_grad()\n", + " if (itr+1) % 5 == 0:\n", + " print(\"qnn:epoch:\", ep,\"qnn:iter:\", (itr+1), \" train loss:\", \"%.8f\" % avg_loss.numpy())\n", + "\n", + " return model, loss_list" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We use a 45-layer QNN to approximate the square-wave function. Note that the number of layers required for precise approximate is related to the truncation error for Fourier series. Usually more layers leads to better approximation results." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\Users\\yuzhan01\\Miniconda3\\envs\\pq_new\\lib\\site-packages\\paddle\\tensor\\creation.py:125: DeprecationWarning: `np.object` is a deprecated alias for the builtin `object`. To silence this warning, use `object` by itself. Doing this will not modify any behavior and is safe. \n", + "Deprecated in NumPy 1.20; for more details and guidance: https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations\n", + " if data.dtype == np.object:\n", + "c:\\Users\\yuzhan01\\Miniconda3\\envs\\pq_new\\lib\\site-packages\\paddle\\fluid\\framework.py:1104: DeprecationWarning: `np.bool` is a deprecated alias for the builtin `bool`. To silence this warning, use `bool` by itself. Doing this will not modify any behavior and is safe. If you specifically wanted the numpy scalar type, use `np.bool_` here.\n", + "Deprecated in NumPy 1.20; for more details and guidance: https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations\n", + " elif dtype == np.bool:\n", + "c:\\Users\\yuzhan01\\Miniconda3\\envs\\pq_new\\lib\\site-packages\\paddle\\fluid\\dygraph\\math_op_patch.py:276: UserWarning: The dtype of left and right variables are not the same, left dtype is paddle.float32, but right dtype is paddle.float64, the right dtype will convert to paddle.float32\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "qnn:epoch: 1 qnn:iter: 5 train loss: 1.35621011\n", + "qnn:epoch: 1 qnn:iter: 10 train loss: 1.06700826\n", + "qnn:epoch: 2 qnn:iter: 5 train loss: 1.59428942\n", + "qnn:epoch: 2 qnn:iter: 10 train loss: 0.32698074\n", + "qnn:epoch: 3 qnn:iter: 5 train loss: 0.30190033\n", + "qnn:epoch: 3 qnn:iter: 10 train loss: 0.09284516\n", + "qnn:epoch: 4 qnn:iter: 5 train loss: 0.11605076\n", + "qnn:epoch: 4 qnn:iter: 10 train loss: 0.06084419\n", + "qnn:epoch: 5 qnn:iter: 5 train loss: 0.10283329\n", + "qnn:epoch: 5 qnn:iter: 10 train loss: 0.07899086\n", + "qnn:epoch: 6 qnn:iter: 5 train loss: 0.06403162\n", + "qnn:epoch: 6 qnn:iter: 10 train loss: 0.05624062\n", + "qnn:epoch: 7 qnn:iter: 5 train loss: 0.05701165\n", + "qnn:epoch: 7 qnn:iter: 10 train loss: 0.05501990\n", + "qnn:epoch: 8 qnn:iter: 5 train loss: 0.05415571\n", + "qnn:epoch: 8 qnn:iter: 10 train loss: 0.05919911\n", + "qnn:epoch: 9 qnn:iter: 5 train loss: 0.05508716\n", + "qnn:epoch: 9 qnn:iter: 10 train loss: 0.05666707\n", + "qnn:epoch: 10 qnn:iter: 5 train loss: 0.05592950\n", + "qnn:epoch: 10 qnn:iter: 10 train loss: 0.05661748\n" + ] + } + ], + "source": [ + "SEED = 2\n", + "QITR = 10\n", + "QLR = 0.1\n", + "train_block = 45\n", + "modelL45, loss_listL45 = train_qnn(x_train, y_train, train_block=train_block, LR=QLR, ITR=QITR, SEED=SEED)\n", + "predictL45 = modelL45(x_test)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then we plot to show the approximation results." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax1 = plt.subplots(1, 1,figsize=(10,6))\n", + "ax1.plot(x_train, y_train, label='Target function', color='#000181', lw=2, linestyle='--')\n", + "ax1.plot(x_test, predictL45[0].numpy(), label='QNN L=45', color='#AE2D68', lw=2,linestyle='-')\n", + "ax1.axvline(20, alpha=0.7,ls='--',c='#280659')\n", + "ax1.set_xlabel(r'$x$', fontdict={'size':22})\n", + "ax1.set_ylabel(r'$f(x)$', fontdict={'size':22})\n", + "plt.tick_params(labelsize=16)\n", + "ax1.legend(prop={'size': 12})\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The single qubit QNN can approximate the target square-wave function that is arguably hard to approximate by classical NNs, which verifies the theoretical results." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Reference\n", + "\n", + "[1] Schuld, Maria, Ryan Sweke, and Johannes Jakob Meyer. \"Effect of data encoding on the expressive power of variational quantum-machine-learning models.\" [Physical Review A 103.3 (2021): 032430.](https://doi.org/10.1103/PhysRevA.103.032430)\n", + "\n", + "[2] Yu, Zhan, et al. \"Power and limitations of single-qubit native quantum neural networks.\" [arXiv preprint arXiv:2205.07848 (2022).](https://doi.org/10.48550/arXiv.2205.07848)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.8.13 ('newpq')", + "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.8.13" + }, + "vscode": { + "interpreter": { + "hash": "de7cd855378eaab3f20deac830fabedba9ee866c40f7d3ccd0ebe0daf90b4a28" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tutorials/machine_learning/figures/EncodingAnalysis-fig-illustration.png b/tutorials/machine_learning/figures/EncodingAnalysis-fig-illustration.png new file mode 100644 index 0000000..52fb831 Binary files /dev/null and b/tutorials/machine_learning/figures/EncodingAnalysis-fig-illustration.png differ diff --git a/tutorials/machine_learning/figures/EncodingAnalysis-fig-ry_circuit.png b/tutorials/machine_learning/figures/EncodingAnalysis-fig-ry_circuit.png new file mode 100644 index 0000000..8bf21ff Binary files /dev/null and b/tutorials/machine_learning/figures/EncodingAnalysis-fig-ry_circuit.png differ diff --git a/tutorials/machine_learning/figures/EncodingAnalysis-fig-u3_circuit.png b/tutorials/machine_learning/figures/EncodingAnalysis-fig-u3_circuit.png new file mode 100644 index 0000000..7203b02 Binary files /dev/null and b/tutorials/machine_learning/figures/EncodingAnalysis-fig-u3_circuit.png differ diff --git a/tutorials/qnn_research/BarrenPlateaus_CN.ipynb b/tutorials/qnn_research/BarrenPlateaus_CN.ipynb index d20d572..8f7acf2 100644 --- a/tutorials/qnn_research/BarrenPlateaus_CN.ipynb +++ b/tutorials/qnn_research/BarrenPlateaus_CN.ipynb @@ -97,7 +97,7 @@ }, "outputs": [], "source": [ - "def rand_circuit(theta, target, num_qubits):\n", + "def rand_circuit(target, num_qubits, theta=None):\n", " # 初始化量子电路\n", " cir = Circuit(num_qubits)\n", " \n", @@ -108,11 +108,11 @@ " # target是一个随机的数组,用来帮助我们抽取随机的单比特门 \n", " for i in range(num_qubits):\n", " if target[i] == 0:\n", - " cir.rz(i, param=theta[i])\n", + " cir.rz(i, param=theta[i] if theta is not None else theta)\n", " elif target[i] == 1:\n", - " cir.ry(i, param=theta[i])\n", + " cir.ry(i, param=theta[i] if theta is not None else theta)\n", " else:\n", - " cir.rx(i, param=theta[i])\n", + " cir.rx(i, param=theta[i] if theta is not None else theta)\n", " \n", " # ============== 第二层 ==============\n", " # 搭建两两相邻的 CZ 门\n", @@ -197,7 +197,7 @@ "class manual_gradient(paddle.nn.Layer):\n", " \n", " # 初始化一个可学习参数列表,并用 [0, 2*pi] 的均匀分布来填充初始值\n", - " def __init__(self, shape, param_attr=paddle.nn.initializer.Uniform(low=0.0, high=2 * pi), dtype='float32'):\n", + " def __init__(self, shape, param_attr=paddle.nn.initializer.Uniform(low=0.0, high=2*pi), dtype='float32'):\n", " super(manual_gradient, self).__init__()\n", " \n", " # 将 Numpy array 转换成 Paddle 中的 Tensor\n", @@ -207,7 +207,7 @@ " def forward(self):\n", " \n", " # 初始化三个 theta 参数列表\n", - " theta_np = np.random.uniform(low=0., high= 2 * pi, size=(THETA_SIZE))\n", + " theta_np = np.random.uniform(low=0., high=2*pi, size=(THETA_SIZE))\n", " theta_plus_np = np.copy(theta_np) \n", " theta_minus_np = np.copy(theta_np) \n", " \n", @@ -222,8 +222,8 @@ " # 生成随机目标,在 rand_circuit 中随机选取电路门\n", " target = np.random.choice(3, N) \n", " \n", - " U_plus = rand_circuit(theta_plus, target, N).unitary_matrix()\n", - " U_minus = rand_circuit(theta_minus, target, N).unitary_matrix()\n", + " U_plus = rand_circuit(target, N, theta_plus).unitary_matrix()\n", + " U_minus = rand_circuit(target, N, theta_minus).unitary_matrix()\n", "\n", " # 计算解析梯度\n", " grad = paddle.real((dagger(U_plus) @ self.H @ U_plus)[0][0] - (dagger(U_minus) @ self.H @ U_minus)[0][0])/2 \n", @@ -513,9 +513,7 @@ "# paddle.seed(SEED)\n", "target = np.random.choice(3, N)\n", "# 在 0 - 2*Pi 间随机生成各参数值\n", - "theta = paddle.rand([THETA_SIZE]).astype('float32') * 2 * pi\n", - "theta.stop_gradient = False\n", - "cir = rand_circuit(theta, target, N)\n", + "cir = rand_circuit(target, N)\n", "print(cir)\n", "# 随机生成哈密顿量\n", "H_l = Hamiltonian(random_pauli_str_generator(N, terms=7))\n", @@ -583,7 +581,7 @@ } ], "source": [ - "grad_mean_list, grad_variance_list = random_sample(cir, loss_func, samples, H_l, mode='single', param=0) " + "grad_mean_list, grad_variance_list = random_sample(cir, loss_func, samples, H_l, mode='single', param=0)" ] }, { @@ -871,13 +869,11 @@ " THETA_SIZE = N \n", " target = np.random.choice(3, N)\n", " # 在 0 - 2*Pi 间随机生成各参数值\n", - " theta = paddle.rand([THETA_SIZE]).astype('float32') * 2 * pi\n", - " theta.stop_gradient = False\n", - " cir = rand_circuit(theta, target, N)\n", + " cir = rand_circuit(target, N)\n", " \n", " H_l = Hamiltonian(random_pauli_str_generator(N, terms=10))\n", " \n", - " grad_mean_list, grad_variance_list = random_sample(cir, loss_func, samples, H_l, mode='max') \n", + " grad_mean_list, grad_variance_list = random_sample(cir, loss_func, samples, H_l, mode='max')\n", " # 记录采样信息\n", " means.append(grad_mean_list)\n", " variances.append(grad_variance_list)\n" @@ -1187,7 +1183,7 @@ } ], "source": [ - "loss,grad = plot_supervised_loss_grad(circuit, loss_func, N=qubit_num, EPOCH = EPOCH, LR = lr,BATCH = BATCH, TRAIN_X=train_x, TRAIN_Y=train_y)" + "loss,grad = plot_supervised_loss_grad(circuit, loss_func, N=qubit_num, EPOCH=EPOCH, LR=lr,BATCH=BATCH, TRAIN_X=train_x, TRAIN_Y=train_y)" ] }, { @@ -1276,11 +1272,8 @@ } ], "metadata": { - "interpreter": { - "hash": "2c4627ff4a417355d61049564f513ba5a6282bdfd3c46a5a6bb293c3faa304ed" - }, "kernelspec": { - "display_name": "Python 3.8.13 ('pd_dep')", + "display_name": "Python 3.8.13 ('pq')", "language": "python", "name": "python3" }, @@ -1337,6 +1330,11 @@ "_Feature" ], "window_display": false + }, + "vscode": { + "interpreter": { + "hash": "1e82098cfee7be27b5e385e3f85fe91d734d6114f7d09dccafdaad2c23171c3e" + } } }, "nbformat": 4, diff --git a/tutorials/qnn_research/BarrenPlateaus_EN.ipynb b/tutorials/qnn_research/BarrenPlateaus_EN.ipynb index 9632ebe..3af2e1e 100644 --- a/tutorials/qnn_research/BarrenPlateaus_EN.ipynb +++ b/tutorials/qnn_research/BarrenPlateaus_EN.ipynb @@ -96,7 +96,7 @@ }, "outputs": [], "source": [ - "def rand_circuit(theta, target, num_qubits):\n", + "def rand_circuit(target, num_qubits, theta=None):\n", " # Initialize the quantum circuit\n", " cir = Circuit(num_qubits)\n", " \n", @@ -107,11 +107,11 @@ " # Fixed-angle Ry rotation gates \n", " for i in range(num_qubits):\n", " if target[i] == 0:\n", - " cir.rz(i, param=theta[i])\n", + " cir.rz(i, param=theta[i] if theta is not None else theta)\n", " elif target[i] == 1:\n", - " cir.ry(i, param=theta[i])\n", + " cir.ry(i, param=theta[i] if theta is not None else theta)\n", " else:\n", - " cir.rx(i, param=theta[i])\n", + " cir.rx(i, param=theta[i] if theta is not None else theta)\n", " \n", " # ============== Second layer ==============\n", " # Build adjacent CZ gates\n", @@ -195,7 +195,7 @@ "class manual_gradient(paddle.nn.Layer):\n", " \n", " # Initialize a list of learnable parameters and fill the initial value with a uniform distribution of [0, 2*pi]\n", - " def __init__(self, shape, param_attr=paddle.nn.initializer.Uniform(low=0.0, high=2 * pi), dtype='float32'):\n", + " def __init__(self, shape, param_attr=paddle.nn.initializer.Uniform(low=0.0, high=2*pi), dtype='float32'):\n", " super(manual_gradient, self).__init__()\n", " \n", " # Convert Numpy array to Tensor in PaddlePaddle\n", @@ -205,7 +205,7 @@ " def forward(self):\n", " \n", " # Initialize three theta parameter lists\n", - " theta_np = np.random.uniform(low=0., high= 2 * pi, size=(THETA_SIZE))\n", + " theta_np = np.random.uniform(low=0., high=2*pi, size=(THETA_SIZE))\n", " theta_plus_np = np.copy(theta_np) \n", " theta_minus_np = np.copy(theta_np) \n", " \n", @@ -220,8 +220,8 @@ " # Generate random targets, randomly select circuit gates in rand_circuit\n", " target = np.random.choice(3, N) \n", " \n", - " U_plus = rand_circuit(theta_plus, target, N).unitary_matrix()\n", - " U_minus = rand_circuit(theta_minus, target, N).unitary_matrix()\n", + " U_plus = rand_circuit(target, N, theta_plus).unitary_matrix()\n", + " U_minus = rand_circuit(target, N, theta_minus).unitary_matrix()\n", "\n", " # Calculate the analytical gradient\n", " grad = paddle.real((dagger(U_plus) @ self.H @ U_plus)[0][0] - (dagger(U_minus) @ self.H @ U_minus)[0][0])/2 \n", @@ -469,7 +469,7 @@ "outputs": [], "source": [ "# Hyper parameter settings\n", - "np.random.seed(42) # Fixed Numpy random seed\n", + "#np.random.seed(1) # Fixed Numpy random seed\n", "N = 2 # Set the number of qubits\n", "samples = 300 # Set the number of sampled random network structures\n", "THETA_SIZE = N # Set the size of the parameter theta\n", @@ -511,9 +511,7 @@ "# paddle.seed(SEED)\n", "target = np.random.choice(3, N)\n", "# Random generate parameters between 0 and 2*Pi \n", - "theta = paddle.rand([THETA_SIZE]).astype('float32') * 2 * pi\n", - "theta.stop_gradient = False\n", - "cir = rand_circuit(theta, target, N)\n", + "cir = rand_circuit(target, N)\n", "print(cir)\n", "# Random generate Hamiltonian information, in Pauli string format\n", "H_l = Hamiltonian(random_pauli_str_generator(N, terms=7))\n", @@ -582,7 +580,7 @@ } ], "source": [ - "grad_mean_list, grad_variance_list = random_sample(cir, loss_func, samples, H_l, mode='single', param=0) " + "grad_mean_list, grad_variance_list = random_sample(cir, loss_func, samples, H_l, mode='single', param=0)" ] }, { @@ -850,13 +848,11 @@ " THETA_SIZE = N \n", " target = np.random.choice(3, N)\n", " # Generate a value from 0 to 2PI\n", - " theta = paddle.rand([THETA_SIZE]).astype('float32') * 2 * pi\n", - " theta.stop_gradient = False\n", - " cir = rand_circuit(theta, target, N)\n", + " cir = rand_circuit(target, N)\n", " \n", " H_l = Hamiltonian(random_pauli_str_generator(N, terms=10))\n", " \n", - " grad_mean_list, grad_variance_list = random_sample(cir, loss_func, samples, H_l, mode='max') \n", + " grad_mean_list, grad_variance_list = random_sample(cir, loss_func, samples, H_l, mode='max')\n", " # Record sampling information\n", " means.append(grad_mean_list)\n", " variances.append(grad_variance_list)" @@ -1165,7 +1161,7 @@ } ], "source": [ - "loss,grad = plot_supervised_loss_grad(circuit, loss_func, N=qubit_num, EPOCH = EPOCH, LR = lr,BATCH = BATCH, TRAIN_X=train_x, TRAIN_Y=train_y)" + "loss,grad = plot_supervised_loss_grad(circuit, loss_func, N=qubit_num, EPOCH=EPOCH, LR=lr,BATCH=BATCH, TRAIN_X=train_x, TRAIN_Y=train_y)" ] }, { @@ -1247,11 +1243,8 @@ } ], "metadata": { - "interpreter": { - "hash": "2c4627ff4a417355d61049564f513ba5a6282bdfd3c46a5a6bb293c3faa304ed" - }, "kernelspec": { - "display_name": "Python 3.8.13 ('pd_dep')", + "display_name": "Python 3.8.13 ('pq')", "language": "python", "name": "python3" }, @@ -1308,6 +1301,11 @@ "_Feature" ], "window_display": false + }, + "vscode": { + "interpreter": { + "hash": "1e82098cfee7be27b5e385e3f85fe91d734d6114f7d09dccafdaad2c23171c3e" + } } }, "nbformat": 4, diff --git a/tutorials/qnn_research/VAns_CN.ipynb b/tutorials/qnn_research/VAns_CN.ipynb index da662a2..91b2ca9 100644 --- a/tutorials/qnn_research/VAns_CN.ipynb +++ b/tutorials/qnn_research/VAns_CN.ipynb @@ -4,7 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# VAns—可变结构电路" + "# VAns: 可变结构电路" ] }, { @@ -45,11 +45,11 @@ "1. 准备一个简单的初始电路,使用经典优化器对电路参数进行调整,最小化目标损失函数,记录最优值。\n", "2. 从一个集合中随机选择量子门模块(如下图所示,各量子门模块仅由 $R_y$ 门、$R_z$ 门,和 $CNOT$ 门组成),随机选择插入模块所作用的量子比特,并将模块添加到电路末尾。添加模块中的量子门的参数都初始化为 $0$ ,这样添加的模块一开始等同于 $I$。\n", "\n", - "![Inserting Blocks](./figures/vans-fig-blocks.png)\n", + "![Inserting Blocks](blocks.png)\n", "\n", "3. 根据下图中的规则化简电路,对化简后的电路进行优化,更新其参数,获得损失函数的当前最优值。比较当前损失函数最优值与上一个记录的最优值,根据设定的阈值决定是否接受新电路。若接受新电路,则继续遍历电路中的量子门,并删除不会降低损失的门,将精简过的电路记为当前电路,并记录其最优损失。若拒绝则直接回到第2步。\n", "\n", - "![Simplification rules](./figures/vans-fig-rules.png)\n", + "![Simplification rules](rules.png)\n", "\n", "4. 重复步骤2-3,达到设定的迭代次数后停止,输出电路和最优损失。" ] @@ -70,20 +70,9 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "import paddle\n", "import paddle_quantum\n", @@ -92,9 +81,7 @@ "from paddle_quantum.ansatz import Circuit\n", "from paddle_quantum.ansatz.vans import Inserter, Simplifier, VAns, cir_decompose\n", "from paddle_quantum.hamiltonian import Hamiltonian\n", - "import warnings\n", "\n", - "warnings.filterwarnings(\"ignore\")\n", "np.random.seed(11)\n", "paddle.seed(11)" ] @@ -214,18 +201,30 @@ "execution_count": 6, "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\Users\\v_zhanglei48\\Anaconda3\\envs\\pq_new\\lib\\site-packages\\paddle\\fluid\\framework.py:744: DeprecationWarning: `np.bool` is a deprecated alias for the builtin `bool`. To silence this warning, use `bool` by itself. Doing this will not modify any behavior and is safe. If you specifically wanted the numpy scalar type, use `np.bool_` here.\n", + "Deprecated in NumPy 1.20; for more details and guidance: https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations\n", + " elif dtype == np.bool:\n", + "c:\\Users\\v_zhanglei48\\Anaconda3\\envs\\pq_new\\lib\\site-packages\\paddle\\tensor\\creation.py:130: DeprecationWarning: `np.object` is a deprecated alias for the builtin `object`. To silence this warning, use `object` by itself. Doing this will not modify any behavior and is safe. \n", + "Deprecated in NumPy 1.20; for more details and guidance: https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations\n", + " if data.dtype == np.object:\n" + ] + }, { "name": "stdout", "output_type": "stream", "text": [ - "iter: 20 loss: [-0.99608546]\n", - "iter: 40 loss: [-1.0926807]\n", - "iter: 60 loss: [-1.1136395]\n", - "iter: 80 loss: [-1.1163319]\n", + "iter: 20 loss: [-0.9960856]\n", + "iter: 40 loss: [-1.0926806]\n", + "iter: 60 loss: [-1.11364]\n", + "iter: 80 loss: [-1.1163325]\n", "iter: 100 loss: [-1.1167175]\n", - "iter: 120 loss: [-1.1167547]\n", + "iter: 120 loss: [-1.1167548]\n", "当前电路:\n", - "--Rx(3.144)----Rz(3.512)----*----Rx(6.286)----Rz(2.441)-------------------------------------------------------------x--\n", + "--Rx(3.144)----Rz(3.512)----*----Rx(6.286)----Rz(3.039)-------------------------------------------------------------x--\n", " | | \n", "----------------------------x----Rx(4.249)----Rz(3.141)----Rx(4.246)----Rz(5.477)----*------------------------------|--\n", " | | \n", @@ -258,7 +257,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "现在我们从量子门模块集合中随机选取模块,随机选取插入模块所作用的量子比特,并将模块插入到当前电路的尾端。注意新插入模块中的量子门的参数都被设置为 $0$,这样添加模块前后的电路实际上是相同的,也就是说更新后的电路可以继承之前电路的损失。下面的代码展示了如何在电路中插入模块。一次插入模块的数量由参数 **insert_rate** 决定。另一个参数 **epsilon** 是用来设定插入模块初始参数与 $0$ 之间的差别。" + "现在我们从量子门模块集合中随机选取模块,随机选取插入模块所作用的量子比特,并将模块插入到当前电路中。注意新插入模块中的量子门的参数都被设置为 $0$,这样添加模块前后的电路实际上是相同的,也就是说更新后的电路可以继承之前电路的损失。下面的代码展示了如何在电路中插入模块。一次插入模块的数量由参数 **insert_rate** 决定。另一个参数 **epsilon** 是用来设定插入模块初始参数与 $0$ 之间的差别。" ] }, { @@ -271,9 +270,9 @@ "output_type": "stream", "text": [ "添加过后的电路:\n", - "--Rx(3.144)----Rz(3.512)----*----Rx(6.286)----Rz(2.441)----------------------------------------------------------------------------------------------------x--\n", + "--Rx(3.144)----Rz(3.512)----*----Rx(6.286)----Rz(3.039)----------------------------------------------------------------------------------------------------x--\n", " | | \n", - "----------------------------x----Rz(0.000)----Rx(0.000)----Rx(-0.00)----Rx(4.249)----Rz(3.141)----Rx(4.246)----Rz(5.477)----*------------------------------|--\n", + "----------------------------x----Rz(0.000)----Rx(0.000)----Rz(-0.00)----Rx(4.249)----Rz(3.141)----Rx(4.246)----Rz(5.477)----*------------------------------|--\n", " | | \n", "--Rx(0.001)----Rz(5.499)----*-----------------------------------------------------------------------------------------------x----Rx(3.141)----Rz(2.688)----|--\n", " | | \n", @@ -316,24 +315,32 @@ "execution_count": 8, "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\Users\\v_zhanglei48\\Anaconda3\\envs\\pq_new\\lib\\site-packages\\paddle\\fluid\\dygraph\\math_op_patch.py:251: UserWarning: The dtype of left and right variables are not the same, left dtype is paddle.float64, but right dtype is paddle.float32, the right dtype will convert to paddle.float64\n", + " warnings.warn(\n" + ] + }, { "name": "stdout", "output_type": "stream", "text": [ - "--Rx(3.144)----Rz(3.512)----*----Rx(6.286)----Rz(2.441)------------------------------------------------x--\n", + "--Rx(3.144)----Rz(3.512)----*----Rx(6.286)----Rz(3.039)------------------------------------------------x--\n", " | | \n", - "----------------------------x----Rz(4.491)----Rx(4.470)----Rz(8.712)----*------------------------------|--\n", + "----------------------------x----Rz(5.962)----Rx(5.858)----Rz(9.426)----*------------------------------|--\n", " | | \n", "--Rx(0.001)----Rz(5.499)----*-------------------------------------------x----Rx(3.141)----Rz(2.688)----|--\n", " | | \n", "----------------------------x----Rz(3.604)----Rx(5.126)----Rz(4.462)-----------------------------------*--\n", " \n", - "iter: 20 loss: [-1.1121466]\n", - "iter: 40 loss: [-1.1123195]\n", - "iter: 60 loss: [-1.1158162]\n", - "iter: 80 loss: [-1.1166465]\n", - "iter: 100 loss: [-1.1167465]\n", - "iter: 120 loss: [-1.1167578]\n", + "iter: 20 loss: [-1.1040964]\n", + "iter: 40 loss: [-1.1159219]\n", + "iter: 60 loss: [-1.1165943]\n", + "iter: 80 loss: [-1.116723]\n", + "iter: 100 loss: [-1.1167536]\n", + "iter: 120 loss: [-1.1167593]\n", "Accpet the new circuit!\n", " start deleting gates\n", " Deletion: reject deletion\n", @@ -399,7 +406,7 @@ "output_type": "stream", "text": [ "当前电路为:\n", - "--Rx(3.142)----*----------------------x--\n", + "--Rx(3.141)----*----------------------x--\n", " | | \n", "---------------x----*-----------------|--\n", " | | \n", @@ -448,30 +455,30 @@ "output_type": "stream", "text": [ "Out iteration 1 for structure optimization:\n", - "iter: 20 loss: [-1.1167215]\n", + "iter: 20 loss: [-1.1167214]\n", "iter: 40 loss: [-1.1166948]\n", - "iter: 60 loss: [-1.1167493]\n", + "iter: 60 loss: [-1.1167494]\n", "iter: 80 loss: [-1.116759]\n", "iter: 100 loss: [-1.1167591]\n", "iter: 120 loss: [-1.1167595]\n", " Current loss: [-1.1167595]\n", " Current cir:\n", - "--Rx(3.142)----*----------------------x--\n", + "--Rx(3.141)----*----------------------x--\n", " | | \n", "---------------x----*-----------------|--\n", " | | \n", - "--------------------x----Rx(3.142)----|--\n", + "--------------------x----Rx(3.141)----|--\n", " | \n", "--------------------------------------*--\n", " \n", "\n", "Out iteration 2 for structure optimization:\n", - "iter: 20 loss: [-1.0998794]\n", - "iter: 40 loss: [-1.1144476]\n", - "iter: 60 loss: [-1.1166729]\n", - "iter: 80 loss: [-1.1167666]\n", - "iter: 100 loss: [-1.1207738]\n", - "iter: 120 loss: [-1.1371526]\n", + "iter: 20 loss: [-1.008932]\n", + "iter: 40 loss: [-1.1085335]\n", + "iter: 60 loss: [-1.133744]\n", + "iter: 80 loss: [-1.1368372]\n", + "iter: 100 loss: [-1.1372123]\n", + "iter: 120 loss: [-1.1372803]\n", " accpet the new circuit!\n", " start deleting gates\n", " Deletion: reject deletion\n", @@ -479,27 +486,28 @@ " Deletion: reject deletion\n", " Deletion: reject deletion\n", " Deletion: reject deletion\n", - " Deletion: reject deletion\n", " Deletion: accept deletion with acceptable loss\n", - " 2 gates are deleted!\n", - " Current loss: -1.1369835138320923\n", + " Deletion: accept deletion with acceptable loss\n", + " Deletion: reject deletion\n", + " 3 gates are deleted!\n", + " Current loss: -1.1331816911697388\n", " Current cir:\n", - "--Rx(3.142)----*------------------------------------------------------------------x--\n", + "--Rx(3.143)----*------------------------------------------------------------------x--\n", " | | \n", - "---------------x----*----Rx(-0.24)----Rz(1.462)----*--------*---------------------|--\n", + "---------------x----*----Rx(0.223)----Rz(2.485)----*--------*---------------------|--\n", " | | | | \n", "--------------------|------------------------------|--------x--------Rx(3.142)----|--\n", " | | | \n", - "--------------------x------------------------------x----Rz(3.103)-----------------*--\n", + "--------------------x------------------------------x----Rz(0.458)-----------------*--\n", " \n", "\n", "Out iteration 3 for structure optimization:\n", - "iter: 20 loss: [-1.108532]\n", - "iter: 40 loss: [-1.1331518]\n", - "iter: 60 loss: [-1.1366322]\n", - "iter: 80 loss: [-1.1372447]\n", - "iter: 100 loss: [-1.1372827]\n", - "iter: 120 loss: [-1.1372826]\n", + "iter: 20 loss: [-0.37809896]\n", + "iter: 40 loss: [-1.0471339]\n", + "iter: 60 loss: [-1.1262195]\n", + "iter: 80 loss: [-1.1365637]\n", + "iter: 100 loss: [-1.1371676]\n", + "iter: 120 loss: [-1.1372685]\n", " accpet the new circuit!\n", " start deleting gates\n", " Deletion: reject deletion\n", @@ -509,86 +517,92 @@ " Deletion: accept deletion with acceptable loss\n", " Deletion: accept deletion with acceptable loss\n", " Deletion: reject deletion\n", - " Deletion: reject deletion\n", - " 3 gates are deleted!\n", - " Current loss: -1.1372838020324707\n", + " Deletion: accept deletion with acceptable loss\n", + " 4 gates are deleted!\n", + " Current loss: -1.1282802820205688\n", " Current cir:\n", - "--Rx(3.142)----*------------------------------------------------------------------x--\n", - " | | \n", - "---------------x----*----Rx(-0.22)----Rz(1.497)----*--------*---------------------|--\n", - " | | | | \n", - "--------------------|------------------------------|--------x--------Rx(3.141)----|--\n", - " | | | \n", - "--------------------x------------------------------x----Rz(3.068)-----------------*--\n", - " \n", + "--Rx(3.143)----*----------------------------------------------------------x--\n", + " | | \n", + "---------------x----*----Rx(0.224)----Rz(2.252)----*----*-----------------|--\n", + " | | | | \n", + "--------------------|------------------------------|----x----Rx(3.146)----|--\n", + " | | | \n", + "--------------------x------------------------------x----------------------*--\n", + " \n", "\n", "Out iteration 4 for structure optimization:\n", - "iter: 20 loss: [-1.137021]\n", - "iter: 40 loss: [-1.1371992]\n", - "iter: 60 loss: [-1.137272]\n", - "iter: 80 loss: [-1.137283]\n", - "iter: 100 loss: [-1.1372836]\n", - "iter: 120 loss: [-1.137284]\n", + "iter: 20 loss: [-0.5603936]\n", + "iter: 40 loss: [-1.0798837]\n", + "iter: 60 loss: [-1.1325867]\n", + "iter: 80 loss: [-1.1360469]\n", + "iter: 100 loss: [-1.1370715]\n", + "iter: 120 loss: [-1.1372685]\n", " accpet the new circuit!\n", " start deleting gates\n", " Deletion: reject deletion\n", " Deletion: reject deletion\n", " Deletion: reject deletion\n", " Deletion: accept deletion with acceptable loss\n", + " Deletion: accept deletion with acceptable loss\n", + " Deletion: accept deletion with acceptable loss\n", + " Deletion: accept deletion with acceptable loss\n", + " Deletion: accept deletion with acceptable loss\n", + " Deletion: accept deletion with acceptable loss\n", " Deletion: reject deletion\n", - " Deletion: reject deletion\n", - " 1 gates are deleted!\n", - " Current loss: -1.1372839212417603\n", + " Deletion: accept deletion with acceptable loss\n", + " Deletion: accept deletion with acceptable loss\n", + " Deletion: accept deletion with acceptable loss\n", + " 9 gates are deleted!\n", + " Current loss: -1.1321836709976196\n", " Current cir:\n", - "--Rx(3.141)----*------------------------------------------------------------------x--\n", - " | | \n", - "---------------x----*----Rx(-0.22)----Rz(1.497)----*--------*---------------------|--\n", - " | | | | \n", - "--------------------|------------------------------|--------x--------Rx(3.141)----|--\n", - " | | | \n", - "--------------------x------------------------------x----Rz(3.068)-----------------*--\n", - " \n", + "--Rx(3.145)----*----------------------------------------------------------x--\n", + " | | \n", + "---------------x----*----Rx(0.228)----Rz(2.075)----*----*-----------------|--\n", + " | | | | \n", + "--------------------|------------------------------|----x----Rx(3.143)----|--\n", + " | | | \n", + "--------------------x------------------------------x----------------------*--\n", + " \n", "\n", "Out iteration 5 for structure optimization:\n", - "iter: 20 loss: [-1.137092]\n", - "iter: 40 loss: [-1.1371393]\n", - "iter: 60 loss: [-1.1372617]\n", + "iter: 20 loss: [-1.136969]\n", + "iter: 40 loss: [-1.1371931]\n", + "iter: 60 loss: [-1.137272]\n", "iter: 80 loss: [-1.1372828]\n", - "iter: 100 loss: [-1.1372836]\n", - "iter: 120 loss: [-1.1372839]\n", + "iter: 100 loss: [-1.1372837]\n", + "iter: 120 loss: [-1.1372838]\n", " accpet the new circuit!\n", " start deleting gates\n", " Deletion: reject deletion\n", " Deletion: reject deletion\n", " Deletion: reject deletion\n", - " Deletion: reject deletion\n", - " Deletion: reject deletion\n", " Deletion: accept deletion with acceptable loss\n", " Deletion: accept deletion with acceptable loss\n", + " Deletion: reject deletion\n", " 2 gates are deleted!\n", - " Current loss: -1.1372840404510498\n", + " Current loss: -1.1372729539871216\n", " Current cir:\n", - "--Rx(3.141)----*------------------------------------------------------------------x--\n", - " | | \n", - "---------------x----*----Rx(-0.22)----Rz(1.497)----*--------*---------------------|--\n", - " | | | | \n", - "--------------------|------------------------------|--------x--------Rx(3.141)----|--\n", - " | | | \n", - "--------------------x------------------------------x----Rz(3.068)-----------------*--\n", - " \n", + "--Rx(3.142)----*----------------------------------------------------------x--\n", + " | | \n", + "---------------x----*----Rx(0.226)----Rz(1.571)----*----*-----------------|--\n", + " | | | | \n", + "--------------------|------------------------------|----x----Rx(3.149)----|--\n", + " | | | \n", + "--------------------x------------------------------x----------------------*--\n", + " \n", "\n", "\n", "\n", - "The final loss: -1.1372840404510498\n", + "The final loss: -1.1372729539871216\n", "The final circuit:\n", - "--Rx(3.141)----*------------------------------------------------------------------x--\n", - " | | \n", - "---------------x----*----Rx(-0.22)----Rz(1.497)----*--------*---------------------|--\n", - " | | | | \n", - "--------------------|------------------------------|--------x--------Rx(3.141)----|--\n", - " | | | \n", - "--------------------x------------------------------x----Rz(3.068)-----------------*--\n", - " \n" + "--Rx(3.142)----*----------------------------------------------------------x--\n", + " | | \n", + "---------------x----*----Rx(0.226)----Rz(1.571)----*----*-----------------|--\n", + " | | | | \n", + "--------------------|------------------------------|----x----Rx(3.149)----|--\n", + " | | | \n", + "--------------------x------------------------------x----------------------*--\n", + " \n" ] } ], @@ -614,16 +628,16 @@ "output_type": "stream", "text": [ "最终电路:\n", - "--Rx(3.141)----*------------------------------------------------------------------x--\n", - " | | \n", - "---------------x----*----Rx(-0.22)----Rz(1.497)----*--------*---------------------|--\n", - " | | | | \n", - "--------------------|------------------------------|--------x--------Rx(3.141)----|--\n", - " | | | \n", - "--------------------x------------------------------x----Rz(3.068)-----------------*--\n", - " \n", + "--Rx(3.142)----*----------------------------------------------------------x--\n", + " | | \n", + "---------------x----*----Rx(0.226)----Rz(1.571)----*----*-----------------|--\n", + " | | | | \n", + "--------------------|------------------------------|----x----Rx(3.149)----|--\n", + " | | | \n", + "--------------------x------------------------------x----------------------*--\n", + " \n", "最终损失:\n", - "-1.1372840404510498\n" + "-1.1372729539871216\n" ] } ], @@ -660,10 +674,10 @@ ], "metadata": { "interpreter": { - "hash": "9043b12ec77a531919bc05f05830335d23baf822720cbea14b03018197d26545" + "hash": "2ab84abaf8d5bbc8765aba8eb82d11e7069f2ff20e8f79b8a9cdeccefd2ac4da" }, "kernelspec": { - "display_name": "Python 3.8.0 ('paddle-quantum-dev')", + "display_name": "Python 3.8.13 ('pq_new')", "language": "python", "name": "python3" }, @@ -677,7 +691,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.0" + "version": "3.8.13" } }, "nbformat": 4, diff --git a/tutorials/qnn_research/VAns_EN.ipynb b/tutorials/qnn_research/VAns_EN.ipynb index cf1d630..5fb6736 100644 --- a/tutorials/qnn_research/VAns_EN.ipynb +++ b/tutorials/qnn_research/VAns_EN.ipynb @@ -2,15 +2,13 @@ "cells": [ { "cell_type": "markdown", - "id": "241d2f96", "metadata": {}, "source": [ - "# VAns—Variable Ansatz" + "# VAns: Variable Ansatz" ] }, { "cell_type": "markdown", - "id": "c94780c9", "metadata": {}, "source": [ "Copyright (c) 2022 Institute for Quantum Computing, Baidu Inc. All Rights Reserved." @@ -18,7 +16,6 @@ }, { "cell_type": "markdown", - "id": "d84c0b5d", "metadata": {}, "source": [ "## Overview" @@ -26,7 +23,6 @@ }, { "cell_type": "markdown", - "id": "96c37591", "metadata": {}, "source": [ "Variational Quantum Algorithms (VQA) are about to tune parameters of quantum circuits to minimize an objective function of interest. The commonly used VQAs, like Variational Quantum Eigensolvers (VQE) and Quantum Approximate Optimization Algorithm (QAOA), perform optimization using a user-defined ansatz with fixed structure. However, if the ansatz is too simple or shallow, the expressivity of the circuit will not be insufficient to get the optimal value for the objective function. On the other side, if the ansatz is overly complicated or long, we may encounter barren plateau effect, which impedes us from obtaining the global minimum value. Thus, a circuit structure design search algorithm will be helpful to find the appropriate circuit for a specific task.\n", @@ -36,7 +32,6 @@ }, { "cell_type": "markdown", - "id": "365bdd44", "metadata": {}, "source": [ "## Pipeline" @@ -44,25 +39,23 @@ }, { "cell_type": "markdown", - "id": "89b09ff3", "metadata": {}, "source": [ "VAns consists of the following steps: \n", "1. Start with an initial circuit. Run the inner optimization, which is just the original VQE process, and get the optimal value. \n", "2. Randomly choose a block from the 'pool' (As the following figure shows, each block only consists of $R_y$, $R_z$, and $CNOT$ gates), and insert the block at the end of circuit. Qubits that the block is applied on are also uniformly sampled. The parameters of the inserted gates are initialized to $0$ so that the inserted circuit is equivalent to identity. \n", "\n", - "![Inserting Blocks](./figures/vans-fig-blocks.png)\n", + "![Inserting Blocks](blocks.png)\n", "\n", "3. Simplify the circuit according to the following rules. Run the optimization process, and get the optimal value of loss function. Compare the loss value to the previously stored one, decide whether to accept the new circuit or not according to a pre-defined threshold. If the circuit is accepted, remove gates that do not lower the loss, set the circuit as the current circuit and store the corresponding loss value.\n", "\n", - "![Simplification rules](./figures/vans-fig-rules.png)\n", + "![Simplification rules](rules.png)\n", "\n", "4. Repeat steps 2-3 for chosen number of iterations, output the resulting circuit and the optimal loss value.\n" ] }, { "cell_type": "markdown", - "id": "6f4130bd", "metadata": {}, "source": [ "## Paddle Quantum Implementation" @@ -70,7 +63,6 @@ }, { "cell_type": "markdown", - "id": "1acdd35e", "metadata": {}, "source": [ "We consider to get the ground state energy of Hydrogen molecule using [Variation Quantum Eigensolver (VQE)](https://qml.baidu.com/tutorials/quantum-simulation/variational-quantum-eigensolver.html) together with VAns to optimize the circuit structure as well. First, we import the required packages." @@ -78,21 +70,9 @@ }, { "cell_type": "code", - "execution_count": 1, - "id": "722c14f5", + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "import paddle\n", "import paddle_quantum\n", @@ -101,16 +81,13 @@ "from paddle_quantum.ansatz import Circuit\n", "from paddle_quantum.ansatz.vans import Inserter, Simplifier, VAns, cir_decompose\n", "from paddle_quantum.hamiltonian import Hamiltonian\n", - "import warnings\n", "\n", - "warnings.filterwarnings(\"ignore\")\n", "np.random.seed(11)\n", "paddle.seed(11)" ] }, { "cell_type": "markdown", - "id": "09179a6b", "metadata": {}, "source": [ "Before using the VAns algorithm, we need to get the Hamiltonian for Hydrogen. Details can be found in the VQE tutorial." @@ -119,7 +96,6 @@ { "cell_type": "code", "execution_count": null, - "id": "c3f64aec", "metadata": {}, "outputs": [], "source": [ @@ -147,7 +123,6 @@ }, { "cell_type": "markdown", - "id": "8e90a098", "metadata": {}, "source": [ "Then we define the loss function, which is simply the expectation value of the Hamiltonian." @@ -156,7 +131,6 @@ { "cell_type": "code", "execution_count": 3, - "id": "930b71a4", "metadata": {}, "outputs": [], "source": [ @@ -167,7 +141,6 @@ }, { "cell_type": "markdown", - "id": "40d33794", "metadata": {}, "source": [ "Next, we also need to set some training hyper-parameters before training." @@ -176,7 +149,6 @@ { "cell_type": "code", "execution_count": 4, - "id": "9c5bfe04", "metadata": {}, "outputs": [], "source": [ @@ -195,7 +167,6 @@ { "cell_type": "code", "execution_count": 5, - "id": "6981e732", "metadata": {}, "outputs": [], "source": [ @@ -213,7 +184,6 @@ }, { "cell_type": "markdown", - "id": "4349bb43", "metadata": {}, "source": [ "### Initial Circuit" @@ -221,7 +191,6 @@ }, { "cell_type": "markdown", - "id": "0d4f3052", "metadata": {}, "source": [ "We give a default initial circuit with uniformly sampled parameters. Then run the optimization process to get the optimal parameters in this initial circuit. The optimization process is the same as in the original VQE. Hyperparameters **iter** and **LR** are used in this process.The obtained loss function and the circuit with optimized parameters will be the initial point for the architecture optimization process. Note that the circuit is simplified when we decided to use this optimized circuit as the current starting point. Details of simplification can be found later." @@ -230,21 +199,32 @@ { "cell_type": "code", "execution_count": 6, - "id": "f76b20bc", "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\Users\\v_zhanglei48\\Anaconda3\\envs\\pq_new\\lib\\site-packages\\paddle\\fluid\\framework.py:744: DeprecationWarning: `np.bool` is a deprecated alias for the builtin `bool`. To silence this warning, use `bool` by itself. Doing this will not modify any behavior and is safe. If you specifically wanted the numpy scalar type, use `np.bool_` here.\n", + "Deprecated in NumPy 1.20; for more details and guidance: https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations\n", + " elif dtype == np.bool:\n", + "c:\\Users\\v_zhanglei48\\Anaconda3\\envs\\pq_new\\lib\\site-packages\\paddle\\tensor\\creation.py:130: DeprecationWarning: `np.object` is a deprecated alias for the builtin `object`. To silence this warning, use `object` by itself. Doing this will not modify any behavior and is safe. \n", + "Deprecated in NumPy 1.20; for more details and guidance: https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations\n", + " if data.dtype == np.object:\n" + ] + }, { "name": "stdout", "output_type": "stream", "text": [ - "iter: 20 loss: [-0.99608546]\n", - "iter: 40 loss: [-1.0926807]\n", - "iter: 60 loss: [-1.1136395]\n", - "iter: 80 loss: [-1.1163319]\n", + "iter: 20 loss: [-0.9960856]\n", + "iter: 40 loss: [-1.0926806]\n", + "iter: 60 loss: [-1.11364]\n", + "iter: 80 loss: [-1.1163325]\n", "iter: 100 loss: [-1.1167175]\n", - "iter: 120 loss: [-1.1167547]\n", + "iter: 120 loss: [-1.1167548]\n", "Current circuit is:\n", - "--Rx(3.144)----Rz(3.512)----*----Rx(6.286)----Rz(2.441)-------------------------------------------------------------x--\n", + "--Rx(3.144)----Rz(3.512)----*----Rx(6.286)----Rz(3.039)-------------------------------------------------------------x--\n", " | | \n", "----------------------------x----Rx(4.249)----Rz(3.141)----Rx(4.246)----Rz(5.477)----*------------------------------|--\n", " | | \n", @@ -268,7 +248,6 @@ }, { "cell_type": "markdown", - "id": "c95a7574", "metadata": {}, "source": [ "### Insertion" @@ -276,16 +255,14 @@ }, { "cell_type": "markdown", - "id": "22f6d70a", "metadata": {}, "source": [ - "Now, we randomly choose a block from the 'pool', and insert the block at the end of circuit. Qubits that the block is applied on are also uniformly sampled. The parameters of the inserted set of gates are initialized to $0$ so that the new circuit can take the advantage of the previously optimized circuit since they act as identity. The code below inserts sets of gates into the circuit. The number of sets of gates depends on the hyperparameter **insert_rate**. Another hyperparameter **epsilon** is used to determine the variance from $0$ of initial parameters." + "Now, we randomly choose a block from the 'pool', and insert the block within the circuit. Qubits that the block is applied on are also uniformly sampled. The parameters of the inserted set of gates are initialized to $0$ so that the new circuit can take the advantage of the previously optimized circuit since they act as identity. The code below inserts sets of gates into the circuit. The number of sets of gates depends on the hyperparameter **insert_rate**. Another hyperparameter **epsilon** is used to determine the variance from $0$ of initial parameters." ] }, { "cell_type": "code", "execution_count": 7, - "id": "8dcdf789", "metadata": {}, "outputs": [ { @@ -293,9 +270,9 @@ "output_type": "stream", "text": [ "Circuit after insertion:\n", - "--Rx(3.144)----Rz(3.512)----*----Rx(6.286)----Rz(2.441)----------------------------------------------------------------------------------------------------x--\n", + "--Rx(3.144)----Rz(3.512)----*----Rx(6.286)----Rz(3.039)----------------------------------------------------------------------------------------------------x--\n", " | | \n", - "----------------------------x----Rz(0.000)----Rx(0.000)----Rx(-0.00)----Rx(4.249)----Rz(3.141)----Rx(4.246)----Rz(5.477)----*------------------------------|--\n", + "----------------------------x----Rz(0.000)----Rx(0.000)----Rz(-0.00)----Rx(4.249)----Rz(3.141)----Rx(4.246)----Rz(5.477)----*------------------------------|--\n", " | | \n", "--Rx(0.001)----Rz(5.499)----*-----------------------------------------------------------------------------------------------x----Rx(3.141)----Rz(2.688)----|--\n", " | | \n", @@ -316,7 +293,6 @@ }, { "cell_type": "markdown", - "id": "cd07eeef", "metadata": {}, "source": [ "### Simplification" @@ -324,7 +300,6 @@ }, { "cell_type": "markdown", - "id": "54a3fdd0", "metadata": {}, "source": [ "Then we will simplify the new circuit. Rules are pretty simple:\n", @@ -339,27 +314,34 @@ { "cell_type": "code", "execution_count": 8, - "id": "d41b36e1", "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\Users\\v_zhanglei48\\Anaconda3\\envs\\pq_new\\lib\\site-packages\\paddle\\fluid\\dygraph\\math_op_patch.py:251: UserWarning: The dtype of left and right variables are not the same, left dtype is paddle.float64, but right dtype is paddle.float32, the right dtype will convert to paddle.float64\n", + " warnings.warn(\n" + ] + }, { "name": "stdout", "output_type": "stream", "text": [ - "--Rx(3.144)----Rz(3.512)----*----Rx(6.286)----Rz(2.441)------------------------------------------------x--\n", + "--Rx(3.144)----Rz(3.512)----*----Rx(6.286)----Rz(3.039)------------------------------------------------x--\n", " | | \n", - "----------------------------x----Rz(4.491)----Rx(4.470)----Rz(8.712)----*------------------------------|--\n", + "----------------------------x----Rz(5.962)----Rx(5.858)----Rz(9.426)----*------------------------------|--\n", " | | \n", "--Rx(0.001)----Rz(5.499)----*-------------------------------------------x----Rx(3.141)----Rz(2.688)----|--\n", " | | \n", "----------------------------x----Rz(3.604)----Rx(5.126)----Rz(4.462)-----------------------------------*--\n", " \n", - "iter: 20 loss: [-1.1121466]\n", - "iter: 40 loss: [-1.1123195]\n", - "iter: 60 loss: [-1.1158162]\n", - "iter: 80 loss: [-1.1166465]\n", - "iter: 100 loss: [-1.1167465]\n", - "iter: 120 loss: [-1.1167578]\n", + "iter: 20 loss: [-1.1040964]\n", + "iter: 40 loss: [-1.1159219]\n", + "iter: 60 loss: [-1.1165943]\n", + "iter: 80 loss: [-1.116723]\n", + "iter: 100 loss: [-1.1167536]\n", + "iter: 120 loss: [-1.1167593]\n", "Accpet the new circuit!\n", " start deleting gates\n", " Deletion: reject deletion\n", @@ -410,7 +392,6 @@ }, { "cell_type": "markdown", - "id": "bb98738a", "metadata": {}, "source": [ "Then we update the current circuit to the new circuit." @@ -419,7 +400,6 @@ { "cell_type": "code", "execution_count": 9, - "id": "27a16d15", "metadata": {}, "outputs": [ { @@ -427,7 +407,7 @@ "output_type": "stream", "text": [ "The current circuit:\n", - "--Rx(3.142)----*----------------------x--\n", + "--Rx(3.141)----*----------------------x--\n", " | | \n", "---------------x----*-----------------|--\n", " | | \n", @@ -447,7 +427,6 @@ }, { "cell_type": "markdown", - "id": "1a8c71b9", "metadata": {}, "source": [ "The insertion and simplification together forms one iteration for the architecture optimization process. The number of iterations is determined by the hyperparameter **iter_out**. " @@ -455,7 +434,6 @@ }, { "cell_type": "markdown", - "id": "d53c077c", "metadata": {}, "source": [ "### Simple version" @@ -463,7 +441,6 @@ }, { "cell_type": "markdown", - "id": "e8383ce1", "metadata": {}, "source": [ "While you can customize your own optimization process by adjusting insertion and simplification processes and implementing the training process, Paddle Quantum provides an elegant version of VAns. You can complete the architecture optimization all at once." @@ -471,8 +448,7 @@ }, { "cell_type": "code", - "execution_count": 54, - "id": "c47505f7", + "execution_count": 10, "metadata": {}, "outputs": [ { @@ -480,30 +456,30 @@ "output_type": "stream", "text": [ "Out iteration 1 for structure optimization:\n", - "iter: 20 loss: [-1.1167217]\n", - "iter: 40 loss: [-1.1166946]\n", + "iter: 20 loss: [-1.1167214]\n", + "iter: 40 loss: [-1.1166948]\n", "iter: 60 loss: [-1.1167494]\n", "iter: 80 loss: [-1.116759]\n", "iter: 100 loss: [-1.1167591]\n", "iter: 120 loss: [-1.1167595]\n", " Current loss: [-1.1167595]\n", " Current cir:\n", - "--Rx(3.142)----*----------------------x--\n", + "--Rx(3.141)----*----------------------x--\n", " | | \n", "---------------x----*-----------------|--\n", " | | \n", - "--------------------x----Rx(3.142)----|--\n", + "--------------------x----Rx(3.141)----|--\n", " | \n", "--------------------------------------*--\n", " \n", "\n", "Out iteration 2 for structure optimization:\n", - "iter: 20 loss: [-1.099879]\n", - "iter: 40 loss: [-1.1144477]\n", - "iter: 60 loss: [-1.1166729]\n", - "iter: 80 loss: [-1.1167666]\n", - "iter: 100 loss: [-1.1207733]\n", - "iter: 120 loss: [-1.1371529]\n", + "iter: 20 loss: [-1.008932]\n", + "iter: 40 loss: [-1.1085335]\n", + "iter: 60 loss: [-1.133744]\n", + "iter: 80 loss: [-1.1368372]\n", + "iter: 100 loss: [-1.1372123]\n", + "iter: 120 loss: [-1.1372803]\n", " accpet the new circuit!\n", " start deleting gates\n", " Deletion: reject deletion\n", @@ -511,27 +487,28 @@ " Deletion: reject deletion\n", " Deletion: reject deletion\n", " Deletion: reject deletion\n", - " Deletion: reject deletion\n", " Deletion: accept deletion with acceptable loss\n", - " 2 gates are deleted!\n", - " Current loss: -1.136983871459961\n", + " Deletion: accept deletion with acceptable loss\n", + " Deletion: reject deletion\n", + " 3 gates are deleted!\n", + " Current loss: -1.1331816911697388\n", " Current cir:\n", - "--Rx(3.142)----*------------------------------------------------------------------x--\n", + "--Rx(3.143)----*------------------------------------------------------------------x--\n", " | | \n", - "---------------x----*----Rx(-0.24)----Rz(1.462)----*--------*---------------------|--\n", + "---------------x----*----Rx(0.223)----Rz(2.485)----*--------*---------------------|--\n", " | | | | \n", "--------------------|------------------------------|--------x--------Rx(3.142)----|--\n", " | | | \n", - "--------------------x------------------------------x----Rz(3.103)-----------------*--\n", + "--------------------x------------------------------x----Rz(0.458)-----------------*--\n", " \n", "\n", "Out iteration 3 for structure optimization:\n", - "iter: 20 loss: [-1.1085321]\n", - "iter: 40 loss: [-1.1331517]\n", - "iter: 60 loss: [-1.1366321]\n", - "iter: 80 loss: [-1.1372447]\n", - "iter: 100 loss: [-1.1372826]\n", - "iter: 120 loss: [-1.1372824]\n", + "iter: 20 loss: [-0.37809896]\n", + "iter: 40 loss: [-1.0471339]\n", + "iter: 60 loss: [-1.1262195]\n", + "iter: 80 loss: [-1.1365637]\n", + "iter: 100 loss: [-1.1371676]\n", + "iter: 120 loss: [-1.1372685]\n", " accpet the new circuit!\n", " start deleting gates\n", " Deletion: reject deletion\n", @@ -541,86 +518,92 @@ " Deletion: accept deletion with acceptable loss\n", " Deletion: accept deletion with acceptable loss\n", " Deletion: reject deletion\n", - " Deletion: reject deletion\n", - " 3 gates are deleted!\n", - " Current loss: -1.1372839212417603\n", + " Deletion: accept deletion with acceptable loss\n", + " 4 gates are deleted!\n", + " Current loss: -1.1282802820205688\n", " Current cir:\n", - "--Rx(3.142)----*------------------------------------------------------------------x--\n", - " | | \n", - "---------------x----*----Rx(-0.22)----Rz(1.497)----*--------*---------------------|--\n", - " | | | | \n", - "--------------------|------------------------------|--------x--------Rx(3.141)----|--\n", - " | | | \n", - "--------------------x------------------------------x----Rz(3.068)-----------------*--\n", - " \n", + "--Rx(3.143)----*----------------------------------------------------------x--\n", + " | | \n", + "---------------x----*----Rx(0.224)----Rz(2.252)----*----*-----------------|--\n", + " | | | | \n", + "--------------------|------------------------------|----x----Rx(3.146)----|--\n", + " | | | \n", + "--------------------x------------------------------x----------------------*--\n", + " \n", "\n", "Out iteration 4 for structure optimization:\n", - "iter: 20 loss: [-1.137021]\n", - "iter: 40 loss: [-1.1371994]\n", - "iter: 60 loss: [-1.1372718]\n", - "iter: 80 loss: [-1.137283]\n", - "iter: 100 loss: [-1.1372836]\n", - "iter: 120 loss: [-1.1372839]\n", + "iter: 20 loss: [-0.5603936]\n", + "iter: 40 loss: [-1.0798837]\n", + "iter: 60 loss: [-1.1325867]\n", + "iter: 80 loss: [-1.1360469]\n", + "iter: 100 loss: [-1.1370715]\n", + "iter: 120 loss: [-1.1372685]\n", " accpet the new circuit!\n", " start deleting gates\n", " Deletion: reject deletion\n", " Deletion: reject deletion\n", " Deletion: reject deletion\n", " Deletion: accept deletion with acceptable loss\n", + " Deletion: accept deletion with acceptable loss\n", + " Deletion: accept deletion with acceptable loss\n", + " Deletion: accept deletion with acceptable loss\n", + " Deletion: accept deletion with acceptable loss\n", + " Deletion: accept deletion with acceptable loss\n", " Deletion: reject deletion\n", - " Deletion: reject deletion\n", - " 1 gates are deleted!\n", - " Current loss: -1.1372839212417603\n", + " Deletion: accept deletion with acceptable loss\n", + " Deletion: accept deletion with acceptable loss\n", + " Deletion: accept deletion with acceptable loss\n", + " 9 gates are deleted!\n", + " Current loss: -1.1321836709976196\n", " Current cir:\n", - "--Rx(3.141)----*------------------------------------------------------------------x--\n", - " | | \n", - "---------------x----*----Rx(-0.22)----Rz(1.497)----*--------*---------------------|--\n", - " | | | | \n", - "--------------------|------------------------------|--------x--------Rx(3.141)----|--\n", - " | | | \n", - "--------------------x------------------------------x----Rz(3.068)-----------------*--\n", - " \n", + "--Rx(3.145)----*----------------------------------------------------------x--\n", + " | | \n", + "---------------x----*----Rx(0.228)----Rz(2.075)----*----*-----------------|--\n", + " | | | | \n", + "--------------------|------------------------------|----x----Rx(3.143)----|--\n", + " | | | \n", + "--------------------x------------------------------x----------------------*--\n", + " \n", "\n", "Out iteration 5 for structure optimization:\n", - "iter: 20 loss: [-1.1370927]\n", - "iter: 40 loss: [-1.1371398]\n", - "iter: 60 loss: [-1.1372617]\n", + "iter: 20 loss: [-1.136969]\n", + "iter: 40 loss: [-1.1371931]\n", + "iter: 60 loss: [-1.137272]\n", "iter: 80 loss: [-1.1372828]\n", - "iter: 100 loss: [-1.1372832]\n", - "iter: 120 loss: [-1.137284]\n", + "iter: 100 loss: [-1.1372837]\n", + "iter: 120 loss: [-1.1372838]\n", " accpet the new circuit!\n", " start deleting gates\n", " Deletion: reject deletion\n", " Deletion: reject deletion\n", " Deletion: reject deletion\n", - " Deletion: reject deletion\n", - " Deletion: reject deletion\n", " Deletion: accept deletion with acceptable loss\n", " Deletion: accept deletion with acceptable loss\n", + " Deletion: reject deletion\n", " 2 gates are deleted!\n", - " Current loss: -1.1372839212417603\n", + " Current loss: -1.1372729539871216\n", " Current cir:\n", - "--Rx(3.141)----*------------------------------------------------------------------x--\n", - " | | \n", - "---------------x----*----Rx(-0.22)----Rz(1.497)----*--------*---------------------|--\n", - " | | | | \n", - "--------------------|------------------------------|--------x--------Rx(3.141)----|--\n", - " | | | \n", - "--------------------x------------------------------x----Rz(3.068)-----------------*--\n", - " \n", + "--Rx(3.142)----*----------------------------------------------------------x--\n", + " | | \n", + "---------------x----*----Rx(0.226)----Rz(1.571)----*----*-----------------|--\n", + " | | | | \n", + "--------------------|------------------------------|----x----Rx(3.149)----|--\n", + " | | | \n", + "--------------------x------------------------------x----------------------*--\n", + " \n", "\n", "\n", "\n", - "The final loss: -1.1372839212417603\n", + "The final loss: -1.1372729539871216\n", "The final circuit:\n", - "--Rx(3.141)----*------------------------------------------------------------------x--\n", - " | | \n", - "---------------x----*----Rx(-0.22)----Rz(1.497)----*--------*---------------------|--\n", - " | | | | \n", - "--------------------|------------------------------|--------x--------Rx(3.141)----|--\n", - " | | | \n", - "--------------------x------------------------------x----Rz(3.068)-----------------*--\n", - " \n" + "--Rx(3.142)----*----------------------------------------------------------x--\n", + " | | \n", + "---------------x----*----Rx(0.226)----Rz(1.571)----*----*-----------------|--\n", + " | | | | \n", + "--------------------|------------------------------|----x----Rx(3.149)----|--\n", + " | | | \n", + "--------------------x------------------------------x----------------------*--\n", + " \n" ] } ], @@ -631,7 +614,6 @@ }, { "cell_type": "markdown", - "id": "5c1ec809", "metadata": {}, "source": [ "The final circuit we got from VAns is" @@ -639,8 +621,7 @@ }, { "cell_type": "code", - "execution_count": 55, - "id": "61d49bb5", + "execution_count": 11, "metadata": {}, "outputs": [ { @@ -648,16 +629,16 @@ "output_type": "stream", "text": [ "Final circuit:\n", - "--Rx(3.141)----*------------------------------------------------------------------x--\n", - " | | \n", - "---------------x----*----Rx(-0.22)----Rz(1.497)----*--------*---------------------|--\n", - " | | | | \n", - "--------------------|------------------------------|--------x--------Rx(3.141)----|--\n", - " | | | \n", - "--------------------x------------------------------x----Rz(3.068)-----------------*--\n", - " \n", + "--Rx(3.142)----*----------------------------------------------------------x--\n", + " | | \n", + "---------------x----*----Rx(0.226)----Rz(1.571)----*----*-----------------|--\n", + " | | | | \n", + "--------------------|------------------------------|----x----Rx(3.149)----|--\n", + " | | | \n", + "--------------------x------------------------------x----------------------*--\n", + " \n", "Final loss:\n", - "-1.1372839212417603\n" + "-1.1372729539871216\n" ] } ], @@ -668,7 +649,6 @@ }, { "cell_type": "markdown", - "id": "b78222d3", "metadata": {}, "source": [ "## Comparison with original VQE" @@ -676,7 +656,6 @@ }, { "cell_type": "markdown", - "id": "f2387269", "metadata": {}, "source": [ "The circuit we got using VAns consists of only 5 parameters and the depth of the circuit is 9. The minimized loss value is $-1.13728392$ Ha. Comparing to the fixed ansatz used in the original VQE tutorial, where the circuit consists of 12 parameters and with depth 11, VAns reduces the number of parameters needed while keeps the circuit shallower. " @@ -684,7 +663,6 @@ }, { "cell_type": "markdown", - "id": "c21e251e", "metadata": {}, "source": [ "_______\n", @@ -697,10 +675,10 @@ ], "metadata": { "interpreter": { - "hash": "9043b12ec77a531919bc05f05830335d23baf822720cbea14b03018197d26545" + "hash": "2ab84abaf8d5bbc8765aba8eb82d11e7069f2ff20e8f79b8a9cdeccefd2ac4da" }, "kernelspec": { - "display_name": "Python 3.8.0 ('paddle-quantum-dev')", + "display_name": "Python 3.8.13 ('pq_new')", "language": "python", "name": "python3" }, @@ -714,7 +692,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.0" + "version": "3.8.13" } }, "nbformat": 4, diff --git a/tutorials/quantum_simulation/QSP_and_QSVT_CN.ipynb b/tutorials/quantum_simulation/QSP_and_QSVT_CN.ipynb new file mode 100644 index 0000000..6e126ee --- /dev/null +++ b/tutorials/quantum_simulation/QSP_and_QSVT_CN.ipynb @@ -0,0 +1,902 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 量子信号处理与量子奇异值变换\n", + "\n", + "*Copyright (c) 2022 Institute for Quantum Computing, Baidu Inc. All Rights Reserved.*" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 概览" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**量子信号处理**(quantum signal processing, QSP)是一个首次由 Guang Hao Low 和 Issac L. Chuang [[1]](https://quantum-journal.org/papers/q-2019-07-12-163/) 提出的量子算法,其使用反射($R_X$)和旋转($R_Z$)算子来模拟标量的多项式变换。对于满足特定条件的多项式(比如切比雪夫多项式),QSP 表明这些多项式变换可以通过单比特上的量子门模拟。使用名为“酉算子线性组合”(linear combination of unitaries, LCU)的技巧,我们只需增加两个额外辅助比特,就能模拟任意复多项式变换。简而言之,QSP可拟合任意能被泰勒级数逼近的函数。\n", + "\n", + "论文 [[2]](https://doi.org/10.1145/3313276.3316366)进一步发展了 QSP 的思想,并提出使用高维旋转算子的量子算法来模拟矩阵的多项式变换。由于矩阵的多项式变换本质上是它的奇异值的多项式变换,因此该算法被称为**量子奇异值变换**(quantum singular value transformation, QSVT)。另外,我们还介绍名为**单比特化**(qubitization)的技术,该技术可使用一个辅助比特就能模拟高维旋转算子,大幅减少了模拟高维度旋转算子的资源消耗,使得我们能够用量子电路实现矩阵的多项式变换。\n", + "\n", + "基于论文 [[2]](https://doi.org/10.1145/3313276.3316366),该教程会向大家介绍 QSP、QSVT 和单比特化的概念及其在量桨上的实现。\n", + "\n", + "接下来,我们先介绍块编码(block-encoding)的概念。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "以下是必要的 libraries 和 packages。" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from numpy.polynomial.polynomial import Polynomial\n", + "from numpy.polynomial import Chebyshev\n", + "import paddle\n", + "\n", + "# 量桨的通用函数\n", + "import paddle_quantum\n", + "from paddle_quantum.ansatz import Circuit\n", + "from paddle_quantum.qinfo import dagger\n", + "from paddle_quantum.linalg import unitary_random_with_hermitian_block, density_matrix_random\n", + "\n", + "# 量桨的 QSVT 模块函数\n", + "from paddle_quantum.qsvt import poly_matrix, ScalarQSP, Phi_verification, reflection_based_quantum_signal_processing, QSVT" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 块编码" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "(量子)块编码是一种给矩阵数据编码的方式。在量子计算中,对于一个给定的矩阵 $A \\in \\mathbb{C}^{2^n \\times 2^n}$,我们能找到一个酉矩阵 $U \\in \\mathbb{C}^{2^m \\times 2^m}$ 满足如下等式\n", + "\n", + "$$\n", + "W U V = \n", + "\\begin{bmatrix}\n", + " A & 0 \\\\\n", + " 0 & 0\n", + "\\end{bmatrix}\n", + "= A \\oplus 0I^{\\otimes (m - n)}, \\tag{1}\n", + "$$\n", + "\n", + "其中,$W, V \\in \\mathbb{C}^{2^m \\times 2^m}$ 是两个正交投影算子,这样的酉矩阵 $U$ 被称为 $A$ 的**块编码**(block encoding)。\n", + "\n", + "一般地,投影算子通常是 $W = V = |0\\rangle^{\\otimes (m - n)}\\langle0|^{\\otimes (m - n)} \\otimes I^{\\otimes n}$,那么酉矩阵 $U$ 在标准正交基下可表示为\n", + "\n", + "$$\n", + "U = \\begin{bmatrix}\n", + " A & ... \\\\\n", + " ... & ...\n", + "\\end{bmatrix}, \\tag{2}\n", + "$$\n", + "\n", + "即 $A$ 位于 $U$ 的左上角。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 编码与解码" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "一般来说,块编码的表达形式并非是唯一的。比如,当 $A$ 可以对角化时,$U$ 可以被构建为\n", + "\n", + "$$\n", + "U = \\begin{bmatrix}\n", + " A & i\\sqrt{I^{\\otimes n} - A^2} \\\\\n", + " i\\sqrt{I^{\\otimes n} - A^2} & A\n", + "\\end{bmatrix}; \\tag{3}\n", + "$$\n", + "\n", + "当 $A$ 是酉矩阵时,我们则可以选择 $U = A \\oplus I^{\\otimes (m - n)}$,即 $U$ 是受控的 $A$。特别地,文献[[3]](http://arxiv.org/abs/2203.10236) [[4]](http://arxiv.org/abs/2206.03505)给出了一些使用量子电路实现块编码的方案。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### 量桨实现" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "num_qubits = 3\n", + "num_block_qubits = 2\n", + "\n", + "# 创建一个 2 比特大小的厄密矩阵 A 的 3 比特大小的块编码 U\n", + "# create a 3-qubit block encoding unitary U of a random 2-qubit Hermitian matrix A\n", + "U = unitary_random_with_hermitian_block(num_qubits)\n", + "A = U[0: 2 ** num_block_qubits, 0: 2 ** num_block_qubits]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "另一方面,我们也可以从 矩阵 $A \\in \\mathbb{C}^{2^n \\times 2^n}$ 的块编码 $U$ 中解码出 $A$ 的信息。用$|0\\rangle^{\\otimes (m - n)}\\langle0|^{\\otimes (m - n)} \\otimes \\rho$表示输入量子态,块编码 $U$ 作为量子电路,那么输出量子态为\n", + "\n", + "$$\n", + "U (|0\\rangle^{\\otimes (m - n)}\\langle0|^{\\otimes (m - n)} \\otimes \\rho) U^\\dagger\n", + "= \\begin{bmatrix}\n", + " A \\rho A^\\dagger & ... \\\\\n", + " ... & ...\n", + "\\end{bmatrix}\n", + "= |0\\rangle^{\\otimes (m - n)}\\langle0|^{\\otimes (m - n)} \\otimes A \\rho A^\\dagger + (I^{\\otimes (m - n)} - |0\\rangle^{\\otimes (m - n)}\\langle0|^{\\otimes (m - n)}) \\otimes ... \\tag{4}\n", + "$$\n", + "\n", + "对 $|0\\rangle^{\\otimes (m - n)}$ 所在的寄存器作测量,若结果为 $0^{\\otimes (m - n)}$,那么我们成功地提取了 $A$ 的信息\n", + "\n", + "\n", + "![block-decoding](figures/QSVT-fig-block-decoding.png \"图 1:块解码的量子实现\")\n", + "\n", + "解码的成功概率取决于测量到 $0^{\\otimes (m - n)}$ 的几率,其会随着 $m - n$ 的增长而指数性下降。然而,若我们可以控制 $m - n$ 的大小,那么这就不再是一个问题。在本教程中我们会假设 $m = n + 1$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### 量桨实现" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "qubits [0] collapse to the state |0> with probability 0.08025970130578351\n" + ] + } + ], + "source": [ + "# 设置密度矩阵后端\n", + "paddle_quantum.set_backend('density_matrix')\n", + "\n", + "# 定义输入态\n", + "rho = density_matrix_random(num_block_qubits)\n", + "zero_state = paddle.eye(2 ** (num_qubits - num_block_qubits), 1) # 创建 0 态的矢量\n", + "input_state = paddle_quantum.State(paddle.kron(zero_state @ dagger(zero_state), rho))\n", + "\n", + "# 定义辅助寄存器\n", + "aux_register = [i for i in range(num_qubits - num_block_qubits)]\n", + "\n", + "# 创建线路\n", + "cir = Circuit(num_qubits)\n", + "cir.oracle(U, [i for i in range(num_qubits)])\n", + "cir.collapse(aux_register, desired_result='0', if_print = True) # 调用塌缩算子\n", + "\n", + "# 获取输出态及输出 rho\n", + "output_state = cir(input_state)\n", + "output_rho = output_state.data[0: 2 ** num_block_qubits, 0: 2 ** num_block_qubits]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "我们可以验证 ``output_rho`` 与 $\\frac{A \\rho A^\\dagger}{\\text{Tr}(A \\rho A^\\dagger)}$ 相同。" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "期望 rho 和 输出 rho 的误差是 8.008999270740103e-08\n" + ] + } + ], + "source": [ + "expect_rho = A @ rho @ dagger(A)\n", + "expect_rho /= paddle.trace(expect_rho)\n", + "print(f\"期望 rho 和 输出 rho 的误差是 {paddle.norm(paddle.abs(expect_rho - output_rho)).item()}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 特例 ($m = 1$)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "当 $m = 1$(因此 $n = 0$)时,$A$ 是一个标量。在这种情况下我们则可以算出 $U$ 对于量子态 $|0\\rangle$ 的期望值来获取 $A$。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 量子信号处理" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "论文 [[2]]((https://doi.org/10.1145/3313276.3316366)) 中的 Theorem 3-4 证明,若一个次数为 $k$ 的复数多项式 $P \\in \\mathbb{C}[x]$ 满足\n", + "\n", + "- 若 $k$ 为偶/奇数,则 $P$ 是一个偶/奇多项式;\n", + "- 对于所有的 $x \\in [-1, 1]$,$|P(x)| \\leq 1$;\n", + "- 对于所有的 $x \\in (-\\infty, -1] \\cup [1, \\infty)$,$|P(x)| \\geq 1$;\n", + "- 若 $k$ 为偶,则 $P(ix)P^*(ix) \\geq 1$,\n", + "\n", + "则存在多项式 $Q \\in \\mathbb{C}[x]$ 和角度矢量 $\\Phi = (\\phi_0, ..., \\phi_k) \\in \\mathbb{R}^{k + 1}$ 使得对于所有的 $x \\in [-1, 1]$,\n", + "\n", + "$$\n", + "W_\\Phi(x) := R_Z(-2\\phi_0) \\prod_{j = 1}^k R_X(\\arccos(x)) R_Z(-2\\phi_j)\n", + "= \\begin{bmatrix}\n", + " P(x) & i Q(x)\\sqrt{1 - x^2} \\\\\n", + " iQ^*(x)\\sqrt{1 - x^2} & P^*(x) \n", + "\\end{bmatrix}. \\tag{5}\n", + "$$\n", + "\n", + "换句话说,我们可以使用 $k + 1$ 个旋转($R_Z$)门和 $k$ 个反射 ($R_X$) 门去获得一个 $P(x)$ 的块编码酉矩阵 $W_\\Phi(x)$。块解码后 $P(x)$ 的模拟就完成了。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "以下是一些满足上述条件的例子。" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "# 简单的奇多项式\n", + "# P = Polynomial([0, 0, 0, 0, 0, 1])\n", + "# P = Polynomial([0, 0.5, 0, 0, 0, 0.5])\n", + "P = Polynomial([0, 1 / 3, 0, 0, 0, 1 / 3, 0, 1 / 3])\n", + "\n", + "# 次数为10的第一类切比雪夫多项式\n", + "# P = Chebyshev([0 for _ in range(10)] + [1]).convert(kind=Polynomial)\n", + "\n", + "# 次数为11的第一类切比雪夫多项式\n", + "# P = Chebyshev([0 for _ in range(11)] + [1]).convert(kind=Polynomial)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "值得注意的是,$W_\\Phi$ 可以模拟切比雪夫多项式变换。对于次数为 $k$ 的第一类切比雪夫多项式,其对应的角度矢量为 $(0, \\pi, ..., \\pi)$ (如 $k$ 为偶数)或 $(\\pi, ..., \\pi)$ (如 $k$ 为奇数)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### 量桨实现" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "量桨的 QSVT 模块有一个内置类 `ScalarQSP` 用于完成量子信号处理,并可以调用函数 `Phi_verification` 来验证是否 $W_\\Phi$ 是 $P$ 的块编码。" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tensor(shape=[8], dtype=float64, place=Place(cpu), stop_gradient=True,\n", + " [ 3.14159265, 1.39460474, -0.44313783, 1.09757975, -1.09757975,\n", + " 0.44313783, -1.39460474, 3.14159265])\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\users\\v_zhanglei48\\baidu\\personal-code\\pq\\paddle_quantum\\QSVT\\qsp.py:242: ComplexWarning: Casting complex values to real discards the imaginary part\n", + " Phi[i] = np.log(P.coef[n] / Q.coef[m]) * -1j / 2\n", + "c:\\users\\v_zhanglei48\\baidu\\personal-code\\pq\\paddle_quantum\\QSVT\\qsp.py:256: ComplexWarning: Casting complex values to real discards the imaginary part\n", + " Phi[0] = -1j * np.log(P.coef[0])\n" + ] + } + ], + "source": [ + "qsp = ScalarQSP(P)\n", + "print(qsp.Phi)\n", + "assert Phi_verification(qsp.Phi.numpy(), P)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "我们也可以通过量子线路来验证 $\\Phi$ 的正确性。" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "--Rz(-6.28)----Rx(-2.21)----Rz(2.789)----Rx(-2.21)----Rz(-0.88)----Rx(-2.21)----Rz(2.195)----Rx(-2.21)----Rz(-2.19)----Rx(-2.21)----Rz(0.886)----Rx(-2.21)----Rz(-2.78)----Rx(-2.21)----Rz(-6.28)--\n", + " \n", + "模拟 P(x) 的误差是 2.931881168672019e-08\n" + ] + } + ], + "source": [ + "x = np.random.rand() * 2 - 1 # 随机在 [-1, 1] 间选取 x\n", + "cir = qsp.block_encoding(x)\n", + "print(cir)\n", + "print(f\"模拟 P(x) 的误差是 {np.abs(cir.unitary_matrix()[0, 0].item() - P(x))}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### QSP 变种" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "另外,参数数量可以被进一步减少至 $k$,通过找到一个角度矢量 $\\Phi \\in \\mathbb{R}^k$ 使得\n", + "\n", + "$$\n", + "R_\\Phi(x):=\\prod_{i = 1}^{k} e^{i \\phi_i \\sigma_z} R(x)\n", + "= \\begin{bmatrix}\n", + " P(x) & ... \\\\\n", + " ... & ...\n", + "\\end{bmatrix}, \\tag{6}\n", + "$$\n", + "\n", + "这里\n", + "\n", + "$$\n", + "R(x) = \\begin{bmatrix}\n", + " x & \\sqrt{1 - x^2} \\\\\n", + " \\sqrt{1 - x^2} & -x\n", + "\\end{bmatrix}. \\tag{7}\n", + "$$\n", + "\n", + "在量桨里我们通过调用 `reflection_based_quantum_signal_processing` 来计算 $\\Phi$。" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[15.70796327 -0.17619159 -2.01393416 -0.47321657 -2.66837608 -1.12765849\n", + " -2.96540107]\n" + ] + } + ], + "source": [ + "print(reflection_based_quantum_signal_processing(P))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> 注: $R_{-\\Phi}(x)$ 是 $P^*(x)$ 的块编码。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 量子奇异值变换" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "接下来,我们介绍量子奇异值变换的概念。\n", + "\n", + "对于 $\\mathcal{X} \\in \\mathbb{C}^{2^m \\times 2^m}$ 为一个矩阵,当它可以被一个酉矩阵 $U$ 和两个正交投影算子 $ W, V$ 表示时,即 $\\mathcal{X} = W U V$,那么矩阵 $\\mathcal{X}$ 有如下表达式:\n", + "\n", + "$$\n", + "\\mathcal{X} = \\sum_{j=1}^{2^m} \\lambda_j |\\omega_j\\rangle \\langle\\nu_j|, \\tag{8}\n", + "$$\n", + "\n", + "其中 $\\{|\\omega_j\\rangle\\}_{j=1}^{2^m}$ 和 $\\{|\\nu_j\\rangle\\}_{j=1}^{2^m}$ 是由 $W$ 和 $V$ 的本征态扩展生成的标准正交基,且 $\\lambda_j$ 是 $\\mathcal{X}$ 的奇异值,\n", + "\n", + "$$\n", + "\\lambda_j = \\begin{cases}\n", + "\\langle\\omega_j| U |\\nu_j\\rangle \\text{ 如 } j \\leq \\text{rank}(\\mathcal{X}) \\\\\n", + "0 \\text{ 其他情况 }\n", + "\\end{cases} \\tag{9}\n", + "$$\n", + "\n", + "对于给定的周期性函数 $f$,矩阵$\\mathcal{X}$ 的**奇异值变换**(SVT) 定义为\n", + "\n", + "$$\n", + "f^{(SV)}(\\mathcal{X}) := \\sum_{j=1}^{2^m} f(\\lambda_j)\n", + "\\begin{cases}\n", + " |\\nu_j\\rangle\\langle\\nu_j| & \\text{ 如 } f \\text{ 是偶函数} \\\\\n", + " |\\omega_j\\rangle\\langle\\nu_j| & \\text{ 如 } f \\text{ 是奇函数}\n", + "\\end{cases}. \\tag{10}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 分解与 QSP" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "论文 [[2]]((https://doi.org/10.1145/3313276.3316366)) 证明在**合适**的基下, 与 $\\lambda_j$ 相关的子空间可以将 $U, V$ 和 $W$ 分解为\n", + "\n", + "$$\n", + "\\begin{split}\n", + "U & = \\bigoplus_{j: \\lambda_j \\in [0, 1)} R(\\lambda_j) \\oplus\n", + " \\bigoplus_{j: \\lambda_j = 1} [1] \\oplus\n", + " \\bigoplus_{j: V|\\nu_j\\rangle = |\\nu_j\\rangle,\\, WU|\\nu_j\\rangle = 0} [1] \\oplus\n", + " \\bigoplus_{j: W|\\omega_j\\rangle = |\\omega_j\\rangle,\\, VU^\\dagger|\\omega_j\\rangle = 0} [1] \\oplus \n", + " \\bigoplus_{j: V|\\nu_j\\rangle = W|\\omega_j\\rangle = 0} [...], \\\\\n", + "V & = \\bigoplus_{j: \\lambda_j \\in [0, 1)} |0\\rangle\\langle0| \\oplus\n", + " \\bigoplus_{j: \\lambda_j = 1} [1] \\oplus\n", + " \\bigoplus_{j: V|\\nu_j\\rangle = |\\nu_j\\rangle,\\, WU|\\nu_j\\rangle = 0} [1] \\oplus\n", + " \\bigoplus_{j: W|\\omega_j\\rangle = |\\omega_j\\rangle,\\, VU^\\dagger|\\omega_j\\rangle = 0} [0] \\oplus \n", + " \\bigoplus_{j: V|\\nu_j\\rangle = W|\\omega_j\\rangle = 0} [0] \\text{ and}\\\\\n", + "W & = \\bigoplus_{j: \\lambda_j \\in [0, 1)} |0\\rangle\\langle0| \\oplus\n", + " \\bigoplus_{j: \\lambda_j = 1} [1] \\oplus\n", + " \\bigoplus_{j: V|\\nu_j\\rangle = |\\nu_j\\rangle,\\, WU|\\nu_j\\rangle = 0} [0] \\oplus\n", + " \\bigoplus_{j: W|\\omega_j\\rangle = |\\omega_j\\rangle,\\, VU^\\dagger|\\omega_j\\rangle = 0} [1] \\oplus \n", + " \\bigoplus_{j: V|\\nu_j\\rangle = W|\\omega_j\\rangle = 0} [0] .\\\\\n", + "\\end{split} \\tag{11}\n", + "$$\n", + "\n", + "$f^{(SV)}$ 也可以被分解为\n", + "\n", + "$$\n", + "f^{(SV)}(\\mathcal{X}) = \\sum_{j: \\lambda_j \\in [0, 1)} f(\\lambda_j)\\,... + \\sum_{j: \\lambda_j = 1} f(1)\\,... + \\sum_{j: \\lambda_j = 0, V |\\nu_j\\rangle \\neq 0} f(0)\\,... + \\sum_{j: \\lambda_j = 0, W |\\omega_j\\rangle \\neq 0} f(0)\\,... + \\sum_{j: \\lambda_j = 0, \\text{otherwise}} f(0)\\,...\\,, \\tag{12}\n", + "$$\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "现在我们将上述分解与量子信号处理联系到一起。\n", + "\n", + "假设函数 $P$ 是一个次数为 $k$ 并满足上节所述条件,则存在 $\\Phi \\in \\mathbb{R}^k$ 使得 $R_\\Phi$ 是 $P$ 的块编码,并且 $P$ 满足 $P(1) = e^{i \\sum_{j=1}^k \\phi_j}$ 和 $P(0) = e^{i \\sum_{j=1}^k (-1)^{j+1} \\phi_j}$。\n", + "\n", + "定义 $U_\\Phi$ 为\n", + "\n", + "$$\n", + "U_\\Phi := \\begin{cases}\n", + "& \\prod_{j = 1}^{k / 2} e^{i\\phi_{2j - 1} (2V - I)} U^\\dagger e^{i\\phi_{2j} (2W - I)} U \\text{ 如 } k \\text{ 是偶数} \\\\\n", + "e^{i\\phi_1 (2W - I)} U & \\prod_{j = 1}^{(k - 1) / 2} e^{i\\phi_{2j} (2V - I)} U^\\dagger e^{i\\phi_{2j+1} (2W - I)} U \\text{ 如 } k \\text{ 是奇数}\n", + "\\end{cases}. \\tag{13}\n", + "$$\n", + "\n", + "那么\n", + "\n", + "$$\n", + "U_\\Phi = \\begin{cases}\n", + " \\bigoplus R_\\Phi(\\lambda_j) \\oplus\n", + " \\bigoplus [e^{i \\sum_{j=1}^k \\phi_j}] \\oplus\n", + " \\bigoplus [e^{i \\sum_{j=1}^k (-1)^{j+1} \\phi_j}] \\oplus\n", + " \\bigoplus [e^{i \\sum_{j=1}^k (-1)^{j+1} \\phi_j}] \\oplus \n", + " \\bigoplus [...]\n", + "\\text{ 如 } k \\text{ 是偶数} \\\\\n", + " \\bigoplus R_\\Phi(\\lambda_j) \\oplus\n", + " \\bigoplus [e^{i \\sum_{j=1}^k \\phi_j}] \\oplus\n", + " \\bigoplus [e^{i \\sum_{j=1}^k (-1)^{j+1} \\phi_j}] \\oplus\n", + " \\bigoplus [e^{i \\sum_{j=1}^k (-1)^{j+1} \\phi_j}] \\oplus \n", + " \\bigoplus [...]\n", + "\\text{ 如 } k \\text{ 是奇数}\n", + "\\end{cases}, \\tag{14}\n", + "$$\n", + "\n", + "因此\n", + "\n", + "$$\n", + "P^{(SV)}(\\mathcal{X}) = \\begin{cases} \n", + " \\bigoplus \\begin{bmatrix}\n", + " P(\\lambda_j) & 0 \\\\\n", + " 0 & 0 \\\\\n", + " \\end{bmatrix} \\oplus\n", + " \\bigoplus [P(1)] \\oplus\n", + " \\bigoplus [P(0)] \\oplus\n", + " \\bigoplus [0] \\oplus \n", + " \\bigoplus [0] = V U_\\Phi V\n", + "\\text{ 如 } k \\text{ 是偶数} \\\\ \n", + " \\bigoplus \\begin{bmatrix}\n", + " P(\\lambda_j) & 0 \\\\\n", + " 0 & 0 \\\\\n", + " \\end{bmatrix} \\oplus\n", + " \\bigoplus [P(1)] \\oplus\n", + " \\bigoplus [0] \\oplus\n", + " \\bigoplus [0] \\oplus \n", + " \\bigoplus [0] = W U_\\Phi V \n", + "\\text{ 如 } k \\text{ 是奇数}\n", + "\\end{cases}. \\tag{15}\n", + "$$\n", + "\n", + "若是我们可以使用量子算法实现 $V U_\\Phi V$ 或者 $W U_\\Phi V$,那么这样的变换就是 $\\mathcal{X}$ 的量子奇异值变换。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### QSVT 和块编码" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "假设相对于正交投影算子 $W$ 和 $V$, $U$ 是一个 $A$ 的块编码酉矩阵。那么 $\\mathcal{X} = A \\oplus 0I^{\\otimes (m - n)}$ 且 $P^{(SV)}(\\mathcal{X}) = P^{(SV)}(A) \\oplus 0I^{\\otimes (m - n)}$. 因此,$U_\\Phi$ 是 $P^{(SV)}(A)$ 的块编码.\n", + "\n", + "当 $W = V$ 时,$\\{|\\omega_j\\rangle \\}_{j=1}^{2^m} = \\{ |\\nu_j\\rangle \\}_{j=1}^{2^m}$。记 $P(x) := \\sum_{i=0}^k c_i x^i$,我们就可以找到\n", + "\n", + "$$\n", + "P^{(SV)}(\\mathcal{X}) = \\sum_{j=1}^{2^m} P(\\lambda_j) |\\nu_j\\rangle \\langle\\nu_j| = P(X) = P(A) \\oplus 0I^{\\otimes (m - n)}. \\tag{16}\n", + "$$\n", + "\n", + "换句话说,当 $W = V$ 时,QSVT 将一个 $A$ 的块编码转为了一个 $P(A)$ 的块编码。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### 量桨实现" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "当 $W = V = |0\\rangle^{\\otimes (m - n)}\\langle0|^{\\otimes (m - n)} \\otimes I^{\\otimes n}$ 时,量桨有一个内置类 `QSVT` 来完成量子奇异值变换。我们可以调用其成员函数 `QSVT.block_encoding_matrix()` 来验证上述理论的正确性。" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "qsvt = QSVT(P, U, num_block_qubits)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\Users\\v_zhanglei48\\Anaconda3\\envs\\personal-code\\lib\\site-packages\\paddle\\fluid\\dygraph\\math_op_patch.py:276: UserWarning: The dtype of left and right variables are not the same, left dtype is paddle.float64, but right dtype is paddle.float32, the right dtype will convert to paddle.float64\n", + " warnings.warn(\n" + ] + } + ], + "source": [ + "# 找到 P(A) 及\n", + "# find P(A) and its expected eigenvalues, note that they are computed in different ways\n", + "expect_PA = poly_matrix(P, A).numpy()\n", + "expect_eig_result = np.sort(list(map(lambda x: P(x), np.linalg.eigvals(A.numpy()))))\n", + "\n", + "# Calculate U_\\Phi and extract eigenvalues of block encoded matrix\n", + "U_Phi = qsvt.block_encoding_matrix().numpy()\n", + "actual_PA = U_Phi[0:2 ** num_block_qubits, 0:2 ** num_block_qubits]\n", + "actual_eig_result = np.sort(np.linalg.eigvals(actual_PA))" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "模拟 P(X) 的误差\n", + " 最大绝对值, 8.429041888826793e-08\n", + " 百分比, 6.291585659021523e-07\n" + ] + } + ], + "source": [ + "print(\"模拟 P(X) 的误差\")\n", + "print(\" 最大绝对值, \", np.amax(abs(expect_PA - actual_PA)))\n", + "print(\" 百分比, \", np.linalg.norm(expect_PA - actual_PA) / np.linalg.norm(expect_PA))" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "模拟 P(X) 的本征值误差\n", + " 最大绝对值, 1.0894675602300946e-07\n", + " 百分比, 5.209560480945378e-07\n" + ] + } + ], + "source": [ + "print(\"模拟 P(X) 的本征值误差\")\n", + "print(\" 最大绝对值, \", np.amax(abs(expect_eig_result - actual_eig_result)))\n", + "print(\" 百分比, \", np.linalg.norm(expect_eig_result - actual_eig_result) / np.linalg.norm(expect_eig_result))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 单比特化: $U_\\Phi$ 的量子实现" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "现在的问题是,如何用量子线路去实现 $U_\\Phi$ ?通常我们认为块编码 $U$ 已经通过量子电路实现,那么剩下来只需实现 $e^{i\\phi (2W - I)}$ 和 $e^{i\\phi (2V - I)}$。值得注意的是,这两个算子本质上是分别在 $W$ 和 $V$ 的投影空间下施加的 $R_Z(-2\\phi)$ 算子。\n", + "\n", + "论文 [[1]](https://quantum-journal.org/papers/q-2019-07-12-163/) 中的 Lemma 10 认为,应该将投影算子的映射空间投射到一个辅助比特上再进行旋转,那么通过纠缠的性质这个旋转也会同时应用于主寄存器上。\n", + "\n", + "这些理论被总结至下图:\n", + "\n", + "![U_Phi](figures/QSVT-fig-U_Phi.png \"图 2: QSVT 的量子实现,这里 k 是 P 的多项式次数\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### 量桨实现" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "当 $W = V = |0\\rangle^{\\otimes (m - n)}\\langle0|^{\\otimes (m - n)} \\otimes I^{\\otimes n}$ 时,量桨可以调用成员函数 `QSVT.block_encoding_circuit` 来创建一个图 2 所示的量子线路。我们可以通过比较该线路的输出态和量子态 $(U_\\Phi \\otimes I)|\\psi\\rangle|0\\rangle$($|\\psi\\rangle \\in \\mathbb{C}^{2^m}$)来证明所建立的线路是可以模拟 $U_\\Phi$ 的。" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "# 设置态矢量后端\n", + "paddle_quantum.set_backend('state_vector')\n", + "\n", + "# 随即量子态满足最后一个量子比特固定为 0 态\n", + "ket_0 = [1.0, 0.0]\n", + "psi = np.array([np.random.rand() + 1j * np.random.rand() for _ in range(2 ** num_qubits)])\n", + "psi = np.kron(psi / np.linalg.norm(psi), ket_0)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "期望量子态和实际量子态的差距是 2.253552555988302e-07\n" + ] + } + ], + "source": [ + "expect_state = np.kron(U_Phi, np.eye(2)) @ psi\n", + "\n", + "cir = qsvt.block_encoding_circuit()\n", + "actual_state = cir(paddle_quantum.State(psi, dtype='complex64')).data.numpy()\n", + "\n", + "print(f\"期望量子态和实际量子态的差距是 {np.linalg.norm(expect_state - actual_state)}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> 注:如果我们在图 2 中的辅助寄存器两边加上 Hadamard 门,那么该量子线路就会转为模拟其多项式的实数变化。再结合 LCU 的技巧,理论上我们只需要额外 1 个量子比特来量子模拟任意范数小于 1 的复数多项式。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 应用: 振幅放大" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "设 $|\\psi\\rangle$ 为 $m$ 比特大小的量子态并满足 $|\\psi\\rangle = \\sin(\\theta)|\\psi_{\\text{good}}\\rangle + \\cos(\\theta) |\\psi_{\\text{bad}}\\rangle$。**振幅放大**是一个用于增大 $|\\psi_{\\text{good}}\\rangle$ 至 1 的量子算法。该算法可以从另一个角度来解释。设定 $U$ 为一个酉矩阵且 $W$ 和 $V$ 为两个正交投影算子,使得 $|\\psi\\rangle = U V |0\\rangle^{\\otimes m}$ 且 $\\sin(\\theta)|\\psi_{\\text{good}}\\rangle = W |\\psi\\rangle$,意味着 $\\sin(\\theta)|\\psi_{\\text{good}}\\rangle = W U V |0\\rangle^{\\otimes m}$。因此,振幅放大本质上是 $\\mathcal{X} = W U V$ 的奇异值变换。\n", + "在这一章节我们会根据论文 [[2]]((https://doi.org/10.1145/3313276.3316366)) 的 Theorem 28,来展示如何用 QSVT 来做定点(即 $\\theta$ 满足 $\\theta = \\frac{\\pi}{2k}$,$k \\in \\mathbb{Z}$)振幅放大算法。\n", + "\n", + "假设 $\\sin(\\theta)|\\psi_{\\text{good}}\\rangle = (|0\\rangle^{\\otimes (m - n)}\\langle0|^{\\otimes (m - n)} \\otimes I^{\\otimes n}) |\\psi\\rangle$ 且 $\\theta = \\frac{\\pi}{2k}$($k \\in \\mathbb{Z}$)。那么就能推导出 $W = V = |0\\rangle^{\\otimes (m - n)}\\langle0|^{\\otimes (m - n)} \\otimes I^{\\otimes n}$ 且 $\\mathcal{X} = A \\oplus 0I^{\\otimes (m - n)}$,这里 $A$ 位于 $U$ 的左上角。 " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "可见 $\\mathcal{X} |0\\rangle^{\\otimes m} = \\sin(\\frac{\\pi}{2k}) |\\psi_\\text{good}\\rangle$。因此,我们旨在找到一个酉矩阵为 $U_\\Phi$ 的量子线路使得 $W U_\\Phi V = \\sin^{-1}(\\theta) \\mathcal{X}$, 从而达到 $W U_\\Phi V |0\\rangle^{\\otimes m} = |\\psi_{\\text{good}}\\rangle$ 的目的。因为 $\\mathcal{X} = W U V$,$\\mathcal{X}$ 的奇异值的绝对值是 $\\sin(\\frac{\\pi}{2k})$。我们进一步选择 $P(x) = (-1)^k T_k (x)$,这里 $T_k$ 是次数为 $k$ 的第一类切比雪夫多项式。最后通过\n", + "\n", + "$$\n", + "P(\\sin(\\frac{\\pi}{2k})) = (-1)^k T_k (\\sin(\\frac{\\pi}{2k})) = (-1)^k \\cos(\\frac{k - 1}{2}\\pi) = 1, \\tag{17}\n", + "$$\n", + "\n", + "我们可得,对于多项式 $P$ 的 $\\mathcal{X}$ 的量子奇异值变换是 $B := \\frac{1}{\\sin(\\frac{\\pi}{2k})} A$ 的块编码。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### 量桨实现" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "设定 $k = 3$ 使得 $\\sin(\\frac{\\pi}{2k}) = \\frac{1}{2}$,同时随机选择酉矩阵 $U$。其左上角为 $A$,我们需要证明 $U$ 的 QSVT 的左上角为 $B = 2A$。" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "P = -1 * Chebyshev([0 for _ in range(3)] + [1]).convert(kind=Polynomial)\n", + "U = unitary_random_with_hermitian_block(num_qubits, is_unitary=True)\n", + "A = U[0:2 ** num_block_qubits, 0:2 ** num_block_qubits].numpy()\n", + "\n", + "amplifier = QSVT(P, U, num_block_qubits)\n", + "U_Phi = amplifier.block_encoding_unitary()" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "QSVT 的精确值为 3.3605192584218457e-07\n" + ] + } + ], + "source": [ + "B = U_Phi[0:2 ** num_block_qubits, 0:2 ** num_block_qubits].numpy()\n", + "print(f\"QSVT 的精确值为 {np.linalg.norm(B - 2 * A)}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "由上可见,我们成功将 $A$ 的大小翻倍。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "___\n", + "## 参考文献\n", + "\n", + "[1] Low, Guang Hao, and Isaac L. Chuang. \"Hamiltonian simulation by qubitization.\" [Quantum 3 (2019): 163.](https://doi.org/10.22331/q-2019-07-12-163)\n", + "\n", + "[2] Gilyén, András, et al. \"Quantum singular value transformation and beyond: exponential improvements for quantum matrix arithmetics.\" [Proceedings of the 51st Annual ACM SIGACT Symposium on Theory of Computing. 2019.](https://doi.org/10.1145/3313276.3316366)\n", + "\n", + "[3] Camps, Daan, et al. \"Explicit Quantum Circuits for Block Encodings of Certain Sparse Matrices.\" [arXiv preprint arXiv:2203.10236 (2022).](http://arxiv.org/abs/2203.10236)\n", + "\n", + "[4] Clader, B. David, et al. \"Quantum Resources Required to Block-Encode a Matrix of Classical Data.\" [arXiv preprint arXiv:2206.03505 (2022).](http://arxiv.org/abs/2206.03505)\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.8.13 ('pq')", + "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.8.13" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "1e82098cfee7be27b5e385e3f85fe91d734d6114f7d09dccafdaad2c23171c3e" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tutorials/quantum_simulation/QSP_and_QSVT_EN.ipynb b/tutorials/quantum_simulation/QSP_and_QSVT_EN.ipynb new file mode 100644 index 0000000..461fd3a --- /dev/null +++ b/tutorials/quantum_simulation/QSP_and_QSVT_EN.ipynb @@ -0,0 +1,905 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Quantum Signal Processing and Quantum Singular Value Transformation\n", + "*Copyright (c) 2022 Institute for Quantum Computing, Baidu Inc. All Rights Reserved.*" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Overview" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Quantum signal processing** (QSP), originally purposed by Guang Hao Low and Issac L. Chuang [[1]](https://quantum-journal.org/papers/q-2019-07-12-163/), is an algorithm for simulation of polynomial transformation of scalars using reflection ($R_X$) and rotation ($R_Z$) operators. QSP states that, for a specific group of polynomials like the Chebyshev polynomials of the first kind, such simulation can be done by a single qubit. Moreover, using a technique called \"linear combination of unitaries\", we can use two more ancilla qubit to simulate all complex polynomials and hence approximate any functions that can be expanded by Taylor series.\n", + "\n", + "The idea of QSP is further generalized by paper [[2]](https://doi.org/10.1145/3313276.3316366), so that it can simulate polynomial transformation of matrices using high-dimensional rotation operators. Since the polynomial transformation of a diagonalizable matrix is intrinsically the polynomial transformation of its singular values, this algorithm is called the **quantum singular value transformation** (QSVT).\n", + "\n", + "Moreover, a technique called **qubitization** is further employed to substantially reduce the complexity of high-dimensional rotation operators, so that one (ancilla) qubit suffices to perform the rotation in larger space.\n", + "\n", + "Based on paper [[2]](https://doi.org/10.1145/3313276.3316366), this tutorial will illustrate quantum signal processing, quantum singular value transformation and qubitization, and how to implement these algorithms in PaddleQuantum. But first of all, we need to present the idea of block encoding." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here are some necessary libraries and packages." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from numpy.polynomial.polynomial import Polynomial\n", + "from numpy.polynomial import Chebyshev\n", + "import paddle\n", + "\n", + "# PaddleQuantum related packages\n", + "import paddle_quantum\n", + "from paddle_quantum.ansatz import Circuit\n", + "from paddle_quantum.qinfo import dagger\n", + "from paddle_quantum.linalg import unitary_random_with_hermitian_block, density_matrix_random\n", + "\n", + "from paddle_quantum.qsvt import poly_matrix, ScalarQSP, Phi_verification, reflection_based_quantum_signal_processing, QSVT" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Block Encoding" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Block encoding is a method to encode matrices of data. In quantum computing, for a matrix $A \\in \\mathbb{C}^{2^n \\times 2^n}$, a unitary $U \\in \\mathbb{C}^{2^m \\times 2^m}$ is said to be a **block encoding** of $A$ if there exists two orthogonal projectors $W, V \\in \\mathbb{C}^{2^m \\times 2^m}$ such that\n", + "\n", + "$$\n", + "W U V = \n", + "\\begin{bmatrix}\n", + " A & 0 \\\\\n", + " 0 & 0\n", + "\\end{bmatrix}\n", + "= A \\oplus 0I^{\\otimes (m - n)}. \\tag{1}\n", + "$$\n", + "\n", + "This could appear more familiar when $W = V = |0\\rangle^{\\otimes (m - n)}\\langle0|^{\\otimes (m - n)} \\otimes I^{\\otimes n}$, where above equation implies\n", + "\n", + "$$\n", + "U = \\begin{bmatrix}\n", + " A & ... \\\\\n", + " ... & ...\n", + "\\end{bmatrix}. \\tag{2}\n", + "$$\n", + "\n", + "That is, $A$ is the left-upper block of matrix $U$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Encoding and Decoding of Matrices" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There are numerous ways to find a block encoding unitary $U$ of a matrix $A$. For example, when $A$ is diagonalizable, $U$ can be constructed as\n", + "\n", + "$$\n", + "U = \\begin{bmatrix}\n", + " A & i\\sqrt{I^{\\otimes n} - A^2} \\\\\n", + " i\\sqrt{I^{\\otimes n} - A^2} & A\n", + "\\end{bmatrix}; \\tag{3}\n", + "$$\n", + "\n", + "when $A$ is a unitary, we can simply select $U = A \\oplus I^{\\otimes (m - n)}$ i.e. $U$ is a controlled $A$. Despite the fact that we do not hold a mature method for doing this in a quantum system, several studies [[3]](http://arxiv.org/abs/2203.10236) [[4]](http://arxiv.org/abs/2206.03505) have been conducted on the quantum implementation of block encodings. In this tutorial, we will assume that there exists an efficient algorithm to block encode a matrix (or at least a Hermitian matrix) in a quantum circuit." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Realization in PaddleQuantum." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "num_qubits = 3\n", + "num_block_qubits = 2\n", + "\n", + "# create a 3-qubit block encoding unitary U of a random 2-qubit Hermitian matrix A\n", + "U = unitary_random_with_hermitian_block(num_qubits)\n", + "A = U[0: 2 ** num_block_qubits, 0: 2 ** num_block_qubits]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As for the decoding part, the information of matrix $A \\in \\mathbb{C}^{2^n \\times 2^n}$ can be extracted by applying its block encoding unitary $U \\in \\mathbb{C}^{2^m \\times 2^m}$ on $|0\\rangle^{\\otimes (m - n)}\\langle0|^{\\otimes (m - n)} \\otimes \\rho$, where $\\rho$ is an $n$-qubit quantum state. Then the output state is\n", + "\n", + "$$\n", + "U (|0\\rangle^{\\otimes (m - n)}\\langle0|^{\\otimes (m - n)} \\otimes \\rho) U^\\dagger\n", + "= \\begin{bmatrix}\n", + " A \\rho A^\\dagger & ... \\\\\n", + " ... & ...\n", + "\\end{bmatrix}\n", + "= |0\\rangle^{\\otimes (m - n)}\\langle0|^{\\otimes (m - n)} \\otimes A \\rho A^\\dagger + (I^{\\otimes (m - n)} - |0\\rangle^{\\otimes (m - n)}\\langle0|^{\\otimes (m - n)}) \\otimes ... \\tag{4}\n", + "$$\n", + "\n", + "Measuring the first quantum register with outcome $0^{\\otimes (m - n)}$ gives the desired information.\n", + "\n", + "![block-decoding](figures/QSVT-fig-block-decoding.png \"Figure 1: Graph Illustration of Block Decoding\")\n", + "\n", + "The success probability of decoding is the probability of measuring $0^{\\otimes (m - n)}$, which can be exponentially small as $m - n$ increases. However, this would no longer be a problem if we can control the size of first register. In this tutorial we will assume $m = n + 1$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Realization in PaddleQuantum" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "qubits [0] collapse to the state |0> with probability 0.10231761176954456\n" + ] + } + ], + "source": [ + "# set density matrix backend\n", + "paddle_quantum.set_backend('density_matrix')\n", + "\n", + "# define input state\n", + "rho = density_matrix_random(num_block_qubits)\n", + "zero_state = paddle.eye(2 ** (num_qubits - num_block_qubits), 1) # create a 0 state (in state_vector form)\n", + "input_state = paddle_quantum.State(paddle.kron(zero_state @ dagger(zero_state), rho))\n", + "\n", + "# define auxiliary register\n", + "aux_register = [i for i in range(num_qubits - num_block_qubits)]\n", + "\n", + "# construct the circuit\n", + "cir = Circuit(num_qubits)\n", + "cir.oracle(U, [i for i in range(num_qubits)])\n", + "cir.collapse(aux_register, desired_result='0', if_print = True) # call Collapse operator\n", + "\n", + "# get output_state and actual output rho\n", + "output_state = cir(input_state)\n", + "output_rho = output_state.data[0: 2 ** num_block_qubits, 0: 2 ** num_block_qubits]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can verify that the ``output_rho`` is the same as $\\frac{A \\rho A^\\dagger}{\\text{Tr}(A \\rho A^\\dagger)}$." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "the difference between input and output is 1.1947226861933412e-07\n" + ] + } + ], + "source": [ + "expect_rho = A @ rho @ dagger(A)\n", + "expect_rho /= paddle.trace(expect_rho)\n", + "print(f\"the difference between input and output is {paddle.norm(paddle.abs(expect_rho - output_rho)).item()}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Special Case ($m = 1$)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "When $m = 1$ (and hence $n = 0$), $A$ is a scalar. In this case we can calculate the expectance of $U$ in terms of state $|0\\rangle$ to receive $A$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Quantum Signal Processing" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Theorem 3-4 in paper [[2]]((https://doi.org/10.1145/3313276.3316366)) state that, for a polynomial $P \\in \\mathbb{C}[x]$ with $\\deg(P) = k$ that satisfies\n", + "\n", + "- if $k$ is even/odd, then $P$ is a even/odd polynomial;\n", + "- $|P(x)| \\leq 1, \\,\\forall x \\in [-1, 1]$;\n", + "- $|P(x)| \\geq 1, \\,\\forall x \\in (-\\infty, -1] \\cup [1, \\infty)$;\n", + "- if $k$ is even, then $P(ix)P^*(ix) \\geq 1$,\n", + "\n", + "there exists a polynomial $Q \\in \\mathbb{C}[x]$ and a vector of angles $\\Phi = (\\phi_0, ..., \\phi_k) \\in \\mathbb{R}^{k + 1}$ such that $\\forall x \\in [-1, 1]$,\n", + "\n", + "$$\n", + "W_\\Phi(x) := R_Z(-2\\phi_0) \\prod_{j = 1}^k R_X(\\arccos(x)) R_Z(-2\\phi_j)\n", + "= \\begin{bmatrix}\n", + " P(x) & i Q(x)\\sqrt{1 - x^2} \\\\\n", + " iQ^*(x)\\sqrt{1 - x^2} & P^*(x) \n", + "\\end{bmatrix}. \\tag{5}\n", + "$$\n", + "\n", + "That is, we can use $k + 1$ rotation ($R_Z$) gates and $k$ reflection ($R_X$) gates to receive a block encoding unitary $W_\\Phi(x)$ of $P(x)$. After block decoding we have completed the simulation of $P(x)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here are some polynomials that satisfy above requirements. " + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "# simple odd polynomials\n", + "# P = Polynomial([0, 0, 0, 0, 0, 1])\n", + "# P = Polynomial([0, 0.5, 0, 0, 0, 0.5])\n", + "P = Polynomial([0, 1 / 3, 0, 0, 0, 1 / 3, 0, 1 / 3])\n", + "\n", + "# Chebyshev polynomials of first kind with degree 10\n", + "# P = Chebyshev([0 for _ in range(10)] + [1]).convert(kind=Polynomial)\n", + "\n", + "# Chebyshev polynomials of first kind with degree 11\n", + "# P = Chebyshev([0 for _ in range(11)] + [1]).convert(kind=Polynomial)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that such structure of $W_\\Phi$ naturally fits the family of Chebyshev polynomials. Indeed, for a Chebyshev polynomials of first kind with order $k$, its corresponding $\\Phi$ is $(0, \\pi, ..., \\pi)$ (if $k$ is even) or $(\\pi, ..., \\pi)$ (if $k$ is odd). " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Realization in PaddleQuantum" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "PaddleQuantum has a built-in class `ScalarQSP` for quantum signal processing and a function `Phi_verification` to verify whether $W_\\Phi$ is a block encoding of $P$." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tensor(shape=[8], dtype=float64, place=Place(cpu), stop_gradient=True,\n", + " [ 3.14159265, 1.39460474, -0.44313783, 1.09757975, -1.09757975,\n", + " 0.44313783, -1.39460474, 3.14159265])\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\users\\v_zhanglei48\\baidu\\personal-code\\pq\\paddle_quantum\\QSVT\\qsp.py:242: ComplexWarning: Casting complex values to real discards the imaginary part\n", + " Phi[i] = np.log(P.coef[n] / Q.coef[m]) * -1j / 2\n", + "c:\\users\\v_zhanglei48\\baidu\\personal-code\\pq\\paddle_quantum\\QSVT\\qsp.py:256: ComplexWarning: Casting complex values to real discards the imaginary part\n", + " Phi[0] = -1j * np.log(P.coef[0])\n" + ] + } + ], + "source": [ + "qsp = ScalarQSP(P)\n", + "print(qsp.Phi)\n", + "assert Phi_verification(qsp.Phi.numpy(), P)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also verify the correctness of $\\Phi$ from the corresponding circuit." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "--Rz(-6.28)----Rx(-1.11)----Rz(2.789)----Rx(-1.11)----Rz(-0.88)----Rx(-1.11)----Rz(2.195)----Rx(-1.11)----Rz(-2.19)----Rx(-1.11)----Rz(0.886)----Rx(-1.11)----Rz(-2.78)----Rx(-1.11)----Rz(-6.28)--\n", + " \n", + "the error of simulating P(x) is 2.1200956324443587e-07\n" + ] + } + ], + "source": [ + "x = np.random.rand() * 2 - 1 # random x in [-1, 1]\n", + "cir = qsp.block_encoding(x)\n", + "print(cir)\n", + "print(f\"the error of simulating P(x) is {np.abs(cir.unitary_matrix()[0, 0].item() - P(x))}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Variant of QSP" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Moreover, the number of parameters can be further simplified to $k$ by finding a vector of angles $\\Phi \\in \\mathbb{R}^k$ such that\n", + "\n", + "$$\n", + "R_\\Phi(x):=\\prod_{i = 1}^{k} e^{i \\phi_i \\sigma_z} R(x)\n", + "= \\begin{bmatrix}\n", + " P(x) & ... \\\\\n", + " ... & ...\n", + "\\end{bmatrix}, \\tag{6}\n", + "$$\n", + "\n", + "where\n", + "\n", + "$$\n", + "R(x) = \\begin{bmatrix}\n", + " x & \\sqrt{1 - x^2} \\\\\n", + " \\sqrt{1 - x^2} & -x\n", + "\\end{bmatrix}. \\tag{7}\n", + "$$\n", + "\n", + "In PaddleQuantum, we can compute such $\\Phi$ by function `reflection_based_quantum_signal_processing`." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[15.70796327 -0.17619159 -2.01393416 -0.47321657 -2.66837608 -1.12765849\n", + " -2.96540107]\n" + ] + } + ], + "source": [ + "print(reflection_based_quantum_signal_processing(P))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> Note: $R_{-\\Phi}(x)$ is a block encoding of $P^*(x)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Quantum Singular Value Transformation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To start with, we need to define the singular value transformation. \n", + "\n", + "Let $\\mathcal{X} \\in \\mathbb{C}^{2^m \\times 2^m}$ be a matrix. Suppose there exists a unitary $U$ and two orthogonal projectors $W$ and $V$ such that $\\mathcal{X} = W U V$. Then there exists two orthonormal basis $\\{|\\omega_j\\rangle\\}_{j=1}^{2^m}$ and $\\{|\\nu_j\\rangle\\}_{j=1}^{2^m}$ extended by eigenstates of $W$ and $V$ respectively, such that\n", + "\n", + "$$\n", + "\\mathcal{X} = \\sum_{j=1}^{2^m} \\lambda_j |\\omega_j\\rangle \\langle\\nu_j|, \\tag{8}\n", + "$$\n", + "\n", + "where \n", + "\n", + "$$\n", + "\\lambda_j = \\begin{cases}\n", + "\\langle\\omega_j| U |\\nu_j\\rangle \\text{ if } j \\leq \\text{rank}(\\mathcal{X}) \\\\\n", + "0 \\text{ otherwise }\n", + "\\end{cases} \\tag{9}\n", + "$$\n", + "\n", + "is a singular value of $\\mathcal{X}$. The **singular value transformation** (SVT) of $\\mathcal{X}$ for a function $f$ is defined to be\n", + "\n", + "$$\n", + "f^{(SV)}(\\mathcal{X}) := \\sum_{j=1}^{2^m} f(\\lambda_j)\n", + "\\begin{cases}\n", + " |\\nu_j\\rangle\\langle\\nu_j| & \\text{ if } f \\text{ is even} \\\\\n", + " |\\omega_j\\rangle\\langle\\nu_j| & \\text{ if } f \\text{ is odd}\n", + "\\end{cases}. \\tag{10}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Decomposition and QSP" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "Paper [[2]]((https://doi.org/10.1145/3313276.3316366)) proves that under **appropriate** choices of basis, $U, V$ and $W$ can be decomposed by subspaces of $\\lambda_j$ such that\n", + "\n", + "$$\n", + "\\begin{split}\n", + "U & = \\bigoplus_{j: \\lambda_j \\in [0, 1)} R(\\lambda_j) \\oplus\n", + " \\bigoplus_{j: \\lambda_j = 1} [1] \\oplus\n", + " \\bigoplus_{j: V|\\nu_j\\rangle = |\\nu_j\\rangle,\\, WU|\\nu_j\\rangle = 0} [1] \\oplus\n", + " \\bigoplus_{j: W|\\omega_j\\rangle = |\\omega_j\\rangle,\\, VU^\\dagger|\\omega_j\\rangle = 0} [1] \\oplus \n", + " \\bigoplus_{j: V|\\nu_j\\rangle = W|\\omega_j\\rangle = 0} [...], \\\\\n", + "V & = \\bigoplus_{j: \\lambda_j \\in [0, 1)} |0\\rangle\\langle0| \\oplus\n", + " \\bigoplus_{j: \\lambda_j = 1} [1] \\oplus\n", + " \\bigoplus_{j: V|\\nu_j\\rangle = |\\nu_j\\rangle,\\, WU|\\nu_j\\rangle = 0} [1] \\oplus\n", + " \\bigoplus_{j: W|\\omega_j\\rangle = |\\omega_j\\rangle,\\, VU^\\dagger|\\omega_j\\rangle = 0} [0] \\oplus \n", + " \\bigoplus_{j: V|\\nu_j\\rangle = W|\\omega_j\\rangle = 0} [0] \\text{ and}\\\\\n", + "W & = \\bigoplus_{j: \\lambda_j \\in [0, 1)} |0\\rangle\\langle0| \\oplus\n", + " \\bigoplus_{j: \\lambda_j = 1} [1] \\oplus\n", + " \\bigoplus_{j: V|\\nu_j\\rangle = |\\nu_j\\rangle,\\, WU|\\nu_j\\rangle = 0} [0] \\oplus\n", + " \\bigoplus_{j: W|\\omega_j\\rangle = |\\omega_j\\rangle,\\, VU^\\dagger|\\omega_j\\rangle = 0} [1] \\oplus \n", + " \\bigoplus_{j: V|\\nu_j\\rangle = W|\\omega_j\\rangle = 0} [0] .\\\\\n", + "\\end{split} \\tag{11}\n", + "$$\n", + "\n", + "We can decompose $f^{(SV)}$ in a similar manner.\n", + "\n", + "$$\n", + "f^{(SV)}(\\mathcal{X}) = \\sum_{j: \\lambda_j \\in [0, 1)} f(\\lambda_j)\\,... + \\sum_{j: \\lambda_j = 1} f(1)\\,... + \\sum_{j: \\lambda_j = 0, V |\\nu_j\\rangle \\neq 0} f(0)\\,... + \\sum_{j: \\lambda_j = 0, W |\\omega_j\\rangle \\neq 0} f(0)\\,... + \\sum_{j: \\lambda_j = 0, \\text{otherwise}} f(0)\\,...\\,, \\tag{12}\n", + "$$\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's connect above decomposition with quantum signal processing. Suppose function $f$ is a polynomial $P \\in \\mathbb{C}[x]$ with degree $k$ that satisfies the requirements in the last section. \n", + "Then there exists $\\Phi \\in \\mathbb{R}^k$ such that $R_\\Phi$ is a block encoding of $P$. Moreover, $P$ satisfies $P(1) = e^{i \\sum_{j=1}^k \\phi_j}$ and $P(0) = e^{i \\sum_{j=1}^k (-1)^{j+1} \\phi_j}$.\n", + "Now Define $U_\\Phi$ as\n", + "\n", + "$$\n", + "U_\\Phi := \\begin{cases}\n", + "& \\prod_{j = 1}^{k / 2} e^{i\\phi_{2j - 1} (2V - I)} U^\\dagger e^{i\\phi_{2j} (2W - I)} U \\text{ if } k \\text{ is even} \\\\\n", + "e^{i\\phi_1 (2W - I)} U & \\prod_{j = 1}^{(k - 1) / 2} e^{i\\phi_{2j} (2V - I)} U^\\dagger e^{i\\phi_{2j+1} (2W - I)} U \\text{ if } k \\text{ is odd}\n", + "\\end{cases}. \\tag{13}\n", + "$$\n", + "\n", + "Then\n", + "\n", + "$$\n", + "U_\\Phi = \\begin{cases}\n", + " \\bigoplus R_\\Phi(\\lambda_j) \\oplus\n", + " \\bigoplus [e^{i \\sum_{j=1}^k \\phi_j}] \\oplus\n", + " \\bigoplus [e^{i \\sum_{j=1}^k (-1)^{j+1} \\phi_j}] \\oplus\n", + " \\bigoplus [e^{i \\sum_{j=1}^k (-1)^{j+1} \\phi_j}] \\oplus \n", + " \\bigoplus [...]\n", + "\\text{ if } k \\text{ is even} \\\\\n", + " \\bigoplus R_\\Phi(\\lambda_j) \\oplus\n", + " \\bigoplus [e^{i \\sum_{j=1}^k \\phi_j}] \\oplus\n", + " \\bigoplus [e^{i \\sum_{j=1}^k (-1)^{j+1} \\phi_j}] \\oplus\n", + " \\bigoplus [e^{i \\sum_{j=1}^k (-1)^{j+1} \\phi_j}] \\oplus \n", + " \\bigoplus [...]\n", + "\\text{ if } k \\text{ is odd}\n", + "\\end{cases} \\tag{14}\n", + "$$\n", + "\n", + "and hence\n", + "\n", + "$$\n", + "P^{(SV)}(\\mathcal{X}) = \\begin{cases} \n", + " \\bigoplus \\begin{bmatrix}\n", + " P(\\lambda_j) & 0 \\\\\n", + " 0 & 0 \\\\\n", + " \\end{bmatrix} \\oplus\n", + " \\bigoplus [P(1)] \\oplus\n", + " \\bigoplus [P(0)] \\oplus\n", + " \\bigoplus [0] \\oplus \n", + " \\bigoplus [0] = V U_\\Phi V\n", + "\\text{ if } k \\text{ is even} \\\\ \n", + " \\bigoplus \\begin{bmatrix}\n", + " P(\\lambda_j) & 0 \\\\\n", + " 0 & 0 \\\\\n", + " \\end{bmatrix} \\oplus\n", + " \\bigoplus [P(1)] \\oplus\n", + " \\bigoplus [0] \\oplus\n", + " \\bigoplus [0] \\oplus \n", + " \\bigoplus [0] = W U_\\Phi V \n", + "\\text{ if } k \\text{ is odd}\n", + "\\end{cases}. \\tag{15}\n", + "$$\n", + "\n", + "If we can realize $V U_\\Phi V$ or $W U_\\Phi V$ by a quantum algorithm, then such transformation is the quantum singular value transformation of $\\mathcal{X}$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### QSVT and Block Encoding" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Suppose $U$ is a block encoding unitary of $A$ with respect to orthogonal projectors $W$ and $V$. Then $\\mathcal{X} = A \\oplus 0I^{\\otimes (m - n)}$ and $P^{(SV)}(\\mathcal{X}) = P^{(SV)}(A) \\oplus 0I^{\\otimes (m - n)}$. Therefore, $U_\\Phi$ is a block encoding of $P^{(SV)}(A)$.\n", + "\n", + "When $W = V$, $\\{|\\omega_j\\rangle \\}_{j=1}^{2^m} = \\{ |\\nu_j\\rangle \\}_{j=1}^{2^m}$. Denote $P(x) := \\sum_{i=0}^k c_i x^i$. We can find that \n", + "\n", + "$$\n", + "P^{(SV)}(\\mathcal{X}) = \\sum_{j=1}^{2^m} P(\\lambda_j) |\\nu_j\\rangle \\langle\\nu_j| = P(X) = P(A) \\oplus 0I^{\\otimes (m - n)}. \\tag{16}\n", + "$$ \n", + "\n", + "That is, when $W = V$, QSVT maps a block encoding of $A$ to a block encoding of $P(A)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Realization in PaddleQuantum" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "PaddleQuantum has a built-in class ``QSVT`` for quantum singular value transformation when $W = V = |0\\rangle^{\\otimes (m - n)}\\langle0|^{\\otimes (m - n)} \\otimes I^{\\otimes n}$. We can call its member function ``QSVT.block_encoding_matrix()`` to verify the correctness of above theories, from entries to entries and from eigenvalues to eigenvalues." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\Users\\v_zhanglei48\\Desktop\\QSVT 教程\\qsp.py:242: ComplexWarning: Casting complex values to real discards the imaginary part\n", + " Phi[i] = np.log(P.coef[n] / Q.coef[m]) * -1j / 2\n", + "c:\\Users\\v_zhanglei48\\Desktop\\QSVT 教程\\qsp.py:256: ComplexWarning: Casting complex values to real discards the imaginary part\n", + " Phi[0] = -1j * np.log(P.coef[0])\n" + ] + } + ], + "source": [ + "qsvt = QSVT(P, U, num_block_qubits)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\Users\\v_zhanglei48\\Anaconda3\\envs\\personal-code\\lib\\site-packages\\paddle\\fluid\\dygraph\\math_op_patch.py:276: UserWarning: The dtype of left and right variables are not the same, left dtype is paddle.float64, but right dtype is paddle.float32, the right dtype will convert to paddle.float64\n", + " warnings.warn(\n" + ] + } + ], + "source": [ + "# find P(A) and its expected eigenvalues, note that they are computed in different ways\n", + "expect_PX = poly_matrix(P, A).numpy()\n", + "expect_eig_result = np.sort(list(map(lambda x: P(x), np.linalg.eigvals(A.numpy()))))\n", + "\n", + "# Calculate U_\\Phi and extract eigenvalues of block encoded matrix\n", + "U_Phi = qsvt.block_encoding_matrix().numpy()\n", + "actual_PX = U_Phi[0:2 ** num_block_qubits, 0:2 ** num_block_qubits]\n", + "actual_eig_result = np.sort(np.linalg.eigvals(actual_PX))" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "error of simulating P(X)\n", + " maximum absolute, 1.1376122748011288e-07\n", + " percentage, 6.770728859410114e-07\n" + ] + } + ], + "source": [ + "print(\"error of simulating P(X)\")\n", + "print(\" maximum absolute, \", np.amax(abs(expect_PX - actual_PX)))\n", + "print(\" percentage, \", np.linalg.norm(expect_PX - actual_PX) / np.linalg.norm(expect_PX))" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "error of eigenvalues of simulating P(X)\n", + " maximum absolute, 1.113571654238586e-07\n", + " percentage, 6.064903308479878e-07\n" + ] + } + ], + "source": [ + "print(\"error of eigenvalues of simulating P(X)\")\n", + "print(\" maximum absolute, \", np.amax(abs(expect_eig_result - actual_eig_result)))\n", + "print(\" percentage, \", np.linalg.norm(expect_eig_result - actual_eig_result) / np.linalg.norm(expect_eig_result))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Qubitization: Quantum Realization of $U_\\Phi$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "One question is, how to realize $U_\\Phi$ by a quantum circuit? Note that we assume $U$ is implementable. Then the remaining problem is the realization of unitaries $e^{i\\phi (2W - I)}$ and $e^{i\\phi (2V - I)}$, which are essentially $R_Z(-2\\phi)$ operators performed in projection spaces of $W$ and $V$ respectively.\n", + "\n", + "To achieve this, Lemma 10 in paper [[1]](https://quantum-journal.org/papers/q-2019-07-12-163/) projects the image space of projector into an ancilla qubit and perform the rotation operation on that qubit. Then by entanglement such rotation is simultaneously applied to the main register. \n", + "\n", + "The results are summarized to the following circuit:\n", + "\n", + "![U_Phi](figures/QSVT-fig-U_Phi.png \"Figure 2: Quantum Circuit for QSVT, where k is the degree of P\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Realization in PaddleQuantum" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "When $W = V = |0\\rangle^{\\otimes (m - n)}\\langle0|^{\\otimes (m - n)} \\otimes I^{\\otimes n}$, PaddleQuantum can create a circuit in Figure 2 by member function ``QSVT.block_encoding_circuit``. We can show that the constructed quantum circuit can simulate $U_\\Phi$, by comparison of the output state and $(U_\\Phi \\otimes I)|\\psi\\rangle|0\\rangle$ for $|\\psi\\rangle \\in \\mathbb{C}^{2^m}$." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "# set state vector backend\n", + "paddle_quantum.set_backend('state_vector')\n", + "\n", + "# arbitrary state such that last qubit is in state |0\\rangle\n", + "ket_0 = [1.0, 0.0]\n", + "psi = np.array([np.random.rand() + 1j * np.random.rand() for _ in range(2 ** num_qubits)])\n", + "psi = np.kron(psi / np.linalg.norm(psi), ket_0)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "the different between expected and actual state is 1.6115462390639646e-07\n" + ] + } + ], + "source": [ + "expect_state = np.kron(U_Phi, np.eye(2)) @ psi\n", + "\n", + "cir = qsvt.block_encoding_circuit()\n", + "actual_state = cir(paddle_quantum.State(psi, dtype='complex64')).data.numpy()\n", + "\n", + "print(f\"the different between expected and actual state is {np.linalg.norm(expect_state - actual_state)}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> Note: if we add Hadamard gates at the beginning and the end of the ancilla register in Figure 2, then the quantum circuit turns to simulate the real part of the polynomial. Combined with the technique of linear combination of unitaries, theocratically we can simulate all complex polynomials with norm smaller than 1 using quantum circuit." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Application: Amplitude Amplification" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let $|\\psi\\rangle$ be a $m$-qubit quantum state such that $|\\psi\\rangle = \\sin(\\theta)|\\psi_{\\text{good}}\\rangle + \\cos(\\theta) |\\psi_{\\text{bad}}\\rangle$. **Amplitude amplification** is a quantum algorithm that can increase the amplitude of $|\\psi_{\\text{good}}\\rangle$ to approximately 1.\n", + "This algorithm can be viewed in a different prospective. Let $U$ be the unitary and $W, V$ be two orthogonal projectors such that $|\\psi\\rangle = U V |0\\rangle^{\\otimes m}$ and $\\sin(\\theta)|\\psi_{\\text{good}}\\rangle = W |\\psi\\rangle$, implying $\\sin(\\theta)|\\psi_{\\text{good}}\\rangle = W U V |0\\rangle^{\\otimes m}$. Therefore, amplitude amplification is essentially a singular value transformation of $\\mathcal{X} = W U V$. In this section we will show how to use QSVT to do fixed-point (i.e. $\\theta = \\frac{\\pi}{2k}$ for some $k \\in \\mathbb{Z}$) amplitude amplification from Theorem 28 of paper [[2]]((https://doi.org/10.1145/3313276.3316366)). " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Suppose $\\sin(\\theta)|\\psi_{\\text{bad}}\\rangle = (|\\psi_{\\text{good}}\\rangle^{\\otimes (m - n)}\\langle0|^{\\otimes (m - n)} \\otimes I^{\\otimes n}) |\\psi\\rangle$ and $\\theta = \\frac{\\pi}{2k}$ for some $k \\in \\mathbb{Z}$. Then $W = V = |0\\rangle^{\\otimes (m - n)}\\langle0|^{\\otimes (m - n)} \\otimes I^{\\otimes n}$ and $\\mathcal{X} = A \\oplus 0I^{\\otimes (m - n)}$, where $A$ is the left-upper block of $U$. \n", + "\n", + "Observe that $\\mathcal{X} |\\psi_{\\text{good}\\rangle^{\\otimes m} = \\sin(\\frac{\\pi}{2k}) |\\psi\\rangle}$. Therefore, we aim to find a quantum circuit with unitary $U_\\Phi$ such that $W U_\\Phi V = \\frac{1}{\\sin(\\frac{\\pi}{2k})} \\mathcal{X}$, implying $W U_\\Phi V |\\psi_{\\text{good}}\\rangle^{\\otimes m} = |0\\rangle$. By $\\mathcal{X} = W U V$, the absolute values of singular values of $\\mathcal{X}$ are $\\sin(\\frac{\\pi}{2k})$. Moreover, choose $P(x) = (-1)^k T_k (x)$, where $T_k$ is the Chebyshev polynomial of the first kind of order $k$. Then\n", + "\n", + "$$\n", + "P(\\sin(\\frac{\\pi}{2k})) = (-1)^k T_k (\\sin(\\frac{\\pi}{2k})) = (-1)^k \\cos(\\frac{k - 1}{2}\\pi) = 1, \\tag{17}\n", + "$$\n", + "\n", + "implies that the QSVT of $\\mathcal{X}$ in terms of polynomial $P$ is a block encoding of $B := \\frac{1}{\\sin(\\frac{\\pi}{2k})} A$, as required." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Realization in PaddleQuantum" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We set $k = 3$ so that $\\sin(\\frac{\\pi}{2k}) = \\frac{1}{2}$, and $U$ is a randomly chosen unitary. We want to show that the left-upper block of $U$ is amplified by 2 after QSVT." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "P = -1 * Chebyshev([0 for _ in range(3)] + [1]).convert(kind=Polynomial)\n", + "U = unitary_random_with_hermitian_block(num_qubits, is_unitary=True)\n", + "A = U[0:2 ** num_block_qubits, 0:2 ** num_block_qubits].numpy()\n", + "\n", + "amplifier = QSVT(P, U, num_block_qubits)\n", + "U_Phi = amplifier.block_encoding_unitary()" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "the accuracy of quantum singular value transformation is 2.805196857025294e-07\n" + ] + } + ], + "source": [ + "B = U_Phi[0:2 ** num_block_qubits, 0:2 ** num_block_qubits].numpy()\n", + "print(f\"the accuracy of quantum singular value transformation is {np.linalg.norm(B - 2 * A)}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As shown above, we successfully amplify $\\mathcal{X}$ by 2." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## References\n", + "\n", + "[1] Low, Guang Hao, and Isaac L. Chuang. \"Hamiltonian simulation by qubitization.\" [Quantum 3 (2019): 163.](https://doi.org/10.22331/q-2019-07-12-163)\n", + "\n", + "[2] Gilyén, András, et al. \"Quantum singular value transformation and beyond: exponential improvements for quantum matrix arithmetics.\" [Proceedings of the 51st Annual ACM SIGACT Symposium on Theory of Computing. 2019.](https://doi.org/10.1145/3313276.3316366)\n", + "\n", + "[3] Camps, Daan, et al. \"Explicit Quantum Circuits for Block Encodings of Certain Sparse Matrices.\" [arXiv preprint arXiv:2203.10236 (2022).](http://arxiv.org/abs/2203.10236)\n", + "\n", + "[4] Clader, B. David, et al. \"Quantum Resources Required to Block-Encode a Matrix of Classical Data.\" [arXiv preprint arXiv:2206.03505 (2022).](http://arxiv.org/abs/2206.03505)\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.8.13 ('pq')", + "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.8.13" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "1e82098cfee7be27b5e385e3f85fe91d734d6114f7d09dccafdaad2c23171c3e" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tutorials/quantum_simulation/SSVQE_CN.ipynb b/tutorials/quantum_simulation/SSVQE_CN.ipynb index 2718db2..c295a53 100644 --- a/tutorials/quantum_simulation/SSVQE_CN.ipynb +++ b/tutorials/quantum_simulation/SSVQE_CN.ipynb @@ -196,7 +196,7 @@ "metadata": {}, "source": [ "## 配置训练模型——模型参数\n", - "在进行量子神经网络的训练之前,我们还需要进行一些训练的超参数设置,主要是学习速率(learning rate, LR)、迭代次数(iteration, ITR。这里我们设定学习速率为 0.3,迭代次数为 50 次。读者不妨自行调整来直观感受下超参数调整对训练效果的影响。" + "在进行量子神经网络的训练之前,我们还需要进行一些训练的超参数设置,主要是学习速率(learning rate, LR)、迭代次数(iteration, ITR。这里我们设定学习速率为 0.3,迭代次数为 100 次。读者不妨自行调整来直观感受下超参数调整对训练效果的影响。" ] }, { diff --git a/tutorials/quantum_simulation/SSVQE_EN.ipynb b/tutorials/quantum_simulation/SSVQE_EN.ipynb index eb8823c..7609f73 100644 --- a/tutorials/quantum_simulation/SSVQE_EN.ipynb +++ b/tutorials/quantum_simulation/SSVQE_EN.ipynb @@ -17,7 +17,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 1, "metadata": { "ExecuteTime": { "end_time": "2021-04-30T09:12:27.747028Z", @@ -53,7 +53,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 2, "metadata": { "ExecuteTime": { "end_time": "2021-04-30T09:12:27.773417Z", @@ -197,7 +197,7 @@ "source": [ "## Hyper-parameters\n", "\n", - "Before training the quantum neural network, we also need to set up several hyper-parameters, mainly the learning rate LR, the number of iterations ITR. Here we set the learning rate to be LR = 0.3 and the number of iterations ITR = 50. One can adjust these hyper-parameters accordingly and check how they influence the training performance." + "Before training the quantum neural network, we also need to set up several hyper-parameters, mainly the learning rate LR, the number of iterations ITR. Here we set the learning rate to be LR = 0.3 and the number of iterations ITR = 100. One can adjust these hyper-parameters accordingly and check how they influence the training performance." ] }, { diff --git a/tutorials/quantum_simulation/VQE_CN.ipynb b/tutorials/quantum_simulation/VQE_CN.ipynb index 1e6c09d..349e137 100644 --- a/tutorials/quantum_simulation/VQE_CN.ipynb +++ b/tutorials/quantum_simulation/VQE_CN.ipynb @@ -311,7 +311,7 @@ "source": [ "### 配置训练模型 - 模型参数\n", "\n", - "在进行量子神经网络的训练之前,我们还需要进行一些训练的超参数设置,主要是学习速率(LR, learning rate)、迭代次数(ITR, iteration)和量子神经网络计算模块的深度(D, Depth)。这里我们设定学习速率为 0.5, 迭代次数为 50 次。读者不妨自行调整来直观感受下超参数调整对训练效果的影响。" + "在进行量子神经网络的训练之前,我们还需要进行一些训练的超参数设置,主要是学习速率(LR, learning rate)、迭代次数(ITR, iteration)和量子神经网络计算模块的深度(D, Depth)。这里我们设定学习速率为 0.4, 迭代次数为 80 次。读者不妨自行调整来直观感受下超参数调整对训练效果的影响。" ] }, { diff --git a/tutorials/quantum_simulation/figures/QSVT-fig-U_Phi.png b/tutorials/quantum_simulation/figures/QSVT-fig-U_Phi.png new file mode 100644 index 0000000..efdaa20 Binary files /dev/null and b/tutorials/quantum_simulation/figures/QSVT-fig-U_Phi.png differ diff --git a/tutorials/quantum_simulation/figures/QSVT-fig-block-decoding.png b/tutorials/quantum_simulation/figures/QSVT-fig-block-decoding.png new file mode 100644 index 0000000..e49382a Binary files /dev/null and b/tutorials/quantum_simulation/figures/QSVT-fig-block-decoding.png differ