diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0e4142e8..ca5b9c34 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,14 +7,15 @@ repos: - id: check-yaml - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.14 + rev: v0.15.1 hooks: - id: ruff-check args: [ --fix ] - id: ruff-format - repo: https://github.com/mwouts/jupytext - rev: v1.19.0 + rev: v1.19.1 hooks: - id: jupytext args: [--sync] + files: ^(examples/|tutorials/) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..afd336ee --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,112 @@ +This is a comprehensive, professional `CONTRIBUTING.md` tailored for your project. +It covers the specific tools you've configured and adds industry-standard best practices for a seamless open-source contribution experience. + +--- + +# Contributing to DeepQuantum + +First off, thank you for considering contributing to DeepQuantum! +It’s people like you who make the open-source community such an amazing place to learn, inspire, and create. + +To maintain high code quality and a consistent developer experience, we have established a modern workflow using **Ruff**, **Jupytext**, and **pre-commit hooks**. +Please follow these guidelines to get started. + +--- + +## Setup Development Environment + +Before you start coding, ensure you have the development dependencies installed. +We recommend using a virtual environment (Conda or venv). + +1. **Install Dependencies**: + ```bash + pip install -r requirements-dev.txt + ``` + +2. **Install pre-commit hooks**: + We use `pre-commit` to automate code linting and formatting. + This ensures your code is compliant before every commit. + ```bash + pre-commit install + ``` + +3. **Initialize Environment**: + It is a good practice to run the hooks against all files once to ensure your local environment is in sync: + ```bash + pre-commit run --all-files + ``` + +--- + +## Coding Standards + +We use **Ruff** as our primary linter and formatter. +Our configuration includes: +- **Line Length**: 120 characters. +- **Quote Style**: Single quotes (`'`). +- **Imports**: Sorted automatically (isort rules). + +### Linting Rules +We enforce a strict set of rules, including Python upgrade suggestions (`UP`), naming conventions (`N`), and code simplifications (`SIM`). +- **In the `src/` directory**: We additionally enforce **Google-style docstrings**. +Please ensure your functions and classes are well-documented. + +### IDE Integration +We recommend installing the **Ruff extension** for VS Code or your preferred IDE. +Enable **Format on Save** to handle most styling issues automatically. + +--- + +## Working with Notebooks (Jupytext) + +Our tutorials and examples are managed using **Jupytext**. +This allows us to maintain Jupyter Notebooks (`.ipynb`) while tracking version-control-friendly Python scripts (`.py:percent`). + +### The Rule of Thumb: +**Maintain the `.ipynb` files only.** +The paired `.py` files are automatically generated or updated via pre-commit hooks. + +### IDE Integration: +We recommend installing the **Jupytext extension** for VS Code or your preferred IDE to sync files automatically. + +> [!IMPORTANT] +> **Version Consistency**: The VS Code Jupytext extension often points to a global path rather than your active Conda environment. +Please ensure that the Jupytext version used by your IDE matches the one in `requirements-dev.txt` to avoid formatting discrepancies. + +--- + +## Maintenance & Updates + +To keep the development tools up to date with the latest security fixes and features, please occasionally update the pre-commit hook versions: + +```bash +pre-commit autoupdate +``` + +--- + +## Pull Request Process + +1. **Create a Branch**: Use a descriptive name like `feat/new-model` or `fix/issue-123`. +2. **Commit Your Changes**: + - If `pre-commit` fails during commit, it will often automatically fix the issues. + - Simply **re-add (`git add`)** the fixed files and commit again. +3. **Reference Issues**: In your PR description, use keywords like `Closes #123` to link the PR to an issue. +4. **Clean History**: We prefer a clean commit history. +Please consider squashing your commits or rebasing on `main` before the final review. + +--- + +## Documentation Guidelines + +For source code, we follow the **Google Docstring Convention**. +- **`D102` / `D105` / `D107`**: We currently ignore missing docstrings in public methods and magic methods to reduce boilerplate, but we highly encourage documenting any complex logic. + +--- + +## Need Help? + +If you have questions about the setup or a specific feature, feel free to: +- Open an **Issue** on GitHub. + +Thank you for contributing to the future of quantum computing! diff --git a/README.md b/README.md index 93b0d64c..0bd4cdd1 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ ![DeepQuantum logo](docs/source/_static/assets/logo_light_v1.png) -[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![docs](https://img.shields.io/badge/docs-link-blue.svg)](https://dqapi.turingq.com/) [![PyPI](https://img.shields.io/pypi/v/deepquantum.svg?logo=pypi)](https://pypi.org/project/deepquantum/) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/deepquantum) @@ -87,6 +86,7 @@ Below are some minimal examples to help you get started. ```python import deepquantum as dq + cir = dq.QubitCircuit(2) cir.h(0) cir.cnot(0, 1) @@ -113,10 +113,10 @@ print(cir.expectation()) - Photonic quantum circuit with the Fock backend, based on Fock basis state ```python -cir = dq.QumodeCircuit(2, [1,1]) -cir.dc([0,1]) +cir = dq.QumodeCircuit(2, [1, 1]) +cir.dc([0, 1]) cir.ps(0, 0.1) -cir.bs([0,1], [0.2,0.3]) +cir.bs([0, 1], [0.2, 0.3]) print(cir()) print(cir.measure()) ``` @@ -124,10 +124,10 @@ print(cir.measure()) - Photonic quantum circuit with the Fock backend, based on Fock state tensor ```python -cir = dq.QumodeCircuit(2, [(1, [1,1])], basis=False) -cir.dc([0,1]) +cir = dq.QumodeCircuit(2, [(1, [1, 1])], basis=False) +cir.dc([0, 1]) cir.ps(0, 0.1) -cir.bs([0,1], [0.2,0.3]) +cir.bs([0, 1], [0.2, 0.3]) print(cir()) print(cir.measure()) ``` @@ -138,7 +138,7 @@ print(cir.measure()) cir = dq.QumodeCircuit(2, 'vac', cutoff=10, backend='gaussian') cir.s(0, 0.1) cir.d(1, 0.1) -cir.bs([0,1], [0.2,0.3]) +cir.bs([0, 1], [0.2, 0.3]) print(cir()) print(cir.measure()) print(cir.photon_number_mean_var(wires=0)) @@ -151,7 +151,7 @@ print(cir.measure_homodyne(wires=1)) cir = dq.QumodeCircuit(2, 'vac', backend='bosonic') cir.cat(0, 0.5, 0.0) cir.gkp(1, 0.5, 0.5) -cir.bs([0,1], [0.2,0.3]) +cir.bs([0, 1], [0.2, 0.3]) print(cir()) print(cir.photon_number_mean_var(wires=0)) print(cir.measure_homodyne(wires=1)) @@ -167,7 +167,7 @@ pattern.e(1, 2) pattern.m(1) pattern.x(2, domain=1) # CNOT -pattern.n([3,4]) +pattern.n([3, 4]) pattern.e(2, 3) pattern.e(0, 3) pattern.e(3, 4) @@ -196,10 +196,11 @@ print(cir() / pattern().full_state) ```python import torch + # OMP_NUM_THREADS=2 torchrun --nproc_per_node=4 main.py -backend = 'gloo' # for CPU +backend = 'gloo' # for CPU # torchrun --nproc_per_node=4 main.py -backend = 'nccl' # for GPU +backend = 'nccl' # for GPU rank, world_size, local_rank = dq.setup_distributed(backend) if backend == 'nccl': device = f'cuda:{local_rank}' @@ -229,9 +230,9 @@ dq.cleanup_distributed() ```python # OMP_NUM_THREADS=2 torchrun --nproc_per_node=4 main.py -backend = 'gloo' # for CPU +backend = 'gloo' # for CPU # torchrun --nproc_per_node=4 main.py -backend = 'nccl' # for GPU +backend = 'nccl' # for GPU rank, world_size, local_rank = dq.setup_distributed(backend) nmode = 4 cutoff = 4 @@ -252,6 +253,22 @@ if rank == 0: dq.cleanup_distributed() ``` +# Contributing + +We welcome contributions from the community! +To maintain high code quality and consistent style, we use a modern development workflow. + +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) +[![Jupytext](https://img.shields.io/badge/jupytext-enabled-blue)](https://github.com/mwouts/jupytext) +[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit) + +Please refer to our **[CONTRIBUTING.md](CONTRIBUTING.md)** for detailed instructions on: +- **Linting & Formatting**: Our coding standards using Ruff. +- **Notebook Management**: How we sync `.ipynb` and `.py` files using Jupytext. +- **Pull Request Process**: How to link issues and submit your changes. + +Before your first commit, remember to run `pre-commit install` in your local environment. + # Citation If you use DeepQuantum in your research, please cite [our paper](https://arxiv.org/abs/2512.18995): diff --git a/docs/source/conf.py b/docs/source/conf.py index 193a76b9..a516b2c1 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -14,7 +14,7 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information project = 'DeepQuantum' -copyright = '2026, TuringQ' +copyright = '2026, TuringQ' # noqa: A001 author = 'TuringQ' release = f'{dq.__version__}' @@ -22,12 +22,12 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = [ - 'sphinx.ext.autodoc', # 自动生成 API 文档 - 'sphinx.ext.napoleon', # 支持 NumPy/Google 风格注释 - 'sphinx.ext.viewcode', # 增加“查看源码”链接 - 'sphinx.ext.mathjax', # 数学公式渲染 - 'sphinx.ext.autosummary', # 自动生成模块摘要 - 'myst_nb', # 核心:解析 Notebook 和 MyST Markdown + 'sphinx.ext.autodoc', # 自动生成 API 文档 + 'sphinx.ext.napoleon', # 支持 NumPy/Google 风格注释 + 'sphinx.ext.viewcode', # 增加“查看源码”链接 + 'sphinx.ext.mathjax', # 数学公式渲染 + 'sphinx.ext.autosummary', # 自动生成模块摘要 + 'myst_nb', # 核心:解析 Notebook 和 MyST Markdown ] add_module_names = False @@ -62,10 +62,10 @@ html_theme_options = { # 启用右侧的“在 GitHub 上编辑/查看”按钮 - 'repository_url': 'https://github.com/TuringQ/deepquantum', # 改成你的仓库地址 - 'use_repository_button': True, # 开启 GitHub 仓库链接 - 'use_download_button': True, # 开启当前页面(ipynb)下载按钮 - 'use_fullscreen_button': True, # 开启全屏阅读按钮 + 'repository_url': 'https://github.com/TuringQ/deepquantum', # 改成你的仓库地址 + 'use_repository_button': True, # 开启 GitHub 仓库链接 + 'use_download_button': True, # 开启当前页面(ipynb)下载按钮 + 'use_fullscreen_button': True, # 开启全屏阅读按钮 # 左侧导航栏配置 'home_page_in_toc': False, 'show_navbar_depth': 3, diff --git a/docs/source/quick_start/basic_usage.md b/docs/source/quick_start/basic_usage.md index e394798c..8660c1c7 100644 --- a/docs/source/quick_start/basic_usage.md +++ b/docs/source/quick_start/basic_usage.md @@ -82,9 +82,9 @@ class MyCircuit(nn.Module): self.cir = self.circuit(nqubit) def circuit(self, nqubit): - cir = dq.QubitCircuit(nqubit) + cir = dq.QubitCircuit(nqubit) cir.hlayer() - # Using 'encode', specify where the variational parameters + # Using 'encode', specify where the variational parameters # are encoded into the quantum circuit cir.rylayer(encode=True) cir.cnot_ring() @@ -93,7 +93,7 @@ class MyCircuit(nn.Module): return cir def forward(self): - # During the forward process, variational parameters are + # During the forward process, variational parameters are # added to the quantum circuit as 'data' self.cir(data=self.params) return self.cir.expectation().mean() @@ -132,7 +132,7 @@ cir() # The bit string from left to right corresponds to the order of wires, which means # the first qubit is at the top, and the last qubit is at the bottom. print(cir.measure()) -# We can also set the sampling number, perform partial measurements, +# We can also set the sampling number, perform partial measurements, # and display ideal probabilities. print(cir.measure(shots=100, wires=[1,2], with_prob=True)) ``` @@ -144,13 +144,13 @@ When employing parameterized quantum circuits in variational quantum algorithms, ```python cir = dq.QubitCircuit(4) cir.xlayer([0,2]) -# Multiple observables can be added, and the results of +# Multiple observables can be added, and the results of # each expectation value will be automatically concatenated # Flexibly specify measurement wires and bases using a list-based combination # e.g., wires=[0,1,2]、basis='xyz' representing the observable whose # wires 0, 1, and 2 corresponds to Pauli-X, Pauli-Y, and Pauli-Z, respectively for i in range(4): - cir.observable(i) + cir.observable(i) cir() # Expectation value can be obtained after running the circuit print(cir.expectation()) ``` @@ -173,4 +173,4 @@ print(cir.post_select(measure_rst)) cir.draw() ``` -Note: defer_measure and post_select do not alter the final state state stored by QubitCircuit, making them incompatible with measure and expectation for conditional measurement. \ No newline at end of file +Note: defer_measure and post_select do not alter the final state state stored by QubitCircuit, making them incompatible with measure and expectation for conditional measurement. diff --git a/docs/source/quick_start/introduction.md b/docs/source/quick_start/introduction.md index fb908422..166ffaa1 100644 --- a/docs/source/quick_start/introduction.md +++ b/docs/source/quick_start/introduction.md @@ -4,4 +4,4 @@ Quantum computing is a rapidly growing field, poised to address the enormous com In the era of deep learning, important considerations for a quantum computing framework include its simulation scale, the ability to leverage GPU power for efficiency, the degree of integration with machine learning/deep learning libraries, and its user-friendliness and convenience. Influential quantum computing frameworks often aim to provide real quantum computing resources to users through cloud platforms. -DeepQuantum is a lightweight quantum programming framework based on PyTorch, designed for programming and simulating quantum computing, quantum neural networks, and hybrid quantum-classical algorithms. It naturally integrates with PyTorch, offering a programming style closely aligned with PyTorch's own. This makes it more accessible to developers with a computer science background and those familiar with or exposed to PyTorch, lowering the learning curve and easing the transition from machine learning (deep learning) to quantum machine learning (quantum deep learning). DeepQuantum is meticulously designed for convenient initialization of quantum neural networks and flexible data encoding. It also implements tensor network algorithms, supporting large-scale quantum circuit simulations based on matrix product states. \ No newline at end of file +DeepQuantum is a lightweight quantum programming framework based on PyTorch, designed for programming and simulating quantum computing, quantum neural networks, and hybrid quantum-classical algorithms. It naturally integrates with PyTorch, offering a programming style closely aligned with PyTorch's own. This makes it more accessible to developers with a computer science background and those familiar with or exposed to PyTorch, lowering the learning curve and easing the transition from machine learning (deep learning) to quantum machine learning (quantum deep learning). DeepQuantum is meticulously designed for convenient initialization of quantum neural networks and flexible data encoding. It also implements tensor network algorithms, supporting large-scale quantum circuit simulations based on matrix product states. diff --git a/docs/source/quick_start/introduction_cn.md b/docs/source/quick_start/introduction_cn.md index 0bd79cd5..04142455 100644 --- a/docs/source/quick_start/introduction_cn.md +++ b/docs/source/quick_start/introduction_cn.md @@ -4,4 +4,4 @@ 在深度学习时代,一个量子计算框架所能支持的模拟规模、是否能够利用GPU的算力来提升效率、与机器学习/深度学习库结合的紧密程度以及其易用性、便捷性都是重要的考量。 同时,具有影响力的量子计算框架往往也肩负着通过云平台为用户提供真实量子计算资源的使命。 -DeepQuantum是一款基于PyTorch的轻量级的量子编程框架,用于量子计算、量子神经网络和混合量子-经典算法的编程和模拟。因此可以很自然地与PyTorch做到无缝衔接,编程风格也十分接近PyTorch本身的用法。 对于计算机背景出身、熟悉或接触过PyTorch的开发者来说更加友好,学习门槛低,很容易上手,更加适合相关开发者从机器学习(深度学习)进入到量子机器学习(量子深度学习)。 经过精心的设计,DeepQuantum在初始化量子神经网络时更加便捷,进行数据编码时更加灵活。 同时,DeepQuantum中也实现了张量网络算法,支持基于矩阵乘积态的大规模量子线路模拟。 \ No newline at end of file +DeepQuantum是一款基于PyTorch的轻量级的量子编程框架,用于量子计算、量子神经网络和混合量子-经典算法的编程和模拟。因此可以很自然地与PyTorch做到无缝衔接,编程风格也十分接近PyTorch本身的用法。 对于计算机背景出身、熟悉或接触过PyTorch的开发者来说更加友好,学习门槛低,很容易上手,更加适合相关开发者从机器学习(深度学习)进入到量子机器学习(量子深度学习)。 经过精心的设计,DeepQuantum在初始化量子神经网络时更加便捷,进行数据编码时更加灵活。 同时,DeepQuantum中也实现了张量网络算法,支持基于矩阵乘积态的大规模量子线路模拟。 diff --git a/examples/basic_gate_MBQC.ipynb b/examples/basic_gate_MBQC.ipynb index 9e061f04..864fb7c6 100644 --- a/examples/basic_gate_MBQC.ipynb +++ b/examples/basic_gate_MBQC.ipynb @@ -728,7 +728,7 @@ "state, measure_rst, prob = cir.defer_measure(with_prob=True)\n", "# MBQC has an extra global phase: np.exp(-1j * alpha / 2)\n", "print(state * np.exp(1j * alpha / 2))\n", - "print(cir.post_select(measure_rst) * np.exp(1j * alpha / 2)) # choose measurement result\n", + "print(cir.post_select(measure_rst) * np.exp(1j * alpha / 2)) # choose measurement result\n", "\n", "cir.draw()" ] @@ -2386,7 +2386,7 @@ ], "source": [ "alpha = np.pi / 2\n", - "beta = np.pi / 3\n", + "beta = np.pi / 3\n", "gamma = np.pi / 4\n", "cir = dq.QubitCircuit(4)\n", "\n", @@ -2412,7 +2412,7 @@ "\n", "state, measure_rst, prob = cir.defer_measure(with_prob=True)\n", "print(state * np.exp(1j * (alpha + beta + gamma) / 2))\n", - "print(cir.post_select(measure_rst) * np.exp(1j * (alpha + beta + gamma) / 2)) # choose measurement result\n", + "print(cir.post_select(measure_rst) * np.exp(1j * (alpha + beta + gamma) / 2)) # choose measurement result\n", "\n", "cir.draw()" ] @@ -4042,7 +4042,7 @@ "# cir.x(2) # input |+>|-> state\n", "\n", "cir.hlayer()\n", - "cir.swap([1,2])\n", + "cir.swap([1, 2])\n", "cir.cz(0, 1)\n", "cir.cz(0, 2)\n", "cir.cz(0, 3)\n", @@ -4061,7 +4061,7 @@ "\n", "state, measure_rst, prob = cir.defer_measure(with_prob=True)\n", "print(state)\n", - "print(cir.post_select(measure_rst)) # choose measurement result\n", + "print(cir.post_select(measure_rst)) # choose measurement result\n", "\n", "cir.draw()" ] diff --git a/examples/basic_gate_MBQC.py b/examples/basic_gate_MBQC.py new file mode 100644 index 00000000..d6869b9d --- /dev/null +++ b/examples/basic_gate_MBQC.py @@ -0,0 +1,221 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.19.1 +# kernelspec: +# display_name: qdl +# language: python +# name: python3 +# --- + +# %% [markdown] +# # Basic gates in measurement-based quantum computation (MBQC) + +# %% +import deepquantum as dq +import numpy as np + +# %% [markdown] +# ## Single-qubit operation $HR_z(-\alpha)|+\rangle$ + +# %% [markdown] +# Matrix representation for $R_z(\alpha)$ and $|+>$ state +# $$ R_z(\alpha) = +# \begin{pmatrix} +# e^{-i\alpha/2} & 0 \\ +# 0 & e^{i\alpha/2} +# \end{pmatrix}\\ +# |+> = \frac{\sqrt{2}}{2} \begin{pmatrix} +# 1 \\ +# 1 \\ +# \end{pmatrix} +# $$ +# +# $$ R_z(\alpha)|+> = +# \frac{\sqrt{2}}{2} \begin{pmatrix} +# e^{-i\alpha/2} \\ +# e^{i\alpha/2} \\ +# \end{pmatrix} +# $$ +# +# The following circuits are equivalent +# +# ![MBQC example](./figure/basic_gate_MBQC/RZ.jpg) +#
图 1: MBQC实现单比特门 $HR_z(-\alpha)|+\rangle$ 操作
+ +# %% +# define the angle of Rz gate +alpha = np.pi / 3 +cir = dq.QubitCircuit(2) + +# prepare cluster state +cir.h(0) +cir.h(1) +cir.cz(0, 1) + +# |alpha> basis: apply phase shift and Hadamard gate +cir.p(0, -alpha) +cir.h(0) +cir.barrier() + +cir.x(1, controls=0, condition=True) + +cir() + +# check the output state +state, measure_rst, prob = cir.defer_measure(with_prob=True) +# MBQC has an extra global phase: np.exp(-1j * alpha / 2) +print(state * np.exp(1j * alpha / 2)) +print(cir.post_select(measure_rst) * np.exp(1j * alpha / 2)) # choose measurement result + +cir.draw() + +# %% [markdown] +# Verify single qubit operation $HR_z(-\alpha)|+\rangle$ in the circuit-based quantum computation (CBCQ) + +# %% +cir = dq.QubitCircuit(1) +# prepare state |+> +cir.h(0) + +# apply Rz and Hadamard gate +cir.rz(0, -alpha) +cir.h(0) + +print(cir()) +cir.draw() + +# %% [markdown] +# ## Random single-qubit-rotation gate + +# %% [markdown] +# 对于任意单比特门的实现,采用三个H-Rz gate级联的形式,最后根据测量结果加上对应的Pauli修正 +# +# ![MBQC example](./figure/basic_gate_MBQC/Single.jpg) +#
图 2: MBQC single gate 的实现
+ +# %% +alpha = np.pi / 2 +beta = np.pi / 3 +gamma = np.pi / 4 +cir = dq.QubitCircuit(4) + +# prepare cluster state +cir.hlayer() +cir.cz(0, 1) +cir.cz(1, 2) +cir.cz(2, 3) +cir.barrier() + +# measurement +cir.p(0, -alpha) +cir.h(0) +cir.p(1, -beta) +cir.h(1) +cir.p(2, -gamma) +cir.h(2) + +cir.x(3, controls=2, condition=True) +cir.z(3, controls=1, condition=True) +cir.x(3, controls=0, condition=True) +cir() + +state, measure_rst, prob = cir.defer_measure(with_prob=True) +print(state * np.exp(1j * (alpha + beta + gamma) / 2)) +print(cir.post_select(measure_rst) * np.exp(1j * (alpha + beta + gamma) / 2)) # choose measurement result + +cir.draw() + +# %% [markdown] +# Verify random single-qubit-rotation gate in circuit based quantum computation(CBQC) + +# %% +# verify +cir = dq.QubitCircuit(1) +cir.h(0) + +# alpha, beta, gamma系数的正负是根据测量结果决定的 +# q0的结果对应rx门的beta系数的正负 +# q1的结果对应第二个rz门的gamma系数的正负 +# alpha始终保持不变 + +cir.rz(0, -alpha) +cir.rx(0, beta * (-1) ** (int(measure_rst[0]) + 1)) +cir.rz(0, gamma * (-1) ** (int(measure_rst[1]) + 1)) +cir.h(0) + +print(cir()) +cir.draw() + +# %% [markdown] +# ## CNOT gate + +# %% [markdown] +# Matrix representation for CNOT gate +# $$ CNOT= +# \begin{pmatrix} +# 1 & 0 & 0 & 0 \\ +# 0 & 1 & 0 & 0 \\ +# 0 & 0 & 0 & 1 \\ +# 0 & 0 & 1 & 0 +# \end{pmatrix} +# = |0><0| \otimes I + |1><1| \otimes X +# $$ +# +# $$ +# CNOT|++>=|++> \\ +# CNOT|+->=|--> \\ +# CNOT|-+>=|-+> \\ +# CNOT|-->=|+-> +# $$ +# +# ![MBQC example](./figure/basic_gate_MBQC/CNOT.jpg) +#
图 3: MBQC CNOT 的实现
+ +# %% +cir = dq.QubitCircuit(4) + +# construct cluster state +# cir.x(1) # input |->|+> state +# cir.x(2) # input |+>|-> state + +cir.hlayer() +cir.swap([1, 2]) +cir.cz(0, 1) +cir.cz(0, 2) +cir.cz(0, 3) +cir.barrier() + +# measurement +cir.h(0) +cir.h(1) +cir.barrier() + +cir.x(3, controls=0, condition=True) +cir.z(2, controls=1, condition=True) +cir.z(3, controls=1, condition=True) + +cir() + +state, measure_rst, prob = cir.defer_measure(with_prob=True) +print(state) +print(cir.post_select(measure_rst)) # choose measurement result + +cir.draw() + +# %% [markdown] +# Verify CNOT gate in circuit-based quantum computation (CBQC) + +# %% +cir = dq.QubitCircuit(2) +# cir.x(0) +# cir.x(1) + +cir.hlayer() +cir.cx(0, 1) +print(cir()) +cir.draw() diff --git a/examples/benchmarks/benchmark_v420/MBQC_simulate/simulate_dq.py b/examples/benchmarks/benchmark_v420/MBQC_simulate/simulate_dq.py index 9b9ba4d2..cf6596b2 100644 --- a/examples/benchmarks/benchmark_v420/MBQC_simulate/simulate_dq.py +++ b/examples/benchmarks/benchmark_v420/MBQC_simulate/simulate_dq.py @@ -9,6 +9,7 @@ # Print version print(dq.__version__) + def benchmark(f, *args, trials=10): r = f(*args) time0 = time.time() @@ -20,8 +21,8 @@ def benchmark(f, *args, trials=10): return r, ts -def random_circuit_simulation(n_qubits, n_gates): +def random_circuit_simulation(n_qubits, n_gates): random.seed(10) cir = dq.QubitCircuit(n_qubits) @@ -32,45 +33,47 @@ def random_circuit_simulation(n_qubits, n_gates): lambda q: cir.x(q), lambda q: cir.rx(q, inputs=random.random() * 2 * torch.pi), lambda q: cir.ry(q, inputs=random.random() * 2 * torch.pi), - lambda q: cir.rz(q, inputs=random.random() * 2 * torch.pi) + lambda q: cir.rz(q, inputs=random.random() * 2 * torch.pi), ] for _ in range(n_gates): # 70% chance for single-qubit gate, 30% for CNOT if random.random() < 0.7: # Random single-qubit gate - qubit = random.randint(0, n_qubits-1) + qubit = random.randint(0, n_qubits - 1) random.choice(single_gates)(qubit) else: # Random CNOT - control = random.randint(0, n_qubits-1) - target = random.randint(0, n_qubits-1) + control = random.randint(0, n_qubits - 1) + target = random.randint(0, n_qubits - 1) # Ensure control and target are different while target == control: - target = random.randint(0, n_qubits-1) + target = random.randint(0, n_qubits - 1) cir.cnot(control=control, target=target) # Transpile circuit to measurement pattern pattern = cir.pattern() def simulation(): - state = pattern().full_state + return pattern().full_state + return benchmark(simulation) + results = {} platform = 'deepquantum' -n_list = [2, 5, 10, 20] -l_list = [5, 10, 100] +nqubits = [2, 5, 10, 20] +ngates = [5, 10, 100] -for n in tqdm(n_list): - for l in tqdm(l_list): - _, ts = random_circuit_simulation(n, l) - results[str(n) + '-' + str(l)] = ts +for n in tqdm(nqubits): + for ng in tqdm(ngates): + _, ts = random_circuit_simulation(n, ng) + results[str(n) + '-' + str(ng)] = ts -with open('simulation_mbqc_'+platform+'_results.data', 'w') as f: +with open('simulation_mbqc_' + platform + '_results.data', 'w') as f: json.dump(results, f) -with open('simulation_mbqc_'+platform+'_results.data', 'r') as f: +with open('simulation_mbqc_' + platform + '_results.data') as f: print(json.load(f)) diff --git a/examples/benchmarks/benchmark_v420/MBQC_simulate/simulate_graphix.py b/examples/benchmarks/benchmark_v420/MBQC_simulate/simulate_graphix.py index 5984ed34..5fbc3943 100644 --- a/examples/benchmarks/benchmark_v420/MBQC_simulate/simulate_graphix.py +++ b/examples/benchmarks/benchmark_v420/MBQC_simulate/simulate_graphix.py @@ -5,6 +5,7 @@ import graphix from tqdm import tqdm + def benchmark(f, *args, trials=10): r = f(*args) time0 = time.time() @@ -16,8 +17,8 @@ def benchmark(f, *args, trials=10): return r, ts -def random_circuit_simulation(n_qubits, n_gates): +def random_circuit_simulation(n_qubits, n_gates): random.seed(10) cir = graphix.Circuit(n_qubits) @@ -27,52 +28,52 @@ def random_circuit_simulation(n_qubits, n_gates): lambda q: cir.x(q), lambda q: cir.rx(q, angle=random.random() * 2), lambda q: cir.ry(q, angle=random.random() * 2), - lambda q: cir.rz(q, angle=random.random() * 2) + lambda q: cir.rz(q, angle=random.random() * 2), ] # Add random number of gates (3-10) - n_gates = n_gates - for _ in range(n_gates): # 70% chance for single-qubit gate, 20% for CNOT if random.random() < 0.7: # Random single-qubit gate - qubit = random.randint(0, n_qubits-1) + qubit = random.randint(0, n_qubits - 1) random.choice(single_gates)(qubit) else: # Random CNOT - control = random.randint(0, n_qubits-1) - target = random.randint(0, n_qubits-1) + control = random.randint(0, n_qubits - 1) + target = random.randint(0, n_qubits - 1) # Ensure control and target are different while target == control: - target = random.randint(0, n_qubits-1) + target = random.randint(0, n_qubits - 1) cir.cnot(control=control, target=target) # Transpile circuit to measurement pattern pattern = cir.transpile().pattern def simulate(): - state_mbqc = pattern.simulate_pattern(backend='statevector', input_state=graphix.states.BasicStates.ZERO) + return pattern.simulate_pattern(backend='statevector', input_state=graphix.states.BasicStates.ZERO) + # Transpile circuit to measurement pattern if n_qubits == 20 and n_gates == 100: return benchmark(simulate, trials=1) else: return benchmark(simulate) + results = {} platform = 'graphix' -n_list = [2, 5, 10, 20] -l_list = [5, 10, 100] +nqubits = [2, 5, 10, 20] +ngates = [5, 10, 100] -for n in tqdm(n_list): - for l in tqdm(l_list): - _, ts = random_circuit_simulation(n, l) - results[str(n) + '-' + str(l)] = ts +for n in tqdm(nqubits): + for ng in tqdm(ngates): + _, ts = random_circuit_simulation(n, ng) + results[str(n) + '-' + str(ng)] = ts -with open('simulate_mbqc_'+platform+'_results.data', 'w') as f: +with open('simulate_mbqc_' + platform + '_results.data', 'w') as f: json.dump(results, f) -with open('simulate_mbqc_'+platform+'_results.data', 'r') as f: +with open('simulate_mbqc_' + platform + '_results.data') as f: print(json.load(f)) diff --git a/examples/benchmarks/benchmark_v420/MBQC_transpile/transpile_dq.py b/examples/benchmarks/benchmark_v420/MBQC_transpile/transpile_dq.py index 83505caa..4366e4d6 100644 --- a/examples/benchmarks/benchmark_v420/MBQC_transpile/transpile_dq.py +++ b/examples/benchmarks/benchmark_v420/MBQC_transpile/transpile_dq.py @@ -9,6 +9,7 @@ # Print version print(dq.__version__) + def benchmark(f, *args, trials=100): r = f(*args) time0 = time.time() @@ -20,8 +21,8 @@ def benchmark(f, *args, trials=100): return r, ts -def random_circuit_transpilation(n_qubits, n_gates): +def random_circuit_transpilation(n_qubits, n_gates): random.seed(10) cir = dq.QubitCircuit(n_qubits) @@ -32,44 +33,45 @@ def random_circuit_transpilation(n_qubits, n_gates): lambda q: cir.x(q), lambda q: cir.rx(q, inputs=random.random() * 2 * torch.pi), lambda q: cir.ry(q, inputs=random.random() * 2 * torch.pi), - lambda q: cir.rz(q, inputs=random.random() * 2 * torch.pi) + lambda q: cir.rz(q, inputs=random.random() * 2 * torch.pi), ] for _ in range(n_gates): # 70% chance for single-qubit gate, 30% for CNOT if random.random() < 0.7: # Random single-qubit gate - qubit = random.randint(0, n_qubits-1) + qubit = random.randint(0, n_qubits - 1) random.choice(single_gates)(qubit) else: # Random CNOT - control = random.randint(0, n_qubits-1) - target = random.randint(0, n_qubits-1) + control = random.randint(0, n_qubits - 1) + target = random.randint(0, n_qubits - 1) # Ensure control and target are different while target == control: - target = random.randint(0, n_qubits-1) + target = random.randint(0, n_qubits - 1) cir.cnot(control=control, target=target) # Transpile circuit to measurement pattern def transpile(): - pattern = cir.pattern() + return cir.pattern() return benchmark(transpile) + results = {} platform = 'deepquantum' -n_list = [2, 5, 10, 20] -l_list = [5, 10, 100] +nqubits = [2, 5, 10, 20] +ngates = [5, 10, 100] -for n in tqdm(n_list): - for l in tqdm(l_list): - _, ts = random_circuit_transpilation(n, l) - results[str(n) + '-' + str(l)] = ts +for n in tqdm(nqubits): + for ng in tqdm(ngates): + _, ts = random_circuit_transpilation(n, ng) + results[str(n) + '-' + str(ng)] = ts -with open('transpile_mbqc_'+platform+'_results.data', 'w') as f: +with open('transpile_mbqc_' + platform + '_results.data', 'w') as f: json.dump(results, f) -with open('transpile_mbqc_'+platform+'_results.data', 'r') as f: +with open('transpile_mbqc_' + platform + '_results.data') as f: print(json.load(f)) diff --git a/examples/benchmarks/benchmark_v420/MBQC_transpile/transpile_graphix.py b/examples/benchmarks/benchmark_v420/MBQC_transpile/transpile_graphix.py index 94fb94bc..7fd72bf3 100644 --- a/examples/benchmarks/benchmark_v420/MBQC_transpile/transpile_graphix.py +++ b/examples/benchmarks/benchmark_v420/MBQC_transpile/transpile_graphix.py @@ -5,6 +5,7 @@ import graphix from tqdm import tqdm + def benchmark(f, *args, trials=100): r = f(*args) time0 = time.time() @@ -16,8 +17,8 @@ def benchmark(f, *args, trials=100): return r, ts -def random_circuit_transpilation(n_qubits, n_gates): +def random_circuit_transpilation(n_qubits, n_gates): random.seed(10) cir = graphix.Circuit(n_qubits) @@ -27,47 +28,47 @@ def random_circuit_transpilation(n_qubits, n_gates): lambda q: cir.x(q), lambda q: cir.rx(q, angle=random.random() * 2), lambda q: cir.ry(q, angle=random.random() * 2), - lambda q: cir.rz(q, angle=random.random() * 2) + lambda q: cir.rz(q, angle=random.random() * 2), ] # Add random number of gates (3-10) - n_gates = n_gates - for _ in range(n_gates): # 70% chance for single-qubit gate, 20% for CNOT if random.random() < 0.7: # Random single-qubit gate - qubit = random.randint(0, n_qubits-1) + qubit = random.randint(0, n_qubits - 1) random.choice(single_gates)(qubit) else: # Random CNOT - control = random.randint(0, n_qubits-1) - target = random.randint(0, n_qubits-1) + control = random.randint(0, n_qubits - 1) + target = random.randint(0, n_qubits - 1) # Ensure control and target are different while target == control: - target = random.randint(0, n_qubits-1) + target = random.randint(0, n_qubits - 1) cir.cnot(control=control, target=target) # Transpile circuit to measurement pattern def transpile(): - pattern = cir.transpile().pattern + return cir.transpile().pattern + # Transpile circuit to measurement pattern return benchmark(transpile) + results = {} platform = 'graphix' -n_list = [2, 5, 10, 20] -l_list = [5, 10, 100] +nqubits = [2, 5, 10, 20] +ngates = [5, 10, 100] -for n in tqdm(n_list): - for l in tqdm(l_list): - _, ts = random_circuit_transpilation(n, l) - results[str(n) + '-' + str(l)] = ts +for n in tqdm(nqubits): + for ng in tqdm(ngates): + _, ts = random_circuit_transpilation(n, ng) + results[str(n) + '-' + str(ng)] = ts -with open('transpile_mbqc_'+platform+'_results.data', 'w') as f: +with open('transpile_mbqc_' + platform + '_results.data', 'w') as f: json.dump(results, f) -with open('transpile_mbqc_'+platform+'_results.data', 'r') as f: +with open('transpile_mbqc_' + platform + '_results.data') as f: print(json.load(f)) diff --git a/examples/benchmarks/benchmark_v420/gradient/gradient_cuda_deepquantum.py b/examples/benchmarks/benchmark_v420/gradient/gradient_cuda_deepquantum.py index b260c4be..91df7a2c 100644 --- a/examples/benchmarks/benchmark_v420/gradient/gradient_cuda_deepquantum.py +++ b/examples/benchmarks/benchmark_v420/gradient/gradient_cuda_deepquantum.py @@ -8,6 +8,7 @@ # Print version print(dq.__version__) + def benchmark(f, *args, trials=10): r = f(*args) time0 = time.time() @@ -19,40 +20,42 @@ def benchmark(f, *args, trials=10): return r, ts -def grad_dq(n, l): + +def grad_dq(n, layer): def get_grad_dq(params): - if params.grad != None: + if params.grad is not None: params.grad.zero_() cir = dq.QubitCircuit(n) - for j in range(l): + for _ in range(layer): for i in range(n - 1): cir.cnot(i, i + 1) cir.rxlayer(encode=True) cir.rzlayer(encode=True) cir.rxlayer(encode=True) - cir.observable(basis='x'*n) + cir.observable(basis='x' * n) cir.to('cuda') cir(data=params) exp = cir.expectation() exp.backward() return params.grad - return benchmark(get_grad_dq, torch.ones([3 * n * l], requires_grad=True, device='cuda')) + return benchmark(get_grad_dq, torch.ones([3 * n * layer], requires_grad=True, device='cuda')) + results = {} platform = 'deepquantum_gpu' -n_list = [2, 6, 10, 14, 18, 22] +n_list = [2, 6, 10, 14, 18, 22] l_list = [1, 5, 10] for n in tqdm(n_list): - for l in l_list: - _, ts = grad_dq(n, l) - results[str(n) + '-' + str(l)] = ts + for layer in l_list: + _, ts = grad_dq(n, layer) + results[str(n) + '-' + str(layer)] = ts -with open('gradient_'+platform+'_results.data', 'w') as f: +with open('gradient_' + platform + '_results.data', 'w') as f: json.dump(results, f) -with open('gradient_'+platform+'_results.data', 'r') as f: +with open('gradient_' + platform + '_results.data') as f: print(json.load(f)) diff --git a/examples/benchmarks/benchmark_v420/gradient/gradient_cuda_mindquantum.py b/examples/benchmarks/benchmark_v420/gradient/gradient_cuda_mindquantum.py index af1d8c31..62430aa5 100644 --- a/examples/benchmarks/benchmark_v420/gradient/gradient_cuda_mindquantum.py +++ b/examples/benchmarks/benchmark_v420/gradient/gradient_cuda_mindquantum.py @@ -10,6 +10,7 @@ # Print version print(mq.__version__) + def benchmark(f, *args, trials=10): r = f(*args) time0 = time.time() @@ -21,13 +22,14 @@ def benchmark(f, *args, trials=10): return r, ts -def grad_mindquantum(n, l): + +def grad_mindquantum(n, layer): sim = Simulator('mqvector_gpu', n) # 选择 MindQuantum 矢量模拟器 - params = [f'theta_{i}' for i in range(3 * n * l)] # 参数名 + params = [f'theta_{i}' for i in range(3 * n * layer)] # 参数名 # 构造量子线路 circ = Circuit() - for j in range(l): + for j in range(layer): for i in range(n - 1): circ += X.on(i + 1, i) # CNOT for i in range(n): @@ -48,21 +50,23 @@ def compute_grad(values): _, grad = grad_fn(values) return np.array(grad) - return benchmark(compute_grad, np.ones(3 * n * l, dtype=np.float32)) + return benchmark(compute_grad, np.ones(3 * n * layer, dtype=np.float32)) + results = {} + platform = 'mindquantum_gpu' n_list = [2, 6, 10, 14, 18, 22] l_list = [1, 5, 10] for n in tqdm(n_list): - for l in l_list: - _, ts = grad_mindquantum(n, l) - results[f"{n}-{l}"] = ts + for layer in l_list: + _, ts = grad_mindquantum(n, layer) + results[f'{n}-{layer}'] = ts -with open('gradient_'+platform+'_results.data', 'w') as f: +with open('gradient_' + platform + '_results.data', 'w') as f: json.dump(results, f) -with open('gradient_'+platform+'_results.data', 'r') as f: +with open('gradient_' + platform + '_results.data') as f: print(json.load(f)) diff --git a/examples/benchmarks/benchmark_v420/gradient/gradient_deepquantum.py b/examples/benchmarks/benchmark_v420/gradient/gradient_deepquantum.py index 7e90cecd..a604cd65 100644 --- a/examples/benchmarks/benchmark_v420/gradient/gradient_deepquantum.py +++ b/examples/benchmarks/benchmark_v420/gradient/gradient_deepquantum.py @@ -8,6 +8,7 @@ # Print version print(dq.__version__) + def benchmark(f, *args, trials=10): r = f(*args) time0 = time.time() @@ -19,24 +20,26 @@ def benchmark(f, *args, trials=10): return r, ts -def grad_dq(n, l): + +def grad_dq(n, layer): def get_grad_dq(params): - if params.grad != None: + if params.grad is not None: params.grad.zero_() cir = dq.QubitCircuit(n) - for j in range(l): + for _ in range(layer): for i in range(n - 1): cir.cnot(i, i + 1) cir.rxlayer(encode=True) cir.rzlayer(encode=True) cir.rxlayer(encode=True) - cir.observable(basis='x'*n) + cir.observable(basis='x' * n) cir(data=params) exp = cir.expectation() exp.backward() return params.grad - return benchmark(get_grad_dq, torch.ones([3 * n * l], requires_grad=True)) + return benchmark(get_grad_dq, torch.ones([3 * n * layer], requires_grad=True)) + results = {} @@ -46,12 +49,12 @@ def get_grad_dq(params): l_list = [1, 5, 10] for n in tqdm(n_list): - for l in l_list: - _, ts = grad_dq(n, l) - results[str(n) + '-' + str(l)] = ts + for layer in l_list: + _, ts = grad_dq(n, layer) + results[str(n) + '-' + str(layer)] = ts -with open('gradient_'+platform+'_results.data', 'w') as f: +with open('gradient_' + platform + '_results.data', 'w') as f: json.dump(results, f) -with open('gradient_'+platform+'_results.data', 'r') as f: +with open('gradient_' + platform + '_results.data') as f: print(json.load(f)) diff --git a/examples/benchmarks/benchmark_v420/gradient/gradient_mindquantum.py b/examples/benchmarks/benchmark_v420/gradient/gradient_mindquantum.py index 860d32f3..9db007ae 100644 --- a/examples/benchmarks/benchmark_v420/gradient/gradient_mindquantum.py +++ b/examples/benchmarks/benchmark_v420/gradient/gradient_mindquantum.py @@ -10,6 +10,7 @@ # Print version print(mq.__version__) + def benchmark(f, *args, trials=10): r = f(*args) time0 = time.time() @@ -21,13 +22,14 @@ def benchmark(f, *args, trials=10): return r, ts -def grad_mindquantum(n, l): + +def grad_mindquantum(n, layer): sim = Simulator('mqvector', n) # 选择 MindQuantum 矢量模拟器 - params = [f'theta_{i}' for i in range(3 * n * l)] # 参数名 + params = [f'theta_{i}' for i in range(3 * n * layer)] # 参数名 # 构造量子线路 circ = Circuit() - for j in range(l): + for j in range(layer): for i in range(n - 1): circ += X.on(i + 1, i) # CNOT for i in range(n): @@ -48,21 +50,23 @@ def compute_grad(values): _, grad = grad_fn(values) return np.array(grad) - return benchmark(compute_grad, np.ones(3 * n * l, dtype=np.float32)) + return benchmark(compute_grad, np.ones(3 * n * layer, dtype=np.float32)) + results = {} + platform = 'mindquantum' n_list = [2, 6, 10, 14, 18, 22] l_list = [1, 5, 10] for n in tqdm(n_list): - for l in l_list: - _, ts = grad_mindquantum(n, l) - results[f"{n}-{l}"] = ts + for layer in l_list: + _, ts = grad_mindquantum(n, layer) + results[f'{n}-{layer}'] = ts -with open('gradient_'+platform+'_results.data', 'w') as f: +with open('gradient_' + platform + '_results.data', 'w') as f: json.dump(results, f) -with open('gradient_'+platform+'_results.data', 'r') as f: +with open('gradient_' + platform + '_results.data') as f: print(json.load(f)) diff --git a/examples/benchmarks/benchmark_v420/gradient/gradient_pennylane.py b/examples/benchmarks/benchmark_v420/gradient/gradient_pennylane.py index 7b849f5f..1045e462 100644 --- a/examples/benchmarks/benchmark_v420/gradient/gradient_pennylane.py +++ b/examples/benchmarks/benchmark_v420/gradient/gradient_pennylane.py @@ -10,6 +10,7 @@ # Print version print(qml.__version__) + def benchmark(f, *args, trials=10): r = f(*args) time0 = time.time() @@ -21,14 +22,15 @@ def benchmark(f, *args, trials=10): return r, ts -def grad_pennylane(n, l): - dev = qml.device("default.qubit", wires=n) - params = pnp.array(np.ones(3 * n * l), requires_grad=True) +def grad_pennylane(n, layer): + dev = qml.device('default.qubit', wires=n) + + params = pnp.array(np.ones(3 * n * layer), requires_grad=True) @qml.qnode(dev) def circuit(w): - for j in range(l): + for j in range(layer): for i in range(n - 1): qml.CNOT(wires=[i, i + 1]) for i in range(n): @@ -44,19 +46,21 @@ def circuit(w): grad_fn = qml.grad(circuit) return benchmark(grad_fn, params) + results = {} + platform = 'pennylane' n_list = [2, 6, 10, 14, 18, 22] l_list = [1, 5, 10] for n in tqdm(n_list): - for l in l_list: - _, ts = grad_pennylane(n, l) - results[f"{n}-{l}"] = ts + for layer in l_list: + _, ts = grad_pennylane(n, layer) + results[f'{n}-{layer}'] = ts -with open('gradient_'+platform+'_results.data', 'w') as f: +with open('gradient_' + platform + '_results.data', 'w') as f: json.dump(results, f) -with open('gradient_'+platform+'_results.data', 'r') as f: +with open('gradient_' + platform + '_results.data') as f: print(json.load(f)) diff --git a/examples/benchmarks/benchmark_v420/hafnian/GlobalHafnian.py b/examples/benchmarks/benchmark_v420/hafnian/GlobalHafnian.py index 6ada658d..4362fce9 100644 --- a/examples/benchmarks/benchmark_v420/hafnian/GlobalHafnian.py +++ b/examples/benchmarks/benchmark_v420/hafnian/GlobalHafnian.py @@ -1,7 +1,4 @@ -import json - import torch -from tqdm import tqdm n_list = [2, 6, 10, 14] l_list = [1, 10, 100] @@ -9,49 +6,52 @@ device = 'cpu' + def random_unitary(nmodes, dtype=torch.complex64, device=device): n = nmodes """Generate a random unitary matrix of size n x n using only PyTorch.""" # 随机复数矩阵 A = Re + i Im real = torch.rand((n, n), dtype=torch.float32, device=device) imag = torch.rand((n, n), dtype=torch.float32, device=device) - A = torch.complex(real, imag) + a = torch.complex(real, imag) # QR分解 - Q, R = torch.linalg.qr(A) + q, r = torch.linalg.qr(a) # 修正 Q 的相位:让 R 的对角线变成正实数 - diag = torch.diagonal(R, 0) + diag = torch.diagonal(r, 0) phase = diag / torch.abs(diag) - U = Q * phase.unsqueeze(0) # 广播乘法 + u = q * phase.unsqueeze(0) # 广播乘法 + + return u.to(dtype) - return U.to(dtype) def random_covariance(nmodes, dtype=torch.complex64, device=device): n = nmodes * 2 """Generate a random covariance matrix of size n x n using only PyTorch.""" - U = random_unitary(n, dtype=dtype, device=device) + u = random_unitary(n, dtype=dtype, device=device) d = torch.rand(n, dtype=torch.float32, device=device) - D = torch.diag(d).to(U.dtype) - cov = U @ D @ U.conj().T + mat_d = torch.diag(d).to(u.dtype) + cov = u @ mat_d @ u.conj().T return cov.to(dtype) + def random_hafnian_matrix(cov): """Generate a random matrix for hafnian calculation.""" nmodes = cov.shape[0] // 2 n = nmodes - Q = cov + torch.eye(n * 2, dtype=cov.dtype, device=cov.device) / 2 + q = cov + torch.eye(n * 2, dtype=cov.dtype, device=cov.device) / 2 - I = torch.eye(n, dtype=cov.dtype, device=cov.device) - Z = torch.zeros((n, n), dtype=cov.dtype, device=cov.device) + identity = torch.eye(n, dtype=cov.dtype, device=cov.device) + zeros = torch.zeros((n, n), dtype=cov.dtype, device=cov.device) - X_top = torch.cat([Z, I], dim=1) - X_bottom = torch.cat([I, Z], dim=1) - X = torch.cat([X_top, X_bottom], dim=0) + x_top = torch.cat([zeros, identity], dim=1) + x_bottom = torch.cat([identity, zeros], dim=1) + x = torch.cat([x_top, x_bottom], dim=0) - Q_inv = torch.inverse(Q) - A = X @ (torch.eye(n * 2, dtype=cov.dtype, device=cov.device) - Q_inv) - return A + q_inv = torch.inverse(q) + a = x @ (torch.eye(n * 2, dtype=cov.dtype, device=cov.device) - q_inv) + return a def test_sequence_hafnian(nmode, number_of_sequence=64, device=device, start=None, end=None): @@ -59,25 +59,22 @@ def test_sequence_hafnian(nmode, number_of_sequence=64, device=device, start=Non # 判断文件是否存在,如果不存在,则保存矩阵U(复数) try: - U = torch.load(f"hafnian/hafnian_matrix_{nmode}_{number_of_sequence}.pt") - if start != None: - U = U[start, end].to(device) - else: - U = U.to(device) + u = torch.load(f'hafnian/hafnian_matrix_{nmode}_{number_of_sequence}.pt') + u = u[start, end].to(device) if start is not None else u.to(device) except FileNotFoundError: - print(f"File hafnian_matrix_{nmode}_{number_of_sequence}.pt not found. Saving matrix U.") + print(f'File hafnian_matrix_{nmode}_{number_of_sequence}.pt not found. Saving matrix U.') cov = random_covariance(nmode, device=device) - U = torch.zeros((number_of_sequence, nmode * 2, nmode * 2), dtype=cov.dtype, device=device) + u = torch.zeros((number_of_sequence, nmode * 2, nmode * 2), dtype=cov.dtype, device=device) for i in range(number_of_sequence): # Generate a random covariance matrix cov = random_covariance(nmode, device=device) - A = random_hafnian_matrix(cov) - A = (A + A.mT) / 2 + a = random_hafnian_matrix(cov) + a = (a + a.mT) / 2 # 把矩阵A添加到U中 - U[i] = A + u[i] = a # Save the matrix U to a file - torch.save(U, f"hafnian/hafnian_matrix_{nmode}_{number_of_sequence}.pt") + torch.save(u, f'hafnian/hafnian_matrix_{nmode}_{number_of_sequence}.pt') print('done') - return U + return u diff --git a/examples/benchmarks/benchmark_v420/hafnian/hafnian_deepquantum_batch.py b/examples/benchmarks/benchmark_v420/hafnian/hafnian_deepquantum_batch.py index ecd9a2da..eb472a42 100644 --- a/examples/benchmarks/benchmark_v420/hafnian/hafnian_deepquantum_batch.py +++ b/examples/benchmarks/benchmark_v420/hafnian/hafnian_deepquantum_batch.py @@ -1,44 +1,52 @@ +import json import time import deepquantum as dq +from GlobalHafnian import test_sequence_hafnian from deepquantum.photonic.hafnian_ import hafnian_batch from tqdm import tqdm -from GlobalHafnian import * +number_of_sequence = 1000 # Print version print(dq.__version__) -def hafnian_batch_dq(n, l): # n modes 的数量 , l batchsize + +def hafnian_batch_dq(nmode, batch_size): """Generate a random hafnian matrix and calculate its hafnian using DeepQuantum.""" - A = test_sequence_hafnian(n, number_of_sequence=number_of_sequence).to('cuda') # (number_of_sequence=l, 2n, 2n) - print(A.device) + mat_a = test_sequence_hafnian(nmode, number_of_sequence=number_of_sequence).to('cuda') + print(mat_a.device) - def get_hafnian_batch_dq(A): + def get_hafnian_batch_dq(matrix): trials = 100 - if l == 100 or l == 1000: + if batch_size == 100 or batch_size == 1000: trials = 1 - hafnian_batch(A[0:1]) + hafnian_batch(matrix[0:1]) time0 = time.time() for i in tqdm(range(trials)): - results = hafnian_batch(A[i*l:(i+1)*l]) + hafnian_batch(matrix[i * batch_size : (i + 1) * batch_size]) time1 = time.time() ts = (time1 - time0) / trials return ts - return get_hafnian_batch_dq(A) + return get_hafnian_batch_dq(mat_a) + results = {} + platform = 'deepquantum_gpu' -for n in tqdm(n_list): - for l in l_list: - print('n =', n, 'l =', l) - ts = hafnian_batch_dq(n, l) - results[str(n)+'+'+str(l)] = ts +nmodes = [2, 6, 10, 14] +batch_sizes = [1, 10, 100] + +for n in tqdm(nmodes): + for bs in batch_sizes: + print(f'n={n}, bs={bs}') + ts = hafnian_batch_dq(n, bs) + results[str(n) + '+' + str(bs)] = ts -with open('hafnian/hafnian_'+platform+'_results.data', 'w') as f: +with open('hafnian/hafnian_' + platform + '_results.data', 'w') as f: json.dump(results, f) -with open('hafnian/hafnian_'+platform+'_results.data', 'r') as f: +with open('hafnian/hafnian_' + platform + '_results.data') as f: print(json.load(f)) diff --git a/examples/benchmarks/benchmark_v420/hafnian/hafnian_piquasso.py b/examples/benchmarks/benchmark_v420/hafnian/hafnian_piquasso.py index 3ec038db..48f2438e 100644 --- a/examples/benchmarks/benchmark_v420/hafnian/hafnian_piquasso.py +++ b/examples/benchmarks/benchmark_v420/hafnian/hafnian_piquasso.py @@ -2,46 +2,52 @@ import time import numpy as np +from GlobalHafnian import test_sequence_hafnian from piquasso._math.hafnian import hafnian_with_reduction from tqdm import tqdm -from GlobalHafnian import * +number_of_sequence = 1000 -def hafnian_pq(n, l): +def hafnian_pq(nmode, batch_size): """Generate a random hafnian matrix and calculate its hafnian using DeepQuantum.""" - A = test_sequence_hafnian(n, number_of_sequence=number_of_sequence) + mat_a = test_sequence_hafnian(nmode, number_of_sequence=number_of_sequence) # 计算 hafnian - def get_hafnian_pq(A): + def get_hafnian_pq(matrix): trials = 10 - if l == 100 or l == 1000: + if batch_size == 100 or batch_size == 1000: trials = 1 - hafnian_with_reduction(np.array(A[0], dtype=np.complex128), np.array([1]*2*n)) + hafnian_with_reduction(np.array(matrix[0], dtype=np.complex128), np.array([1] * 2 * nmode)) time0 = time.time() for i in tqdm(range(trials)): - for j in range(i*l, (i+1)*l): - results = hafnian_with_reduction(np.array(A[j], dtype=np.complex128), np.array([1]*2*n)) + for j in range(i * batch_size, (i + 1) * batch_size): + hafnian_with_reduction(np.array(matrix[j], dtype=np.complex128), np.array([1] * 2 * nmode)) time1 = time.time() ts = (time1 - time0) / trials return ts - return get_hafnian_pq(A) + return get_hafnian_pq(mat_a) + # 进行测试 results = {} + platform = 'piquasso' -for n in tqdm(n_list): - for l in l_list: - print(f"n={n}, l={l}") - ts = hafnian_pq(n, l) - results[str(n)+'+'+str(l)] = ts +nmodes = [2, 6, 10, 14] +batch_sizes = [1, 10, 100] + +for n in tqdm(nmodes): + for bs in batch_sizes: + print(f'n={n}, bs={bs}') + ts = hafnian_pq(n, bs) + results[str(n) + '+' + str(bs)] = ts # 保存结果 with open(f'hafnian/hafnian_{platform}_results.data', 'w') as f: json.dump(results, f) # 读取并打印 -with open(f'hafnian/hafnian_{platform}_results.data', 'r') as f: +with open(f'hafnian/hafnian_{platform}_results.data') as f: print(json.load(f)) diff --git a/examples/benchmarks/benchmark_v420/hafnian/hafnian_strawberryfields.py b/examples/benchmarks/benchmark_v420/hafnian/hafnian_strawberryfields.py index 04cb0638..f8741388 100644 --- a/examples/benchmarks/benchmark_v420/hafnian/hafnian_strawberryfields.py +++ b/examples/benchmarks/benchmark_v420/hafnian/hafnian_strawberryfields.py @@ -3,48 +3,55 @@ import numpy as np import strawberryfields as sf +from GlobalHafnian import test_sequence_hafnian from thewalrus import hafnian from tqdm import tqdm -from GlobalHafnian import * +number_of_sequence = 1000 # Print version print(sf.__version__) -def hafnian_sf(n, l): + +def hafnian_sf(nmode, batch_size): """Generate a random hafnian matrix and calculate its hafnian using DeepQuantum.""" - A = test_sequence_hafnian(n, number_of_sequence=number_of_sequence) + mat_a = test_sequence_hafnian(nmode, number_of_sequence=number_of_sequence) # 计算 hafnian - def get_hafnian_sf(A): + def get_hafnian_sf(matrix): trials = 100 - if l == 100 or l == 1000: + if batch_size == 100 or batch_size == 1000: trials = 1 - hafnian(np.array(A[0])) + hafnian(np.array(matrix[0])) time0 = time.time() for i in tqdm(range(trials)): - for j in range(i*l, (i+1)*l): - results = hafnian(np.array(A[j])) + for j in range(i * batch_size, (i + 1) * batch_size): + hafnian(np.array(matrix[j])) time1 = time.time() ts = (time1 - time0) / trials return ts - return get_hafnian_sf(A) + return get_hafnian_sf(mat_a) + # 进行测试 results = {} + platform = 'strawberryfields' -for n in tqdm(n_list): - for l in l_list: - print(f"n={n}, l={l}") - ts = hafnian_sf(n, l) - results[str(n)+'+'+str(l)] = ts +nmodes = [2, 6, 10, 14] +batch_sizes = [1, 10, 100] + +for n in tqdm(nmodes): + for bs in batch_sizes: + print(f'n={n}, bs={bs}') + ts = hafnian_sf(n, bs) + results[str(n) + '+' + str(bs)] = ts # 保存结果 with open(f'hafnian/hafnian_{platform}_results.data', 'w') as f: json.dump(results, f) # 读取并打印 -with open(f'hafnian/hafnian_{platform}_results.data', 'r') as f: +with open(f'hafnian/hafnian_{platform}_results.data') as f: print(json.load(f)) diff --git a/examples/benchmarks/benchmark_v420/hessian/hessian_cuda_deepquantum.py b/examples/benchmarks/benchmark_v420/hessian/hessian_cuda_deepquantum.py index b4302e8d..4f837b50 100644 --- a/examples/benchmarks/benchmark_v420/hessian/hessian_cuda_deepquantum.py +++ b/examples/benchmarks/benchmark_v420/hessian/hessian_cuda_deepquantum.py @@ -9,6 +9,7 @@ # Print version print(dq.__version__) + def benchmark(f, *args, trials=1): # r = f(*args) time0 = time.time() @@ -20,16 +21,17 @@ def benchmark(f, *args, trials=1): return r, ts -def hessian_dq(n, l): + +def hessian_dq(n, layer): def f(params): cir = dq.QubitCircuit(n) - for j in range(l): + for _ in range(layer): for i in range(n - 1): cir.cnot(i, i + 1) cir.rxlayer(encode=True) cir.rzlayer(encode=True) cir.rxlayer(encode=True) - cir.observable(basis='x'*n) + cir.observable(basis='x' * n) cir.to('cuda') cir(data=params) return cir.expectation() @@ -37,7 +39,8 @@ def f(params): def get_hs_dq(x): return hessian(f, x) - return benchmark(get_hs_dq, torch.ones([3 * n * l], device='cuda')) + return benchmark(get_hs_dq, torch.ones([3 * n * layer], device='cuda')) + results = {} @@ -47,12 +50,12 @@ def get_hs_dq(x): l_list = [1, 5, 10] for n in tqdm(n_list): - for l in tqdm(l_list): - _, ts = hessian_dq(n, l) - results[str(n) + '-' + str(l)] = ts + for layer in tqdm(l_list): + _, ts = hessian_dq(n, layer) + results[str(n) + '-' + str(layer)] = ts -with open('hessian_'+platform+'_results.data', 'w') as f: +with open('hessian_' + platform + '_results.data', 'w') as f: json.dump(results, f) -with open('hessian_'+platform+'_results.data', 'r') as f: +with open('hessian_' + platform + '_results.data') as f: print(json.load(f)) diff --git a/examples/benchmarks/benchmark_v420/hessian/hessian_deepquantum.py b/examples/benchmarks/benchmark_v420/hessian/hessian_deepquantum.py index 666e851b..f364b6a1 100644 --- a/examples/benchmarks/benchmark_v420/hessian/hessian_deepquantum.py +++ b/examples/benchmarks/benchmark_v420/hessian/hessian_deepquantum.py @@ -9,6 +9,7 @@ # Print version print(dq.__version__) + def benchmark(f, *args, trials=10): # r = f(*args) time0 = time.time() @@ -20,38 +21,40 @@ def benchmark(f, *args, trials=10): return r, ts -def hessian_dq(n, l): + +def hessian_dq(n, layer): def f(params): cir = dq.QubitCircuit(n) - for j in range(l): + for _ in range(layer): for i in range(n - 1): cir.cnot(i, i + 1) cir.rxlayer(encode=True) cir.rzlayer(encode=True) cir.rxlayer(encode=True) - cir.observable(basis='x'*n) + cir.observable(basis='x' * n) cir(data=params) return cir.expectation() def get_hs_dq(x): return hessian(f, x) - return benchmark(get_hs_dq, torch.ones([3 * n * l])) + return benchmark(get_hs_dq, torch.ones([3 * n * layer])) + results = {} platform = 'deepquantum' -n_list = [2, 6, 10, 14, 18] +n_list = [2, 6, 10, 14, 18] l_list = [1, 5, 10] for n in tqdm(n_list): - for l in tqdm(l_list): - _, ts = hessian_dq(n, l) - results[str(n) + '-' + str(l)] = ts + for layer in tqdm(l_list): + _, ts = hessian_dq(n, layer) + results[str(n) + '-' + str(layer)] = ts -with open('hessian_'+platform+'_results.data', 'w') as f: +with open('hessian_' + platform + '_results.data', 'w') as f: json.dump(results, f) -with open('hessian_'+platform+'_results.data', 'r') as f: +with open('hessian_' + platform + '_results.data') as f: print(json.load(f)) diff --git a/examples/benchmarks/benchmark_v420/hessian/hessian_pennylane.py b/examples/benchmarks/benchmark_v420/hessian/hessian_pennylane.py index f80b532b..294325de 100644 --- a/examples/benchmarks/benchmark_v420/hessian/hessian_pennylane.py +++ b/examples/benchmarks/benchmark_v420/hessian/hessian_pennylane.py @@ -10,6 +10,7 @@ # Print version print(qml.__version__) + def benchmark(f, *args, trials=1): # r = f(*args) time0 = time.time() @@ -21,14 +22,15 @@ def benchmark(f, *args, trials=1): return r, ts -def hessian_pennylane(n, l): - dev = qml.device("default.qubit", wires=n) - params = pnp.array(np.ones(3 * n * l), requires_grad=True, dtype='float32') +def hessian_pennylane(n, layer): + dev = qml.device('default.qubit', wires=n) + + params = pnp.array(np.ones(3 * n * layer), requires_grad=True, dtype='float32') @qml.qnode(dev) def circuit(w): - for j in range(l): + for j in range(layer): for i in range(n - 1): qml.CNOT(wires=[i, i + 1]) for i in range(n): @@ -43,19 +45,21 @@ def circuit(w): hessian_fn = qml.jacobian(qml.grad(circuit)) return benchmark(hessian_fn, params) + results = {} -n_list = [2, 6, 10, 14, 18, 22] -l_list = [1, 5, 10] platform = 'pennylane' +n_list = [2, 6, 10, 14, 18, 22] +l_list = [1, 5, 10] + for n in tqdm(n_list): - for l in tqdm(l_list): - _, ts = hessian_pennylane(n, l) - results[f"{n}-{l}"] = ts + for layer in tqdm(l_list): + _, ts = hessian_pennylane(n, layer) + results[f'{n}-{layer}'] = ts -with open('hessian_'+platform+'_results.data', 'w') as f: +with open('hessian_' + platform + '_results.data', 'w') as f: json.dump(results, f) -with open('hessian_'+platform+'_results.data', 'r') as f: +with open('hessian_' + platform + '_results.data') as f: print(json.load(f)) diff --git a/examples/benchmarks/benchmark_v420/loop_torontonian/GlobalTor.py b/examples/benchmarks/benchmark_v420/loop_torontonian/GlobalTor.py index 23eb39ff..ef89aa10 100644 --- a/examples/benchmarks/benchmark_v420/loop_torontonian/GlobalTor.py +++ b/examples/benchmarks/benchmark_v420/loop_torontonian/GlobalTor.py @@ -1,6 +1,7 @@ +import numpy as np import torch - -import json +from thewalrus.quantum.conversions import Amat, Xmat +from thewalrus.random import random_covariance from tqdm import tqdm n_list = [2, 6, 10, 14, 18] @@ -9,23 +10,20 @@ device = 'cpu' -import numpy as np -from thewalrus.random import random_covariance -from thewalrus.quantum.conversions import Amat, Xmat - np.random.seed(42) -def generate_psd_matrix(n): +def generate_psd_matrix(n): cov = random_covariance(n) - O = Xmat(n) @ Amat(cov) - return O + o = Xmat(n) @ Amat(cov) + return o + for nmode in tqdm(n_list): - U = torch.zeros((number_of_sequence, nmode * 2, nmode * 2), dtype=torch.complex128, device=device) + u = torch.zeros((number_of_sequence, nmode * 2, nmode * 2), dtype=torch.complex128, device=device) for j in tqdm(range(number_of_sequence)): # Generate a random covariance matrix - U[j] = torch.tensor(generate_psd_matrix(nmode), device=device) + u[j] = torch.tensor(generate_psd_matrix(nmode), device=device) # Save the matrix U to a file - torch.save(U, f"tor_matrix_{nmode}_{number_of_sequence}.pt") + torch.save(u, f'tor_matrix_{nmode}_{number_of_sequence}.pt') print('done') diff --git a/examples/benchmarks/benchmark_v420/loop_torontonian/torontonian_deepquantum.py b/examples/benchmarks/benchmark_v420/loop_torontonian/torontonian_deepquantum.py index dc1241fd..00cb2dfe 100644 --- a/examples/benchmarks/benchmark_v420/loop_torontonian/torontonian_deepquantum.py +++ b/examples/benchmarks/benchmark_v420/loop_torontonian/torontonian_deepquantum.py @@ -11,40 +11,42 @@ device = 'cpu' -def torontonian_dq(n, l): - A = torch.load(f"tor_matrix_{n}_{1000}.pt").to(device) + +def torontonian_dq(nmode, batch_size): + mat_a = torch.load(f'tor_matrix_{nmode}_{1000}.pt').to(device) def get_torontonian_dq(matrix): trials = 10 - if l == 100 or l == 1000: + if batch_size == 100 or batch_size == 1000: trials = 1 - gamma = torch.diagonal(A[0:1], dim1=1, dim2=2) - torch.vmap(torontonian)(A[0:1], gamma) + gamma = torch.diagonal(matrix[0:1], dim1=1, dim2=2) + torch.vmap(torontonian)(matrix[0:1], gamma) time0 = time.time() for i in range(trials): - gamma = torch.diagonal(A[i*l:(i+1)*l], dim1=1, dim2=2) - results = torch.vmap(torontonian)(A[i*l:(i+1)*l], gamma) + gamma = torch.diagonal(matrix[i * batch_size : (i + 1) * batch_size], dim1=1, dim2=2) + torch.vmap(torontonian)(matrix[i * batch_size : (i + 1) * batch_size], gamma) time1 = time.time() ts = (time1 - time0) / trials return ts - return get_torontonian_dq(A) + return get_torontonian_dq(mat_a) + results = {} platform = 'deepquantum' -n_list = [2, 6, 10, 14] -l_list = [1, 10, 100, 1000] +nmodes = [2, 6, 10, 14] +batch_sizes = [1, 10, 100, 1000] -for n in tqdm(n_list): - for l in tqdm(l_list): - print(n,l) - ts = torontonian_dq(n, l) - results[str(n)+'+'+str(l)] = ts +for n in tqdm(nmodes): + for bs in tqdm(batch_sizes): + print(n, bs) + ts = torontonian_dq(n, bs) + results[str(n) + '+' + str(bs)] = ts -with open('loop_torontonian_'+platform+'_results.data', 'w') as f: +with open('loop_torontonian_' + platform + '_results.data', 'w') as f: json.dump(results, f) -with open('loop_torontonian_'+platform+'_results.data', 'r') as f: +with open('loop_torontonian_' + platform + '_results.data') as f: print(json.load(f)) diff --git a/examples/benchmarks/benchmark_v420/loop_torontonian/torontonian_piquasso.py b/examples/benchmarks/benchmark_v420/loop_torontonian/torontonian_piquasso.py index e19f9617..8f6aeb44 100644 --- a/examples/benchmarks/benchmark_v420/loop_torontonian/torontonian_piquasso.py +++ b/examples/benchmarks/benchmark_v420/loop_torontonian/torontonian_piquasso.py @@ -10,43 +10,45 @@ # Print version print(piquasso.__version__) + def _tor_pq_loop(mat): gamma = mat.diagonal() return loop_torontonian(mat, gamma) -def torontonian_pq(n, l): - A = torch.load(f"tor_matrix_{n}_{1000}.pt").numpy() + +def torontonian_pq(nmode, batch_size): + mat_a = torch.load(f'tor_matrix_{nmode}_{1000}.pt').numpy() def get_torontonian_pq(matrix): trials = 10 - if l == 100 or l == 1000: + if batch_size == 100 or batch_size == 1000: trials = 1 - np.vectorize(_tor_pq_loop, signature='(n,n)->()')(A[0:1]) + np.vectorize(_tor_pq_loop, signature='(n,n)->()')(matrix[0:1]) time0 = time.time() for i in range(trials): - results = np.vectorize(_tor_pq_loop, signature='(n,n)->()')(A[i*l:(i+1)*l]) - # print(results) + np.vectorize(_tor_pq_loop, signature='(n,n)->()')(matrix[i * batch_size : (i + 1) * batch_size]) time1 = time.time() ts = (time1 - time0) / trials return ts - return get_torontonian_pq(A) + return get_torontonian_pq(mat_a) + results = {} platform = 'piquasso' -n_list = [2, 6, 10, 14, 18] -l_list = [1, 10, 100, 1000] +nmodes = [2, 6, 10, 14, 18] +batch_sizes = [1, 10, 100, 1000] -for n in tqdm(n_list): - for l in tqdm(l_list): - print(n,l) - ts = torontonian_pq(n, l) - results[str(n)+'+'+str(l)] = ts +for n in tqdm(nmodes): + for bs in tqdm(batch_sizes): + print(n, bs) + ts = torontonian_pq(n, bs) + results[str(n) + '+' + str(bs)] = ts -with open('loop_torontonian_'+platform+'_results.data', 'w') as f: +with open('loop_torontonian_' + platform + '_results.data', 'w') as f: json.dump(results, f) -with open('loop_torontonian_'+platform+'_results.data', 'r') as f: +with open('loop_torontonian_' + platform + '_results.data') as f: print(json.load(f)) diff --git a/examples/benchmarks/benchmark_v420/loop_torontonian/torontonian_strawberryfields.py b/examples/benchmarks/benchmark_v420/loop_torontonian/torontonian_strawberryfields.py index aeec8079..f5a1f742 100644 --- a/examples/benchmarks/benchmark_v420/loop_torontonian/torontonian_strawberryfields.py +++ b/examples/benchmarks/benchmark_v420/loop_torontonian/torontonian_strawberryfields.py @@ -10,43 +10,45 @@ # Print version print(sf.__version__) + def _tor_sf_loop(mat): gamma = mat.diagonal() return ltor(mat, gamma) -def torontonian_sf(n, l): - A = torch.load(f"tor_matrix_{n}_{1000}.pt").numpy() + +def torontonian_sf(nmode, batch_size): + mat_a = torch.load(f'tor_matrix_{nmode}_{1000}.pt').numpy() def get_torontonian_sf(matrix): trials = 10 - # if l == 100 or l == 1000: - if l == 1000: + if batch_size == 1000: trials = 1 - np.vectorize(_tor_sf_loop,signature='(n,n)->()')(A[0:1]) + np.vectorize(_tor_sf_loop, signature='(n,n)->()')(matrix[0:1]) time0 = time.time() for i in range(trials): - results = np.vectorize(_tor_sf_loop,signature='(n,n)->()')(A[i*l:(i+1)*l]) + np.vectorize(_tor_sf_loop, signature='(n,n)->()')(matrix[i * batch_size : (i + 1) * batch_size]) time1 = time.time() ts = (time1 - time0) / trials return ts - return get_torontonian_sf(A) + return get_torontonian_sf(mat_a) + results = {} platform = 'strawberryfields' -n_list = [2, 6, 10, 14] -l_list = [1, 10, 100, 1000] +nmodes = [2, 6, 10, 14] +batch_sizes = [1, 10, 100, 1000] -for n in tqdm(n_list): - for l in tqdm(l_list): - print(n,l) - ts = torontonian_sf(n, l) - results[str(n)+'+'+str(l)] = ts +for n in tqdm(nmodes): + for bs in tqdm(batch_sizes): + print(n, bs) + ts = torontonian_sf(n, bs) + results[str(n) + '+' + str(bs)] = ts -with open('loop_torontonian_'+platform+'_results.data', 'w') as f: +with open('loop_torontonian_' + platform + '_results.data', 'w') as f: json.dump(results, f) -with open('loop_torontonian_'+platform+'_results.data', 'r') as f: +with open('loop_torontonian_' + platform + '_results.data') as f: print(json.load(f)) diff --git a/examples/benchmarks/benchmark_v420/permanent/GlobalPerm.py b/examples/benchmarks/benchmark_v420/permanent/GlobalPerm.py index 10a0a519..c216c6d4 100644 --- a/examples/benchmarks/benchmark_v420/permanent/GlobalPerm.py +++ b/examples/benchmarks/benchmark_v420/permanent/GlobalPerm.py @@ -1,5 +1,6 @@ +import numpy as np import torch - +from scipy.stats import unitary_group from tqdm import tqdm n_list = [2, 6, 10, 14, 18, 22, 26, 30] @@ -7,16 +8,13 @@ device = 'cpu' -import numpy as np -from scipy.stats import unitary_group - np.random.seed(42) for nmode in tqdm(n_list): - U = torch.zeros((number_of_sequence, nmode, nmode), dtype=torch.complex128, device=device) + u = torch.zeros((number_of_sequence, nmode, nmode), dtype=torch.complex128, device=device) for j in range(number_of_sequence): # Generate a random covariance matrix - U[j] = torch.tensor(unitary_group.rvs(nmode), device=device) + u[j] = torch.tensor(unitary_group.rvs(nmode), device=device) # Save the matrix U to a file - torch.save(U, f"u_matrix_{nmode}_{number_of_sequence}.pt") + torch.save(u, f'u_matrix_{nmode}_{number_of_sequence}.pt') print('done') diff --git a/examples/benchmarks/benchmark_v420/permanent/permanent_cuda_deepquantum.py b/examples/benchmarks/benchmark_v420/permanent/permanent_cuda_deepquantum.py index 02208adf..c8f92229 100644 --- a/examples/benchmarks/benchmark_v420/permanent/permanent_cuda_deepquantum.py +++ b/examples/benchmarks/benchmark_v420/permanent/permanent_cuda_deepquantum.py @@ -11,44 +11,45 @@ device = 'cuda' -def permanent_dq(n, l): - A = torch.load(f"u_matrix_{n}_{1000}.pt").to(device) + +def permanent_dq(nmode, batch_size): + mat_a = torch.load(f'u_matrix_{nmode}_{1000}.pt').to(device) def get_perm_dq(matrix): trials = 1 - if l == 10 or l == 100 or l == 1000: + if batch_size == 10 or batch_size == 100 or batch_size == 1000: trials = 1 torch.vmap(permanent)(matrix[0:1]) time0 = time.time() for i in range(trials): - if n > 21 and l >= 100: - results = torch.vmap(permanent, chunk_size=1)(matrix[i*l:(i+1)*l]) - if n == 18 and l == 1000: - results = torch.vmap(permanent, chunk_size=200)(matrix[i*l:(i+1)*l]) + if nmode > 21 and batch_size >= 100: + torch.vmap(permanent, chunk_size=1)(matrix[i * batch_size : (i + 1) * batch_size]) + if nmode == 18 and batch_size == 1000: + torch.vmap(permanent, chunk_size=200)(matrix[i * batch_size : (i + 1) * batch_size]) else: - results = torch.vmap(permanent)(matrix[i*l:(i+1)*l]) + torch.vmap(permanent)(matrix[i * batch_size : (i + 1) * batch_size]) time1 = time.time() ts = (time1 - time0) / trials return ts - return get_perm_dq(A) + return get_perm_dq(mat_a) results = {} platform = 'deepquantum_gpu' -n_list = [2, 6, 10, 14, 18, 22] -l_list = [1, 10, 100, 1000] +nmodes = [2, 6, 10, 14, 18, 22] +batch_sizes = [1, 10, 100, 1000] -for n in tqdm(n_list): - for l in tqdm(l_list): - print(n,l) - ts = permanent_dq(n, l) - results[str(n)+'+'+str(l)] = ts +for n in tqdm(nmodes): + for bs in tqdm(batch_sizes): + print(n, bs) + ts = permanent_dq(n, bs) + results[str(n) + '+' + str(bs)] = ts -with open('permanent_'+platform+'_results.data', 'w') as f: +with open('permanent_' + platform + '_results.data', 'w') as f: json.dump(results, f) -with open('permanent_'+platform+'_results.data', 'r') as f: +with open('permanent_' + platform + '_results.data') as f: print(json.load(f)) diff --git a/examples/benchmarks/benchmark_v420/permanent/permanent_deepquantum.py b/examples/benchmarks/benchmark_v420/permanent/permanent_deepquantum.py index e9d2d407..6f4f0120 100644 --- a/examples/benchmarks/benchmark_v420/permanent/permanent_deepquantum.py +++ b/examples/benchmarks/benchmark_v420/permanent/permanent_deepquantum.py @@ -11,42 +11,43 @@ device = 'cpu' -def permanent_dq(n, l): - A = torch.load(f"u_matrix_{n}_{1000}.pt").to(device) + +def permanent_dq(nmode, batch_size): + mat_a = torch.load(f'u_matrix_{nmode}_{1000}.pt').to(device) def get_perm_dq(matrix): trials = 1 - if l == 10 or l == 100 or l == 1000: + if batch_size == 10 or batch_size == 100 or batch_size == 1000: trials = 1 torch.vmap(permanent)(matrix[0:1]) time0 = time.time() for i in range(trials): - if n > 21 and l == 1000: - results = torch.vmap(permanent, chunk_size=125)(matrix[i*l:(i+1)*l]) + if nmode > 21 and batch_size == 1000: + torch.vmap(permanent, chunk_size=125)(matrix[i * batch_size : (i + 1) * batch_size]) else: - results = torch.vmap(permanent)(matrix[i*l:(i+1)*l]) - # print(results) + torch.vmap(permanent)(matrix[i * batch_size : (i + 1) * batch_size]) time1 = time.time() ts = (time1 - time0) / trials return ts - return get_perm_dq(A) + return get_perm_dq(mat_a) + results = {} platform = 'deepquantum' -n_list = [2, 6, 10, 14, 18, 22] -l_list = [1, 10, 100, 1000] +nmodes = [2, 6, 10, 14, 18, 22] +batch_sizes = [1, 10, 100, 1000] -for n in tqdm(n_list): - for l in tqdm(l_list): - print(n,l) - ts = permanent_dq(n, l) - results[str(n)+'+'+str(l)] = ts +for n in tqdm(nmodes): + for bs in tqdm(batch_sizes): + print(n, bs) + ts = permanent_dq(n, bs) + results[str(n) + '+' + str(bs)] = ts -with open('permanent_'+platform+'_results.data', 'w') as f: +with open('permanent_' + platform + '_results.data', 'w') as f: json.dump(results, f) -with open('permanent_'+platform+'_results.data', 'r') as f: +with open('permanent_' + platform + '_results.data') as f: print(json.load(f)) diff --git a/examples/benchmarks/benchmark_v420/permanent/permanent_piquasso.py b/examples/benchmarks/benchmark_v420/permanent/permanent_piquasso.py index 0b6ecd23..dc698dfc 100644 --- a/examples/benchmarks/benchmark_v420/permanent/permanent_piquasso.py +++ b/examples/benchmarks/benchmark_v420/permanent/permanent_piquasso.py @@ -15,39 +15,40 @@ def _perm_pq(mat): r = np.ones(n) return permanent(mat, r, r) -def perm_pq(n, l): - A = torch.load(f"u_matrix_{n}_{1000}.pt").numpy() + +def perm_pq(nmode, batch_size): + mat_a = torch.load(f'u_matrix_{nmode}_{1000}.pt').numpy() def get_perm_pq(matrix): trials = 10 - if l == 100 or l == 1000: + if batch_size == 100 or batch_size == 1000: trials = 1 - np.vectorize(_perm_pq, signature='(n,n)->()')(A[0:1]) + np.vectorize(_perm_pq, signature='(n,n)->()')(matrix[0:1]) time0 = time.time() for i in range(trials): - results = np.vectorize(_perm_pq, signature='(n,n)->()')(A[i*l:(i+1)*l]) - # print(results) + np.vectorize(_perm_pq, signature='(n,n)->()')(matrix[i * batch_size : (i + 1) * batch_size]) time1 = time.time() ts = (time1 - time0) / trials return ts - return get_perm_pq(A) + return get_perm_pq(mat_a) + results = {} platform = 'piquasso' -l_list = [1, 10, 100] -n_list = [2, 6, 10, 14, 18, 22, 26, 30] +nmodes = [2, 6, 10, 14, 18, 22, 26, 30] +batch_sizes = [1, 10, 100] -for n in tqdm(n_list): - for l in tqdm(l_list): - print(n,l) - ts = perm_pq(n, l) - results[str(n)+'+'+str(l)] = ts +for n in tqdm(nmodes): + for bs in tqdm(batch_sizes): + print(n, bs) + ts = perm_pq(n, bs) + results[str(n) + '+' + str(bs)] = ts -with open('permanent_'+platform+'_results.data', 'w') as f: +with open('permanent_' + platform + '_results.data', 'w') as f: json.dump(results, f) -with open('permanent_'+platform+'_results.data', 'r') as f: +with open('permanent_' + platform + '_results.data') as f: print(json.load(f)) diff --git a/examples/benchmarks/benchmark_v420/permanent/permanent_strawberryfields.py b/examples/benchmarks/benchmark_v420/permanent/permanent_strawberryfields.py index 3706786b..82edb2d4 100644 --- a/examples/benchmarks/benchmark_v420/permanent/permanent_strawberryfields.py +++ b/examples/benchmarks/benchmark_v420/permanent/permanent_strawberryfields.py @@ -10,39 +10,41 @@ # Print version print(sf.__version__) -def perm_sf(n, l): - A = torch.load(f"u_matrix_{n}_{1000}.pt").numpy() + +def perm_sf(nmode, batch_size): + mat_a = torch.load(f'u_matrix_{nmode}_{1000}.pt').numpy() def get_perm_sf(matrix): trials = 10 - if l == 1000: + if batch_size == 1000: trials = 1 - np.vectorize(perm,signature='(n,n)->()')(A[0:1]) + np.vectorize(perm, signature='(n,n)->()')(matrix[0:1]) time0 = time.time() for i in range(trials): - results = np.vectorize(perm,signature='(n,n)->()')(A[i*l:(i+1)*l]) + np.vectorize(perm, signature='(n,n)->()')(matrix[i * batch_size : (i + 1) * batch_size]) time1 = time.time() ts = (time1 - time0) / trials return ts - return get_perm_sf(A) + return get_perm_sf(mat_a) + results = {} platform = 'strawberryfields' -n_list = [2, 6, 10, 14, 18, 22, 26, 30] -l_list = [1, 10, 100, 1000] +nmodes = [2, 6, 10, 14, 18, 22, 26, 30] +batch_sizes = [1, 10, 100, 1000] -for n in tqdm(n_list): - for l in tqdm(l_list): - print(n,l) - ts = perm_sf(n, l) - results[str(n)+'+'+str(l)] = ts +for n in tqdm(nmodes): + for bs in tqdm(batch_sizes): + print(n, bs) + ts = perm_sf(n, bs) + results[str(n) + '+' + str(bs)] = ts -with open('permanent_'+platform+'_results.data', 'w') as f: +with open('permanent_' + platform + '_results.data', 'w') as f: json.dump(results, f) -with open('permanent_'+platform+'_results.data', 'r') as f: +with open('permanent_' + platform + '_results.data') as f: print(json.load(f)) diff --git a/examples/benchmarks/benchmark_v420/torontonian/GlobalTor.py b/examples/benchmarks/benchmark_v420/torontonian/GlobalTor.py index 9a315226..13919d97 100644 --- a/examples/benchmarks/benchmark_v420/torontonian/GlobalTor.py +++ b/examples/benchmarks/benchmark_v420/torontonian/GlobalTor.py @@ -1,7 +1,7 @@ import numpy as np import torch -from thewalrus.random import random_covariance from thewalrus.quantum.conversions import Amat, Xmat +from thewalrus.random import random_covariance from tqdm import tqdm n_list = [2, 6, 10, 14, 18] @@ -10,16 +10,18 @@ np.random.seed(42) + def generate_psd_matrix(n): cov = random_covariance(n) - O = Xmat(n) @ Amat(cov) - return O + o = Xmat(n) @ Amat(cov) + return o + for nmode in tqdm(n_list): - U = torch.zeros((number_of_sequence, nmode * 2, nmode * 2), dtype=torch.complex128, device=device) + u = torch.zeros((number_of_sequence, nmode * 2, nmode * 2), dtype=torch.complex128, device=device) for j in tqdm(range(number_of_sequence)): # Generate a random covariance matrix - U[j] = torch.tensor(generate_psd_matrix(nmode), device=device) + u[j] = torch.tensor(generate_psd_matrix(nmode), device=device) # Save the matrix U to a file - torch.save(U, f"tor_matrix_{nmode}_{number_of_sequence}.pt") + torch.save(u, f'tor_matrix_{nmode}_{number_of_sequence}.pt') print('done') diff --git a/examples/benchmarks/benchmark_v420/torontonian/torontonian_cuda_deepquantum.py b/examples/benchmarks/benchmark_v420/torontonian/torontonian_cuda_deepquantum.py index 968027fe..c870dc2d 100644 --- a/examples/benchmarks/benchmark_v420/torontonian/torontonian_cuda_deepquantum.py +++ b/examples/benchmarks/benchmark_v420/torontonian/torontonian_cuda_deepquantum.py @@ -11,39 +11,41 @@ device = 'cuda' -def torontonian_dq(n, l): - A = torch.load(f"tor_matrix_{n}_{1000}.pt").to(device) + +def torontonian_dq(nmode, batch_size): + mat_a = torch.load(f'tor_matrix_{nmode}_{1000}.pt').to(device) def get_torontonian_dq(matrix): trials = 10 - if l == 100 or l == 1000: + if batch_size == 100 or batch_size == 1000: trials = 1 - torontonian(A[0]) + torontonian(matrix[0]) time0 = time.time() for i in range(trials): - results = torch.vmap(torontonian)(A[i*l:(i+1)*l]) + torch.vmap(torontonian)(matrix[i * batch_size : (i + 1) * batch_size]) time1 = time.time() ts = (time1 - time0) / trials return ts - return get_torontonian_dq(A) + return get_torontonian_dq(mat_a) + results = {} platform = 'deepquantum_gpu' -n_list = [2, 6, 10, 14] -l_list = [1, 10, 100, 1000] +nmodes = [2, 6, 10, 14] +batch_sizes = [1, 10, 100, 1000] -for n in tqdm(n_list): - for l in tqdm(l_list): - print(n,l) - ts = torontonian_dq(n, l) - results[str(n)+'+'+str(l)] = ts +for n in tqdm(nmodes): + for bs in tqdm(batch_sizes): + print(n, bs) + ts = torontonian_dq(n, bs) + results[str(n) + '+' + str(bs)] = ts -with open('torontonian_'+platform+'_results.data', 'w') as f: +with open('torontonian_' + platform + '_results.data', 'w') as f: json.dump(results, f) -with open('torontonian_'+platform+'_results.data', 'r') as f: +with open('torontonian_' + platform + '_results.data') as f: print(json.load(f)) diff --git a/examples/benchmarks/benchmark_v420/torontonian/torontonian_deepquantum.py b/examples/benchmarks/benchmark_v420/torontonian/torontonian_deepquantum.py index 4b1cd4e2..5eefd51a 100644 --- a/examples/benchmarks/benchmark_v420/torontonian/torontonian_deepquantum.py +++ b/examples/benchmarks/benchmark_v420/torontonian/torontonian_deepquantum.py @@ -11,39 +11,41 @@ device = 'cpu' -def torontonian_dq(n, l): - A = torch.load(f"tor_matrix_{n}_{1000}.pt").to(device) + +def torontonian_dq(nmode, batch_size): + mat_a = torch.load(f'tor_matrix_{nmode}_{1000}.pt').to(device) def get_torontonian_dq(matrix): trials = 10 - if l == 100 or l == 1000: + if batch_size == 100 or batch_size == 1000: trials = 1 - torontonian(A[0]) + torontonian(matrix[0]) time0 = time.time() for i in range(trials): - results = torch.vmap(torontonian)(A[i*l:(i+1)*l]) + torch.vmap(torontonian)(matrix[i * batch_size : (i + 1) * batch_size]) time1 = time.time() ts = (time1 - time0) / trials return ts - return get_torontonian_dq(A) + return get_torontonian_dq(mat_a) + results = {} platform = 'deepquantum' -n_list = [2, 6, 10, 14, 18] -l_list = [1, 10, 100, 1000] +nmodes = [2, 6, 10, 14, 18] +batch_sizes = [1, 10, 100, 1000] -for n in tqdm(n_list): - for l in tqdm(l_list): - print(n,l) - ts = torontonian_dq(n, l) - results[str(n)+'+'+str(l)] = ts +for n in tqdm(nmodes): + for bs in tqdm(batch_sizes): + print(n, bs) + ts = torontonian_dq(n, bs) + results[str(n) + '+' + str(bs)] = ts -with open('torontonian_'+platform+'_results.data', 'w') as f: +with open('torontonian_' + platform + '_results.data', 'w') as f: json.dump(results, f) -with open('torontonian_'+platform+'_results.data', 'r') as f: +with open('torontonian_' + platform + '_results.data') as f: print(json.load(f)) diff --git a/examples/benchmarks/benchmark_v420/torontonian/torontonian_piquasso.py b/examples/benchmarks/benchmark_v420/torontonian/torontonian_piquasso.py index c0b78a24..b2315c82 100644 --- a/examples/benchmarks/benchmark_v420/torontonian/torontonian_piquasso.py +++ b/examples/benchmarks/benchmark_v420/torontonian/torontonian_piquasso.py @@ -4,45 +4,47 @@ import numpy as np import piquasso import torch -from piquasso._math.torontonian import torontonian, loop_torontonian +from piquasso._math.torontonian import torontonian from tqdm import tqdm # Print version print(piquasso.__version__) -def torontonian_pq(n, l): - A = torch.load(f"tor_matrix_{n}_{1000}.pt").numpy() + +def torontonian_pq(nmode, batch_size): + mat_a = torch.load(f'tor_matrix_{nmode}_{1000}.pt').numpy() def get_torontonian_pq(matrix): trials = 10 - if l == 100 or l == 1000: + if batch_size == 100 or batch_size == 1000: trials = 1 - np.vectorize(torontonian,signature='(n,n)->()')(A[0:1]) + np.vectorize(torontonian, signature='(n,n)->()')(matrix[0:1]) time0 = time.time() for i in range(trials): - results = np.vectorize(torontonian,signature='(n,n)->()')(A[i*l:(i+1)*l]) + np.vectorize(torontonian, signature='(n,n)->()')(matrix[i * batch_size : (i + 1) * batch_size]) time1 = time.time() ts = (time1 - time0) / trials return ts - return get_torontonian_pq(A) + return get_torontonian_pq(mat_a) + results = {} platform = 'piquasso' -n_list = [2, 6, 10, 14, 18] -l_list = [1, 10, 100, 1000] +nmodes = [2, 6, 10, 14, 18] +batch_sizes = [1, 10, 100, 1000] -for n in tqdm(n_list): - for l in tqdm(l_list): - print(n,l) - ts = torontonian_pq(n, l) - results[str(n)+'+'+str(l)] = ts +for n in tqdm(nmodes): + for bs in tqdm(batch_sizes): + print(n, bs) + ts = torontonian_pq(n, bs) + results[str(n) + '+' + str(bs)] = ts -with open('torontonian_'+platform+'_results.data', 'w') as f: +with open('torontonian_' + platform + '_results.data', 'w') as f: json.dump(results, f) -with open('torontonian_'+platform+'_results.data', 'r') as f: +with open('torontonian_' + platform + '_results.data') as f: print(json.load(f)) diff --git a/examples/benchmarks/benchmark_v420/torontonian/torontonian_strawberryfields.py b/examples/benchmarks/benchmark_v420/torontonian/torontonian_strawberryfields.py index 5ba12e2f..d2868da8 100644 --- a/examples/benchmarks/benchmark_v420/torontonian/torontonian_strawberryfields.py +++ b/examples/benchmarks/benchmark_v420/torontonian/torontonian_strawberryfields.py @@ -10,39 +10,41 @@ # Print version print(sf.__version__) -def torontonian_sf(n, l): - A = torch.load(f"tor_matrix_{n}_{1000}.pt").numpy() + +def torontonian_sf(nmode, batch_size): + mat_a = torch.load(f'tor_matrix_{nmode}_{1000}.pt').numpy() def get_torontonian_sf(matrix): trials = 10 - if l == 100 or l == 1000: + if batch_size == 100 or batch_size == 1000: trials = 1 - np.vectorize(tor,signature='(n,n)->()')(A[0:1]) + np.vectorize(tor, signature='(n,n)->()')(matrix[0:1]) time0 = time.time() for i in range(trials): - results = np.vectorize(tor,signature='(n,n)->()')(A[i*l:(i+1)*l]) + np.vectorize(tor, signature='(n,n)->()')(matrix[i * batch_size : (i + 1) * batch_size]) time1 = time.time() ts = (time1 - time0) / trials return ts - return get_torontonian_sf(A) + return get_torontonian_sf(mat_a) + results = {} platform = 'strawberryfields' -n_list = [2, 6, 10, 14] -l_list = [1, 10, 100, 1000] +nmodes = [2, 6, 10, 14] +batch_sizes = [1, 10, 100, 1000] -for n in tqdm(n_list): - for l in tqdm(l_list): - print(n,l) - ts = torontonian_sf(n, l) - results[str(n)+'+'+str(l)] = ts +for n in tqdm(nmodes): + for bs in tqdm(batch_sizes): + print(n, bs) + ts = torontonian_sf(n, bs) + results[str(n) + '+' + str(bs)] = ts -with open('torontonian_'+platform+'_results.data', 'w') as f: +with open('torontonian_' + platform + '_results.data', 'w') as f: json.dump(results, f) -with open('torontonian_'+platform+'_results.data', 'r') as f: +with open('torontonian_' + platform + '_results.data') as f: print(json.load(f)) diff --git a/examples/benchmarks/gradient_benchmark.py b/examples/benchmarks/gradient_benchmark.py index ea5daea3..6b22251a 100644 --- a/examples/benchmarks/gradient_benchmark.py +++ b/examples/benchmarks/gradient_benchmark.py @@ -1,27 +1,24 @@ """ -Gradient evaluation comparison between qiskit, tensorcircuit and deepquantum +Gradient evaluation comparison between qiskit, tensorcircuit, pyvqnet and deepquantum Modified from the implementation of tensorcircuit """ -import time import json +import time from functools import reduce from operator import xor -import numpy as np - -from qiskit.opflow import X, StateFn -from qiskit.circuit import QuantumCircuit, ParameterVector -from qiskit.opflow.gradients import Gradient, Hessian +import deepquantum as dq +import numpy as np +import pyqpanda as pq import tensorcircuit as tc - import torch -from torch.autograd.functional import hessian -import deepquantum as dq - from pyvqnet.qnn import grad from pyvqnet.qnn.measure import expval -import pyqpanda as pq +from qiskit.circuit import ParameterVector, QuantumCircuit +from qiskit.opflow import StateFn, X +from qiskit.opflow.gradients import Gradient, Hessian +from torch.autograd.functional import hessian def benchmark(f, *args, trials=10): @@ -31,22 +28,19 @@ def benchmark(f, *args, trials=10): for _ in range(trials): r = f(*args) time2 = time.time() - if trials > 0: - time21 = (time2 - time1) / trials - else: - time21 = 0 + time21 = (time2 - time1) / trials if trials > 0 else 0 ts = (time1 - time0, time21) - print('staging time: %.6f s' % ts[0]) + print(f'staging time: {ts[0]:.6f} s') if trials > 0: - print('running time: %.6f s' % ts[1]) + print(f'running time: {ts[1]:.6f} s') return r, ts -def grad_qiskit(n, l, trials=2): +def grad_qiskit(n, layer, trials=2): hamiltonian = reduce(xor, [X for _ in range(n)]) wavefunction = QuantumCircuit(n) - params = ParameterVector('theta', length=3 * n * l) - for j in range(l): + params = ParameterVector('theta', length=3 * n * layer) + for j in range(layer): for i in range(n - 1): wavefunction.cnot(i, i + 1) for i in range(n): @@ -65,14 +59,14 @@ def get_grad_qiskit(values): grad_result = grad.assign_parameters(value_dict).eval() return grad_result - return benchmark(get_grad_qiskit, np.ones([3 * n * l]), trials=trials) + return benchmark(get_grad_qiskit, np.ones([3 * n * layer]), trials=trials) -def hessian_qiskit(n, l, trials=0): +def hessian_qiskit(n, layer, trials=0): hamiltonian = reduce(xor, [X for _ in range(n)]) wavefunction = QuantumCircuit(n) - params = ParameterVector("theta", length=3 * n * l) - for j in range(l): + params = ParameterVector('theta', length=3 * n * layer) + for j in range(layer): for i in range(n - 1): wavefunction.cnot(i, i + 1) for i in range(n): @@ -91,13 +85,13 @@ def get_hs_qiskit(values): grad_result = grad.assign_parameters(value_dict).eval() return grad_result - return benchmark(get_hs_qiskit, np.ones([3 * n * l]), trials=trials) + return benchmark(get_hs_qiskit, np.ones([3 * n * layer]), trials=trials) -def grad_tc(n, l, trials=10): +def grad_tc(n, layer, trials=10): def f(params): c = tc.Circuit(n) - for j in range(l): + for j in range(layer): for i in range(n - 1): c.cnot(i, i + 1) for i in range(n): @@ -109,13 +103,13 @@ def f(params): return tc.backend.real(c.expectation(*[[tc.gates.x(), [i]] for i in range(n)])) get_grad_tc = tc.backend.jit(tc.backend.grad(f)) - return benchmark(get_grad_tc, tc.backend.ones([3 * n * l], dtype='float32')) + return benchmark(get_grad_tc, tc.backend.ones([3 * n * layer], dtype='float32')) -def hessian_tc(n, l, trials=10): +def hessian_tc(n, layer, trials=10): def f(params): c = tc.Circuit(n) - for j in range(l): + for j in range(layer): for i in range(n - 1): c.cnot(i, i + 1) for i in range(n): @@ -127,15 +121,15 @@ def f(params): return tc.backend.real(c.expectation(*[[tc.gates.x(), [i]] for i in range(n)])) get_hs_tc = tc.backend.jit(tc.backend.hessian(f)) - return benchmark(get_hs_tc, tc.backend.ones([3 * n * l], dtype='float32')) + return benchmark(get_hs_tc, tc.backend.ones([3 * n * layer], dtype='float32')) -def grad_dq(n, l, trials=10): +def grad_dq(n, layer, trials=10): def get_grad_dq(params): - if params.grad != None: + if params.grad is not None: params.grad.zero_() cir = dq.QubitCircuit(n) - for j in range(l): + for _ in range(layer): for i in range(n - 1): cir.cnot(i, i + 1) cir.rxlayer(encode=True) @@ -147,13 +141,13 @@ def get_grad_dq(params): exp.backward() return params.grad - return benchmark(get_grad_dq, torch.ones([3 * n * l], requires_grad=True)) + return benchmark(get_grad_dq, torch.ones([3 * n * layer], requires_grad=True)) -def hessian_dq(n, l, trials=10): +def hessian_dq(n, layer, trials=10): def f(params): cir = dq.QubitCircuit(n) - for j in range(l): + for _ in range(layer): for i in range(n - 1): cir.cnot(i, i + 1) cir.rxlayer(encode=True) @@ -166,16 +160,16 @@ def f(params): def get_hs_dq(x): return hessian(f, x) - return benchmark(get_hs_dq, torch.ones([3 * n * l])) + return benchmark(get_hs_dq, torch.ones([3 * n * layer])) -def grad_pyvqnet(n, l ,trials=10): +def grad_pyvqnet(n, layer, trials=10): def pqctest(param): machine = pq.CPUQVM() machine.init_qvm() qubits = machine.qAlloc_many(n) circuit = pq.QCircuit() - for j in range(l): + for j in range(layer): for i in range(n - 1): circuit.insert(pq.CNOT(qubits[i], qubits[i + 1])) for i in range(n): @@ -184,50 +178,51 @@ def pqctest(param): circuit.insert(pq.RX(qubits[i], param[3 * n * j + i + 2 * n])) prog = pq.QProg() prog.insert(circuit) - Xn_string = ', '.join([f'X{i}' for i in range(n)]) - pauli_dict = {Xn_string:1.} + xn_string = ', '.join([f'X{i}' for i in range(n)]) + pauli_dict = {xn_string: 1.0} exp = expval(machine, prog, pauli_dict, qubits) return exp def get_grad(values): return grad(pqctest, values) - return benchmark(get_grad, np.ones([3 * n * l]), trials=trials) + return benchmark(get_grad, np.ones([3 * n * layer]), trials=trials) + results = {} for n in [4, 6, 8, 10, 12]: - for l in [2, 4, 6]: - _, ts = grad_qiskit(n, l) - results[str(n) + '-' + str(l) + '-' + 'grad' + '-qiskit'] = ts - _, ts = hessian_qiskit(n, l) - results[str(n) + '-' + str(l) + '-' + 'hs' + '-qiskit'] = ts + for layer in [2, 4, 6]: + _, ts = grad_qiskit(n, layer) + results[str(n) + '-' + str(layer) + '-' + 'grad' + '-qiskit'] = ts + _, ts = hessian_qiskit(n, layer) + results[str(n) + '-' + str(layer) + '-' + 'hs' + '-qiskit'] = ts with tc.runtime_backend('tensorflow'): - _, ts = grad_tc(n, l) - results[str(n) + '-' + str(l) + '-' + 'grad' + '-tc-tf'] = ts - _, ts = hessian_tc(n, l) - results[str(n) + '-' + str(l) + '-' + 'hs' + '-tc-tf'] = ts + _, ts = grad_tc(n, layer) + results[str(n) + '-' + str(layer) + '-' + 'grad' + '-tc-tf'] = ts + _, ts = hessian_tc(n, layer) + results[str(n) + '-' + str(layer) + '-' + 'hs' + '-tc-tf'] = ts with tc.runtime_backend('jax'): - _, ts = grad_tc(n, l) - results[str(n) + '-' + str(l) + '-' + 'grad' + '-tc-jax'] = ts - _, ts = hessian_tc(n, l) - results[str(n) + '-' + str(l) + '-' + 'hs' + '-tc-jax'] = ts + _, ts = grad_tc(n, layer) + results[str(n) + '-' + str(layer) + '-' + 'grad' + '-tc-jax'] = ts + _, ts = hessian_tc(n, layer) + results[str(n) + '-' + str(layer) + '-' + 'hs' + '-tc-jax'] = ts with tc.runtime_backend('pytorch'): - _, ts = grad_tc(n, l) - results[str(n) + '-' + str(l) + '-' + 'grad' + '-tc-pytorch'] = ts - # _, ts = hessian_tc(n, l) - # results[str(n) + '-' + str(l) + '-' + 'hs' + '-tc-pytorch'] = ts - _, ts = grad_dq(n, l) - results[str(n) + '-' + str(l) + '-' + 'grad' + '-dq'] = ts - _, ts = hessian_dq(n, l) - results[str(n) + '-' + str(l) + '-' + 'hs' + '-dq'] = ts - _, ts = grad_pyvqnet(n, l) - results[str(n) + '-' + str(l) + '-' + 'grad' + '-pyvqnet'] = ts + _, ts = grad_tc(n, layer) + results[str(n) + '-' + str(layer) + '-' + 'grad' + '-tc-pytorch'] = ts + # _, ts = hessian_tc(n, layer) + # results[str(n) + '-' + str(layer) + '-' + 'hs' + '-tc-pytorch'] = ts + _, ts = grad_dq(n, layer) + results[str(n) + '-' + str(layer) + '-' + 'grad' + '-dq'] = ts + _, ts = hessian_dq(n, layer) + results[str(n) + '-' + str(layer) + '-' + 'hs' + '-dq'] = ts + _, ts = grad_pyvqnet(n, layer) + results[str(n) + '-' + str(layer) + '-' + 'grad' + '-pyvqnet'] = ts # print(results) with open('gradient_results.data', 'w') as f: json.dump(results, f) -with open('gradient_results.data', 'r') as f: +with open('gradient_results.data') as f: print(json.load(f)) diff --git a/examples/demos/bosonic/breeding_cat/bosonic_breeding.ipynb b/examples/demos/bosonic/breeding_cat/bosonic_breeding.ipynb index 86f3cd2b..ab6bb1c6 100644 --- a/examples/demos/bosonic/breeding_cat/bosonic_breeding.ipynb +++ b/examples/demos/bosonic/breeding_cat/bosonic_breeding.ipynb @@ -143,14 +143,14 @@ "source": [ "r = 1\n", "alpha = torch.tensor(2, dtype=torch.double)\n", - "alpha_prime = (np.cosh(r) + np.sinh(r)) * alpha / 2 # r和d的对易关系\n", + "alpha_prime = (np.cosh(r) + np.sinh(r)) * alpha / 2 # r和d的对易关系\n", "print(alpha_prime)\n", "cir = dq.QumodeCircuit(nmode=1, init_state='vac', backend='bosonic')\n", - "cir.cat(wires=0, r=alpha_prime, theta=0, p=0) # plus cat state\n", + "cir.cat(wires=0, r=alpha_prime, theta=0, p=0) # plus cat state\n", "cir.s(0, r)\n", "cir.to(torch.double)\n", "state = cir()\n", - "sq_cat = dq.BosonicState(state=state, nmode=1) # squeezed plus cat state" + "sq_cat = dq.BosonicState(state=state, nmode=1) # squeezed plus cat state" ] }, { @@ -69747,7 +69747,7 @@ ], "source": [ "marginal_q = sq_cat.marginal(wire=0, phi=0)\n", - "marginal_p = sq_cat.marginal(wire=0, phi=np.pi/2)\n", + "marginal_p = sq_cat.marginal(wire=0, phi=np.pi / 2)\n", "wigner = sq_cat.wigner(wire=0)" ] }, @@ -69783,11 +69783,11 @@ "alpha = torch.tensor(2, dtype=torch.double)\n", "alpha_prime = (np.cosh(r) + np.sinh(r)) * alpha / 2\n", "cir = dq.QumodeCircuit(nmode=2, init_state='vac', backend='bosonic')\n", - "cir.cat(wires=0, r=alpha_prime, theta=0, p=0) # plus cat state\n", - "cir.cat(wires=1, r=alpha_prime, theta=0, p=0) # plus cat state\n", + "cir.cat(wires=0, r=alpha_prime, theta=0, p=0) # plus cat state\n", + "cir.cat(wires=1, r=alpha_prime, theta=0, p=0) # plus cat state\n", "cir.s(0, r)\n", "cir.s(1, r)\n", - "cir.bs(wires=[0,1], inputs=[np.pi/4, 0])\n", + "cir.bs(wires=[0, 1], inputs=[np.pi / 4, 0])\n", "cir.homodyne_p(wires=0)\n", "cir.to(torch.double)\n", "state = cir()\n", @@ -69878,7 +69878,11 @@ "outputs": [], "source": [ "idx = torch.tensor([1, 3])\n", - "bs_lst = [cir.state_measured[0][k][..., idx[:, None], idx], cir.state_measured[1][k][..., idx, :], cir.state_measured[2][k]]\n", + "bs_lst = [\n", + " cir.state_measured[0][k][..., idx[:, None], idx],\n", + " cir.state_measured[1][k][..., idx, :],\n", + " cir.state_measured[2][k],\n", + "]\n", "state_collapse = dq.BosonicState(bs_lst, nmode=1)" ] }, @@ -71845,7 +71849,7 @@ ], "source": [ "marginal_q1 = state_collapse.marginal(wire=0, phi=0)\n", - "marginal_p1 = state_collapse.marginal(wire=0, phi=np.pi/2)" + "marginal_p1 = state_collapse.marginal(wire=0, phi=np.pi / 2)" ] }, { @@ -151312,7 +151316,7 @@ "outputs": [], "source": [ "cir2 = dq.QumodeCircuit(nmode=2, init_state=[state_collapse, state_collapse], backend='bosonic')\n", - "cir2.bs(wires=[0,1], inputs=[np.pi/4, 0.])\n", + "cir2.bs(wires=[0, 1], inputs=[np.pi / 4, 0.0])\n", "cir2.homodyne_p(wires=0)\n", "cir2.to(torch.double)\n", "state2 = cir2()\n", @@ -151341,7 +151345,7 @@ ], "source": [ "for i in range(len(sample2)):\n", - " if abs(sample2[i]-0) < 0.002:\n", + " if abs(sample2[i] - 0) < 0.002:\n", " k = i\n", " print(i, sample2[i])" ] @@ -151367,7 +151371,11 @@ "outputs": [], "source": [ "idx = torch.tensor([1, 3])\n", - "bs_lst2 = [cir2.state_measured[0][k][...,idx[:, None], idx], cir2.state_measured[1][k][...,idx, :], cir2.state_measured[2][k]]\n", + "bs_lst2 = [\n", + " cir2.state_measured[0][k][..., idx[:, None], idx],\n", + " cir2.state_measured[1][k][..., idx, :],\n", + " cir2.state_measured[2][k],\n", + "]\n", "state_collapse2 = dq.BosonicState(bs_lst2, nmode=1)" ] }, @@ -153359,7 +153367,7 @@ ], "source": [ "marginal_q2 = state_collapse2.marginal(wire=0, phi=0)\n", - "marginal_p2 = state_collapse2.marginal(wire=0, phi=np.pi/2)" + "marginal_p2 = state_collapse2.marginal(wire=0, phi=np.pi / 2)" ] }, { diff --git a/examples/demos/bosonic/breeding_cat/bosonic_breeding.py b/examples/demos/bosonic/breeding_cat/bosonic_breeding.py new file mode 100644 index 00000000..f97ee1f8 --- /dev/null +++ b/examples/demos/bosonic/breeding_cat/bosonic_breeding.py @@ -0,0 +1,208 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.19.1 +# kernelspec: +# display_name: dq +# language: python +# name: python3 +# --- + +# %% [markdown] +# # GKP态理论基础 + +# %% [markdown] +# GKP态是光量子计算中一种特殊的量子态,它的全名是Gottesman-Knill-Preskill (GKP) 码态。这个态是一种量子纠错码,用于量子计算中来保护量子信息免受噪声干扰。这种态的核心思想是将量子信息编码在相空间(通常是坐标和动量)中的特定位置,使得即使存在一定的噪声和误差,仍然能够恢复原始信息。在连续变量量子计算中,方形晶格形状的GKP态的Wigner函数由一组狄拉克 $\delta$ 函数决定, +# $$ +# W_{\mathrm{gkp}}^{0}(q,p)=\sum_{s,t--\infty}^{\infty}(-1)^{st}\delta\left(p-\frac{s\sqrt{\pi\hbar}}{2}\right)\delta\left(q-l\sqrt{\pi\hbar}\right). +# $$ +# 可以看到它是一个规律的离散的分布,用坐标本征态的表示如下, +# $$\left|k\right\rangle_{\mathrm{gkp}}=\sum_{s=-\infty}^\infty\left|\sqrt{\pi\hbar}(2s+k)\right\rangle_q,k=\{0,1\}$$ +# 使用GKP态编码量子比特,一般的量子态可以表示成 +# $$|\psi\rangle=\cos\frac{\theta}{2}|0\rangle_{\mathrm{gkp}}+e^{-i\phi}\sin\frac{\theta}{2}|1\rangle_{\mathrm{gkp}}$$ +# 但是在实际模拟中一般会考虑有限能量的GKP态,即作用一个Fock阻尼算符 $\hat{E}(\epsilon)$, +# $$\hat{E}(\epsilon) = e^{-\epsilon \hat{n}}$$ +# 它等效于很多个高斯波包的叠加,对应的包络也是高斯函数,如下图 +# +#
+# +#

+# +#

+#
+# + +# %% [markdown] +# 文章[1]附录中提出了一种基于压缩猫态有效制备近似GKP态的方法,通过breeding技术将两个峰的猫态作为初态然后经过一个分束器,对其中一个模式做正交分量p测量,那么剩下的一个模式会有纠缠坍缩发生,通过对正交分量q做Homodyne测量可以发现坍缩的态是三个峰叠加的非高斯态。 +# +#
+# +#

+# +#

+#
+# +#
+# +#

+# +#

+#
+# +# 将这个坍缩后的量子态作为两模线路的两个输入再次经过一个分束器,对其中一个模式做正交分量p测量,未被测量的部分将会坍缩成五个峰的非高斯态。依次重复上面的过程可以逐步制备出近似GKP态。 +# +#
+# +#

+# +#

+#
+ +# %% [markdown] +# 这里对第一次breeding过程做理论分析, +# 压缩猫态的定义如下 +# $$|\mathrm{sq.~cat}\rangle\propto\left(\hat{D}(\alpha/2)+\hat{D}(-\alpha/2)\right)\hat{S}(r)|\mathrm{vac}\rangle,$$ +# 它的正交分量q方向波函数是两个高斯分布的叠加 +# $$\psi_{\mathrm{sq.~cat}}(q)=\langle q|\mathrm{sq.~cat}\rangle\propto\exp\left(-\frac{\left(q-\frac{\alpha}{\sqrt{2}}\right)^2}{2e^{-2r}}\right)+\exp\left(-\frac{\left(q+\frac{\alpha}{\sqrt{2}}\right)^2}{2e^{-2r}}\right),$$ +# 经过一个分束器然后对第二模做正交分量p测量等价于将对应的量子态投影到基矢$\bra{p_2}$ 上,投影的结果为三个高斯态的叠加 +# $$\int dq_2e^{-ip_mq_2}\Psi(q_1,q_2)\propto e^{-\frac{(q_1+\alpha)^2}{2e^{-2r}}}+2\cos(p_m\alpha)e^{-\frac{q_1^2}{2e^{-2r}}}+e^{-\frac{(q_1-\alpha)^2}{2e^{-2r}}}$$ +# 第一次breeding过程就得到三个峰的非高斯态。 +# + +# %% [markdown] +# # Breeding过程代码模拟 + +# %% +import deepquantum as dq +import numpy as np +import torch + +# %% [markdown] +# ## 压缩猫态的制备 + +# %% [markdown] +# 这里使用Bosonic后端直接制备一个压缩猫态,需要注意的是,和上面压缩猫态理论不同,这里并非先将压缩算符作用到真空态再作用位移算符,而是将压缩算符直接作用到猫态,因此需要将压缩算符和位移算符对易,对易之后位移算符的 $\alpha$ 需要更新为 $\frac{\alpha}{2}(\cosh(r) + \sinh(r))$。 + +# %% +r = 1 +alpha = torch.tensor(2, dtype=torch.double) +alpha_prime = (np.cosh(r) + np.sinh(r)) * alpha / 2 # r和d的对易关系 +print(alpha_prime) +cir = dq.QumodeCircuit(nmode=1, init_state='vac', backend='bosonic') +cir.cat(wires=0, r=alpha_prime, theta=0, p=0) # plus cat state +cir.s(0, r) +cir.to(torch.double) +state = cir() +sq_cat = dq.BosonicState(state=state, nmode=1) # squeezed plus cat state + +# %% +marginal_q = sq_cat.marginal(wire=0, phi=0) +marginal_p = sq_cat.marginal(wire=0, phi=np.pi / 2) +wigner = sq_cat.wigner(wire=0) + +# %% [markdown] +# ## 第一次breeding + +# %% [markdown] +# 将上面的压缩猫态作为两模线路的初态,经过一个50:50的分束器,然后对第一模做正交分量p方向的Homodyne测量,第二模会纠缠坍缩并且在正交分量q方向出现三个峰。 + +# %% +r = 1.5 +alpha = torch.tensor(2, dtype=torch.double) +alpha_prime = (np.cosh(r) + np.sinh(r)) * alpha / 2 +cir = dq.QumodeCircuit(nmode=2, init_state='vac', backend='bosonic') +cir.cat(wires=0, r=alpha_prime, theta=0, p=0) # plus cat state +cir.cat(wires=1, r=alpha_prime, theta=0, p=0) # plus cat state +cir.s(0, r) +cir.s(1, r) +cir.bs(wires=[0, 1], inputs=[np.pi / 4, 0]) +cir.homodyne_p(wires=0) +cir.to(torch.double) +state = cir() +sample = cir.measure_homodyne(shots=5000) + +# %% +cir.draw() + +# %% [markdown] +# 这里为了可重复性,可以挑选出测量值为0的结果 + +# %% +for i in range(len(sample)): + if abs(sample[i]) < 0.001: + k = i + print(i, sample[i]) + +# %% [markdown] +# 通过`cir.state_measured`可以得到测量坍缩后的完整量子态,这里我们只需要做偏迹操作提取第二模的量子态然后验证。 + +# %% +idx = torch.tensor([1, 3]) +bs_lst = [ + cir.state_measured[0][k][..., idx[:, None], idx], + cir.state_measured[1][k][..., idx, :], + cir.state_measured[2][k], +] +state_collapse = dq.BosonicState(bs_lst, nmode=1) + +# %% +marginal_q1 = state_collapse.marginal(wire=0, phi=0) +marginal_p1 = state_collapse.marginal(wire=0, phi=np.pi / 2) + +# %% [markdown] +# 可以从下面的 ``wigner`` 函数看出这里非高斯态已经出现了类似GKP态的图像 + +# %% +wigner = state_collapse.wigner(wire=0) + +# %% [markdown] +# ## 第二次breeding + +# %% [markdown] +# 类似的,将第一次breeding之后测量坍缩的结果作为两模线路的两个输入,经过一个50:50的分束器,然后对第一模做正交分量p方向的Homodyne测量,第二模会纠缠坍缩并且在正交分量q方向出现五个峰。 + +# %% +cir2 = dq.QumodeCircuit(nmode=2, init_state=[state_collapse, state_collapse], backend='bosonic') +cir2.bs(wires=[0, 1], inputs=[np.pi / 4, 0.0]) +cir2.homodyne_p(wires=0) +cir2.to(torch.double) +state2 = cir2() +sample2 = cir2.measure_homodyne(shots=1000) + +# %% +for i in range(len(sample2)): + if abs(sample2[i] - 0) < 0.002: + k = i + print(i, sample2[i]) + +# %% [markdown] +# 通过`cir.state_measured`可以得到测量坍缩后的完整量子态,这里我们只需要做偏迹操作提取第二模的量子态然后验证。 + +# %% +idx = torch.tensor([1, 3]) +bs_lst2 = [ + cir2.state_measured[0][k][..., idx[:, None], idx], + cir2.state_measured[1][k][..., idx, :], + cir2.state_measured[2][k], +] +state_collapse2 = dq.BosonicState(bs_lst2, nmode=1) + +# %% +marginal_q2 = state_collapse2.marginal(wire=0, phi=0) +marginal_p2 = state_collapse2.marginal(wire=0, phi=np.pi / 2) + +# %% [markdown] +# 通过 ``wigner`` 函数的图像可以看到第二次breeding之后的量子态更接近GKP态 + +# %% +wigner = state_collapse2.wigner(wire=0) + +# %% [markdown] +# # 参考文献 + +# %% [markdown] +# [1] Aghaee Rad H, Ainsworth T, Alexander R N, et al. Scaling and networking a modular photonic quantum computer[J]. Nature, 2025: 1-8. diff --git a/examples/demos/gbs/boson_sampling/boson_sampling.ipynb b/examples/demos/gbs/boson_sampling/boson_sampling.ipynb index 0d4feaba..dded6466 100644 --- a/examples/demos/gbs/boson_sampling/boson_sampling.ipynb +++ b/examples/demos/gbs/boson_sampling/boson_sampling.ipynb @@ -18,7 +18,7 @@ } }, "source": [ - "玻色采样由Aaronson和Arkhipov引入[1],它描述了这样一个物理过程:多个全同的单光子通过线性光学器件组成的多模光量子线路相互干涉后, \n", + "玻色采样由Aaronson和Arkhipov引入[1],它描述了这样一个物理过程:多个全同的单光子通过线性光学器件组成的多模光量子线路相互干涉后,\n", "通过多次采样可以得到输出端口对应的概率分布,如下图所示。\n", "\n", "
\n", @@ -71,7 +71,7 @@ "|\\langle n_1,n_2,...,n_N|W|\\psi\\rangle|^2 = \\frac{|Per(U_{st})|^2}{m_1!...m_N!n_1...n_N!}\n", "$$\n", "\n", - "这里的 $U_{st}$ 是通过对 $U$ 取行取列组合来得到,具体来说,根据输入 $|\\psi\\rangle = |m_1, m_2,...,m_N\\rangle$ 取对应的第 $i$ 行并且重复$m_i$ 次,如果 $m_i=0$ 则不取,根据输出 $|n_1,n_2,...,n_N\\rangle$ 取对应的第 $j$ 列并且重复 $m_j$ 次,如果 $m_j=0$ 则不取。 \n", + "这里的 $U_{st}$ 是通过对 $U$ 取行取列组合来得到,具体来说,根据输入 $|\\psi\\rangle = |m_1, m_2,...,m_N\\rangle$ 取对应的第 $i$ 行并且重复$m_i$ 次,如果 $m_i=0$ 则不取,根据输出 $|n_1,n_2,...,n_N\\rangle$ 取对应的第 $j$ 列并且重复 $m_j$ 次,如果 $m_j=0$ 则不取。\n", "\n", "比如下面的2光子玻色采样例子[2]\n", "\n", @@ -141,8 +141,8 @@ "outputs": [], "source": [ "## 构建一个由ps门和bs门组成的4模线路,设置初态为[1,1,0,0]\n", - "import numpy as np\n", - "import deepquantum as dq" + "import deepquantum as dq\n", + "import numpy as np" ] }, { @@ -171,16 +171,16 @@ } ], "source": [ - "init_state = [1,1,0,0]\n", + "init_state = [1, 1, 0, 0]\n", "cir = dq.QumodeCircuit(nmode=4, init_state=init_state, backend='fock')\n", "for k in range(4):\n", - " cir.ps(wires=[k], inputs=np.pi/3)\n", - "cir.bs(wires=[0,1], inputs=[np.pi/4,np.pi/3])\n", - "cir.bs(wires=[2,3], inputs=[np.pi/4,np.pi/3])\n", - "cir.bs(wires=[1,2], inputs=[np.pi/4,np.pi/3])\n", - "cir.bs(wires=[0,1], inputs=[np.pi/3,np.pi/4])\n", - "cir.bs(wires=[2,3], inputs=[np.pi/3,np.pi/4])\n", - "#线路可视化\n", + " cir.ps(wires=[k], inputs=np.pi / 3)\n", + "cir.bs(wires=[0, 1], inputs=[np.pi / 4, np.pi / 3])\n", + "cir.bs(wires=[2, 3], inputs=[np.pi / 4, np.pi / 3])\n", + "cir.bs(wires=[1, 2], inputs=[np.pi / 4, np.pi / 3])\n", + "cir.bs(wires=[0, 1], inputs=[np.pi / 3, np.pi / 4])\n", + "cir.bs(wires=[2, 3], inputs=[np.pi / 3, np.pi / 4])\n", + "# 线路可视化\n", "cir.draw()" ] }, @@ -207,9 +207,9 @@ "source": [ "# 线路进行演化\n", "state = cir()\n", - "#对演化之后的结果采样\n", + "# 对演化之后的结果采样\n", "sample = cir.measure(shots=1024)\n", - "print('final state',state)\n", + "print('final state', state)\n", "print('sample results', sample)" ] }, @@ -242,33 +242,47 @@ "outputs": [], "source": [ "## 计算光量子线路的酉矩阵\n", - "u_ps = np.diag([np.exp(1j*np.pi/3), np.exp(1j*np.pi/3), np.exp(1j*np.pi/3), np.exp(1j*np.pi/3) ])\n", + "u_ps = np.diag([np.exp(1j * np.pi / 3), np.exp(1j * np.pi / 3), np.exp(1j * np.pi / 3), np.exp(1j * np.pi / 3)])\n", "\n", - "u_bs1 = np.array([[np.cos(np.pi/4), -np.exp(-1j*np.pi/3)*np.sin(np.pi/4)],\n", - " [np.exp(1j*np.pi/3)*np.sin(np.pi/4), np.cos(np.pi/4)]])\n", - "u_bs1 = np.block([[u_bs1, np.zeros([2,2])],\n", - " [np.zeros([2,2]), np.eye(2)]])\n", + "u_bs1 = np.array(\n", + " [\n", + " [np.cos(np.pi / 4), -np.exp(-1j * np.pi / 3) * np.sin(np.pi / 4)],\n", + " [np.exp(1j * np.pi / 3) * np.sin(np.pi / 4), np.cos(np.pi / 4)],\n", + " ]\n", + ")\n", + "u_bs1 = np.block([[u_bs1, np.zeros([2, 2])], [np.zeros([2, 2]), np.eye(2)]])\n", "\n", - "u_bs2 = np.array([[np.cos(np.pi/4), -np.exp(-1j*np.pi/3)*np.sin(np.pi/4)],\n", - " [np.exp(1j*np.pi/3)*np.sin(np.pi/4), np.cos(np.pi/4)]])\n", - "u_bs2 = np.block([[np.eye(2), np.zeros([2,2])],\n", - " [np.zeros([2,2]), u_bs2]])\n", + "u_bs2 = np.array(\n", + " [\n", + " [np.cos(np.pi / 4), -np.exp(-1j * np.pi / 3) * np.sin(np.pi / 4)],\n", + " [np.exp(1j * np.pi / 3) * np.sin(np.pi / 4), np.cos(np.pi / 4)],\n", + " ]\n", + ")\n", + "u_bs2 = np.block([[np.eye(2), np.zeros([2, 2])], [np.zeros([2, 2]), u_bs2]])\n", "\n", - "u_bs3 =np.array([[np.cos(np.pi/4), -np.exp(-1j*np.pi/3)*np.sin(np.pi/4)],\n", - " [np.exp(1j*np.pi/3)*np.sin(np.pi/4), np.cos(np.pi/4)]])\n", - "u_bs3 = np.block([[1, np.zeros(2), 0],\n", - " [np.zeros([2,1]),u_bs3, np.zeros([2,1])],\n", - " [0, np.zeros(2), 1]])\n", + "u_bs3 = np.array(\n", + " [\n", + " [np.cos(np.pi / 4), -np.exp(-1j * np.pi / 3) * np.sin(np.pi / 4)],\n", + " [np.exp(1j * np.pi / 3) * np.sin(np.pi / 4), np.cos(np.pi / 4)],\n", + " ]\n", + ")\n", + "u_bs3 = np.block([[1, np.zeros(2), 0], [np.zeros([2, 1]), u_bs3, np.zeros([2, 1])], [0, np.zeros(2), 1]])\n", "\n", - "u_bs4 = np.array([[np.cos(np.pi/3), -np.exp(-1j*np.pi/4)*np.sin(np.pi/3)],\n", - " [np.exp(1j*np.pi/4)*np.sin(np.pi/3), np.cos(np.pi/3)]])\n", - "u_bs4 = np.block([[u_bs4, np.zeros([2,2])],\n", - " [np.zeros([2,2]), np.eye(2)]])\n", + "u_bs4 = np.array(\n", + " [\n", + " [np.cos(np.pi / 3), -np.exp(-1j * np.pi / 4) * np.sin(np.pi / 3)],\n", + " [np.exp(1j * np.pi / 4) * np.sin(np.pi / 3), np.cos(np.pi / 3)],\n", + " ]\n", + ")\n", + "u_bs4 = np.block([[u_bs4, np.zeros([2, 2])], [np.zeros([2, 2]), np.eye(2)]])\n", "\n", - "u_bs5 = np.array([[np.cos(np.pi/3), -np.exp(-1j*np.pi/4)*np.sin(np.pi/3)],\n", - " [np.exp(1j*np.pi/4)*np.sin(np.pi/3), np.cos(np.pi/3)]])\n", - "u_bs5 = np.block([[np.eye(2), np.zeros([2,2])],\n", - " [np.zeros([2,2]), u_bs5]])\n", + "u_bs5 = np.array(\n", + " [\n", + " [np.cos(np.pi / 3), -np.exp(-1j * np.pi / 4) * np.sin(np.pi / 3)],\n", + " [np.exp(1j * np.pi / 4) * np.sin(np.pi / 3), np.cos(np.pi / 3)],\n", + " ]\n", + ")\n", + "u_bs5 = np.block([[np.eye(2), np.zeros([2, 2])], [np.zeros([2, 2]), u_bs5]])\n", "\n", "u_total = u_bs5 @ u_bs4 @ u_bs3 @ u_bs2 @ u_bs1 @ u_ps" ] @@ -302,8 +316,8 @@ } ], "source": [ - "out_state = [1,1,0,0]\n", - "u_sub = u_total[:2][:,:2]\n", + "out_state = [1, 1, 0, 0]\n", + "u_sub = u_total[:2][:, :2]\n", "print(u_sub)" ] }, @@ -340,9 +354,9 @@ } ], "source": [ - "per = u_sub[0,0]*u_sub[1,1] + u_sub[0,1]*u_sub[1,0]\n", + "per = u_sub[0, 0] * u_sub[1, 1] + u_sub[0, 1] * u_sub[1, 0]\n", "amp = per\n", - "prob = abs(per)**2\n", + "prob = abs(per) ** 2\n", "print(amp, state[dq.FockState(out_state)])" ] }, diff --git a/examples/demos/gbs/boson_sampling/boson_sampling.py b/examples/demos/gbs/boson_sampling/boson_sampling.py new file mode 100644 index 00000000..480c828b --- /dev/null +++ b/examples/demos/gbs/boson_sampling/boson_sampling.py @@ -0,0 +1,210 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.19.1 +# kernelspec: +# display_name: dq_v3 +# language: python +# name: dq_v3 +# --- + +# %% [markdown] +# # 玻色采样(Boson Sampling) + +# %% [markdown] +# 玻色采样由Aaronson和Arkhipov引入[1],它描述了这样一个物理过程:多个全同的单光子通过线性光学器件组成的多模光量子线路相互干涉后, +# 通过多次采样可以得到输出端口对应的概率分布,如下图所示。 +# +#
+# +#

+# +#

+#
+# +# 同时玻色采样的概率分布可以由理论计算得到,数学上对应着积和式(permanent)的计算。 +# +# 一个 $n \times n$ 矩阵 $A$ 的积和式的定义如下, +# +# $$ +# \mathrm{Perm}(A) = \sum_{\sigma\in S_n}\prod_{i=1}^n a_{i,\sigma_i} +# $$ +# +# 其中 $S_n$ 为 $n$ 阶置换群,即包含所有 $n$ 元排列的集合,$n=2$ 时 +# +# $$ +# A= \begin{pmatrix} +# a_{11}&a_{12}\\ +# a_{21}&a_{22} +# \end{pmatrix}, +# \ \ \ Perm(A) = a_{11}a_{22}+a_{12}a_{21} +# $$ +# +# 对玻色采样的精确模拟需要精确地求解积和式这一 $\#P$ 难的问题。而即便在近似条件下模拟玻色采样,Aaronson 等人同样证明了其困难性。 +# +# 假设输入的量子态为 $N$ 模的Fock态 $|\psi\rangle$,$|\psi\rangle = |m_1, m_2,...,m_N\rangle$, $U$ 表示光量子线路对应的酉矩阵, 对应的生成算符变换如下: +# +# $$(\hat{a}^+_{out})_k = \sum_{i=0}^NU_{kj}(\hat{a}^+_{in})_j$$ +# +# 探测到特定的量子态组合 $|n_1,n_2,...,n_N\rangle$ 的概率为 +# +# $$ +# |\langle n_1,n_2,...,n_N|W|\psi \rangle|^2 +# $$ +# +# 这里 $W$ 表示 $U$ 对量子态的作用,因为在光量子线路中的酉矩阵 $U$ 直接作用对象是生成算符和湮灭算符,所以需要 $W$ 表示对量子态的作用,具体的, +# 输出的振幅可以表示成 +# +# $$ +# \langle n_1,n_2,...,n_N|W|\psi\rangle = \frac{Per(U_{st})}{\sqrt{m_1!...m_N!n_1...n_N!}} +# $$ +# +# 输出的概率可以写成 +# +# $$ +# |\langle n_1,n_2,...,n_N|W|\psi\rangle|^2 = \frac{|Per(U_{st})|^2}{m_1!...m_N!n_1...n_N!} +# $$ +# +# 这里的 $U_{st}$ 是通过对 $U$ 取行取列组合来得到,具体来说,根据输入 $|\psi\rangle = |m_1, m_2,...,m_N\rangle$ 取对应的第 $i$ 行并且重复$m_i$ 次,如果 $m_i=0$ 则不取,根据输出 $|n_1,n_2,...,n_N\rangle$ 取对应的第 $j$ 列并且重复 $m_j$ 次,如果 $m_j=0$ 则不取。 +# +# 比如下面的2光子玻色采样例子[2] +# +#
+# +#

+# +#

+#
+# +# 假设两个光子从1、2端口输入,那么从2、3端口输出的的概率 $P_{2,3}$ 如下, +# +# $$ +# P_{2,3} = U_{1,2}U_{2,3} + U_{1,3}U_{2,2} =\mathrm{Perm}(U_{sub}) = \mathrm{Perm}\begin{pmatrix}U_{1,2} & U_{2,2}\\ +# U_{1,3} & U_{2,3}\end{pmatrix} +# $$ +# +# $U_{sub}$ 是对应的酉矩阵 $U$ 取第1、2行和第2、3列构成的子矩阵的转置。 +# +# +# +# 在量子模拟中,玻色采样可以用来模拟多体量子系统的动力学行为,玻色采样被用作证明量子计算机超越经典计算机能力的一种方式,即所谓的量子优越性。 +# + +# %% [markdown] +# 我们以下面的4模线路为例来演示玻色采样 +#
+# +#

+# +#

+#
+ +# %% [markdown] +# # 代码示例 + +# %% +## 构建一个由ps门和bs门组成的4模线路,设置初态为[1,1,0,0] +import deepquantum as dq +import numpy as np + +# %% +init_state = [1, 1, 0, 0] +cir = dq.QumodeCircuit(nmode=4, init_state=init_state, backend='fock') +for k in range(4): + cir.ps(wires=[k], inputs=np.pi / 3) +cir.bs(wires=[0, 1], inputs=[np.pi / 4, np.pi / 3]) +cir.bs(wires=[2, 3], inputs=[np.pi / 4, np.pi / 3]) +cir.bs(wires=[1, 2], inputs=[np.pi / 4, np.pi / 3]) +cir.bs(wires=[0, 1], inputs=[np.pi / 3, np.pi / 4]) +cir.bs(wires=[2, 3], inputs=[np.pi / 3, np.pi / 4]) +# 线路可视化 +cir.draw() + +# %% +# 线路进行演化 +state = cir() +# 对演化之后的结果采样 +sample = cir.measure(shots=1024) +print('final state', state) +print('sample results', sample) + +# %% [markdown] +# 根据前面的讨论可以知道输出的概率是可以理论计算的,下面我们将分步计算输出的概率并验证 + +# %% [markdown] +# 1. 计算光量子线路对应的酉矩阵 + +# %% +## 计算光量子线路的酉矩阵 +u_ps = np.diag([np.exp(1j * np.pi / 3), np.exp(1j * np.pi / 3), np.exp(1j * np.pi / 3), np.exp(1j * np.pi / 3)]) + +u_bs1 = np.array( + [ + [np.cos(np.pi / 4), -np.exp(-1j * np.pi / 3) * np.sin(np.pi / 4)], + [np.exp(1j * np.pi / 3) * np.sin(np.pi / 4), np.cos(np.pi / 4)], + ] +) +u_bs1 = np.block([[u_bs1, np.zeros([2, 2])], [np.zeros([2, 2]), np.eye(2)]]) + +u_bs2 = np.array( + [ + [np.cos(np.pi / 4), -np.exp(-1j * np.pi / 3) * np.sin(np.pi / 4)], + [np.exp(1j * np.pi / 3) * np.sin(np.pi / 4), np.cos(np.pi / 4)], + ] +) +u_bs2 = np.block([[np.eye(2), np.zeros([2, 2])], [np.zeros([2, 2]), u_bs2]]) + +u_bs3 = np.array( + [ + [np.cos(np.pi / 4), -np.exp(-1j * np.pi / 3) * np.sin(np.pi / 4)], + [np.exp(1j * np.pi / 3) * np.sin(np.pi / 4), np.cos(np.pi / 4)], + ] +) +u_bs3 = np.block([[1, np.zeros(2), 0], [np.zeros([2, 1]), u_bs3, np.zeros([2, 1])], [0, np.zeros(2), 1]]) + +u_bs4 = np.array( + [ + [np.cos(np.pi / 3), -np.exp(-1j * np.pi / 4) * np.sin(np.pi / 3)], + [np.exp(1j * np.pi / 4) * np.sin(np.pi / 3), np.cos(np.pi / 3)], + ] +) +u_bs4 = np.block([[u_bs4, np.zeros([2, 2])], [np.zeros([2, 2]), np.eye(2)]]) + +u_bs5 = np.array( + [ + [np.cos(np.pi / 3), -np.exp(-1j * np.pi / 4) * np.sin(np.pi / 3)], + [np.exp(1j * np.pi / 4) * np.sin(np.pi / 3), np.cos(np.pi / 3)], + ] +) +u_bs5 = np.block([[np.eye(2), np.zeros([2, 2])], [np.zeros([2, 2]), u_bs5]]) + +u_total = u_bs5 @ u_bs4 @ u_bs3 @ u_bs2 @ u_bs1 @ u_ps + +# %% [markdown] +# 2. 计算输出结果及对应的子矩阵 + +# %% +out_state = [1, 1, 0, 0] +u_sub = u_total[:2][:, :2] +print(u_sub) + +# %% [markdown] +# 3. 计算子矩阵对应的permanent可以得到对应概率 + +# %% +per = u_sub[0, 0] * u_sub[1, 1] + u_sub[0, 1] * u_sub[1, 0] +amp = per +prob = abs(per) ** 2 +print(amp, state[dq.FockState(out_state)]) + +# %% [markdown] +# # 附录 + +# %% [markdown] +# [1] Scott Aaronson and Alex Arkhipov. The computational complexity of linear optics. Theory of Computing, 9(1):143–252, 2013. doi:10.4086/toc.2013.v009a004. +# +# [2] Gard, B. T., Motes, K. R., Olson, J. P., Rohde, P. P., & Dowling, J. P. (2015). An introduction to boson-sampling. In From atomic to mesoscale: The role of quantum coherence in systems of various complexities (pp. 167-192). diff --git a/examples/demos/gbs/dense_graph/GBS_dense_graph_problem.ipynb b/examples/demos/gbs/dense_graph/gbs_dense_graph_problem.ipynb similarity index 94% rename from examples/demos/gbs/dense_graph/GBS_dense_graph_problem.ipynb rename to examples/demos/gbs/dense_graph/gbs_dense_graph_problem.ipynb index 79ec1a8a..88bd2bdf 100644 --- a/examples/demos/gbs/dense_graph/GBS_dense_graph_problem.ipynb +++ b/examples/demos/gbs/dense_graph/gbs_dense_graph_problem.ipynb @@ -15,7 +15,7 @@ "source": [ "1. 图和子图\n", "\n", - "数学上图 $G$ 的定义如下: \n", + "数学上图 $G$ 的定义如下:\n", "\n", "$$G=(V,E)$$\n", "\n", @@ -78,12 +78,12 @@ }, "outputs": [], "source": [ + "from collections import defaultdict\n", + "\n", "import deepquantum.photonic as dqp\n", "import networkx as nx\n", "import numpy as np\n", "import torch\n", - "\n", - "from collections import defaultdict\n", "from strawberryfields.apps.subgraph import resize" ] }, @@ -126,7 +126,8 @@ "ExecuteTime": { "end_time": "2024-07-11T07:30:46.958983Z", "start_time": "2024-07-11T07:30:46.947021Z" - } + }, + "lines_to_next_cell": 2 }, "outputs": [ { @@ -141,7 +142,6 @@ } ], "source": [ - "\n", "a = dqp.utils.load_adj('densegraph_adj')\n", "graph = nx.from_numpy_array(a)\n", "s = range(16)\n", @@ -164,7 +164,8 @@ "ExecuteTime": { "end_time": "2024-06-03T07:58:13.348022Z", "start_time": "2024-06-03T07:58:13.344483Z" - } + }, + "lines_to_next_cell": 2 }, "source": [ "量子-经典混合算法中先通过高斯玻色采样得到概率较高的样本然后转化成对应的子图,这些子图可以作为经典算法的搜索起点,可以有效的提高最后结果的准确度。\n", @@ -175,7 +176,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "4882e3cd", "metadata": { "ExecuteTime": { @@ -188,22 +189,19 @@ }, "outputs": [], "source": [ - "def search_subgpraph(samples: list,\n", - " graph: nx.Graph,\n", - " min_size: int,\n", - " max_size: int):\n", + "def search_subgpraph(samples: list, graph: nx.Graph, min_size: int, max_size: int):\n", " \"\"\"Get the densest subgraph with size in [min_size, max_size],\n", - " using classical algorithm with samples from GBS\n", + " using classical algorithm with samples from GBS\n", " \"\"\"\n", " dic_list = defaultdict(list)\n", " for i in range(len(samples)):\n", - " temp= samples[i]\n", + " temp = samples[i]\n", " num = 1\n", - " for key in temp.keys():\n", - " if num < 50: # only need 50 samples\n", + " for key in temp:\n", + " if num < 50: # only need 50 samples\n", " idx = torch.nonzero(torch.tensor(key)).squeeze()\n", " r = resize(idx.tolist(), graph, min_size=min_size, max_size=max_size)\n", - " for j in range(min_size, max_size+2, 2):\n", + " for j in range(min_size, max_size + 2, 2):\n", " density = nx.density(graph.subgraph(r[j]))\n", " temp_value = (r[j], np.round(density, 5))\n", " if temp_value not in dic_list[j]:\n", @@ -214,7 +212,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "03bffcc6", "metadata": { "ExecuteTime": { @@ -234,7 +232,7 @@ "source": [ "# 后处理得到节点数为8和10个子图对应的样本\n", "sample_re = dqp.utils.load_sample('densegraph_sample')\n", - "gbs = dqp.GBS_Graph(adj_mat=torch.tensor(a, dtype = torch.float64), cutoff=2)\n", + "gbs = dqp.GraphGBS(adj_mat=torch.tensor(a, dtype=torch.float64), cutoff=2)\n", "state = gbs()\n", "subgraph_sample = gbs.postselect(sample_re, [8, 10])\n", "\n", @@ -284,7 +282,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "93be9a37", "metadata": { "ExecuteTime": { @@ -304,8 +302,8 @@ "source": [ "subgraph_sample = gbs.postselect(sample_re, [6])\n", "subgraph_density = gbs.graph_density(graph, subgraph_sample[0])\n", - "key = list(subgraph_density.keys())\n", - "print(key[0],subgraph_density[key[0]])" + "key = list(subgraph_density)\n", + "print(key[0], subgraph_density[key[0]])" ] }, { diff --git a/examples/demos/gbs/dense_graph/gbs_dense_graph_problem.py b/examples/demos/gbs/dense_graph/gbs_dense_graph_problem.py new file mode 100644 index 00000000..02fca4f4 --- /dev/null +++ b/examples/demos/gbs/dense_graph/gbs_dense_graph_problem.py @@ -0,0 +1,159 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.19.1 +# kernelspec: +# display_name: dq_v3 +# language: python +# name: dq_v3 +# --- + +# %% [markdown] +# # 高斯玻色采样应用到稠密子图问题 + +# %% [markdown] +# 1. 图和子图 +# +# 数学上图 $G$ 的定义如下: +# +# $$G=(V,E)$$ +# +# 集合V中的元素称为节点,集合E中的元素时两个节点组成的无序对,称为边。 +# 集合V称为点集,E称为边集。 +# 在图的定义中边的概念定义了节点上的一个对称关系,即邻接关系(adjacency relation)。对于两个节点 $x$,$y$,如果 $(x,y)$ 是一条边,则称他们是邻接的,因此一张图可以用一个 $n\times n$ 的邻接矩阵A来表示。比如对于四个节点的全连接图对应的A如下。 +# $$A =\begin{pmatrix} +# 0&1&1&1\\ +# 1&0&1&1\\ +# 1&1&0&1\\ +# 1&1&1&0 +# \end{pmatrix}$$ +# +# haf(A) = 3, 表示完美匹配数为3。 +# +# 子图对应的点集和边集分别是图G的点集的子集和边集的子集,稠密子图直观上对应着连接密集的子图,图密度的定义如下 +# +# $$d(G) = \frac{2|E|}{|V|(|V|-1)}$$ +# +# $|E|$ 表示对应的边的条数,$|V|$ 表示对应的节点个数。 +# 那么稠密子图就对应着图密度很大的子图。 + +# %% [markdown] +# 2. GBS采样和稠密子图 +# +# 参考文献[1]中讨论了图 $G$ 的完美匹配数和图密度 $d(G)$ 的关系,hafnian的计算对应图 $G$ 的完美匹配数,那么hafnian值越大那么图的稠密度越高。 +# 由前面讨论可知高斯玻色采样下hafnian值越大也对应着采样概率越高,即概率越高的样本对应的子图的稠密度越大。 +# 在使用粒子数分辨探测器时,通过后选择对采样的样本筛选出只有0,1的结果,这些结果中出现概率较高的Fock态所映射的子图就对应了稠密子图。 +# 同时还可以用经典算法来寻找稠密子图,这里用到的经典算法如下, +# +# a. 选择子图规模大小 $[k_{min},k_{max}]$。 +# +# b. 子图规模从大到小搜索(shrinking),从全图开始,对于 $k>k_{min}$,每次搜索随机删除一个连接数最少的节点,剩下的节点组合成当前规模下的稠密子图。 +# +# c. 子图规模从小到大搜索(growth),对于 $k +# +#

+# +#

+#
+# +# 这里的经典算法是基于贪心算法实现的,即每一次迭代寻找连接数最少的那个节点然后移除就可得到当前规模下的稠密子图, 但是如果有多个节点的连接数相同,那么它会随机选择一个节点移除,这就导致了目标稠密子图包含的节点有可能被移除,最终导致得到的结果有偏差。 + +# %% +a = dqp.utils.load_adj('densegraph_adj') +graph = nx.from_numpy_array(a) +s = range(16) +r = resize(s, graph, min_size=1, max_size=15) +r[6], nx.density(graph.subgraph(r[6])) + + +# %% [markdown] +# ### 量子-经典混合算法 + +# %% [markdown] +# 量子-经典混合算法中先通过高斯玻色采样得到概率较高的样本然后转化成对应的子图,这些子图可以作为经典算法的搜索起点,可以有效的提高最后结果的准确度。 +# +# 这里先读取已有的高斯玻色采样数据,采样次数为十万次,``gbs.postselect`` 函数先挑出那些对应子图节点数为8、10的样本,然后将这些样本子图作为经典搜索算法的起点,可以得到一个最终收敛到节点为6的子图字典。 +# 字典中包含了节点数为6的图密度较大的多个子图, 我们取图密度最大的那个子图就是最终的结果。 + + +# %% code_folding=[0] +def search_subgpraph(samples: list, graph: nx.Graph, min_size: int, max_size: int): + """Get the densest subgraph with size in [min_size, max_size], + using classical algorithm with samples from GBS + """ + dic_list = defaultdict(list) + for i in range(len(samples)): + temp = samples[i] + num = 1 + for key in temp: + if num < 50: # only need 50 samples + idx = torch.nonzero(torch.tensor(key)).squeeze() + r = resize(idx.tolist(), graph, min_size=min_size, max_size=max_size) + for j in range(min_size, max_size + 2, 2): + density = nx.density(graph.subgraph(r[j])) + temp_value = (r[j], np.round(density, 5)) + if temp_value not in dic_list[j]: + dic_list[j].append(temp_value) + num = num + 1 + return dic_list + + +# %% +# 后处理得到节点数为8和10个子图对应的样本 +sample_re = dqp.utils.load_sample('densegraph_sample') +gbs = dqp.GraphGBS(adj_mat=torch.tensor(a, dtype=torch.float64), cutoff=2) +state = gbs() +subgraph_sample = gbs.postselect(sample_re, [8, 10]) + +# 采用shrinking 方法得到节点数为6和8的稠密子图 +dense_sub_graph = search_subgpraph(subgraph_sample, graph, min_size=6, max_size=8) +print(dense_sub_graph[6][0]) + +# %% [markdown] +# ### 量子算法 + +# %% [markdown] +# 量子算法直接将高斯玻色采样后的6个节点对应的样本挑选出来处理,因为根据前面的讨论可以知道,对应的子图越稠密那么其样本出现的概率也就越大。 +# 这里先读取已有的高斯玻色采样数据,采样次数为十万次,``gbs.postselect`` 函数先挑出那些对应子图节点数为6的样本,然后``gbs.graph_density`` 函数将 +# 这些样本映射成子图再计算子图的图密度,最后按图密度从大到小排列给出对应的子图及其图密度。 +# 从最后的结果可以看到,高斯玻色采样成功采到了图密度最高的6个节点的子图,对应的图密度为0.9333,对应的节点为[10,11,12,13,14,15]。 + +# %% +sample_re = dqp.utils.load_sample('densegraph_sample') + +# %% +subgraph_sample = gbs.postselect(sample_re, [6]) +subgraph_density = gbs.graph_density(graph, subgraph_sample[0]) +key = list(subgraph_density) +print(key[0], subgraph_density[key[0]]) + +# %% [markdown] +# ## 附录 + +# %% [markdown] +# [1] J. M. Arrazola and T. R. Bromley, Physical Review Letters 121, 030503 (2018). diff --git a/examples/demos/gbs/gaussian_boson_sampling/gaussian_boson_sampling.ipynb b/examples/demos/gbs/gaussian_boson_sampling/gaussian_boson_sampling.ipynb index e15a9834..1f16fbde 100644 --- a/examples/demos/gbs/gaussian_boson_sampling/gaussian_boson_sampling.ipynb +++ b/examples/demos/gbs/gaussian_boson_sampling/gaussian_boson_sampling.ipynb @@ -205,14 +205,14 @@ } ], "source": [ - "import numpy as np\n", "import deepquantum as dq\n", + "import numpy as np\n", "\n", - "squeezing = [1]*6\n", + "squeezing = [1] * 6\n", "unitary = np.eye(6)\n", "gbs = dq.photonic.GaussianBosonSampling(nmode=6, squeezing=squeezing, unitary=unitary)\n", "gbs()\n", - "gbs.draw() #画出采样线路" + "gbs.draw() # 画出采样线路" ] }, { diff --git a/examples/demos/gbs/gaussian_boson_sampling/gaussian_boson_sampling.py b/examples/demos/gbs/gaussian_boson_sampling/gaussian_boson_sampling.py new file mode 100644 index 00000000..35b3fbcc --- /dev/null +++ b/examples/demos/gbs/gaussian_boson_sampling/gaussian_boson_sampling.py @@ -0,0 +1,186 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.19.1 +# kernelspec: +# display_name: dq_draw +# language: python +# name: python3 +# --- + +# %% [markdown] +# # 高斯玻色采样(Gaussian Boson Sampling) + +# %% [markdown] +# ## 数学背景 + +# %% [markdown] +#
+# +#

+# +#

+#
+# +# 高斯玻色采样(GBS)可以作为玻色采样的一种变体,不同之处在于输入的量子态是高斯压缩态而不是离散的Fock态。 +# 压缩态是高斯态的一种,高斯态指的是这个量子态对应的Wigner函数是高斯分布,比如相干态。 +# 单模压缩态的Wigner函数对应的高斯分布在 $X$,$P$ 两个正交分量上会压缩或者拉伸,单模压缩态可以将压缩门作用到真空态上得到,也可以用下面的Fock态基矢展开[2],需要注意的是这里的Fock态光子数从0到无穷大取偶数,因此输出的量子态的空间是无限大且只包含偶数光子数的Fock态空间。 +# +#
+# +#

+# +#

+#
+# +# GBS采样概率的理论计算和玻色采样类似,不同之处在于对粒子数分辨探测器和阈值探测器两种情况,分别需要用hafnian函数和torotonian函数来计算。 + +# %% [markdown] +# 1. 粒子数分辨探测器 +# +# 在探测端口使用粒子数分辨探测器时对应数学上需要计算hafnian函数, +# 对于 $2m\times 2m$ 对称矩阵 $A$ 的hafnian定义如下[3], +# +#
+# +#

+# +#

+#
+# +# 这里PMP表示所有完美匹配排列的集合,当 $n=4$ 时,$PMP(4) = {(0,1)(2,3),(0,2)(1,3),(0,3)(1,2)}$,对应的矩阵 $B$ 对应的hafnian如下 +# +# $$ +# haf(B) = B_{0,1}B_{2,3}+B_{0,2}B_{1,3} + B_{0,3}B_{1,2} +# $$ +# +# 在图论中,hafnian计算了图 $G$ 对应的邻接矩阵A描述的图的完美匹配数(这里图 $G$ 是无权重,无环的无向图),比如邻接矩阵 +# $A =\begin{pmatrix} +# 0&1&1&1\\ +# 1&0&1&1\\ +# 1&1&0&1\\ +# 1&1&1&0 +# \end{pmatrix}$,haf(A)=3,对应的完美匹配图如下。 +# +#
+# +#

+# +#

+#
+# +# 当计算的图是二分图时,得到的hafnian计算结果就是permanent。 +# +#
+# +#

+# +#

+#
+# +# 因此任何计算hafnian的算法也可以用来计算permanent,同样的计算hafnian也是 $\#P$ 难问题。 +# +# 对于粒子数探测器,输出的Fock态 $S = (s_1, s_2,..,s_m)$ 时,对应的 $s_i=0,1,2...$, +# 输出态的概率理论计算如下 +# +#
+# +#

+# +#

+#
+# +# 这里 $Q,A,X$ 的定义如下, +# +#
+# +#

+# +#

+#
+# +# $Q,A$ 由输出量子态的协方差矩阵 $\Sigma$ 决定 ( $\Sigma$ 描述的是 $a,a^+$ 的协方差矩阵),子矩阵 $A_s$ +# 由输出的Fock态决定,具体来说取矩阵 $A$ 的 $i, i+m$ 行和列并且重复 $s_i$ 次来构造 $A_s$ 。 +# 如果 $s_i=0$,那么就不取对应的行和列,如果所有的 $s_i=1$, 那么对应的子矩阵 $A_s = A$。 +# +# 考虑高斯态是纯态的时候, 矩阵$A$可以写成直和的形式,$A = B \oplus B^*$, $B$ 是 $m\times m$ 的对称矩阵。这种情况下输出Fock态的概率如下 +# +#
+# +#

+# +#

+#
+# +# 这里的子矩阵 $B_s$ 通过取 $i$ 行和 $i$ 列并且重复 $s_i$ 次来构造,同时这里hafnian函数计算的矩阵维度减半,可以实现概率计算的加速。 +# +# 当所有模式输出的光子数 $s_i = 0,1$ 时,对应的 $A_s$ 是A的子矩阵,也对应到邻接矩阵A对应的图 $G$ 的子图,利用这个性质可以解决很多子图相关的问题,比如稠密子图,最大团问题等。 + +# %% [markdown] +# 2. 阈值探测器 +# +# 使用阈值探测器时对应的输出Fock态概率 $S = (s_1, s_2,..,s_m),s_i\in \{0,1\}$,此时理论概率的计算需要用到Torontonian函数[4] +# +#
+# +#

+# +#

+#
+#
+# +#

+# +#

+#
+# +# 这里 $O_s = I-(\Sigma^{-1})_s$,直观上来看对于阈值探测器对应的特定的Fock态输出只需要将粒子数分辨探测器对应的多个Fock态概率求和即可。 +# + +# %% [markdown] +# ## 代码演示 + +# %% [markdown] +# 下面简单演示6个模式的高斯玻色采样任务 + +# %% +import deepquantum as dq +import numpy as np + +squeezing = [1] * 6 +unitary = np.eye(6) +gbs = dq.photonic.GaussianBosonSampling(nmode=6, squeezing=squeezing, unitary=unitary) +gbs() +gbs.draw() # 画出采样线路 + +# %% [markdown] +# 设置粒子数分辨探测器开始采样并输出Fock态结果 + +# %% +gbs.detector = 'pnrd' +result = gbs.measure(shots=1024, mcmc=True) +print(result) + +# %% [markdown] +# 设置阈值探测器开始采样并输出Fock态结果 + +# %% +gbs.detector = 'threshold' +result = gbs.measure(shots=1024, mcmc=True) +print(result) + +# %% [markdown] +# ## 附录 + +# %% [markdown] +# [1] Lvovsky, Alexander I. "Squeezed light." Photonics: Scientific Foundations, Technology and Applications 1 (2015): 121-163. +# +# [2]Bromley, Thomas R., et al. "Applications of near-term photonic quantum computers: software and algorithms." Quantum Science and Technology 5.3 (2020): 034010. +# +# [3]Quesada, Nicolás, Juan Miguel Arrazola, and Nathan Killoran. "Gaussian boson sampling using threshold detectors." Physical Review A 98.6 (2018): 062322. +# +# [4]J. M. Arrazola and T. R. Bromley, Physical Review Letters 121, 030503 (2018) diff --git a/examples/demos/gbs/gbs_clustering/GBS_clustering.ipynb b/examples/demos/gbs/gbs_clustering/gbs_clustering.ipynb similarity index 88% rename from examples/demos/gbs/gbs_clustering/GBS_clustering.ipynb rename to examples/demos/gbs/gbs_clustering/gbs_clustering.ipynb index 8f94b17e..ffe18127 100644 --- a/examples/demos/gbs/gbs_clustering/GBS_clustering.ipynb +++ b/examples/demos/gbs/gbs_clustering/gbs_clustering.ipynb @@ -18,7 +18,7 @@ } }, "source": [ - "聚类算法[1]是一种无监督学习算法,用于将数据集中的对象分组成为多个类别(或簇),使得同一类别内的对象彼此相似,而不同类别之间的对象则不相似。聚类算法的目标是发现数据中的内在结构,而无需预先标记数据。常见的经典聚类算法有K均值聚类 (K-Means Clustering), 密度聚类 (DBSCAN), 谱聚类 (Spectral Clustering)等方法。 " + "聚类算法[1]是一种无监督学习算法,用于将数据集中的对象分组成为多个类别(或簇),使得同一类别内的对象彼此相似,而不同类别之间的对象则不相似。聚类算法的目标是发现数据中的内在结构,而无需预先标记数据。常见的经典聚类算法有K均值聚类 (K-Means Clustering), 密度聚类 (DBSCAN), 谱聚类 (Spectral Clustering)等方法。" ] }, { @@ -101,11 +101,11 @@ }, "outputs": [], "source": [ + "import itertools\n", + "\n", "import deepquantum.photonic as dqp\n", - "import matplotlib.pyplot as plt\n", "import networkx as nx\n", "import numpy as np\n", - "import itertools\n", "import torch" ] }, @@ -119,7 +119,7 @@ } }, "source": [ - "这里采用西瓜数据集进行聚类, 每一个数据点包含密度和含糖率两个特征, 分别对应data_ws第一行和第二行。 " + "这里采用西瓜数据集进行聚类, 每一个数据点包含密度和含糖率两个特征, 分别对应data_ws第一行和第二行。" ] }, { @@ -130,13 +130,55 @@ "ExecuteTime": { "end_time": "2024-07-11T07:32:53.880374Z", "start_time": "2024-07-11T07:32:53.874393Z" - } + }, + "lines_to_next_cell": 2 }, "outputs": [], "source": [ - "data_ws = np.array([[0.697,0.774,0.634,0.608,0.556,0.393,0.451,0.427,0.666,0.243,0.245,0.343,0.639,0.657,0.725,0.593, 0.6223, 0.75 ],\n", - "\n", - " [0.46,0.376,0.264,0.318,0.215,0.237,0.149,0.211,0.091,0.267,0.057,0.099,0.161,0.198,0.445,0.082, 0.062,0.405 ]])" + "data_ws = np.array(\n", + " [\n", + " [\n", + " 0.697,\n", + " 0.774,\n", + " 0.634,\n", + " 0.608,\n", + " 0.556,\n", + " 0.393,\n", + " 0.451,\n", + " 0.427,\n", + " 0.666,\n", + " 0.243,\n", + " 0.245,\n", + " 0.343,\n", + " 0.639,\n", + " 0.657,\n", + " 0.725,\n", + " 0.593,\n", + " 0.6223,\n", + " 0.75,\n", + " ],\n", + " [\n", + " 0.46,\n", + " 0.376,\n", + " 0.264,\n", + " 0.318,\n", + " 0.215,\n", + " 0.237,\n", + " 0.149,\n", + " 0.211,\n", + " 0.091,\n", + " 0.267,\n", + " 0.057,\n", + " 0.099,\n", + " 0.161,\n", + " 0.198,\n", + " 0.445,\n", + " 0.082,\n", + " 0.062,\n", + " 0.405,\n", + " ],\n", + " ]\n", + ")" ] }, { @@ -160,7 +202,9 @@ { "cell_type": "markdown", "id": "6081192f", - "metadata": {}, + "metadata": { + "lines_to_next_cell": 2 + }, "source": [ "选择合适的距离将数据点映射成对应图" ] @@ -179,16 +223,17 @@ "source": [ "def distance(p1, p2):\n", " \"\"\"Euclidean distance of point1 and point 2\"\"\"\n", - " dis = np.sqrt(sum(abs(p1-p2)**2))\n", + " dis = np.sqrt(sum(abs(p1 - p2) ** 2))\n", " return dis\n", "\n", + "\n", "def construct_adj_mat(data, d_0, dis_func):\n", " \"\"\"Construct the adjacent matrix for the given data\"\"\"\n", " num_data = data.shape[-1]\n", " a = np.zeros([num_data, num_data])\n", " for i in itertools.combinations(range(num_data), 2):\n", - " dis = dis_func(data[:,i[0]], data[:,i[1]])\n", - " if dis <=d_0:\n", + " dis = dis_func(data[:, i[0]], data[:, i[1]])\n", + " if dis <= d_0:\n", " a[i] = 1\n", " return a + a.transpose()" ] @@ -205,7 +250,7 @@ }, "outputs": [], "source": [ - "#构造邻接矩阵\n", + "# 构造邻接矩阵\n", "a = construct_adj_mat(data_ws, 0.2, distance)\n", "g = nx.from_numpy_array(a)\n", "# nx.draw(g, pos=data_ws2.transpose(),with_labels=True)" @@ -239,7 +284,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "1951337e", "metadata": { "ExecuteTime": { @@ -259,23 +304,23 @@ } ], "source": [ - "gbs = dqp.GBS_Graph(adj_mat=a, cutoff=2)\n", + "gbs = dqp.GraphGBS(adj_mat=a, cutoff=2)\n", "state = gbs()\n", "sample_re = dqp.utils.load_sample('clustering')\n", "\n", "g_density = gbs.graph_density(g, sample_re)\n", - "d_0 = 0.8 #设置阈值图密度\n", - "max_node = 12 # 从较大的子图开始寻找\n", + "d_0 = 0.8 # 设置阈值图密度\n", + "max_node = 12 # 从较大的子图开始寻找\n", "flag = False\n", "while not flag:\n", " for i in sample_re:\n", - " if sum(i.state)==max_node and g_density[i][1]>d_0:\n", + " if sum(i.state) == max_node and g_density[i][1] > d_0:\n", " print(i)\n", " target = torch.nonzero(i.state).mT\n", " print(target)\n", " print(g_density[i])\n", " flag = True\n", - " max_node = max_node - 2\n" + " max_node = max_node - 2" ] }, { @@ -381,7 +426,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "id": "049ad481", "metadata": { "ExecuteTime": { @@ -410,7 +455,7 @@ } ], "source": [ - "gbs2 = dqp.GBS_Graph(adj_mat=a_2, cutoff=2)\n", + "gbs2 = dqp.GraphGBS(adj_mat=a_2, cutoff=2)\n", "state2 = gbs2()\n", "sample_re2 = gbs2.measure(shots=10000, mcmc=True)" ] @@ -438,12 +483,12 @@ ], "source": [ "g_density = gbs.graph_density(g2, sample_re2)\n", - "d_0 = 0.6 #设置阈值图密度, 需要不断调整\n", - "max_node = 10 # 从较大的子图开始寻找\n", + "d_0 = 0.6 # 设置阈值图密度, 需要不断调整\n", + "max_node = 10 # 从较大的子图开始寻找\n", "flag = False\n", "while not flag:\n", " for i in sample_re2:\n", - " if sum(i.state)==max_node and g_density[i][1]>d_0:\n", + " if sum(i.state) == max_node and g_density[i][1] > d_0:\n", " print(i)\n", " target = torch.nonzero(i.state).mT\n", " print(target)\n", @@ -457,7 +502,7 @@ "id": "3a3c82dc", "metadata": {}, "source": [ - "这里成功的找到了处理后的第2,3,4,5,6,7个数据对应的稠密子图,也就是说这些数据应该被聚类在一起,同时剩下的四个点也应该被聚类在一起, \n", + "这里成功的找到了处理后的第2,3,4,5,6,7个数据对应的稠密子图,也就是说这些数据应该被聚类在一起,同时剩下的四个点也应该被聚类在一起,\n", "因为他们对应的子图密度为1。经过上面的GBS算法我们可以看到原始数据就能够被分成3个聚类。最后的聚类结果如下。\n", "
\n", " \n", @@ -483,7 +528,7 @@ "[1] S. J. Russell, P. Norvig, M.-W. Chang, J. Devlin, and\n", "A. Dragan, Artificial Intelligence: A Modern Approach.\n", "Hoboken: Pearson College Div, 4\n", - "th ed., Nov. 2020. \n", + "th ed., Nov. 2020.\n", "\n", "[2]Schubert E, Sander J, Ester M, et al. DBSCAN revisited, revisited: why and how you should (still) use DBSCAN[J]. ACM Transactions on Database Systems (TODS), 2017, 42(3): 1-21.\n", "\n", diff --git a/examples/demos/gbs/gbs_clustering/gbs_clustering.py b/examples/demos/gbs/gbs_clustering/gbs_clustering.py new file mode 100644 index 00000000..4f4a5ef8 --- /dev/null +++ b/examples/demos/gbs/gbs_clustering/gbs_clustering.py @@ -0,0 +1,252 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.19.1 +# kernelspec: +# display_name: dq_draw +# language: python +# name: python3 +# --- + +# %% [markdown] +# # 基于高斯玻色采样的聚类算法 + +# %% [markdown] +# 聚类算法[1]是一种无监督学习算法,用于将数据集中的对象分组成为多个类别(或簇),使得同一类别内的对象彼此相似,而不同类别之间的对象则不相似。聚类算法的目标是发现数据中的内在结构,而无需预先标记数据。常见的经典聚类算法有K均值聚类 (K-Means Clustering), 密度聚类 (DBSCAN), 谱聚类 (Spectral Clustering)等方法。 + +# %% [markdown] +# K均值聚类算法的核心是寻找聚类的中心, 首先随机选择K个中心点,然后计算每个数据点和中心点的距离,将其分类为最接近中心点$\mu_j$的组, 全部计算完成后将每个组的中心$\mu_j$重新计算,然后重复迭代这些步骤直到中心点不再更新就完成了聚类过程。 +# 密度聚类算法[2]能够识别出具有不同密度区域的数据点,并将它们分组为不同的簇。DBSCAN 的核心思想是根据数据点的密度来确定簇,而不是依赖于事先设定的簇的数量,DBSCAN使用的方法很简单,它任意选择一个没有类别的核心对象作为种子,然后找到所有这个核心对象能够密度可达的样本集合,即为一个聚类簇。 +# 接着继续选择另一个没有类别的核心对象去寻找密度可达的样本集合,这样就得到另一个聚类簇 (这样的得到都肯定是密度相连的),一直运行到所有核心对象都有类别为止。 + +# %% [markdown] +# 与此同时,高斯玻色采样也可以应用于聚类算法[3], 根据[GBS稠密子图案例](../dense_graph/gbs_dense_graph_problem.ipynb)的讨论可知高斯玻色采样用于图问题时, 图密度较大的子图对应的样本往往有较大概率被采到, 这些子图的节点对应的数据点就属于同一个聚类,因此通过这种方法就完成了一次聚类,然后将这个聚类对应的子图移除,继续高斯玻色采样过程,可以持续的找到多个不同的聚类。 + +# %% [markdown] +# # 算法说明 + +# %% [markdown] +# 基于高斯玻色采样的聚类算法如下 +# 1. 通过分类数据集构造邻接矩阵A, +# 设置一个阈值距离$d_0$以及距离度量 $D_{ij}=d(x_i,x_j)$, 邻接矩阵A的构造如下 +# +# $$A_{ij} = \begin{cases} 1 & D_{ij}\le d_0 \\ +# 0 & D_{ij}>d_0 \end{cases} $$ +# +# 2. 将邻接矩阵A放入高斯玻色采样设备中进行N次采样。 +# +# 3. 挑选出子图对应的采样结果,即只包含0,1的样本,计算每个子图密度, +# 设置一个阈值图密度$D_0$, 挑选出最大的子图,如果图密度大于 $D_0$ +# 则找到一个聚类,移除子图及其节点, 更新邻接矩阵A。 否则降低阈值图密度 $D_0$,回到2 +# +# 4. 当节点数剩余较少时停止迭代,完成聚类过程。 + +# %% [markdown] +# # 代码演示 + +# %% [markdown] +# 首先导入相关库 + +# %% +import itertools + +import deepquantum.photonic as dqp +import networkx as nx +import numpy as np +import torch + +# %% [markdown] +# 这里采用西瓜数据集进行聚类, 每一个数据点包含密度和含糖率两个特征, 分别对应data_ws第一行和第二行。 + +# %% +data_ws = np.array( + [ + [ + 0.697, + 0.774, + 0.634, + 0.608, + 0.556, + 0.393, + 0.451, + 0.427, + 0.666, + 0.243, + 0.245, + 0.343, + 0.639, + 0.657, + 0.725, + 0.593, + 0.6223, + 0.75, + ], + [ + 0.46, + 0.376, + 0.264, + 0.318, + 0.215, + 0.237, + 0.149, + 0.211, + 0.091, + 0.267, + 0.057, + 0.099, + 0.161, + 0.198, + 0.445, + 0.082, + 0.062, + 0.405, + ], + ] +) + + +# %% [markdown] +#
+# +#

+# +#

+#
+ +# %% [markdown] +# 选择合适的距离将数据点映射成对应图 + + +# %% +def distance(p1, p2): + """Euclidean distance of point1 and point 2""" + dis = np.sqrt(sum(abs(p1 - p2) ** 2)) + return dis + + +def construct_adj_mat(data, d_0, dis_func): + """Construct the adjacent matrix for the given data""" + num_data = data.shape[-1] + a = np.zeros([num_data, num_data]) + for i in itertools.combinations(range(num_data), 2): + dis = dis_func(data[:, i[0]], data[:, i[1]]) + if dis <= d_0: + a[i] = 1 + return a + a.transpose() + + +# %% +# 构造邻接矩阵 +a = construct_adj_mat(data_ws, 0.2, distance) +g = nx.from_numpy_array(a) +# nx.draw(g, pos=data_ws2.transpose(),with_labels=True) + +# %% [markdown] +#
+# +#

+# +#

+#
+ +# %% [markdown] +# 开始第一轮寻找较大的稠密子图进行聚类 + +# %% +gbs = dqp.GraphGBS(adj_mat=a, cutoff=2) +state = gbs() +sample_re = dqp.utils.load_sample('clustering') + +g_density = gbs.graph_density(g, sample_re) +d_0 = 0.8 # 设置阈值图密度 +max_node = 12 # 从较大的子图开始寻找 +flag = False +while not flag: + for i in sample_re: + if sum(i.state) == max_node and g_density[i][1] > d_0: + print(i) + target = torch.nonzero(i.state).mT + print(target) + print(g_density[i]) + flag = True + max_node = max_node - 2 + +# %% [markdown] +# 这里成功的找到了第2,3,4,8,12,13,15,16个数据对应的稠密子图,也就是说这些数据应该被聚类在一起 + +# %% [markdown] +# 2. 现在将第一轮找到的聚类节点移除,开始第二轮寻找较大的稠密子图进行聚类 + +# %% [markdown] +#
+# +#

+# +#

+#
+ +# %% +data_ws2 = np.delete(data_ws, target, 1) + +# %% [markdown] +# 3. 重复上面的高斯玻色采样过程,寻找最大的稠密子图 + +# %% +a_2 = construct_adj_mat(data_ws2, 0.2, distance) +g2 = nx.from_numpy_array(a_2) + +# %% [markdown] +#
+# +#

+# +#

+#
+ +# %% +gbs2 = dqp.GraphGBS(adj_mat=a_2, cutoff=2) +state2 = gbs2() +sample_re2 = gbs2.measure(shots=10000, mcmc=True) + +# %% +g_density = gbs.graph_density(g2, sample_re2) +d_0 = 0.6 # 设置阈值图密度, 需要不断调整 +max_node = 10 # 从较大的子图开始寻找 +flag = False +while not flag: + for i in sample_re2: + if sum(i.state) == max_node and g_density[i][1] > d_0: + print(i) + target = torch.nonzero(i.state).mT + print(target) + print(g_density[i]) + flag = True + max_node = max_node - 2 + +# %% [markdown] +# 这里成功的找到了处理后的第2,3,4,5,6,7个数据对应的稠密子图,也就是说这些数据应该被聚类在一起,同时剩下的四个点也应该被聚类在一起, +# 因为他们对应的子图密度为1。经过上面的GBS算法我们可以看到原始数据就能够被分成3个聚类。最后的聚类结果如下。 +#
+# +#

+# +#

+#
+ +# %% [markdown] +# # 附录 + +# %% [markdown] +# [1] S. J. Russell, P. Norvig, M.-W. Chang, J. Devlin, and +# A. Dragan, Artificial Intelligence: A Modern Approach. +# Hoboken: Pearson College Div, 4 +# th ed., Nov. 2020. +# +# [2]Schubert E, Sander J, Ester M, et al. DBSCAN revisited, revisited: why and how you should (still) use DBSCAN[J]. ACM Transactions on Database Systems (TODS), 2017, 42(3): 1-21. +# +# [3]Bonaldi N, Rossi M, Mattioli D, et al. Boost clustering with Gaussian Boson Sampling: a full quantum approach[J]. arXiv preprint arXiv:2307.13348, 2023. +# diff --git a/examples/demos/gbs/homodyne_tomography/Homodyne_Tomography.ipynb b/examples/demos/gbs/homodyne_tomography/homodyne_tomography.ipynb similarity index 84% rename from examples/demos/gbs/homodyne_tomography/Homodyne_Tomography.ipynb rename to examples/demos/gbs/homodyne_tomography/homodyne_tomography.ipynb index dafe0743..dfe92656 100644 --- a/examples/demos/gbs/homodyne_tomography/Homodyne_Tomography.ipynb +++ b/examples/demos/gbs/homodyne_tomography/homodyne_tomography.ipynb @@ -144,7 +144,7 @@ "id": "eceb14a5", "metadata": {}, "source": [ - "3. 考虑多模的情况下,$\\langle A_{M}\\rangle$ 对应的二重积分变成多重积分, 概率分布 $p(x, \\phi)$ 变成联合概率分布 $p(x_1, \\phi_1, x_2,\\phi_2, ...)$, \n", + "3. 考虑多模的情况下,$\\langle A_{M}\\rangle$ 对应的二重积分变成多重积分, 概率分布 $p(x, \\phi)$ 变成联合概率分布 $p(x_1, \\phi_1, x_2,\\phi_2, ...)$,\n", "核函数 $K(x, \\phi, A)$ 变成 $K(x_1, x_2, ... \\phi_1, \\phi_2,.., A)$。\n", "$$\n", "\\begin{aligned}\\langle A_{M}\\rangle&=\\int_{0}^{\\pi}\\frac{d\\phi_{1}\\cdots d\\phi_{M}}{\\pi^{M}}\\int_{-\\infty}^{+\\infty}dx_{1}\\cdots dx_{M} p(x_{1},\\phi_{1},\\cdots,x_{M},\\phi_{M})\\\\&\\times K_{A_{M}}(x_{1},\\phi_{1},\\cdots,x_{M},\\phi_{M}) ,\\end{aligned}\n", @@ -177,7 +177,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "d1fa7509", "metadata": { "ExecuteTime": { @@ -202,20 +202,23 @@ ], "source": [ "import deepquantum as dq\n", - "import matplotlib.pyplot as plt\n", - "import torch\n", + "import mpmath\n", + "import numpy as np\n", + "import scipy\n", + "from mpmath import mp\n", + "from scipy.special import comb, factorial\n", "\n", "nmode = 1\n", "cir = dq.QumodeCircuit(nmode=nmode, init_state='vac', cutoff=3, backend='gaussian')\n", "cir.s(0, r=1)\n", - "state =cir()\n", + "state = cir()\n", "probs = cir(is_prob=True)\n", - "cir.draw()\n" + "cir.draw()" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "id": "004e6e28", "metadata": { "ExecuteTime": { @@ -225,37 +228,35 @@ }, "outputs": [], "source": [ - "import numpy as np\n", - "import scipy\n", - "from scipy.special import factorial, comb\n", - "import mpmath\n", - "from mpmath import mp\n", - "mp.dps = 25; mp.pretty = True\n", + "mp.dps = 25\n", + "mp.pretty = True\n", + "\n", "\n", - "def K_A(x, phi, n, labda):\n", + "def kernel_a(x, phi, n, lambd):\n", " \"\"\"\n", - " Kernel function for calculating expetation value of |n+labda>\n", " \n", "

\n", @@ -434,16 +441,16 @@ "source": [ "#####using monte-carlo mean value method\n", "points_list = [10, 100, 1000, 5000, 10000, 20000, 30000, 40000, 50000]\n", - "val = [ ]\n", + "val = []\n", "for num_points in points_list:\n", " a = np.array([0, -6])\n", " b = np.array([np.pi, 6])\n", - " random_samples = a + (b-a)*np.random.rand(num_points, 2)\n", - " phi = random_samples[:,0]\n", - " x = random_samples[:,1]\n", - " vec_fun_real = np.vectorize(fun_real) # vectorize 并行化\n", + " random_samples = a + (b - a) * np.random.rand(num_points, 2)\n", + " phi = random_samples[:, 0]\n", + " x = random_samples[:, 1]\n", + " vec_fun_real = np.vectorize(fun_real) # vectorize 并行化\n", " re = vec_fun_real(phi, x, 0, 0, mu_x, mu_p, sigma_x, sigma_p)\n", - " volume = np.prod(b-a)\n", + " volume = np.prod(b - a)\n", " integral = volume * np.mean(re)\n", " val.append(integral)\n", " print(num_points, integral)" @@ -562,15 +569,15 @@ "cir = dq.QumodeCircuit(nmode=nmode, init_state='vac', cutoff=3, backend='gaussian')\n", "cir.s(0, r=1)\n", "cir.s(1, r=1)\n", - "cir.bs([0,1], [np.pi/4, np.pi/4])\n", - "state =cir()\n", + "cir.bs([0, 1], [np.pi / 4, np.pi / 4])\n", + "state = cir()\n", "probs = cir(is_prob=True)\n", "cir.draw()" ] }, { "cell_type": "code", - "execution_count": 25, + "execution_count": null, "id": "869f28b4", "metadata": { "ExecuteTime": { @@ -581,27 +588,36 @@ "outputs": [], "source": [ "def get_mu_sigma(phi_1, phi_2, mu, sigma):\n", - " mu_1 = 0.5*(np.cos(phi_1)*mu[0] + np.sin(phi_1)*mu[2])\n", - " mu_2 = 0.5*(np.cos(phi_2)*mu[1] + np.sin(phi_2)*mu[3])\n", - " cov_x1x1 = 0.25*(np.cos(phi_1)**2*sigma[0,0] + np.sin(phi_1)**2*sigma[2,2]+np.sin(2*phi_1)*sigma[0,2])\n", - " cov_x2x2 = 0.25*(np.cos(phi_2)**2*sigma[1,1] + np.sin(phi_2)**2*sigma[3,3]+np.sin(2*phi_2)*sigma[1,3])\n", - " cov_x1x2 = 0.25*(np.cos(phi_1)*np.cos(phi_2)*sigma[0,1] + np.cos(phi_1)*np.sin(phi_2)*sigma[0,3] + \\\n", - " np.sin(phi_1)*np.cos(phi_2)*sigma[2,1] + np.sin(phi_1)*np.sin(phi_2)*sigma[2,3])\n", + " mu_1 = 0.5 * (np.cos(phi_1) * mu[0] + np.sin(phi_1) * mu[2])\n", + " mu_2 = 0.5 * (np.cos(phi_2) * mu[1] + np.sin(phi_2) * mu[3])\n", + " cov_x1x1 = 0.25 * (\n", + " np.cos(phi_1) ** 2 * sigma[0, 0] + np.sin(phi_1) ** 2 * sigma[2, 2] + np.sin(2 * phi_1) * sigma[0, 2]\n", + " )\n", + " cov_x2x2 = 0.25 * (\n", + " np.cos(phi_2) ** 2 * sigma[1, 1] + np.sin(phi_2) ** 2 * sigma[3, 3] + np.sin(2 * phi_2) * sigma[1, 3]\n", + " )\n", + " cov_x1x2 = 0.25 * (\n", + " np.cos(phi_1) * np.cos(phi_2) * sigma[0, 1]\n", + " + np.cos(phi_1) * np.sin(phi_2) * sigma[0, 3]\n", + " + np.sin(phi_1) * np.cos(phi_2) * sigma[2, 1]\n", + " + np.sin(phi_1) * np.sin(phi_2) * sigma[2, 3]\n", + " )\n", " mu_ = np.array([mu_1, mu_2])\n", - " sigma_ =np.array([[cov_x1x1, cov_x1x2],\n", - " [cov_x1x2, cov_x2x2]])\n", + " sigma_ = np.array([[cov_x1x1, cov_x1x2], [cov_x1x2, cov_x2x2]])\n", " return mu_, sigma_\n", - "def fun_twomode(phi_1, phi_2, x_1, x_2, n_1, labda_1, n_2, labda_2, mu, sigma):\n", - " \"\"\"\n", - " calculating expectation value of operator |n_1+labda_1> +# +#

+# +#

+#
+ +# %% [markdown] +# 在光量子系统中由于量子系统很脆弱,经过探测之后就会被破坏,因此为了获得量子态在不同正交分量值下的几率分布就需要制备很多份全同的量子态。量子层析的基本过程是首先在不同正交相位角 $\theta$ 下测量未知量子态的正交分量值,然后对其进行统计求得 $X_\theta$ 对应的正交分量概率分布 $Pr(X_\theta)$,之后可以用最大似然估计算法推断出最有可能满足这一系列正交分量几率分布的量子态的密度矩阵与Wigner函数, 也可以通过用这些概率分布来计算对应算符的期望。 + +# %% [markdown] +# # 单模线路量子态层析 + +# %% [markdown] +# ## 理论基础 + +# %% [markdown] +# 1. 给定一个算符, 一般会考虑用算符基矢展开, 光量子计算中可以将位移算符 $D(\alpha) = e^{\alpha a^\dagger - \alpha^* a}$ 作为一组正交完备的算符基矢, 那么作用在希尔伯特空间的任意算符$A$ 都可以用这一组基矢展开[2]。 +# $$ +# A=\int_{\mathbb{C}}\frac{d^2\alpha}{\pi} \mathrm{Tr}[A \mathcal{D}^\dagger(\alpha)]\mathcal{D}(\alpha)=\int_{0}^{\pi}\frac{d\phi}{\pi}\int_{-\infty}^{+\infty}dr\frac{|r|}{4}\mathrm{Tr}[A e^{irX_\phi}]e^{-irX_\phi} +# $$ +# 这里算符的内积用trace来定义, $\langle A|B\rangle = Tr[A^\dagger B]$。 +# 采用极坐标参数 $ \alpha = -ir e^{i\phi/2}$。正交分量 $X_\phi$ 的定义如下: +# $$X_\phi=(a^\dagger e^{i\phi}+ae^{-i\phi})/2$$ +# 那么观测量 $\langle A \rangle$ 的表示如下: +# $$ +# \begin{aligned} +# \left\langle A\right\rangle & =Tr(A\rho) \\ +# &=Tr(\int\frac{d^2\alpha}\pi Tr(AD^\dagger(\alpha))D(\alpha)\rho) \\ +# &=\int_0^\pi\frac{d\phi}\pi\int_{-\infty}^\infty dr\frac{|r|}4Tr(Ae^{irX_\phi})Tr(\rho e^{-irX_\phi}) \\ +# &=\int_{0}^{\pi}\frac{d\phi}{\pi}\int_{-\infty}^{\infty}dx\int_{-\infty}^{\infty}dr\frac{|r|}{4}Tr(Ae^{irX_{\phi}})p(\phi,x)e^{-irx} \\ +# &=\int_0^\pi\frac{d\phi}\pi\int_{-\infty}^\infty dxp(\phi,x)\int_{-\infty}^\infty dr\frac{|r|}4Tr(Ae^{ir(X_\phi-x)}) \\ +# &=\int_0^\pi\frac{d\phi}\pi\int_{-\infty}^\infty dxp(\phi,x)K(\phi,x,A) +# \end{aligned} +# $$ +# 这里$p(\phi, x) = \langle X_\phi|\rho|X_\phi\rangle$, 表示测量正交分量 $X_\phi$ 的分布,核函数 $K(\phi,x,A)$ 的定义如下: +# $$ +# K_A(x,\phi)\equiv\int_{-\infty}^{+\infty}dr\frac{|r|}{4}\mathrm{Tr}[A e^{ir(X_\phi-x)}] +# $$ +# 对于单模情况的Fock基矢表示的算符$A$ ($A=|n+\lambda\rangle\langle n|$), 核函数的定义如下[3]: +# $$ +# \begin{aligned}K(\phi,x,|n+\lambda\rangle\langle n|)&=2e^{-i\lambda\phi}\sqrt{\frac{n!}{(n+\lambda)!}}e^{-x^2}\sum_{\nu=0}^n\frac{(-1)^\nu}{\nu!}\left(\begin{array}{c}n+\lambda\\n-\nu\end{array}\right)\\&\times(2\nu+\lambda+1)!\operatorname{Re}\left[(-i)^\lambda\mathcal{D}_{-(2\nu+\lambda+2)}(-2ix)\right]\end{aligned} +# $$ +# 这里 $D_l(x)$ 是抛物线圆柱函数(parabolic cylinder funtion),可以通过`mpmath`计算。 + +# %% [markdown] +# 2. 考虑单模情况下 $X_\phi$ 概率分布 $p(\phi, x)$, $X_\phi$ 可以化简 +# $$ +# X_\phi = \frac{1}{2}(\cos\phi(a^\dagger+a) + i\sin\phi(a^\dagger-a))=\frac{1}{2}(\cos\phi x_0 + \sin\phi p_0) +# $$ +# +# $x_0$, $p_0$的分布可以从边缘分布计算得到 +# $$ +# x_0 \sim N(\bar{x}_0, \sigma^2_{x_0}) \\ +# p_0 \sim N(\bar{p}_0, \sigma^2_{p_0}) +# $$ +# $X_\phi$ 对应的分布也是高斯分布,对应的平均值和方差如下 +# $$ +# \mu({X_\phi}) = \frac{1}{2}(\cos\phi \bar{x}_0 + \sin\phi \bar{p}_0), \ \ \sigma({X_\phi}) = \frac{1}{4}(\cos^2\phi \sigma^2_{x_0}+\sin^2\phi \sigma^2_{p_0}) \\ +# p(X_\phi) \sim N(\frac{1}{2}(\cos\phi \bar{x}_0+\sin\phi \bar{p}_0), \frac{1}{4}(\cos^2\phi \sigma^2_{x_0}+\sin^2\phi \sigma^2_{p_0})) +# $$ + +# %% [markdown] +# 3. 考虑多模的情况下,$\langle A_{M}\rangle$ 对应的二重积分变成多重积分, 概率分布 $p(x, \phi)$ 变成联合概率分布 $p(x_1, \phi_1, x_2,\phi_2, ...)$, +# 核函数 $K(x, \phi, A)$ 变成 $K(x_1, x_2, ... \phi_1, \phi_2,.., A)$。 +# $$ +# \begin{aligned}\langle A_{M}\rangle&=\int_{0}^{\pi}\frac{d\phi_{1}\cdots d\phi_{M}}{\pi^{M}}\int_{-\infty}^{+\infty}dx_{1}\cdots dx_{M} p(x_{1},\phi_{1},\cdots,x_{M},\phi_{M})\\&\times K_{A_{M}}(x_{1},\phi_{1},\cdots,x_{M},\phi_{M}) ,\end{aligned} +# $$ +# $$ +# K_{A_M}(x_1,\phi_1,\cdots)\equiv\int_{-\infty}^{+\infty}dr_1\cdots dr_M\prod_{m=1}^M\frac{|r_m|}{4}\mathrm{Tr}[A_M e^{ir_m(X_{\phi_m}-x_m)}] +# $$ + +# %% [markdown] +# ## 代码实现 + +# %% [markdown] +# 下面以单模压缩态为例, 计算算符$|n+\lambda\rangle\langle n|$的平均值 + +# %% +import deepquantum as dq +import mpmath +import numpy as np +import scipy +from mpmath import mp +from scipy.special import comb, factorial + +nmode = 1 +cir = dq.QumodeCircuit(nmode=nmode, init_state='vac', cutoff=3, backend='gaussian') +cir.s(0, r=1) +state = cir() +probs = cir(is_prob=True) +cir.draw() + +# %% +mp.dps = 25 +mp.pretty = True + + +def kernel_a(x, phi, n, lambd): + """ + Kernel function for calculating expetation value of |n+lambd> +# +#

+# +#

+# +# 这里随机的点落在积分区域的概率正比于积分区域的面积, 因此红点所占的比例乘以矩形的面积就是函数 $f(x)$ 的积分。 +# 2. 期望法 +#
+# +#

+# +#

+#
+# +# $$ +# \frac{1}{b-a}\int_a^b f(x)dx \approx \frac{1}{N}\sum_{i=1}^Nf(X_i) \\ +# \int_a^b f(x)dx \approx (b-a)\frac{1}{N}\sum_{i=1}^Nf(X_i) +# $$ + +# %% [markdown] +# 下面用蒙特卡洛期望法复现上面的二重积分的计算结果 + +# %% +#####using monte-carlo mean value method +points_list = [10, 100, 1000, 5000, 10000, 20000, 30000, 40000, 50000] +val = [] +for num_points in points_list: + a = np.array([0, -6]) + b = np.array([np.pi, 6]) + random_samples = a + (b - a) * np.random.rand(num_points, 2) + phi = random_samples[:, 0] + x = random_samples[:, 1] + vec_fun_real = np.vectorize(fun_real) # vectorize 并行化 + re = vec_fun_real(phi, x, 0, 0, mu_x, mu_p, sigma_x, sigma_p) + volume = np.prod(b - a) + integral = volume * np.mean(re) + val.append(integral) + print(num_points, integral) + +# %% [markdown] +# 可以看到最后结果会收敛到0.64左右。 + +# %% [markdown] +# # 两模线路的量子态层析 + +# %% [markdown] +# 考虑两模光量子线路,算符$H$的平均值如下: +# $$ +# \begin{aligned} +# \langle H\rangle&=\int_0^\pi\int_0^\pi\frac{d\phi_1d\phi_2}{\pi^2}\int_{-\infty}^\infty\int_{-\infty}^\infty dx_1dx_2 p(\phi_1,\phi_2,x_1,x_2)K(\phi_1,\phi_2,x_1,x_2,H) +# \end{aligned} +# $$ +# +# $p(\phi_1,\phi_2,x_1,x_2)$ 的定义如下: +# $$ +# \begin{aligned} +# &p(\phi_1,\phi_2,x_1,x_2) = p(\frac{1}{2}(\cos\phi_1 x_1 + \sin\phi_1 p_1), \frac{1}{2}(\cos\phi_2 x_2 + \sin\phi_2 p_2)) +# = p(X_1, X_2) \\ +# &X_1 = \frac{1}{2}(\cos\phi_1 x_1 + \sin\phi_1 p_1)\\ +# &X_2 = \frac{1}{2}(\cos\phi_2 x_2 + \sin\phi_2 p_2) +# \end{aligned} +# $$ +# +# 对应的平均值及协方差如下: +# +# $$ +# \begin{aligned} +# &\mu(X_1) = \frac{1}{2}(\cos\phi_1 \mu_{x_1} + \sin\phi_1 \mu_{p_1}) \\ +# &\mu(X_2) = \frac{1}{2}(\cos\phi_2 \mu_{x_2} + \sin\phi_2 \mu_{p_2}) \\ +# &cov(X_1, X_2) = cov(\frac{1}{2}(\cos\phi_1 x_1 + \sin\phi_1 p_1), \frac{1}{2}(\cos\phi_2 x_2 + \sin\phi_2 p_2)) = \\ +# &\frac{1}{4}(\cos\phi_1 \cos\phi_2 cov(x_1, x_2) + \cos\phi_1 \sin\phi_2 cov(x_1, p_2) + \sin\phi_1 \cos\phi_2 cov(p_1, x_2) + \sin\phi_1 \sin\phi_2 cov(p_1, p_2) \\ +# &cov(X_1, X_1) = \frac{1}{4}(\cos^2\phi_1 cov(x_1, x_1) + sin^2\phi_1 cov(p_1, p_1) + 2\cos\phi_1\sin\phi_1cov(x_1, p_1)) \\ +# &cov(X_2, X_2) = \frac{1}{4}(\cos^2\phi_2cov(x_2, x_2) + sin^2\phi_2 cov(p_2, p_2) + 2\cos\phi_2\sin\phi_2cov(x_2, p_2)) \\ +# \end{aligned} +# $$ +# +# $K(\phi_1,\phi_2,x_1,x_2,H)$ 的定义如下 +# $$ +# \begin{aligned}K(\phi_{1},\phi_{2},x_{1},x_{2},H)&=\int_{-\infty}^{\infty}dr_{1}dr_{2}\Pi_{m=1}^{2}\frac{|r_{m}|}{4}Tr(He^{ir_{m}(X_{\phi_{m}}-x_{m})}) +# \end{aligned} +# $$ +# 当$H = H_1 \otimes H_2$ 时,核函数可以简单表示成两个独立部分的乘积。 +# $$ +# K(\phi_1,\phi_2,x_1,x_2,H) = K(\phi_1,x_1,H_1)\cdot K(\phi_2,x_2,H_2) +# $$ +# 比如考虑两模算符$H = |00\rangle\langle 00|$,使用下面的化简公式 +# $$ +# |00\rangle\langle00| = (|0\rangle\otimes |0\rangle)(\langle0|\otimes\langle0|) = (|0\rangle\langle0|)_1\otimes(|0\rangle\langle0|)_2 +# $$ +# 令 +# $$ +# O_1 = (|0\rangle\langle0|)_1 \\ +# O_2 = (|0\rangle\langle0|)_2 +# $$ +# 对应的核函数可以写成 +# $$ +# K(\phi_1,\phi_2,x_1,x_2,O) = K(\phi_1,x_1,O_1)\cdot K(\phi_2,x_2,O_2) +# $$ + +# %% [markdown] +# 下面以两模线路为例, 计算$|00\rangle \langle 00|$的期望值, + +# %% +nmode = 2 +cir = dq.QumodeCircuit(nmode=nmode, init_state='vac', cutoff=3, backend='gaussian') +cir.s(0, r=1) +cir.s(1, r=1) +cir.bs([0, 1], [np.pi / 4, np.pi / 4]) +state = cir() +probs = cir(is_prob=True) +cir.draw() + + +# %% +def get_mu_sigma(phi_1, phi_2, mu, sigma): + mu_1 = 0.5 * (np.cos(phi_1) * mu[0] + np.sin(phi_1) * mu[2]) + mu_2 = 0.5 * (np.cos(phi_2) * mu[1] + np.sin(phi_2) * mu[3]) + cov_x1x1 = 0.25 * ( + np.cos(phi_1) ** 2 * sigma[0, 0] + np.sin(phi_1) ** 2 * sigma[2, 2] + np.sin(2 * phi_1) * sigma[0, 2] + ) + cov_x2x2 = 0.25 * ( + np.cos(phi_2) ** 2 * sigma[1, 1] + np.sin(phi_2) ** 2 * sigma[3, 3] + np.sin(2 * phi_2) * sigma[1, 3] + ) + cov_x1x2 = 0.25 * ( + np.cos(phi_1) * np.cos(phi_2) * sigma[0, 1] + + np.cos(phi_1) * np.sin(phi_2) * sigma[0, 3] + + np.sin(phi_1) * np.cos(phi_2) * sigma[2, 1] + + np.sin(phi_1) * np.sin(phi_2) * sigma[2, 3] + ) + mu_ = np.array([mu_1, mu_2]) + sigma_ = np.array([[cov_x1x1, cov_x1x2], [cov_x1x2, cov_x2x2]]) + return mu_, sigma_ + + +def fun_twomode(phi_1, phi_2, x_1, x_2, n_1, lambd_1, n_2, lambd_2, mu, sigma): + r""" + calculating expectation value of operator |n_1+lambd_1> list:\n", - " \"\"\"Shrinks an input subgraph until it forms a clique, code from strawberryfields.\n", - " \"\"\"\n", + "def clique_shrink(samples, graph, node_select='uniform') -> list:\n", + " \"\"\"Shrinks an input subgraph until it forms a clique, code from strawberryfields.\"\"\"\n", " small_clique = []\n", " max_node = 0\n", - " for key in samples.keys():\n", + " for key in samples:\n", " idx = torch.nonzero(torch.tensor(key)).squeeze()\n", " subgraph = idx.tolist()\n", " if not set(subgraph).issubset(graph.nodes):\n", - " raise ValueError(\"Input is not a valid subgraph\")\n", + " raise ValueError('Input is not a valid subgraph')\n", "\n", " if isinstance(node_select, (list, np.ndarray)):\n", " if len(node_select) != graph.number_of_nodes():\n", - " raise ValueError(\"Number of node weights must match number of nodes\")\n", + " raise ValueError('Number of node weights must match number of nodes')\n", " w = {n: node_select[i] for i, n in enumerate(graph.nodes)}\n", - " node_select = \"weight\"\n", + " node_select = 'weight'\n", "\n", " subgraph = graph.subgraph(subgraph).copy() # A copy is required to be able to modify the\n", " # structure of the subgraph\n", @@ -151,22 +150,23 @@ " degrees = np.array(subgraph.degree)\n", " degrees_min = np.argwhere(degrees[:, 1] == degrees[:, 1].min()).flatten()\n", "\n", - " if node_select == \"uniform\":\n", + " if node_select == 'uniform':\n", " to_remove_index = np.random.choice(degrees_min)\n", - " elif node_select == \"weight\":\n", + " elif node_select == 'weight':\n", " weights = np.array([w[degrees[n][0]] for n in degrees_min])\n", " to_remove_index = np.random.choice(np.where(weights == weights.min())[0])\n", " else:\n", - " raise ValueError(\"Node selection method not recognized\")\n", + " raise ValueError('Node selection method not recognized')\n", "\n", " to_remove = degrees[to_remove_index][0]\n", " subgraph.remove_node(to_remove)\n", - " if len(subgraph.nodes())>=max_node: #只保留找到较大的团作为起点\n", + " if len(subgraph.nodes()) >= max_node: # 只保留找到较大的团作为起点\n", " max_node = len(subgraph.nodes())\n", " small_clique.append(sorted(subgraph.nodes()))\n", "\n", " return small_clique\n", "\n", + "\n", "def plot_subgraph(graph, subgraph_idx):\n", " \"\"\"plot the subgraph in graph G\"\"\"\n", "\n", @@ -174,14 +174,8 @@ " edge_list = list(sub_g.edges)\n", "\n", " pos = nx.spring_layout(graph)\n", - " nx.draw(graph,\n", - " pos,\n", - " with_labels=True,\n", - " node_color='gray',\n", - " edge_color=\"gray\",\n", - " node_size=200,\n", - " font_size=10)\n", - " nx.draw_networkx_edges(graph, pos,edgelist=edge_list,edge_color='blue')\n", + " nx.draw(graph, pos, with_labels=True, node_color='gray', edge_color='gray', node_size=200, font_size=10)\n", + " nx.draw_networkx_edges(graph, pos, edgelist=edge_list, edge_color='blue')\n", " nx.draw_networkx_nodes(graph, pos, subgraph_idx, node_color='dodgerblue')\n", " plt.show()" ] @@ -221,7 +215,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "4e0989f4", "metadata": { "ExecuteTime": { @@ -234,7 +228,7 @@ "a = dqp.utils.load_adj('clique_adj')\n", "graph = nx.from_numpy_array(a)\n", "\n", - "gbs = dqp.GBS_Graph(adj_mat=a, cutoff=2)\n", + "gbs = dqp.GraphGBS(adj_mat=a, cutoff=2)\n", "state = gbs()\n", "\n", "sample_re = dqp.utils.load_sample('clique_sample')" @@ -269,18 +263,18 @@ ], "source": [ "# 后选择出节点数为8,10,12对应的样本\n", - "subgraph_sample = gbs.postselect(sample_re, [8,10,12])\n", - "clique_set = []# 用来记录所有最大团\n", + "subgraph_sample = gbs.postselect(sample_re, [8, 10, 12])\n", + "clique_set = [] # 用来记录所有最大团\n", "max_len = 0\n", "for sample in subgraph_sample:\n", - " temp = clique_shrink(sample, graph)# 将每个样本对应的团找出来\n", + " temp = clique_shrink(sample, graph) # 将每个样本对应的团找出来\n", " for small_clique in temp:\n", - " max_clique_ = clique.search(small_clique, graph, 20)# 将每个团作为搜索的起点\n", - " if len(max_clique_)>=max_len:# 这里只保留节点数大于等于上一次的结果\n", + " max_clique_ = clique.search(small_clique, graph, 20) # 将每个团作为搜索的起点\n", + " if len(max_clique_) >= max_len: # 这里只保留节点数大于等于上一次的结果\n", " max_len = len(max_clique_)\n", " if max_clique_ not in clique_set:\n", " clique_set.append(max_clique_)\n", - "print(clique_set[-3],clique_set[-2], clique_set[-1])" + "print(clique_set[-3], clique_set[-2], clique_set[-1])" ] }, { diff --git a/examples/demos/gbs/max_clique/gbs_max_clique.py b/examples/demos/gbs/max_clique/gbs_max_clique.py new file mode 100644 index 00000000..38ee29d2 --- /dev/null +++ b/examples/demos/gbs/max_clique/gbs_max_clique.py @@ -0,0 +1,175 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.19.1 +# kernelspec: +# display_name: dq_v3 +# language: python +# name: dq_v3 +# --- + +# %% [markdown] +# # 最大团问题 + +# %% [markdown] +# 团(Clique)是包含了它节点之间所有边的子图,也就是图密度为1的完全图。 +# 最大团问题(the maximum clique problem) 就是要寻找图中的节点数 +# 最多的团,数学上的定义是:$argmax_S \{|S|:d(S)=1\}$。 +# 寻找最大团是一个NP难的问题,但是它的应用范围很广,包括生物信息,社交网络,金融,通信等方面[1,2]。 + +# %% [markdown] +# ## 经典算法 + +# %% [markdown] +# 经典算法寻找最大团是基于局域搜索算法,也就是从一个较小的团开始然后在附近搜索较大的团。 +# 具体来说,第一阶段寻找图G的一个起始的团 $C$, 假设团 $C$ 包含的节点数为 $k$ (k 在搜索过程中是不断更新的), 然后计算出图 $G$ 剩余点中连接 $C$ 中所有 $k$ 个节点的集合 $c_0(C)$, 从 $c_0(C)$ 挑选出一个节点加入团 $C$ 得到一个新的团 $C$ 和集合 $c_0(C)$,节点数 $k$ 更新为 $k+1$,重复这个增长过程直到团的节点不再增长。 +# 第二阶段挑选图G剩余点中连接 $C$ 中k-1个节点的集合 $c_1(C)$,如果集合 $c_1(C)$ 非空,那么挑出一个节点替换掉 $C$ 中不连接那个节点组成一个新的团,然后继续阶段一的增长过程。局域搜索算法就是在交替进行阶段一和阶段二直到最后团的节点数不再增长,得到的结果就是当前图里的最大团。 + +# %% [markdown] +# ## 结合高斯玻色采样的经典算法 + +# %% [markdown] +# 将一个图 $G$ 对应的邻接矩阵编码到高斯玻色采样线路中,对采样得到的样本进行后选择,其中概率较高的样本对应的是 $G$ 的稠密子图。 +# 团作为最稠密的子图,在给定子图规模情况下,从GBS中采样出来的概率是最大的。 +# 因此GBS的采样结果可以作为局域搜索算法的起点,而不是任选一个节点作为起点。 +# 并非所有采到的子图都是团, 但是可以通过移除连接数较少的那些点来得到一个团,然后这些团可以作为局域搜索算法的起点。 +# 结合GBS的局域搜索算法具体描述如下: +# 1. 将图G的邻接矩阵A编码到高斯玻色采样线路中产生N个样本。 +# +# 2. 对样本进行后选择处理挑选出子图对应的样本,对于每个后选择的样本, 重复挑出子图连接数最少的节点移除直到形成一个团。 +# +# 3. 采用局域搜索算法,基于2中的团计算集合 $c_0(C)$, 随机挑选出 $c_0(C)$ 的节点加入得到更大的团, 重复这个过程直到节点数不再增长。 +# +# 4. 基于3的最后结果计算集合 $c_1(C)$,随机挑选出 $c_1(C)$ 的节点替换团 $C$ 中不连接的那个节点得到新的团,然后将这个新的团作为起点回到3。 +# +# 5. 重复步骤3和4直到这个过程收敛。 +# +# 6. 挑选出基于这些后选择样本得到的最大团。 +# + +# %% [markdown] +# ## 代码演示 + +# %% +import deepquantum.photonic as dqp +import matplotlib.pyplot as plt +import networkx as nx +import numpy as np +import torch +from strawberryfields.apps import clique + + +# %% +def clique_shrink(samples, graph, node_select='uniform') -> list: + """Shrinks an input subgraph until it forms a clique, code from strawberryfields.""" + small_clique = [] + max_node = 0 + for key in samples: + idx = torch.nonzero(torch.tensor(key)).squeeze() + subgraph = idx.tolist() + if not set(subgraph).issubset(graph.nodes): + raise ValueError('Input is not a valid subgraph') + + if isinstance(node_select, (list, np.ndarray)): + if len(node_select) != graph.number_of_nodes(): + raise ValueError('Number of node weights must match number of nodes') + w = {n: node_select[i] for i, n in enumerate(graph.nodes)} + node_select = 'weight' + + subgraph = graph.subgraph(subgraph).copy() # A copy is required to be able to modify the + # structure of the subgraph + while not clique.is_clique(subgraph): + degrees = np.array(subgraph.degree) + degrees_min = np.argwhere(degrees[:, 1] == degrees[:, 1].min()).flatten() + + if node_select == 'uniform': + to_remove_index = np.random.choice(degrees_min) + elif node_select == 'weight': + weights = np.array([w[degrees[n][0]] for n in degrees_min]) + to_remove_index = np.random.choice(np.where(weights == weights.min())[0]) + else: + raise ValueError('Node selection method not recognized') + + to_remove = degrees[to_remove_index][0] + subgraph.remove_node(to_remove) + if len(subgraph.nodes()) >= max_node: # 只保留找到较大的团作为起点 + max_node = len(subgraph.nodes()) + small_clique.append(sorted(subgraph.nodes())) + + return small_clique + + +def plot_subgraph(graph, subgraph_idx): + """plot the subgraph in graph G""" + + sub_g = graph.subgraph(subgraph_idx).copy() + edge_list = list(sub_g.edges) + + pos = nx.spring_layout(graph) + nx.draw(graph, pos, with_labels=True, node_color='gray', edge_color='gray', node_size=200, font_size=10) + nx.draw_networkx_edges(graph, pos, edgelist=edge_list, edge_color='blue') + nx.draw_networkx_nodes(graph, pos, subgraph_idx, node_color='dodgerblue') + plt.show() + + +# %% [markdown] +# 这里以下面18个节点的随机图为例解释寻找中间的最大团,这个随机图是通过``erdos_renyi``函数以连接概率为0.7生成的。 +# +#
+# +#

+# +#

+#
+# + +# %% [markdown] +# 现在先将随机图 $G$ 的的邻接矩阵编码到GBS采样参数中, 然后进行十万次采样, 为了方便这里我们直接读取已有的采样样本。 + +# %% +a = dqp.utils.load_adj('clique_adj') +graph = nx.from_numpy_array(a) + +gbs = dqp.GraphGBS(adj_mat=a, cutoff=2) +state = gbs() + +sample_re = dqp.utils.load_sample('clique_sample') + +# %% [markdown] +# 在采样的样本中挑选子图规模为8、10、12的样本,然后通过``clique_shrink`` 函数将每个样本对应的团找出来, 然后将这些团作为``clique_search``函数的起点,采用局域搜索算法来寻找最大团,最后将结果整理。 + +# %% +# 后选择出节点数为8,10,12对应的样本 +subgraph_sample = gbs.postselect(sample_re, [8, 10, 12]) +clique_set = [] # 用来记录所有最大团 +max_len = 0 +for sample in subgraph_sample: + temp = clique_shrink(sample, graph) # 将每个样本对应的团找出来 + for small_clique in temp: + max_clique_ = clique.search(small_clique, graph, 20) # 将每个团作为搜索的起点 + if len(max_clique_) >= max_len: # 这里只保留节点数大于等于上一次的结果 + max_len = len(max_clique_) + if max_clique_ not in clique_set: + clique_set.append(max_clique_) +print(clique_set[-3], clique_set[-2], clique_set[-1]) + +# %% [markdown] +# 这里将找到的最大团列表的最后三个在图G中标记出来 + +# %% +plot_subgraph(graph, clique_set[-3]) + +# %% [markdown] +# ## 附录 + +# %% [markdown] +# [1] N. Malod-Dognin, R. Andonov, and N. Yanev, in International Symposium on Experimental +# Algorithms (Springer, 2010), pp. 106–117 +# +# [2]M. G. Ravetti and P. Moscato, PloS One 3, e3111 (2008). +# +# [3] Q. Wu and J.-K. Hao, European Journal of Operational Research 242, 693 (2015) diff --git a/examples/demos/gbs/similar_graph/similar_graph.ipynb b/examples/demos/gbs/similar_graph/similar_graph.ipynb index 97dc1d38..114a6b86 100644 --- a/examples/demos/gbs/similar_graph/similar_graph.ipynb +++ b/examples/demos/gbs/similar_graph/similar_graph.ipynb @@ -235,12 +235,12 @@ "outputs": [], "source": [ "import deepquantum.photonic as dqp\n", - "import torch " + "import torch" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "97902b3e", "metadata": { "ExecuteTime": { @@ -257,18 +257,19 @@ }, "outputs": [], "source": [ - "\n", "## orbit sample\n", "def orbit_sample(orbit, samples):\n", " \"\"\"Pick the sample belonging to the given orbit\"\"\"\n", " orbit_sample_dict = {}\n", - " set_orbit = sorted(orbit)\n", - " for k in list(samples.keys()):\n", - " temp = list(filter(lambda x: x!= 0, list(k)))\n", + " set_orbit = sorted(orbit)\n", + " for k in list(samples):\n", + " temp = list(filter(lambda x: x != 0, list(k)))\n", " temp = sorted(temp)\n", " if temp == set_orbit:\n", " orbit_sample_dict[k] = samples[k]\n", " return orbit_sample_dict\n", + "\n", + "\n", "## event sample\n", "def event_sample(k, n, samples):\n", " \"\"\"\n", @@ -277,10 +278,10 @@ " n: maximum number of photons for single mode+1\n", " \"\"\"\n", "\n", - " orbit_list = integer_partition(k, n-1)\n", + " orbit_list = integer_partition(k, n - 1)\n", " orbit_list_sort = [sorted(orbit) for orbit in orbit_list]\n", " orbit_sample_list = [{} for i in range(len(orbit_list))]\n", - " for i in list(samples.keys()):\n", + " for i in list(samples):\n", " temp = list(filter(lambda x: x != 0, list(i)))\n", " temp = sorted(temp)\n", " if temp in orbit_list_sort:\n", @@ -288,10 +289,14 @@ " orbit_sample_list[idx][i] = samples[i]\n", " return orbit_sample_list\n", "\n", + "\n", "def integer_partition(m, n):\n", - " \"\"\" integer partition\"\"\"\n", + " \"\"\"integer partition\"\"\"\n", " results = []\n", - " def back_trace(m, n, result=[]):\n", + "\n", + " def back_trace(m, n, result=None):\n", + " if result is None:\n", + " result = []\n", " if m == 0: # 如果 m 等于 0,说明已经找到了一个分解方式,将其加入结果列表\n", " results.append(result)\n", " return\n", @@ -299,9 +304,11 @@ " return\n", " back_trace(m, n - 1, result) # 不使用 n 进行分解,继续递归\n", " back_trace(m - n, n, result + [n]) # 使用 n 进行分解,继续递归\n", + "\n", " back_trace(m, n)\n", " return results\n", "\n", + "\n", "## feature map\n", "def feature_map_event_sample(event_photon_numbers, n, samples):\n", " \"\"\"Map a set of graph G to the feature vectors using the event examples.\"\"\"\n", @@ -315,9 +322,9 @@ " for i in range(len(e_k_n)):\n", " temp_sum = temp_sum + sum(e_k_n[i].values())\n", " count_list.append(temp_sum)\n", - " feature_vector = (torch.stack(count_list) / total_num_samples).reshape(1,-1)\n", + " feature_vector = (torch.stack(count_list) / total_num_samples).reshape(1, -1)\n", " all_feature_vectors.append(feature_vector.squeeze())\n", - " return torch.stack(all_feature_vectors)\n" + " return torch.stack(all_feature_vectors)" ] }, { @@ -386,7 +393,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "f083acea", "metadata": { "ExecuteTime": { @@ -410,8 +417,8 @@ } ], "source": [ - "feature_vec = feature_map_event_sample([6,8,10], 2, [samples_1, samples_2, samples_3, samples_4])\n", - "feature_vec" + "feature_vec = feature_map_event_sample([6, 8, 10], 2, [samples_1, samples_2, samples_3, samples_4])\n", + "print(feature_vec)" ] }, { @@ -468,7 +475,7 @@ } }, "source": [ - "[1] K. Bradler, S. Friedland, J. Izaac, N. Killoran, and D. Su, arXiv:1810.10644 (2018). \n", + "[1] K. Bradler, S. Friedland, J. Izaac, N. Killoran, and D. Su, arXiv:1810.10644 (2018).\n", "\n", "[2] K. Bradler, R. Israel, M. Schuld, and D. Su, arXiv:1910.04022 (2019)." ] diff --git a/examples/demos/gbs/similar_graph/similar_graph.py b/examples/demos/gbs/similar_graph/similar_graph.py new file mode 100644 index 00000000..428a8e68 --- /dev/null +++ b/examples/demos/gbs/similar_graph/similar_graph.py @@ -0,0 +1,242 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.19.1 +# kernelspec: +# display_name: dq_v3 +# language: python +# name: dq_v3 +# --- + +# %% [markdown] +# # 图相似问题 + +# %% [markdown] +# 图相似性问题是指在图论中研究的一个重要问题,它涉及到比较两个或多个图之间的相似程度或差异程度。 +# 这个问题在许多领域都有广泛的应用,包括生物信息学、社交网络分析、图像识别等。 +# 在图相似性问题中,通常需要定义一个度量或指标来衡量图之间的相似性。一些常见的图相似性度量包括: +# 1. 图结构比较:考虑图的结构信息,比较节点之间的连接关系。常用的方法包括计算节点的邻居、子图、路径等结构特征,并根据这些特征计算图之间的相似度。 +# 2. 图的频谱特征:利用图的谱分解特征来比较图之间的相似性。谱方法将图表示为其邻接矩阵的特征值或特征向量,然后使用这些特征来比较图的相似性。 +# 3. 子图匹配:寻找两个图之间的相同子图或最大匹配子图。这种方法通常用于发现图中的共同模式或结构。 +# 4. 图的距离度量:定义图之间的距离或相似度度量,例如编辑距离、哈密尔顿距离、树编辑距离等。这些度量可以衡量图之间的结构差异或相似度。 +# 5. 图的嵌入表示:将图映射到低维向量空间中,然后利用向量表示来比较图之间的相似性。这种方法通常使用图嵌入技术,如节点嵌入或图嵌入。 +# +# 图相似性问题的解决方法取决于具体的应用场景和所关注的问题。 +# 在实际应用中,常常需要综合考虑图的结构特征、频谱特征、距离度量等多方面因素,来全面评估图之间的相似性。 + +# %% [markdown] +# ## 图同构(graph isomorphism) + +# %% [markdown] +# 通过图同构可以来判断图之间的相似程度。 +# 图同构描述的是图论中两个图的完全等价关系,在图论的观点下,两个同构的图被当作同一个图来研究。 +# 只有节点数目相同的两个图才有可能同构。 +# 称两个简单图 $G$ 和 $H$ 是同构,如果存在一个双射(bijective)$f$,使得图 $G$ 中任意两个节点 $i, j$ 相连接,当且仅当 $H$ 中对应的的两个节点$f(i), f(j)$ 相连接。 +# 两个图称为同构如果他们之间存在这样的同构映射关系。 +# 同构的两个图有相同的图结构,因此必然是相似的两个图,然而非同构图也可能有相似的图结构,比如下面的第二个例子 +# +#
+# +#

+# +#

+#
+#
+# +#

+# +#

+#
+ +# %% [markdown] +# ## 特征映射(feature map) + +# %% [markdown] +# 为了更好的衡量图相似度,引入了特征映射(feature map)的方法, 特征映射 $\phi$ 将每个图 $G$ 映射成高维空间的特征向量 $\phi(G)$,图相似度通过核函数 $K$ (kernel function) 来衡量,核函数 $K$ 的作用是计算特征向量之间的距离,比如通过内积计算,此时核函数定义为, +# +# $$ +# K(G, H) = \langle\phi(G),\phi(H) \rangle +# $$ +# +# 需要注意的是特征映射的方法不仅仅适用于图相似度问题,更多的应用在机器学习中的分类问题上。 +# 接下来我们将介绍如何使用高斯玻色采样来构造图的特征映射。 + +# %% [markdown] +# ## GBS 采样和图相似问题 + +# %% [markdown] +# 文献[1]阐述了两个图是同构的当且仅当他们对应的高斯玻色采样概率分布相同, 但是实际实验中图G的GBS的所有样本概率分布是很难得到的, 其原因在于 +# 样本所在的量子态空间随图的规模是指数增大的,那么很多量子态对应的概率是指数下降的,因此在有限的采样次数下很难反映出整体的概率分布。 +# 为了避免这个问题, 我们引入粗粒化采样(coarse-grain)的方法[1], 粗粒化方法将一个全排列对应的所有样本放入同一个样本集合里,这个样本集合称为orbit。 比如样本 $S_1=(0,2,3,0,1,2,0)$ 和 $S_2=(0,3,2,0,2,1,0)$ 属于同一个orbit $(3,2,2,1)$。 +# +# 尽管一个orbit里面包含了许多样本,但是orbit的数量依旧很多,因为末态的数量是随着规模指数增加的, 因此我们可以进一步进行粗粒化[2]。 +# 这里我们引入事件(event)的概念, 用 $E_{k,n}$ 表示, 它是由这些样本组成的集合, 每个样本总光子数为 $k$ 个同时每个模式最大光子数不超过 $n$ 个。 +# 比如上面的 $S_1$ 在事件 $E_{8,3}$ 中,而不在 $E_{8,2}$ 中,因为最大光子数为3。 +# 下图是一个具体的例子 +# + +# %% [markdown] +#
+# +#

+# +#

+#
+ +# %% [markdown] +# 这里我们用 $p_{k,n}(G)$ 表示图G通过高斯玻色采样采到一个样本在事件 $E_{k,n}$ 的概率,现在我们来构造对应的特征映射,它将一个图G映射到一个高维的向量, 向量的每个元素是对应的样本事件概率 $E_{k,n}$, +# +# $$ +# f_{{k},n}(G) = (p_{k_1, n}(G), p_{k_2, n}(G),...,p_{k_D, n}(G)) +# $$ +# $k = \{k_1, k_2,..k_D\}$ 表示一组总的光子数, $n$ 表示每个模式对应的最大光子数 + +# %% [markdown] +# ## 算法说明 +# 1. 将图$G$对应的邻接矩阵编码到高斯玻色采样设备中并产生N个样本。 +# 2. 给定一组总光子数 $k = \{k_1, k_2,...,k_D\}$ 和每个模式对应的最大光子数n, 遍历所有样本, 将样本放入对应事件 $E_{k_i,n}$ 中, 将每个事件包含的样本数记为 $N_i$。 +# 3. 计算每个事件 $E_{k_i,n}$ 对应的概率, 这里用样本概率替代理论概率 $\tilde{p}_{k_i,n}(G) = \frac{N_i}{N}\approx p_{k_i,n}(G) $。 +# 4. 构造特征映射向量 $f_{{k},n}(G) = (\tilde{p}_{k_1, n}(G), \tilde{p}_{k_2, n}(G),...,\tilde{p}_{k_D, n}(G))$。 +# +# + +# %% [markdown] +# ## 数据集说明 +# + +# %% [markdown] +# 这里我们使用mutag数据集的前四个图来作为例子,如下图所示,下图中第一个和第四个图是相似的, 第二个和第三个图是同构的, 我们将通过高斯玻色采样实现四个图的分类。其他数据可以在[这里](https://chrsmrrs.github.io/datasets/docs/datasets/)找到 + +# %% [markdown] +#
+# +#

+# +#

+#
+ +# %% [markdown] +# ## 代码演示 + +# %% +import deepquantum.photonic as dqp +import torch + + +# %% code_folding=[2, 13, 31, 34, 46] +## orbit sample +def orbit_sample(orbit, samples): + """Pick the sample belonging to the given orbit""" + orbit_sample_dict = {} + set_orbit = sorted(orbit) + for k in list(samples): + temp = list(filter(lambda x: x != 0, list(k))) + temp = sorted(temp) + if temp == set_orbit: + orbit_sample_dict[k] = samples[k] + return orbit_sample_dict + + +## event sample +def event_sample(k, n, samples): + """ + Pick the sample belonging to the given even E_{k,n} + k: total number of photons in all modes + n: maximum number of photons for single mode+1 + """ + + orbit_list = integer_partition(k, n - 1) + orbit_list_sort = [sorted(orbit) for orbit in orbit_list] + orbit_sample_list = [{} for i in range(len(orbit_list))] + for i in list(samples): + temp = list(filter(lambda x: x != 0, list(i))) + temp = sorted(temp) + if temp in orbit_list_sort: + idx = orbit_list_sort.index(temp) + orbit_sample_list[idx][i] = samples[i] + return orbit_sample_list + + +def integer_partition(m, n): + """integer partition""" + results = [] + + def back_trace(m, n, result=None): + if result is None: + result = [] + if m == 0: # 如果 m 等于 0,说明已经找到了一个分解方式,将其加入结果列表 + results.append(result) + return + if m < 0 or n == 0: # 如果 m 小于 0 或者 n 等于 0,说明无法找到满足条件的分解方式,直接返回 + return + back_trace(m, n - 1, result) # 不使用 n 进行分解,继续递归 + back_trace(m - n, n, result + [n]) # 使用 n 进行分解,继续递归 + + back_trace(m, n) + return results + + +## feature map +def feature_map_event_sample(event_photon_numbers, n, samples): + """Map a set of graph G to the feature vectors using the event examples.""" + all_feature_vectors = [] + for sample in samples: + count_list = [] + total_num_samples = sum(sample.values()) + for k in event_photon_numbers: + e_k_n = event_sample(k, n, sample) + temp_sum = 0 + for i in range(len(e_k_n)): + temp_sum = temp_sum + sum(e_k_n[i].values()) + count_list.append(temp_sum) + feature_vector = (torch.stack(count_list) / total_num_samples).reshape(1, -1) + all_feature_vectors.append(feature_vector.squeeze()) + return torch.stack(all_feature_vectors) + + +# %% [markdown] +# 先将四个mutag数据图对应的邻接矩阵编码放入高斯玻色采样线路中, 使用 ``deepquantum.photonic.GBS_Graph.measure`` 进行采样,默认使用5条马尔可夫链进行采样, 每条链采样次数为30000次, 我们对应的采样结果为 +# $samples\_1$, $samples\_2$, $samples\_3$, $samples\_4$, 这里我们直接读取已经采样数据。 + +# %% +a_1 = dqp.utils.load_adj('mutag_1_adj') +a_2 = dqp.utils.load_adj('mutag_2_adj') +a_3 = dqp.utils.load_adj('mutag_3_adj') +a_4 = dqp.utils.load_adj('mutag_4_adj') + +# %% +samples_1 = dqp.utils.load_sample('mutag_1_sample') +samples_2 = dqp.utils.load_sample('mutag_2_sample') +samples_3 = dqp.utils.load_sample('mutag_3_sample') +samples_4 = dqp.utils.load_sample('mutag_4_sample') + +# %% [markdown] +# 现在将每个样本的结果通过特征映射函数``feature_map_event_sample``处理, 每个图都映射成一个高维矢量,这里我们采用事件$E_{6,2}, E_{8,2},E_{10,2}$作为分类标准, 得到一个三维的矢量, 因为我们的光子截断数为2,为了更好的分类所以这里采用三维矢量映射而不是二维, +# 映射后的矢量如下图所示。 + +# %% +feature_vec = feature_map_event_sample([6, 8, 10], 2, [samples_1, samples_2, samples_3, samples_4]) +print(feature_vec) + +# %% [markdown] +# 将下面三维矢量可视化如下图所示 + +# %% [markdown] +#
+# +#

+# +#

+#
+ +# %% [markdown] +# # 附录 + +# %% [markdown] +# [1] K. Bradler, S. Friedland, J. Izaac, N. Killoran, and D. Su, arXiv:1810.10644 (2018). +# +# [2] K. Bradler, R. Israel, M. Schuld, and D. Su, arXiv:1910.04022 (2019). diff --git a/examples/demos/gbs/variational_gbs/variational_gbs.ipynb b/examples/demos/gbs/variational_gbs/variational_gbs.ipynb index a5d6325e..a9cd5951 100644 --- a/examples/demos/gbs/variational_gbs/variational_gbs.ipynb +++ b/examples/demos/gbs/variational_gbs/variational_gbs.ipynb @@ -11,10 +11,10 @@ "\n", "*前置模块*\n", "\n", - "[关于玻色采样的介绍](https://github.com/TuringQ/deepquantum/tree/main/examples/gbs/boson_sampling/boson_sampling.ipynb)\n", + "[关于玻色采样的介绍](https://github.com/TuringQ/deepquantum/tree/main/examples/demos/gbs/boson_sampling/boson_sampling.ipynb)\n", "\n", "\n", - "[关于高斯玻色采样的介绍](https://github.com/TuringQ/deepquantum/tree/main/examples/gbs/gaussian_boson_sampling/gaussian_boson_sampling.ipynb)\n", + "[关于高斯玻色采样的介绍](https://github.com/TuringQ/deepquantum/tree/main/examples/demos/gbs/gaussian_boson_sampling/gaussian_boson_sampling.ipynb)\n", "\n", "引言\n", "------\n", @@ -37,7 +37,7 @@ "\n", "[变分量子算法案例](https://deepquantum.turingq.com/category/quantum-variational-algorithm/)\n", "\n", - "对于光量子模块,[光量子入门介绍](https://github.com/TuringQ/deepquantum/blob/main/docs/photonic_basics.ipynb) 演示了如何搭建含参数的光量子线路,并用Fock后端进行采样测量。\n", + "对于光量子模块,[光量子入门介绍](https://github.com/TuringQ/deepquantum/blob/main/tutorials/photonic_basics.ipynb) 演示了如何搭建含参数的光量子线路,并用Fock后端进行采样测量。\n", "\n", "那么,对于独具特色的高斯玻色采样(Gaussian Boson Sampling,简称GBS)任务,我们是否也能完成对于变分线路的构建和训练呢?\n", "\n" @@ -57,7 +57,7 @@ "\\Pr(S) = \\frac{1}{\\mathcal{N}} \\frac{|\\text{Haf}(A_S)|^2}{s_1!\\ldots s_m!},\n", "\\end{equation*}\n", "\n", - "其中,$S=(s_1, s_2, \\ldots, s_m)$, $s_i$是在第$i$个mode探测到的光子数。 \n", + "其中,$S=(s_1, s_2, \\ldots, s_m)$, $s_i$是在第$i$个mode探测到的光子数。\n", "而$\\mathcal{N}$ 是一个归一化常数,$A$是一个任意的特征值在 $-1$ 和$1$间的对称矩阵。\n", "矩阵 $A$也可以通过一个常数因子进行重新缩放,相当于定义在目标概率分布中总的平均光子数。\n", "\n", @@ -90,12 +90,15 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import deepquantum as dq\n", - "import torch" + "import matplotlib.pyplot as plt\n", + "import networkx as nx\n", + "import torch\n", + "import torch.nn as nn" ] }, { @@ -107,7 +110,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -133,9 +136,6 @@ } ], "source": [ - "import networkx as nx\n", - "import numpy as np\n", - "\n", "graph = nx.lollipop_graph(3, 2)\n", "\n", "# 计算邻接矩阵\n", @@ -143,7 +143,7 @@ "print('邻接矩阵A:', a)\n", "\n", "# 可视化图像\n", - "nx.draw(graph, with_labels=True)\n" + "nx.draw(graph, with_labels=True)" ] }, { @@ -198,11 +198,11 @@ "\n", "# 高斯玻色采样\n", "sample = gbs.measure(mcmc=True)\n", - "print('采样结果为:',sample)\n", + "print('采样结果为:', sample)\n", "\n", "# 计算每个节点理论平均光子数\n", "photon_number = gbs.photon_number_mean_var()[0]\n", - "print('每个节点平均光子数为:',photon_number)" + "print('每个节点平均光子数为:', photon_number)" ] }, { @@ -256,18 +256,18 @@ "source": [ "# 生成初始参数\n", "nr_modes = len(a)\n", - "params = torch.randn(nr_modes,dtype=torch.float64)\n", - "print('初始化参数为: ',params)\n", + "params = torch.randn(nr_modes, dtype=torch.float64)\n", + "print('初始化参数为: ', params)\n", "\n", "# 编码进权重矩阵\n", "weights = torch.exp(-params)\n", - "print('指数权重为: ',weights)\n", + "print('指数权重为: ', weights)\n", "w = torch.diag(weights)\n", - "print('权重矩阵为: ',w)\n", + "print('权重矩阵为: ', w)\n", "\n", "# 实现WAW参数化\n", "waw = w @ torch.tensor(a) @ w\n", - "print('WAW矩阵为: ',waw)" + "print('WAW矩阵为: ', waw)" ] }, { @@ -285,7 +285,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -298,14 +298,14 @@ } ], "source": [ - "#根据精度需求设定cutoff\n", - "#设定平均光子数为6(也可设置其它)\n", - "gbs = dq.photonic.GBS_Graph(adj_mat=waw, cutoff=3,mean_photon_num=6)\n", + "# 根据精度需求设定cutoff\n", + "# 设定平均光子数为6(也可设置其它)\n", + "gbs = dq.photonic.GraphGBS(adj_mat=waw, cutoff=3, mean_photon_num=6)\n", "gbs()\n", "\n", "# 计算每个节点理论平均光子数\n", "photon_number = gbs.photon_number_mean_var()[0]\n", - "print('每个节点平均光子数为:',photon_number)" + "print('每个节点平均光子数为:', photon_number)" ] }, { @@ -333,15 +333,17 @@ } ], "source": [ - "#构建期望模式子集\n", + "# 构建期望模式子集\n", "subset = [0, 1, 2]\n", "\n", - "#构建损失函数\n", + "\n", + "# 构建损失函数\n", "def target(s):\n", " not_subset = [k for k in range(len(s)) if k not in subset]\n", " return sum(s[not_subset]) - sum(s[subset])\n", "\n", - "print('loss值为: ',target(photon_number))" + "\n", + "print('loss值为: ', target(photon_number))" ] }, { @@ -355,51 +357,46 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "import deepquantum as dq\n", - "import torch\n", - "import networkx as nx\n", - "import torch.nn as nn\n", - "\n", "graph = nx.lollipop_graph(3, 2)\n", "a = nx.to_numpy_array(graph)\n", "nr_modes = len(a)\n", - "subset = [0,1,2]\n", + "subset = [0, 1, 2]\n", "loss_history = []\n", "result = []\n", "\n", - "class VGBS(nn.Module):\n", "\n", + "class VGBS(nn.Module):\n", " def __init__(self):\n", - " super(VGBS,self).__init__()\n", + " super().__init__()\n", " self.params = nn.Parameter(torch.randn(nr_modes, dtype=torch.float64), requires_grad=False)\n", " loss_history.clear()\n", "\n", - " def target(self,s):\n", + " def target(self, s):\n", " not_subset = [k for k in range(len(s)) if k not in subset]\n", " return sum(s[not_subset]) - sum(s[subset])\n", "\n", - " def loss(self,x):\n", + " def loss(self, x):\n", " if not isinstance(x, torch.Tensor):\n", " x = torch.tensor(x).to(self.params.dtype).reshape(-1)\n", " weights = torch.exp(-x)\n", " w = torch.diag(weights)\n", - " waw= w @ torch.tensor(a) @ w\n", + " waw = w @ torch.tensor(a) @ w\n", "\n", - " gbs = dq.photonic.GBS_Graph(adj_mat=waw, cutoff=5,mean_photon_num=6)\n", + " gbs = dq.photonic.GBS_Graph(adj_mat=waw, cutoff=5, mean_photon_num=6)\n", " gbs()\n", " photon_number = gbs.photon_number_mean_var()[0]\n", - " print('每个节点平均光子数为: ',photon_number)\n", - " l = self.target(photon_number)\n", + " print('每个节点平均光子数为: ', photon_number)\n", + " loss = self.target(photon_number)\n", "\n", - " loss_history.append(l.item())\n", + " loss_history.append(loss.item())\n", " result.clear()\n", " result.append(photon_number.tolist())\n", "\n", - " return l\n" + " return loss" ] }, { @@ -626,17 +623,10 @@ "model = VGBS()\n", "\n", "# 定义优化器参数\n", - "spsa_hyperparam = {\n", - " 'a': 1,\n", - " 'c': 0.01,\n", - " 'A': 200,\n", - " 'nepoch': 1000,\n", - " 'alpha': 0.602,\n", - " 'gamma': 0.101\n", - "}\n", + "spsa_hyperparam = {'a': 1, 'c': 0.01, 'A': 200, 'nepoch': 1000, 'alpha': 0.602, 'gamma': 0.101}\n", "optimizer = dq.optimizer.OptimizerSPSA(model.loss, model.params)\n", "optimizer.set_hyperparam(spsa_hyperparam)\n", - "param_best = torch.tensor(optimizer.run(100)).float()\n" + "param_best = torch.tensor(optimizer.run(100)).float()" ] }, { @@ -659,7 +649,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -684,7 +674,6 @@ } ], "source": [ - "import matplotlib.pyplot as plt\n", "plt.figure(figsize=(10, 6))\n", "plt.plot(loss_history)\n", "plt.xlabel('Epoch')\n", @@ -704,7 +693,9 @@ { "cell_type": "code", "execution_count": 10, - "metadata": {}, + "metadata": { + "lines_to_next_cell": 2 + }, "outputs": [ { "data": { @@ -719,12 +710,14 @@ ], "source": [ "result_scaled = [x * 800 for x in result[0]]\n", - "nx.draw(graph, node_size = result_scaled, with_labels=True)" + "nx.draw(graph, node_size=result_scaled, with_labels=True)" ] }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "lines_to_next_cell": 2 + }, "source": [ "针对损失函数的进一步改进\n", "-----------------------\n", @@ -745,7 +738,9 @@ { "cell_type": "code", "execution_count": 11, - "metadata": {}, + "metadata": { + "lines_to_next_cell": 2 + }, "outputs": [], "source": [ "def target_kl(s):\n", @@ -754,14 +749,16 @@ }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "lines_to_next_cell": 2 + }, "source": [ "更新损失函数,再次开始优化:" ] }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -3065,53 +3062,46 @@ ], "source": [ "class VGBS(nn.Module):\n", - "\n", " def __init__(self):\n", - " super(VGBS,self).__init__()\n", + " super().__init__()\n", " self.params = nn.Parameter(torch.randn(nr_modes, dtype=torch.float64), requires_grad=False)\n", " loss_history.clear()\n", "\n", " # 采用KL散度定义的损失函数\n", - " def target_kl(self,s):\n", + " def target_kl(self, s):\n", " return -sum(torch.log(s[subset] / 2))\n", "\n", - " def loss(self,x):\n", + " def loss(self, x):\n", " if not isinstance(x, torch.Tensor):\n", " x = torch.tensor(x).to(self.params.dtype).reshape(-1)\n", " weights = torch.exp(-x)\n", " w = torch.diag(weights)\n", - " waw= w @ torch.tensor(a) @ w\n", + " waw = w @ torch.tensor(a) @ w\n", "\n", - " gbs = dq.photonic.GBS_Graph(adj_mat=waw, cutoff=5,mean_photon_num=6)\n", + " gbs = dq.photonic.GBS_Graph(adj_mat=waw, cutoff=5, mean_photon_num=6)\n", " gbs()\n", " photon_number = gbs.photon_number_mean_var()[0]\n", - " print('每个节点平均光子数为: ',photon_number)\n", - " l = self.target_kl(photon_number)\n", + " print('每个节点平均光子数为: ', photon_number)\n", + " loss = self.target_kl(photon_number)\n", "\n", - " loss_history.append(l.item())\n", + " loss_history.append(loss.item())\n", " result.clear()\n", " result.append(photon_number.tolist())\n", "\n", - " return l\n", + " return loss\n", + "\n", "\n", "model = VGBS()\n", "loss_history = []\n", "\n", "# 定义优化器参数\n", - "spsa_hyperparam = {\n", - " 'a': 1,\n", - " 'c': 0.01,\n", - " 'A': 200,\n", - " 'nepoch': 1000,\n", - " 'alpha': 0.602,\n", - " 'gamma': 0.101\n", - "}\n", + "spsa_hyperparam = {'a': 1, 'c': 0.01, 'A': 200, 'nepoch': 1000, 'alpha': 0.602, 'gamma': 0.101}\n", "optimizer = dq.optimizer.OptimizerSPSA(model.loss, model.params)\n", "optimizer.set_hyperparam(spsa_hyperparam)\n", "param_best = torch.tensor(optimizer.run(1000)).float()\n", "\n", "result_scaled = [x * 800 for x in result[0]]\n", - "nx.draw(graph, node_size = result_scaled, with_labels=True)" + "nx.draw(graph, node_size=result_scaled, with_labels=True)" ] }, { diff --git a/examples/demos/gbs/variational_gbs/variational_gbs.py b/examples/demos/gbs/variational_gbs/variational_gbs.py new file mode 100644 index 00000000..6069d78a --- /dev/null +++ b/examples/demos/gbs/variational_gbs/variational_gbs.py @@ -0,0 +1,364 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.19.1 +# kernelspec: +# display_name: dq_draw +# language: python +# name: python3 +# --- + +# %% [markdown] +# +# 变分高斯玻色采样的训练 +# ====================================== +# +# +# *前置模块* +# +# [关于玻色采样的介绍](https://github.com/TuringQ/deepquantum/tree/main/examples/demos/gbs/boson_sampling/boson_sampling.ipynb) +# +# +# [关于高斯玻色采样的介绍](https://github.com/TuringQ/deepquantum/tree/main/examples/demos/gbs/gaussian_boson_sampling/gaussian_boson_sampling.ipynb) +# +# 引言 +# ------ +# +# 受到机器学习中神经网络成功的启发,许多应用层面的量子算法依赖于变分量子线路的训练,包括: +# +# 1. **量子神经网络(Quantum Neural Networks, QNNs)**:一类模仿经典神经网络结构的量子算法,它们使用可变分的量子网络来表示信息,并利用量子力学原理进行信息处理。 +# +# 2. **量子支持向量机(Quantum Support Vector Machine, QSVM)**:使用变分量子线路定义核函数,用于解决凸优化、分类问题等。 +# +# 3. **量子近似优化算法(Quantum Approximate Optimization Algorithm, QAOA)**:通过调整量子线路的参数来找到优化问题的近似最优解。 +# +# 4. **变分量子本征求解器(Variational Quantum Eigensolver, VQE)**:一种用于求解分子能量基态问题的量子算法,通过训练量子线路的参数来近似哈密顿量的最低本征值。 +# +# 5. **量子机器学习算法(Quantum Machine Learning Algorithms)**:使用可变分的量子算法来加速机器学习任务,例如量子数据编码、量子特征提取等。 +# +# 6. **量子随机特征(Quantum Random Feature, QRF)**:将量子计算与经典机器学习模型结合的方法,通过量子线路生成高维空间中的随机特征,以提高模型的性能。 +# +# 在DeepQuantum常规量子线路中,我们也已经展示了若干从简单、中级到困难的变分量子算法的案例👇 +# +# [变分量子算法案例](https://deepquantum.turingq.com/category/quantum-variational-algorithm/) +# +# 对于光量子模块,[光量子入门介绍](https://github.com/TuringQ/deepquantum/blob/main/tutorials/photonic_basics.ipynb) 演示了如何搭建含参数的光量子线路,并用Fock后端进行采样测量。 +# +# 那么,对于独具特色的高斯玻色采样(Gaussian Boson Sampling,简称GBS)任务,我们是否也能完成对于变分线路的构建和训练呢? +# +# + +# %% [markdown] +# +# 理论基础 +# ------ +# +# 在[关于高斯玻色采样的介绍](https://github.com/TuringQ/deepquantum/tree/main/examples/gbs/gaussian_boson_sampling/gaussian_boson_sampling.ipynb)中,我们对高斯玻色采样(GBS)进行了细致的介绍。形如[玻色采样](https://github.com/TuringQ/deepquantum/tree/main/examples/gbs/boson_sampling/boson_sampling.ipynb)的概率分布,对于GBS设备,观察到特定输出分布$S$的概率$\Pr(S)$如下: +# +# \begin{equation*} +# \Pr(S) = \frac{1}{\mathcal{N}} \frac{|\text{Haf}(A_S)|^2}{s_1!\ldots s_m!}, +# \end{equation*} +# +# 其中,$S=(s_1, s_2, \ldots, s_m)$, $s_i$是在第$i$个mode探测到的光子数。 +# 而$\mathcal{N}$ 是一个归一化常数,$A$是一个任意的特征值在 $-1$ 和$1$间的对称矩阵。 +# 矩阵 $A$也可以通过一个常数因子进行重新缩放,相当于定义在目标概率分布中总的平均光子数。 +# +# 我们希望对这种分布进行**训练**,以执行特定任务。例如,希望再现给定数据集的统计特性,或者优化线路以高概率采样特定模式。以此,任何变分量子算法都有可能在GBS设备上实现。 +# +# 用数学随机优化模型来表示,给定一个函数$h(S)$和参数$\theta$,我们可以通过采样得到概率分布$P_{\theta}(S)$。而任务的目标则是找到合适的参数$\theta$,来最小化如下期望值: +# +# \begin{equation*} +# C (\theta) = \sum_{S} h(S) P_{\theta}(S). +# \end{equation*} +# +# 此案例将聚焦一个简单的5节点的棒棒糖🍭图。通过变分高斯玻色采样的训练,我们期望在特定的节点观察到尽可能多的光子,而在别的节点观察到尽量少的光子。 +# +# +# 完成此变分案例需要以下3步:(i)选用合适的方法编码参数;(ii)调用DeepQuantum模块完成GBS采样模拟;(iii)根据采样结果,选取合适损失函数和优化器完成优化。 +# +# +# + +# %% [markdown] +# 问题转化与参数化 +# ----------------------- +# 我们将会调用DeepQuantum中GBS模块,详情可见[API文档](https://dqapi.turingq.com/deepquantum.photonic.html#deepquantum.photonic.ansatz.GBS_Graph) +# +# 首先调用DeepQauntum和相关包: + +# %% +import deepquantum as dq +import matplotlib.pyplot as plt +import networkx as nx +import torch +import torch.nn as nn + +# %% [markdown] +# 调用networkx包以生成5节点的棒棒糖🍭图,并获得邻接矩阵以对应GBS中特征值在 $-1$ 和$1$间的的对称矩阵$A$。 + +# %% +graph = nx.lollipop_graph(3, 2) + +# 计算邻接矩阵 +a = nx.to_numpy_array(graph) +print('邻接矩阵A:', a) + +# 可视化图像 +nx.draw(graph, with_labels=True) + +# %% [markdown] +# 此时,若无参数化需要,GBS可通过邻接矩阵$A$采样生成概率分布$P(S)$: + +# %% +gbs = dq.photonic.ansatz.GBS_Graph(adj_mat=a, cutoff=3, mean_photon_num=6) +gbs() + +# 高斯玻色采样 +sample = gbs.measure(mcmc=True) +print('采样结果为:', sample) + +# 计算每个节点理论平均光子数 +photon_number = gbs.photon_number_mean_var()[0] +print('每个节点平均光子数为:', photon_number) + +# %% [markdown] +# 为了实现变分优化,需要编码参数$\theta$进GBS设备,即参数化矩阵$A$。论文[Training Gaussian Boson Sampling Distributions](https://arxiv.org/abs/2004.04770)中引入了“WAW”的参数化方式,即将对称矩阵$A$转化为 +# +# \begin{equation*} +# A \rightarrow A_W = W A W, +# \end{equation*} +# +# 其中$W = \text{diag}(\sqrt{w_1}, \sqrt{w_2}, \ldots, \sqrt{w_m})$ 是对角权重矩阵, $m$是GBS模式数。 +# 这样的构造既可以方便的通过权重$w$实现参数化,又保留了$A$对称的特性。另外,在计算$A_W$的hafnian值时,可通过以下分解分离参数化部分,不会额外增加hafnian的计算难度: +# +# \begin{equation*} +# \text{Haf}(A_W) = \text{Haf}(A)\text{det}(W), +# \end{equation*} +# +# 于是,我们可以方便地编码可训练参数$\theta = (\theta_1, \ldots, \theta_d)$ 进权重 $w_k$。这里,我们选用指数嵌入的形式, +# +# \begin{equation*} +# w_k = \exp(-\theta_k), +# \end{equation*} + +# %% +# 生成初始参数 +nr_modes = len(a) +params = torch.randn(nr_modes, dtype=torch.float64) +print('初始化参数为: ', params) + +# 编码进权重矩阵 +weights = torch.exp(-params) +print('指数权重为: ', weights) +w = torch.diag(weights) +print('权重矩阵为: ', w) + +# 实现WAW参数化 +waw = w @ torch.tensor(a) @ w +print('WAW矩阵为: ', waw) + +# %% [markdown] +# 调用DeepQuantum模块完成GBS采样模拟 +# ----------------------- +# 如前模块所示,调用DeepQuantum实现GBS采样模拟十分便捷。高斯玻色采样(GBS)分布由对称矩阵 $A$ 决定,在经过WAW方法参数化后,我们只需要输入waw矩阵。 +# +# 总的平均光子数是分布的一个超参数:一般而言,不同的选择可能会导致训练中得到不同的结果。实际上,随着权重被优化,平均光子数在训练过程中可能会发生变化,但不会影响最终相对的概率分布。 +# +# 最后,GBS设备可以操作具有分辨光子数能力的探测器或阈值探测器,这里我们只使用每个模式上的平均光子数。 + +# %% +# 根据精度需求设定cutoff +# 设定平均光子数为6(也可设置其它) +gbs = dq.photonic.GraphGBS(adj_mat=waw, cutoff=3, mean_photon_num=6) +gbs() + +# 计算每个节点理论平均光子数 +photon_number = gbs.photon_number_mean_var()[0] +print('每个节点平均光子数为:', photon_number) + +# %% [markdown] +# 选取损失函数和优化器,完成优化 +# ----------------------- +# 根据案例开头的需求,在一个5节点的棒棒糖图中,通过变分高斯玻色采样的训练,我们期望在特定的节点观察到尽可能多的光子,而在别的节点观察到尽量少的光子。不失一般性,我们致力于增加棒棒糖图的“糖果”部分中的光子数,这对应于模式子集``[0, 1, 2]``。 +# +# 损失函数的构建很多样,先采用最简单的线性损失函数: + +# %% +# 构建期望模式子集 +subset = [0, 1, 2] + + +# 构建损失函数 +def target(s): + not_subset = [k for k in range(len(s)) if k not in subset] + return sum(s[not_subset]) - sum(s[subset]) + + +print('loss值为: ', target(photon_number)) + +# %% [markdown] +# 接下来仅需通过优化器,最小化损失函数的值,便可完成对于变分高斯玻色采样设备的训练。 +# +# 为了方便调用优化器,我们整合上面代码,组合成一个`VGBS`的`class`。 + +# %% +graph = nx.lollipop_graph(3, 2) +a = nx.to_numpy_array(graph) +nr_modes = len(a) +subset = [0, 1, 2] +loss_history = [] +result = [] + + +class VGBS(nn.Module): + def __init__(self): + super().__init__() + self.params = nn.Parameter(torch.randn(nr_modes, dtype=torch.float64), requires_grad=False) + loss_history.clear() + + def target(self, s): + not_subset = [k for k in range(len(s)) if k not in subset] + return sum(s[not_subset]) - sum(s[subset]) + + def loss(self, x): + if not isinstance(x, torch.Tensor): + x = torch.tensor(x).to(self.params.dtype).reshape(-1) + weights = torch.exp(-x) + w = torch.diag(weights) + waw = w @ torch.tensor(a) @ w + + gbs = dq.photonic.GBS_Graph(adj_mat=waw, cutoff=5, mean_photon_num=6) + gbs() + photon_number = gbs.photon_number_mean_var()[0] + print('每个节点平均光子数为: ', photon_number) + loss = self.target(photon_number) + + loss_history.append(loss.item()) + result.clear() + result.append(photon_number.tolist()) + + return loss + + +# %% [markdown] +# 选取DeepQuantum内建SPSA优化器,设定优化器参数,完成优化。 + +# %% +# 生成刚创建的VGBS模型 +model = VGBS() + +# 定义优化器参数 +spsa_hyperparam = {'a': 1, 'c': 0.01, 'A': 200, 'nepoch': 1000, 'alpha': 0.602, 'gamma': 0.101} +optimizer = dq.optimizer.OptimizerSPSA(model.loss, model.params) +optimizer.set_hyperparam(spsa_hyperparam) +param_best = torch.tensor(optimizer.run(100)).float() + +# %% [markdown] +# 由优化结果可见,前三个“糖果”节点平均光子数大于“棒子”节点平均光子数,优化成功! +# +# + +# %% [markdown] +# 可视化结果 +# ----------------------- +# 调用`matplotlib`库,绘制优化过程中损失函数随迭代次数下降曲线。 +# 可见在该问题上,虽然使用的是非梯度算法,DeepQuantum自带的`OptimizerSPSA`优化器收敛非常迅速。 + +# %% +plt.figure(figsize=(10, 6)) +plt.plot(loss_history) +plt.xlabel('Epoch') +plt.ylabel('Loss') +plt.title('Loss over time') + +# %% [markdown] +# 绘制棒棒糖图查看变分优化结果。 +# 其中,每个节点的大小代表平均光子数的多少。 +# 明显可见位于“糖果”处的平均光子数远高于“棒子”,实现了本案例训练的目标。 + +# %% +result_scaled = [x * 800 for x in result[0]] +nx.draw(graph, node_size=result_scaled, with_labels=True) + + +# %% [markdown] +# 针对损失函数的进一步改进 +# ----------------------- +# 我们可以观察到,在上个模块中,虽然“糖果”节点处`[0,1,2]`的光子数远高于其它节点,但当前的简单线性损失函数无法很好控制`[0,1,2]`节点的相对光子数。 +# 不失一般性,我们如果额外要求`[0,1,2]`节点的光子数相等,该如何处理? +# +# 其实,这项任务本质可以被认为是:训练一个变分高斯采样线路,使其输出的概率分布与目标概率分布相一致。对于概率分布的训练可以通过最小化Kullback-Leibler(KL)散度来执行,这在去掉常数项后可以写成: +# +# \begin{equation*} +# KL(\theta) = -\frac{1}{T}\sum_S \log[P_{\theta}(S)]. +# \end{equation*} +# +# +# 在这种情况下,$ S $ 是概率分布中一个元素,$ P(S) $ 是从GBS分布中抽样时观察到该元素的概率,而 $ T $ 是元素的总数。 +# 据此,我们可以写出新的损失函数`target_kl`: + + +# %% +def target_kl(s): + return -sum(torch.log(s[subset] / 2)) + + +# %% [markdown] +# 更新损失函数,再次开始优化: + + +# %% +class VGBS(nn.Module): + def __init__(self): + super().__init__() + self.params = nn.Parameter(torch.randn(nr_modes, dtype=torch.float64), requires_grad=False) + loss_history.clear() + + # 采用KL散度定义的损失函数 + def target_kl(self, s): + return -sum(torch.log(s[subset] / 2)) + + def loss(self, x): + if not isinstance(x, torch.Tensor): + x = torch.tensor(x).to(self.params.dtype).reshape(-1) + weights = torch.exp(-x) + w = torch.diag(weights) + waw = w @ torch.tensor(a) @ w + + gbs = dq.photonic.GBS_Graph(adj_mat=waw, cutoff=5, mean_photon_num=6) + gbs() + photon_number = gbs.photon_number_mean_var()[0] + print('每个节点平均光子数为: ', photon_number) + loss = self.target_kl(photon_number) + + loss_history.append(loss.item()) + result.clear() + result.append(photon_number.tolist()) + + return loss + + +model = VGBS() +loss_history = [] + +# 定义优化器参数 +spsa_hyperparam = {'a': 1, 'c': 0.01, 'A': 200, 'nepoch': 1000, 'alpha': 0.602, 'gamma': 0.101} +optimizer = dq.optimizer.OptimizerSPSA(model.loss, model.params) +optimizer.set_hyperparam(spsa_hyperparam) +param_best = torch.tensor(optimizer.run(1000)).float() + +result_scaled = [x * 800 for x in result[0]] +nx.draw(graph, node_size=result_scaled, with_labels=True) + +# %% [markdown] +# 每个“糖果”节点输出近似2个光子,而其余节点几乎没有输出光子,优化结果非常完美! + +# %% [markdown] +# # 附录 + +# %% [markdown] +# [1] Leonardo Banchi, Nicolás Quesada, and Juan Miguel Arrazola. Training Gaussian Boson Sampling Distributions. arXiv:2004.04770. 2020. diff --git a/examples/demos/gbs/vibronic_excitations/vibronic_excitations.ipynb b/examples/demos/gbs/vibronic_excitations/vibronic_excitations.ipynb index b98bf8a0..ead0a775 100644 --- a/examples/demos/gbs/vibronic_excitations/vibronic_excitations.ipynb +++ b/examples/demos/gbs/vibronic_excitations/vibronic_excitations.ipynb @@ -30,9 +30,9 @@ "metadata": {}, "outputs": [], "source": [ - "import numpy as np\n", "import deepquantum as dq\n", - "import matplotlib.pyplot as plt" + "import matplotlib.pyplot as plt\n", + "import numpy as np" ] }, { @@ -57,8 +57,8 @@ "metadata": {}, "outputs": [], "source": [ - "ri = np.genfromtxt(\"./data/formic_ri.csv\", delimiter=\",\", skip_header=0)[:, np.newaxis]\n", - "rf = np.genfromtxt(\"./data/formic_rf.csv\", delimiter=\",\", skip_header=0)[:, np.newaxis]" + "ri = np.genfromtxt('./data/formic_ri.csv', delimiter=',', skip_header=0)[:, np.newaxis]\n", + "rf = np.genfromtxt('./data/formic_rf.csv', delimiter=',', skip_header=0)[:, np.newaxis]" ] }, { @@ -74,8 +74,8 @@ "metadata": {}, "outputs": [], "source": [ - "li = np.genfromtxt(\"./data/formic_li.csv\", delimiter=\",\", skip_header=0)\n", - "lf = np.genfromtxt(\"./data/formic_lf.csv\", delimiter=\",\", skip_header=0)" + "li = np.genfromtxt('./data/formic_li.csv', delimiter=',', skip_header=0)\n", + "lf = np.genfromtxt('./data/formic_lf.csv', delimiter=',', skip_header=0)" ] }, { @@ -91,8 +91,8 @@ "metadata": {}, "outputs": [], "source": [ - "omega = np.genfromtxt(\"./data/formic_omega.csv\", delimiter=\",\", skip_header=0)\n", - "omegap = np.genfromtxt(\"./data/formic_omegap.csv\", delimiter=\",\", skip_header=0)" + "omega = np.genfromtxt('./data/formic_omega.csv', delimiter=',', skip_header=0)\n", + "omegap = np.genfromtxt('./data/formic_omegap.csv', delimiter=',', skip_header=0)" ] }, { @@ -126,7 +126,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -159,12 +159,12 @@ " for lf_elf in lf:\n", " u.append(np.sum(li_ele * lf_elf))\n", "u = np.array(u[-1::-1]).reshape(7, 7).T\n", - "u" + "print(u)" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -189,10 +189,10 @@ "m = np.diag([m_c, m_c, m_c, m_o, m_o, m_o, m_o, m_o, m_o, m_h, m_h, m_h, m_h, m_h, m_h])\n", "for i in range(len(omegap)):\n", " d = lf[i].T @ np.sqrt(m) @ (ri - rf)\n", - " l = np.sqrt(h / (4 * np.pi**2 * 100 * omegap[i] * c * mu)) / (10**-10)\n", - " delta.append(d / l)\n", + " denominator = np.sqrt(h / (4 * np.pi**2 * 100 * omegap[i] * c * mu)) / (10**-10)\n", + " delta.append(d / denominator)\n", "delta = np.array(delta[-1::-1])\n", - "delta" + "print(delta)" ] }, { @@ -219,10 +219,10 @@ } ], "source": [ - "plt.imshow(abs(u), cmap=\"Greens\")\n", + "plt.imshow(abs(u), cmap='Greens')\n", "plt.colorbar()\n", - "plt.xlabel(\"Mode index\")\n", - "plt.ylabel(\"Mode index\")\n", + "plt.xlabel('Mode index')\n", + "plt.ylabel('Mode index')\n", "plt.tight_layout()\n", "plt.show()" ] @@ -244,11 +244,7 @@ "pre_transition_squeezing = np.sqrt(omega[-1::-1])\n", "post_transition_squeezing = np.sqrt(omegap[-1::-1])\n", "\n", - "j_mat = (\n", - " np.diag(post_transition_squeezing)\n", - " @ u\n", - " @ np.linalg.inv(np.diag(pre_transition_squeezing))\n", - ")\n", + "j_mat = np.diag(post_transition_squeezing) @ u @ np.linalg.inv(np.diag(pre_transition_squeezing))\n", "\n", "cl, lambda_1, cr = np.linalg.svd(j_mat)\n", "\n", @@ -297,10 +293,10 @@ "source": [ "cir = dq.photonic.QumodeCircuit(\n", " nmode=modes,\n", - " init_state=\"vac\",\n", + " init_state='vac',\n", " # init_state=init_state,\n", " cutoff=cutoff,\n", - " backend=\"gaussian\",\n", + " backend='gaussian',\n", ")\n", "\n", "for i in range(modes):\n", @@ -371,10 +367,10 @@ "counts2 = np.sum(sample2, axis=0)\n", "\n", "plt.figure(figsize=(8, 4))\n", - "plt.ylabel(\"Photon number\")\n", - "plt.xlabel(r\"Frequency (cm$^{-1}$)\")\n", + "plt.ylabel('Photon number')\n", + "plt.xlabel(r'Frequency (cm$^{-1}$)')\n", "plt.xticks(range(len(omegap)), np.round(omegap, 1), rotation=90)\n", - "plt.bar(range(len(omegap)), counts2, color=\"green\")\n", + "plt.bar(range(len(omegap)), counts2, color='green')\n", "plt.tight_layout()\n", "plt.show()" ] @@ -410,10 +406,10 @@ "source": [ "cir = dq.photonic.QumodeCircuit(\n", " nmode=modes,\n", - " init_state=\"vac\",\n", + " init_state='vac',\n", " # init_state=init_state,\n", " cutoff=cutoff,\n", - " backend=\"gaussian\",\n", + " backend='gaussian',\n", ")\n", "\n", "cir.d(wires=[5], r=1.0)\n", @@ -486,10 +482,10 @@ "counts4 = np.sum(sample4, axis=0)\n", "\n", "plt.figure(figsize=(8, 4))\n", - "plt.ylabel(\"Photon number\")\n", - "plt.xlabel(r\"Frequency (cm$^{-1}$)\")\n", + "plt.ylabel('Photon number')\n", + "plt.xlabel(r'Frequency (cm$^{-1}$)')\n", "plt.xticks(range(len(omegap)), np.round(omegap, 1), rotation=90)\n", - "plt.bar(range(len(omegap)), counts4, color=\"green\")\n", + "plt.bar(range(len(omegap)), counts4, color='green')\n", "plt.tight_layout()\n", "plt.show()" ] diff --git a/examples/demos/gbs/vibronic_excitations/vibronic_excitations.py b/examples/demos/gbs/vibronic_excitations/vibronic_excitations.py new file mode 100644 index 00000000..b21d7a02 --- /dev/null +++ b/examples/demos/gbs/vibronic_excitations/vibronic_excitations.py @@ -0,0 +1,215 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.19.1 +# kernelspec: +# display_name: dq_draw +# language: python +# name: python3 +# --- + +# %% [markdown] +# # 高斯玻色采样模拟分子振动激发 + +# %% [markdown] +# 分子中振动模式的激发会影响化学反应的结果。 +# 振动激发可以为分子提供额外的动能,以克服特定反应的能量壁垒,并有助于控制分子的稳定性。 +# 然而,对涉及分子振动和电子态同时变化的过程进行所有振动能级激发概率模拟是具有挑战性的。 +# 本例中,我们演示如何使用高斯玻色取样(GBS)来模拟分子振动激发。 + +# %% [markdown] +# ### 甲酸分子的振动激发 + +# %% +import deepquantum as dq +import matplotlib.pyplot as plt +import numpy as np + +# %% [markdown] +# 我们还需要描述分子振动模式和分子在振动跃迁过程中几何结构变化的分子参数。 +# 这些参数由杜辛斯基矩阵和位移向量表示,这些参数是从原子坐标、原子质量、振动频率以及分子的正常模式获得的。 +# 这些分子参数可以通过电子结构计算获得。 + +# %% [markdown] +# $1^1A'$ 态及$1^2A'$ 态的平衡结构坐标如下 + +# %% +ri = np.genfromtxt('./data/formic_ri.csv', delimiter=',', skip_header=0)[:, np.newaxis] +rf = np.genfromtxt('./data/formic_rf.csv', delimiter=',', skip_header=0)[:, np.newaxis] + +# %% [markdown] +# $1^1A'$ 态及 $1^2A'$ 态的质量加权简正坐标如下 + +# %% +li = np.genfromtxt('./data/formic_li.csv', delimiter=',', skip_header=0) +lf = np.genfromtxt('./data/formic_lf.csv', delimiter=',', skip_header=0) + +# %% [markdown] +# $1^1A'$ 态及 $1^2A'$ 态简正模式频率如下 + +# %% +omega = np.genfromtxt('./data/formic_omega.csv', delimiter=',', skip_header=0) +omegap = np.genfromtxt('./data/formic_omegap.csv', delimiter=',', skip_header=0) + +# %% [markdown] +# 计算中用到的物理学常量 + +# %% +c = 299792458.0 # 光速 +mu = 1.6605390666 * 10**-27 # 原子质量单位 +h = 6.62607015 * 10**-34 # 普朗克常数 + +m_c = 12 # 碳原子相对原子质量 +m_h = 1.007825037 # 氢原子相对原子质量 +m_o = 15.994914640 # 氧原子相对原子质量 + +# %% [markdown] +# Duschinsky 矩阵 $U$ 及位移矢量 $\delta$ 计算如下 + +# %% +u = [] +for li_ele in li: + for lf_elf in lf: + u.append(np.sum(li_ele * lf_elf)) +u = np.array(u[-1::-1]).reshape(7, 7).T +print(u) + +# %% +delta = [] +m = np.diag([m_c, m_c, m_c, m_o, m_o, m_o, m_o, m_o, m_o, m_h, m_h, m_h, m_h, m_h, m_h]) +for i in range(len(omegap)): + d = lf[i].T @ np.sqrt(m) @ (ri - rf) + denominator = np.sqrt(h / (4 * np.pi**2 * 100 * omegap[i] * c * mu)) / (10**-10) + delta.append(d / denominator) +delta = np.array(delta[-1::-1]) +print(delta) + +# %% [markdown] +# Duschinsky矩阵非对角元素描述了不同电子态之间简正模式的混合情况 + +# %% +plt.imshow(abs(u), cmap='Greens') +plt.colorbar() +plt.xlabel('Mode index') +plt.ylabel('Mode index') +plt.tight_layout() +plt.show() + +# %% [markdown] +# 非对角元素的存在显示了不同电子态之间简正模式的混合。 +# 由 Duschinsky 矩阵和位移向量计算 GBS 参数如下: + +# %% +pre_transition_squeezing = np.sqrt(omega[-1::-1]) +post_transition_squeezing = np.sqrt(omegap[-1::-1]) + +j_mat = np.diag(post_transition_squeezing) @ u @ np.linalg.inv(np.diag(pre_transition_squeezing)) + +cl, lambda_1, cr = np.linalg.svd(j_mat) + +delta_2 = np.linalg.inv(j_mat) @ delta / np.sqrt(2) +delta_2 = delta_2.flatten() +lambda_2 = np.log(lambda_1) + +# %% [markdown] +# 我们现在可以计算每个振动模式中的平均振动量子数 + +# %% +modes = 7 # 简正模式数量 +cutoff = 3 +shots = 500000 + +# %% +cir = dq.photonic.QumodeCircuit( + nmode=modes, + init_state='vac', + # init_state=init_state, + cutoff=cutoff, + backend='gaussian', +) + +for i in range(modes): + cir.d(wires=[i], r=delta_2[i]) + +cir.any(cr, wires=list(range(modes))) + +for i in range(modes): + cir.s(wires=[i], r=-lambda_2[i]) + +cir.any(cl, wires=list(range(modes))) + +state = cir() + +# 线路可视化 +cir.draw() + +# %% +sample = cir.measure(shots=shots, mcmc=True) + +# %% +sample2 = [] +for ele in sample.items(): + # print(ele[0].state) + sample2.append(ele[0].state * ele[1]) +counts2 = np.sum(sample2, axis=0) + +plt.figure(figsize=(8, 4)) +plt.ylabel('Photon number') +plt.xlabel(r'Frequency (cm$^{-1}$)') +plt.xticks(range(len(omegap)), np.round(omegap, 1), rotation=90) +plt.bar(range(len(omegap)), counts2, color='green') +plt.tight_layout() +plt.show() + +# %% [markdown] +# 现在模拟一个光激发过程,其中涉及从电子基态的预激发振动态进行振动跃迁。 +# 预激发振动态可以通过应用位移门来模拟。 +# 我们向第6个振动模式插入平均一个振动量子,并计算激发电子态中每个振动模式的平均光子数。 + +# %% +cir = dq.photonic.QumodeCircuit( + nmode=modes, + init_state='vac', + # init_state=init_state, + cutoff=cutoff, + backend='gaussian', +) + +cir.d(wires=[5], r=1.0) + +for i in range(modes): + cir.d(wires=[i], r=delta_2[i]) + +cir.any(cr, wires=list(range(modes))) + +for i in range(modes): + cir.s(wires=[i], r=-lambda_2[i]) + +cir.any(cl, wires=list(range(modes))) + +state = cir() + +# 线路可视化 +cir.draw() + +# %% +sample3 = cir.measure(shots=shots, mcmc=True) + +# %% +sample4 = [] +for ele in sample3.items(): + # print(ele[0].state) + sample4.append(ele[0].state * ele[1]) +counts4 = np.sum(sample4, axis=0) + +plt.figure(figsize=(8, 4)) +plt.ylabel('Photon number') +plt.xlabel(r'Frequency (cm$^{-1}$)') +plt.xticks(range(len(omegap)), np.round(omegap, 1), rotation=90) +plt.bar(range(len(omegap)), counts4, color='green') +plt.tight_layout() +plt.show() diff --git a/examples/demos/gbs/vibronic_spectra/vibronic_spectra.ipynb b/examples/demos/gbs/vibronic_spectra/vibronic_spectra.ipynb index c6f131ba..105a51a2 100644 --- a/examples/demos/gbs/vibronic_spectra/vibronic_spectra.ipynb +++ b/examples/demos/gbs/vibronic_spectra/vibronic_spectra.ipynb @@ -75,7 +75,7 @@ "metadata": {}, "source": [ "
\n", - "\t\t\n", + "\t\n", "
\n", "\n", "
\n", @@ -125,7 +125,7 @@ "FCF_{n',n}\t=\\left|\\left\\langle \\phi'|\\phi\\right\\rangle \\right|^{2}\n", "\t=\\left|\\left\\langle n'\\right|U_{Dokt}\\left|n\\right\\rangle \\right|^{2}\n", "$$\n", - "\t\n", + "\n", "玻色子采样的计算在于对 qumodes 进行旋转矩阵操作。\n", "在我们计算振动光谱时,使用上面的 Doktorov 算符。其过程为\n", "\n", @@ -191,10 +191,10 @@ "metadata": {}, "outputs": [], "source": [ - "import numpy as np\n", "import deepquantum as dq\n", - "import torch\n", - "import matplotlib.pyplot as plt" + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import torch" ] }, { @@ -210,8 +210,8 @@ "metadata": {}, "outputs": [], "source": [ - "ri = np.genfromtxt(\"./data/formic_ri.csv\", delimiter=\",\", skip_header=0)[:, np.newaxis]\n", - "rf = np.genfromtxt(\"./data/formic_rf.csv\", delimiter=\",\", skip_header=0)[:, np.newaxis]" + "ri = np.genfromtxt('./data/formic_ri.csv', delimiter=',', skip_header=0)[:, np.newaxis]\n", + "rf = np.genfromtxt('./data/formic_rf.csv', delimiter=',', skip_header=0)[:, np.newaxis]" ] }, { @@ -227,8 +227,8 @@ "metadata": {}, "outputs": [], "source": [ - "li = np.genfromtxt(\"./data/formic_li.csv\", delimiter=\",\", skip_header=0)\n", - "lf = np.genfromtxt(\"./data/formic_lf.csv\", delimiter=\",\", skip_header=0)" + "li = np.genfromtxt('./data/formic_li.csv', delimiter=',', skip_header=0)\n", + "lf = np.genfromtxt('./data/formic_lf.csv', delimiter=',', skip_header=0)" ] }, { @@ -244,8 +244,8 @@ "metadata": {}, "outputs": [], "source": [ - "omega = np.genfromtxt(\"./data/formic_omega.csv\", delimiter=\",\", skip_header=0)\n", - "omegap = np.genfromtxt(\"./data/formic_omegap.csv\", delimiter=\",\", skip_header=0)" + "omega = np.genfromtxt('./data/formic_omega.csv', delimiter=',', skip_header=0)\n", + "omegap = np.genfromtxt('./data/formic_omegap.csv', delimiter=',', skip_header=0)" ] }, { @@ -279,7 +279,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -312,12 +312,12 @@ " for lf_elf in lf:\n", " u.append(np.sum(li_ele * lf_elf))\n", "u = np.array(u[-1::-1]).reshape(7, 7).T\n", - "u" + "print(u)" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -342,10 +342,10 @@ "m = np.diag([m_c, m_c, m_c, m_o, m_o, m_o, m_o, m_o, m_o, m_h, m_h, m_h, m_h, m_h, m_h])\n", "for i in range(len(omegap)):\n", " d = lf[i].T @ np.sqrt(m) @ (ri - rf)\n", - " l = np.sqrt(h / (4 * np.pi**2 * 100 * omegap[i] * c * mu)) / (10**-10)\n", - " delta.append(d / l)\n", + " denominator = np.sqrt(h / (4 * np.pi**2 * 100 * omegap[i] * c * mu)) / (10**-10)\n", + " delta.append(d / denominator)\n", "delta = np.array(delta[-1::-1])\n", - "delta" + "print(delta)" ] }, { @@ -387,11 +387,7 @@ "pre_transition_squeezing = np.sqrt(omega[-1::-1])\n", "post_transition_squeezing = np.sqrt(omegap[-1::-1])\n", "\n", - "j_mat = (\n", - " np.diag(post_transition_squeezing)\n", - " @ u\n", - " @ np.linalg.inv(np.diag(pre_transition_squeezing))\n", - ")\n", + "j_mat = np.diag(post_transition_squeezing) @ u @ np.linalg.inv(np.diag(pre_transition_squeezing))\n", "\n", "cl, lambda_1, cr = np.linalg.svd(j_mat)\n", "\n", @@ -440,10 +436,10 @@ "source": [ "cir = dq.photonic.QumodeCircuit(\n", " nmode=modes,\n", - " init_state=\"vac\",\n", + " init_state='vac',\n", " # init_state=init_state,\n", " cutoff=cutoff,\n", - " backend=\"gaussian\",\n", + " backend='gaussian',\n", ")\n", "\n", "for i in range(modes):\n", @@ -521,10 +517,10 @@ " wave_number.append(torch.sum(ele[0].state * torch.tensor(omegap)))\n", " counts.append(ele[1])\n", "\n", - "plt.bar(wave_number, counts, width=100, color=\"g\")\n", + "plt.bar(wave_number, counts, width=100, color='g')\n", "\n", - "plt.xlabel(r\"Energy $(cm^{-1})$\")\n", - "plt.ylabel(r\"Counts\")\n", + "plt.xlabel(r'Energy $(cm^{-1})$')\n", + "plt.ylabel(r'Counts')\n", "plt.xlim(-1000, 8000)\n", "plt.show()" ] @@ -537,7 +533,7 @@ "\n", "\n", "
\n", - "\t\t\n", + "\t\n", "
\n", "\n", "
\n", diff --git a/examples/demos/gbs/vibronic_spectra/vibronic_spectra.py b/examples/demos/gbs/vibronic_spectra/vibronic_spectra.py new file mode 100644 index 00000000..5e3edcd0 --- /dev/null +++ b/examples/demos/gbs/vibronic_spectra/vibronic_spectra.py @@ -0,0 +1,323 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.19.1 +# kernelspec: +# display_name: dq_draw +# language: python +# name: python3 +# --- + +# %% [markdown] +# # 高斯玻色采样模拟分子振动光谱 + +# %% [markdown] +# 此研究涉及两个主要领域。其中之一是关于量子信息和量子计算机的研究。我们选择**玻色采样**进行讨论。 +# 目前,有多种物理实现方式可以进行玻色子取样,但最常见的是光子解决方案。 +# 在该方案中,单光子态被用作输入到干涉仪,在输出端测量光子数。 +# 干涉装置相对简单;而制备单光子和测量光子数对仪器的要求较高。 +# 因此,如果我们考虑使用相干激光,可以让实验简便一些。 +# 由于相干态可以用高斯分布描述,这种类型的玻色子采样被称为高斯玻色采样。 +# +# +# 另一个主要领域是**光谱学**。我们以分子振动光谱为例,模拟不同带电态之间转变的概率,从而近似确定分子振动谱。 +# 这种方法可以使用高斯玻色采样来实现。 +# 参考 [Huh](https://doi.org/10.1038/nphoton.2015.153) 的研究工作,我们在基于玻色采样的模拟器上实现该模拟过程。 + +# %% [markdown] +# ## 高斯玻色采样 +# +# 量子比特是一个抽象的二维量子物理系统。 +# 其希尔伯特空间通常可由两个基态 $\left|0\right\rangle$ 和 $\left|1\right\rangle$ 来描述。 +# 不同量子比特可以处于叠加态和纠缠状态。 +# 我们可将其推广到3维、4维以至有限的 $d$-维空间中。 +# +# 以上内容可以进一步推广到无限维度。 +# 玻色子粒子是这种类型的量子物理系统的一个例子,其中在给定状态中可以有无限多个粒子。 +# 这样的系统仍然是量子系统,仍具有叠加和纠缠性。 +# 在可数无限维度情况下,量子态被称为 qumodes。 +# +# 在真实物理系统中,任何物理量都需要通过多次测量来确定其状态分布。 +# 因此量子计算机都是对抽样问题的物理实现。 +# 在可数无限维度的情况下,这被称为玻色子采样。 +# +# 在多种玻色采样的物理实现方式中,最简单的仍然是光子方案。 +# 量子光学设备在 qumodes 上为幺正算符。常用高斯算符如下: +# +# +# $$\mathcal{P}_{i}\left(\phi\right)=e^{-i\phi a_{i}^{\dagger}a_{i}}$$ +# +# $$\mathcal{D}_{i}\left(\alpha\right)=e^{\alpha a_{i}^{\dagger}-\alpha^{*}a_{i}}$$ +# +# $$\mathcal{S}_{i}\left(\zeta\right)=e^{\frac{1}{2}\left(\zeta^{*}a_{i}^{2}-\zeta a_{i}^{\dagger2}\right)}$$ +# +# $$\mathcal{B}_{ij}\left(\theta\right)=e^{\theta\left(a_{i}^{\dagger}a_{j}-a_{i}a_{j}^{\dagger}\right)}$$ +# +# +# 事实上,一个纯的光子数态,即 $\left|n\right\rangle$, 在技术上很难做到。 +# $\left|1\right\rangle$ 意味着特定量子模式中只有一个光子。 +# 精确地产生一个光子是一项巨大挑战。 +# 而相干光是每个量子模式的可数无限状态之和,高斯玻色采样以相干光作为输入,是一种更简单的解决方案。 + +# %% [markdown] +# ## 分子的振动光谱简要描述 + +# %% [markdown] +#
+# +#
+# +#
+# Franck–Condon原理能级图 +#
+ +# %% [markdown] +# 光谱学的一个主要问题是检查原子和分子之间电荷状态的跃迁。 +# 分子吸收的光频率便是取决于不同电子态之间的允许跃迁。 +# 这些电子跃迁可能伴随着分子振动能量的变化。 +# 在这种情况下,代表更强烈吸收光的频率的吸收线被称为振动光谱。 +# 然而,除了氢原子可以被精确解决外,随着电子数量增加,量子力学方程变得指数级复杂,使得对它们的计算变得不可能。因此必须使用近似方法。 +# +# 如果假设核和电子状态是独立的,对于极其复杂的分子系统,一个很好的近似称为玻恩-奥本海默近似。 +# 由于原子核的质量较大,其对电子壳层变化反应更慢,因此在 Franck-Condon 原理中可以将原子核视为恒定。 +# +# 通过用简单的量子谐振子来近似电子的振动,可以将电子的哈密顿算符写成反应坐标的函数。 +# 然后,电子势能表面已经可以用抛物线描述。 +# +# $$\mathcal{H}=p^{2}+q^{2}$$ +# +# Duschinsky 提出一种近似方法,不同电子态的简正坐标之间存在线性关系 +# +# $$q'=U_{D}q+d$$ +# +# Doktorov 证明,在这些条件下,对于这样的量子谐振子,不同势能面上的量子态 $\left|\phi\right\rangle$ 和 $\left|\phi'\right\rangle$ 之间的关系可以用以下方式给出: +# +# $$\left|\phi'\right\rangle =U_{Dokt}\left|\phi\right\rangle $$ +# +# 其中,Doktorov 算子定义为 +# +# $$U_{Dok}=D\left(d\right)S^{\dagger}\left(\Omega'\right)R\left(U_{Dusch}\right)S_{\Omega}\left(\Omega\right)$$ +# +# - 旋转算符的输入参数是Duschinsky混合矩阵本身 +# +# - 位移算符D的参数是Duschinsky位移 +# - 压缩算符是从分子的物理特性推导出来的。$\Omega$ 和 $\Omega'$ 是分子内原子在发生跃迁前后谐波的角频率 +# +# 上面哈密顿量的本征值问题的解是 Fock 空间中的相干态。高斯算符作用于它们之后,将一个相干态转换为另一个相干态。 +# 不同电子态之间的转移概率,即所谓的 Franck-Condon 因子,可以近似表示为 +# +# $$ +# FCF_{n',n} =\left|\left\langle \phi'|\phi\right\rangle \right|^{2} +# =\left|\left\langle n'\right|U_{Dokt}\left|n\right\rangle \right|^{2} +# $$ +# +# 玻色子采样的计算在于对 qumodes 进行旋转矩阵操作。 +# 在我们计算振动光谱时,使用上面的 Doktorov 算符。其过程为 +# +# 1. 制备相干态 +# +# 2. 将相干态传输到一个玻色采样设备,以获得转移概率 +# 3. 进行第二次压缩,测量各模式分布,进一步计算FCF +# +# + +# %% [markdown] +# ### Hessian matrix +# +# 对于一双原子分子,假设谐振子势能 +# +# $$V=\frac{k}{2}(r-r_e)^2$$ +# +# 其中 $r$ 是两个核之间的距离,$r_e$ 是它们的平衡距离。 +# 简单起见,可只考虑X轴。我们可以用两个原子的笛卡尔坐标 $x_1$(对应原子1)和 $x_2$(对应原子2)来表示这一点。 +# 则,$r = x_2 - x_1$。 +# +# 势能的二阶导数是: +# +# $$\frac{d^{2}V}{dx_{1}^{2}}=k$$ +# +# $$\frac{d^{2}V}{dx_{2}^{2}}=k$$ +# +# $$\frac{d^{2}V}{dx_{1}dx_{2}}=\frac{d^{2}V}{dx_{2}dx_{1}}=-k$$ +# +# +# Hessian矩阵(仅针对x方向)为: +# +# $$ +# H=\left(\begin{array}{cc} +# k & -k\\ +# -k & k +# \end{array}\right) +# $$ +# +# 质量加权的Hessian矩阵是: +# +# $$ +# F=\left(\begin{array}{cc} +# \frac{k}{m_{1}} & -\frac{k}{\sqrt{m_{1}m_{2}}}\\ +# -\frac{k}{\sqrt{m_{1}m_{2}}} & \frac{k}{m_{2}} +# \end{array}\right) +# $$ +# +# 如果我们有了系统的Hessian矩阵和质量加权Hessian矩阵,就可以由特征向量定义质量加权简正模式。 +# 接着,利用分子的平衡结构坐标数据、质量加权简正坐标等数据可以求解所需高斯算符参数。 +# +# +# [Jankowiak](https://pubs.aip.org/aip/jcp/article-abstract/127/23/234101/906357/Vibronic-transitions-in-large-molecular-systems?redirectedFrom=fulltext)的补充材料提供了一些分子的相关数据。 +# 我们以甲酸($1^{1}A'\rightarrow1^{2}A'$)为例计算分子振动光谱: + +# %% +import deepquantum as dq +import matplotlib.pyplot as plt +import numpy as np +import torch + +# %% [markdown] +# $1^1A'$ 态及$1^2A'$ 态的平衡结构坐标如下 + +# %% +ri = np.genfromtxt('./data/formic_ri.csv', delimiter=',', skip_header=0)[:, np.newaxis] +rf = np.genfromtxt('./data/formic_rf.csv', delimiter=',', skip_header=0)[:, np.newaxis] + +# %% [markdown] +# $1^1A'$ 态及 $1^2A'$ 态的质量加权简正坐标如下 + +# %% +li = np.genfromtxt('./data/formic_li.csv', delimiter=',', skip_header=0) +lf = np.genfromtxt('./data/formic_lf.csv', delimiter=',', skip_header=0) + +# %% [markdown] +# $1^1A'$ 态及 $1^2A'$ 态简正模式频率如下 + +# %% +omega = np.genfromtxt('./data/formic_omega.csv', delimiter=',', skip_header=0) +omegap = np.genfromtxt('./data/formic_omegap.csv', delimiter=',', skip_header=0) + +# %% [markdown] +# 计算中用到的物理学常量 + +# %% +c = 299792458.0 # 光速 +mu = 1.6605390666 * 10**-27 # 原子质量单位 +h = 6.62607015 * 10**-34 # 普朗克常数 + +m_c = 12 # 碳原子相对原子质量 +m_h = 1.007825037 # 氢原子相对原子质量 +m_o = 15.994914640 # 氧原子相对原子质量 + +# %% [markdown] +# Duschinsky 矩阵 $U$ 及位移矢量 $\delta$ 计算如下 + +# %% +u = [] +for li_ele in li: + for lf_elf in lf: + u.append(np.sum(li_ele * lf_elf)) +u = np.array(u[-1::-1]).reshape(7, 7).T +print(u) + +# %% +delta = [] +m = np.diag([m_c, m_c, m_c, m_o, m_o, m_o, m_o, m_o, m_o, m_h, m_h, m_h, m_h, m_h, m_h]) +for i in range(len(omegap)): + d = lf[i].T @ np.sqrt(m) @ (ri - rf) + denominator = np.sqrt(h / (4 * np.pi**2 * 100 * omegap[i] * c * mu)) / (10**-10) + delta.append(d / denominator) +delta = np.array(delta[-1::-1]) +print(delta) + +# %% [markdown] +# 在用于计算振动光谱的GBS算法中,以上这些化学参数足以确定GBS设备的配置。 +# 利用这些数据,我们即可计算甲酸分子振动光谱。 + +# %% [markdown] +# 实际上,在可能仅涉及有限数量的光子的情况下需要**非线性相互作用**,第二次挤压操作在光学装置中难以实现。 +# 通常,我们需要将两次挤压操作压缩为一次: +# +# $$U_{Dok}=R_{C_{L}}S_{\Sigma}^{\dagger}R_{C_{R}}D_{2^{-1/2}J^{-1}\delta}$$ +# +# 其中 +# $$J=\Omega'U\Omega^{-1}$$ +# $$\delta=\hbar^{-1/2}\Omega'd$$ +# $$J=C_{L}\Sigma C_{R}^{t}$$ + +# %% [markdown] +# 各 GBS 参数计算如下: + +# %% +pre_transition_squeezing = np.sqrt(omega[-1::-1]) +post_transition_squeezing = np.sqrt(omegap[-1::-1]) + +j_mat = np.diag(post_transition_squeezing) @ u @ np.linalg.inv(np.diag(pre_transition_squeezing)) + +cl, lambda_1, cr = np.linalg.svd(j_mat) + +delta_2 = np.linalg.inv(j_mat) @ delta / np.sqrt(2) +delta_2 = delta_2.flatten() +lambda_2 = np.log(lambda_1) + +# %% [markdown] +# ### 甲酸分子振动光谱计算如下所示 + +# %% +modes = 7 # 简正模式数量 +cutoff = 3 +shots = 500000 + +# %% +cir = dq.photonic.QumodeCircuit( + nmode=modes, + init_state='vac', + # init_state=init_state, + cutoff=cutoff, + backend='gaussian', +) + +for i in range(modes): + cir.d(wires=[i], r=delta_2[i]) + +cir.any(cr, wires=list(range(modes))) + +for i in range(modes): + cir.s(wires=[i], r=-lambda_2[i]) + +cir.any(cl, wires=list(range(modes))) + +state = cir() + +# 线路可视化 +cir.draw() + +# %% +sample = cir.measure(shots=shots, mcmc=True) + +# %% +wave_number = [] +counts = [] +for ele in sample.items(): + # print(ele[0].state) + wave_number.append(torch.sum(ele[0].state * torch.tensor(omegap))) + counts.append(ele[1]) + +plt.bar(wave_number, counts, width=100, color='g') + +plt.xlabel(r'Energy $(cm^{-1})$') +plt.ylabel(r'Counts') +plt.xlim(-1000, 8000) +plt.show() + +# %% [markdown] +# ### 参考结果 +# +# +#
+# +#
+# +#
+# 甲酸分子振动光谱图 +#
diff --git a/examples/demos/gbs/vqe_ground_energy_for_H2_alg/vqe_ground_energy_h2.ipynb b/examples/demos/gbs/vqe_ground_energy_for_H2_alg/vqe_ground_energy_h2.ipynb index 657e5acb..2e5ec390 100644 --- a/examples/demos/gbs/vqe_ground_energy_for_H2_alg/vqe_ground_energy_h2.ipynb +++ b/examples/demos/gbs/vqe_ground_energy_for_H2_alg/vqe_ground_energy_h2.ipynb @@ -151,10 +151,10 @@ "对应的量子态可以通过生成算符作用到真空态 $|-\\rangle_F$ 表示\n", "\n", "$$\n", - "|0,1 \\rangle_F = f^\\dagger_0f^\\dagger_1|-\\rangle_F, \\ \\ \n", + "|0,1 \\rangle_F = f^\\dagger_0f^\\dagger_1|-\\rangle_F, \\ \\\n", "|0,2 \\rangle_F = f^\\dagger_0f^\\dagger_2|-\\rangle_F, \\ \\\n", "|0,3 \\rangle_F = f^\\dagger_0f^\\dagger_3|-\\rangle_F \\\\\n", - "|1,2 \\rangle_F = f^\\dagger_1f^\\dagger_2|-\\rangle_F, \\ \\ \n", + "|1,2 \\rangle_F = f^\\dagger_1f^\\dagger_2|-\\rangle_F, \\ \\\n", "|1,3 \\rangle_F = f^\\dagger_1f^\\dagger_3|-\\rangle_F, \\ \\\n", "|2,3 \\rangle_F = f^\\dagger_2f^\\dagger_3|-\\rangle_F \\\\\n", "$$\n", @@ -237,7 +237,7 @@ "|0,1\\rangle_{F}\\leftrightarrow|0,0\\rangle_{B} ,\\quad|0,2\\rangle_{F}\\leftrightarrow|1,0\\rangle_{B} ,\\quad|0,3\\rangle_{F}\\leftrightarrow|2,0\\rangle_{B} ,\\\\|1,2\\rangle_{F}\\leftrightarrow|0,1\\rangle_{B} ,\\quad|1,3\\rangle_{F}\\leftrightarrow|1,1\\rangle_{B} ,\\quad|2,3\\rangle_{F}\\leftrightarrow|0,2\\rangle_{B}\n", "$$\n", "\n", - "对于费米子系统能量最低的基矢是两个电子分别占据最低的两个轨道, 对应着量子态 $|0, 1\\rangle_F$, 映射到玻色系统中对应着真空态 $|0, 0\\rangle_B$。 " + "对于费米子系统能量最低的基矢是两个电子分别占据最低的两个轨道, 对应着量子态 $|0, 1\\rangle_F$, 映射到玻色系统中对应着真空态 $|0, 0\\rangle_B$。" ] }, { @@ -324,7 +324,7 @@ "&E_0^0 \\leftrightarrow{I}\\otimes|0\\rangle\\left\\langle0\\right|, \\\\\n", "&E_1^1\\leftrightarrow{I}\\otimes\\left|1\\right\\rangle\\left\\langle1\\right|+\\left|0,0\\right\\rangle\\left\\langle0,0\\right| \\\\\n", "&E_0^1 \\leftrightarrow\\sum_{j=1}^{L} |j-1,1\\rangle \\langle j,0| , \\\\\n", - "&\\text{E}_0 ^{2}\\leftrightarrow\\Big(\\sum^{L-1}|j-1,2\\rangle \\langle j+1,0| \\Big)-|0,1\\rangle \\langle0,0| , \n", + "&\\text{E}_0 ^{2}\\leftrightarrow\\Big(\\sum^{L-1}|j-1,2\\rangle \\langle j+1,0| \\Big)-|0,1\\rangle \\langle0,0| ,\n", "\\end{aligned}$$\n", "因为这里只涉及到二电子体系,对于更大的体系可以参考文献[1],这里不再讨论。\n", "\n", @@ -405,7 +405,7 @@ "# %matplotlib notebook\n", "fig = plt.figure()\n", "# R_values = np.linspace(0.1, 3, 50)\n", - "R_values = np.linspace(0.1, 10, 50*4)\n", + "R_values = np.linspace(0.1, 10, 50 * 4)\n", "plt.plot(R_values, dic['g1'][0], label='g1', color='red')\n", "plt.plot(R_values, dic['g2'][0], label='g2', color='blue')\n", "plt.plot(R_values, dic['g3'][0], label='g3', color='black', ls='--')\n", @@ -530,6 +530,20 @@ "## 两模玻色采样线路的VQE实现" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "e0321bde", + "metadata": {}, + "outputs": [], + "source": [ + "state_01 = dq.FockState([0, 1])\n", + "state_10 = dq.FockState([1, 0])\n", + "state_11 = dq.FockState([1, 1])\n", + "state_20 = dq.FockState([2, 0])\n", + "state_02 = dq.FockState([0, 2])" + ] + }, { "cell_type": "code", "execution_count": null, @@ -543,56 +557,42 @@ "outputs": [], "source": [ "def exp_h(paras):\n", - " w1, w2, w3 = torch.nn.functional.normalize(abs(paras[0:3]), dim=0) # 归一化\n", + " w1, w2, w3 = torch.nn.functional.normalize(abs(paras[0:3]), dim=0) # 归一化\n", " amp_00 = w1\n", " ############################\n", " nmode = 2\n", - " cir2 = dq.QumodeCircuit(nmode=nmode, init_state=[0,1], cutoff=3, backend='fock', basis=True)\n", - " cir2.ps(0, inputs=paras[3] )\n", + " cir2 = dq.QumodeCircuit(nmode=nmode, init_state=[0, 1], cutoff=3, backend='fock', basis=True)\n", + " cir2.ps(0, inputs=paras[3])\n", " cir2.ps(1, inputs=paras[4])\n", - " cir2.bs([0,1], inputs=[paras[5], paras[6]])\n", + " cir2.bs([0, 1], inputs=[paras[5], paras[6]])\n", " cir2.ps(0, inputs=paras[7])\n", " cir2.ps(1, inputs=[8])\n", " state2 = cir2(is_prob=False)\n", " amp_01 = w2 * state2[state_01]\n", " amp_10 = w2 * state2[state_10]\n", " ############################\n", - " cir3 = dq.QumodeCircuit(nmode=nmode, init_state=[1,1], cutoff=3, backend='fock', basis=True)\n", - " cir3.ps(0, inputs=paras[9] )\n", + " cir3 = dq.QumodeCircuit(nmode=nmode, init_state=[1, 1], cutoff=3, backend='fock', basis=True)\n", + " cir3.ps(0, inputs=paras[9])\n", " cir3.ps(1, inputs=paras[10])\n", - " cir3.bs([0,1], inputs=[paras[11], paras[12]])\n", + " cir3.bs([0, 1], inputs=[paras[11], paras[12]])\n", " cir3.ps(0, inputs=paras[13])\n", " cir3.ps(1, inputs=[14])\n", " state3 = cir3(is_prob=False)\n", " amp_11 = w3 * state3[state_11]\n", " amp_20 = w3 * state3[state_20]\n", " amp_02 = w3 * state3[state_02]\n", - " exp_h = g_1*abs(amp_00)**2 + g_2*abs(amp_02)**2 + g_3*(abs(amp_01)**2+abs(amp_20)**2) + \\\n", - " g_4 *(abs(amp_10)**2 + abs(amp_11)**2) + g_5*(amp_00.conj()*amp_02+amp_00*amp_02.conj()) - \\\n", - " g_5* (amp_20.conj()*amp_01+amp_20*amp_01.conj()) # see\n", + " exp_h = (\n", + " g_1 * abs(amp_00) ** 2\n", + " + g_2 * abs(amp_02) ** 2\n", + " + g_3 * (abs(amp_01) ** 2 + abs(amp_20) ** 2)\n", + " + g_4 * (abs(amp_10) ** 2 + abs(amp_11) ** 2)\n", + " + g_5 * (amp_00.conj() * amp_02 + amp_00 * amp_02.conj())\n", + " - g_5 * (amp_20.conj() * amp_01 + amp_20 * amp_01.conj())\n", + " ) # see\n", "\n", " return (exp_h).real" ] }, - { - "cell_type": "code", - "execution_count": 5, - "id": "131f735b", - "metadata": { - "ExecuteTime": { - "end_time": "2024-10-08T05:26:45.174972Z", - "start_time": "2024-10-08T05:26:45.162019Z" - } - }, - "outputs": [], - "source": [ - "state_01 = dq.FockState([0,1])\n", - "state_10 = dq.FockState([1,0])\n", - "state_11 = dq.FockState([1,1])\n", - "state_20 = dq.FockState([2,0])\n", - "state_02 = dq.FockState([0,2])" - ] - }, { "cell_type": "code", "execution_count": null, @@ -616,7 +616,7 @@ } ], "source": [ - "energy_bs = [ ]\n", + "energy_bs = []\n", "for idx in range(50):\n", " g_1 = g1[idx]\n", " g_2 = g2[idx]\n", @@ -627,16 +627,16 @@ " w123 = torch.tensor([0.5, 0.5, 0.4], requires_grad=True)\n", " angles = torch.nn.Parameter(torch.randn(12))\n", " paras = torch.cat([w123, angles])\n", - " optimizer = torch.optim.Adam([w123, angles], lr=0.1)\n", + " optimizer = torch.optim.Adam([w123, angles], lr=0.1)\n", "\n", - " for epoch in range(150):\n", + " for _ in range(150):\n", " optimizer.zero_grad()\n", " paras = torch.cat([w123, angles])\n", " loss = exp_h(paras)\n", - " loss.backward() # backpropagetion\n", - " optimizer.step() # update parameters\n", + " loss.backward() # backpropagetion\n", + " optimizer.step() # update parameters\n", " energy_bs.append(loss)\n", - " print(idx,loss,end='\\r')" + " print(idx, loss, end='\\r')" ] }, { @@ -686,13 +686,13 @@ ], "source": [ "R_values = R_values[0:50]\n", - "hartree_dis = R_values/0.529177 # using Bohr radius\n", + "hartree_dis = R_values / 0.529177 # using Bohr radius\n", "openfermion_h2 = np.load('openfermion_h2_v3.npy')\n", "openfermion_h2_fci = np.load('openfermion_h2_fci.npy')\n", "# %matplotlib notebook\n", - "fig = plt.figure()\n", - "nuclear_v = 1/hartree_dis\n", - "plt.plot(R_values, torch.stack(energy_bs).mT[0].detach().numpy() + nuclear_v,lw=4, label='vqe')\n", + "fig = plt.figure()\n", + "nuclear_v = 1 / hartree_dis\n", + "plt.plot(R_values, torch.stack(energy_bs).mT[0].detach().numpy() + nuclear_v, lw=4, label='vqe')\n", "plt.plot(R_values, openfermion_h2[0:50], ls='--', label='openfermion_hf_2_orbitals')\n", "plt.plot(R_values, openfermion_h2_fci[0:50], ls='--', label='openfermion_fci', color='black')\n", "plt.ylabel('Hartree energy')\n", @@ -726,7 +726,8 @@ "ExecuteTime": { "end_time": "2024-10-08T02:49:18.677707Z", "start_time": "2024-10-08T02:49:18.538207Z" - } + }, + "lines_to_next_cell": 2 }, "outputs": [ { @@ -752,7 +753,7 @@ "cir.d(1, r=1)\n", "cir.ps(0)\n", "cir.ps(1)\n", - "cir.bs([0,1])\n", + "cir.bs([0, 1])\n", "cir.d(0)\n", "cir.d(1)\n", "cir.draw()\n", @@ -772,7 +773,7 @@ "outputs": [], "source": [ "def exp_h_gbs_fock(paras):\n", - " s1, s2 = torch.nn.functional.normalize(abs(paras[0:2]), dim=0) # 归一化\n", + " s1, s2 = torch.nn.functional.normalize(abs(paras[0:2]), dim=0) # 归一化\n", " nmode = 2\n", " cir = dq.QumodeCircuit(nmode=nmode, init_state='vac', cutoff=3, backend='fock', basis=False)\n", " cir.s(0, r=s1)\n", @@ -781,30 +782,35 @@ " cir.d(1, r=paras[3])\n", " cir.ps(0, paras[4])\n", " cir.ps(1, paras[5])\n", - " cir.bs([0,1], inputs=[paras[6], paras[7]])\n", + " cir.bs([0, 1], inputs=[paras[6], paras[7]])\n", " cir.d(0, r=paras[8])\n", " cir.d(1, r=paras[9])\n", " # cir.to(torch.double)\n", " state = cir()\n", - " p_00 = state[0][0,0]\n", - " p_01 = state[0][0,1]\n", - " p_10 = state[0][1,0]\n", - " p_11 = state[0][1,1]\n", - " p_20 = state[0][2,0]\n", - " p_02 = state[0][0,2]\n", + " p_00 = state[0][0, 0]\n", + " p_01 = state[0][0, 1]\n", + " p_10 = state[0][1, 0]\n", + " p_11 = state[0][1, 1]\n", + " p_20 = state[0][2, 0]\n", + " p_02 = state[0][0, 2]\n", " p_list = torch.stack([p_00, p_01, p_10, p_11, p_20, p_02])\n", - " p_00_,p_01_,p_10_,p_11_,p_20_,p_02_ = torch.nn.functional.normalize(p_list, dim=0)\n", + " p_00_, p_01_, p_10_, p_11_, p_20_, p_02_ = torch.nn.functional.normalize(p_list, dim=0)\n", "\n", - " exp_h = g_1*abs(p_00_)**2 + g_2*abs(p_02_)**2 + g_3*(abs(p_01_)**2+ abs(p_20_)**2) + \\\n", - " g_4 *(abs(p_10_)**2 + abs(p_11_)**2) + g_5*(p_00_.conj()*p_02_+p_00_*p_02_.conj()) - \\\n", - " g_5* (p_20_.conj()*p_01_+p_20_*p_01_.conj()) # see\n", + " exp_h = (\n", + " g_1 * abs(p_00_) ** 2\n", + " + g_2 * abs(p_02_) ** 2\n", + " + g_3 * (abs(p_01_) ** 2 + abs(p_20_) ** 2)\n", + " + g_4 * (abs(p_10_) ** 2 + abs(p_11_) ** 2)\n", + " + g_5 * (p_00_.conj() * p_02_ + p_00_ * p_02_.conj())\n", + " - g_5 * (p_20_.conj() * p_01_ + p_20_ * p_01_.conj())\n", + " ) # see\n", "\n", " return (exp_h).real" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "id": "5ad04c36", "metadata": { "ExecuteTime": { @@ -822,7 +828,7 @@ } ], "source": [ - "energy_gbs = [ ]\n", + "energy_gbs = []\n", "for idx in range(50):\n", " g_1 = g1[idx]\n", " g_2 = g2[idx]\n", @@ -831,19 +837,19 @@ " g_5 = g5[idx]\n", "\n", " angles = torch.nn.Parameter(torch.randn(10))\n", - " optimizer = torch.optim.Adam([angles], lr=0.1)\n", - " for epoch in range(150):\n", + " optimizer = torch.optim.Adam([angles], lr=0.1)\n", + " for _ in range(150):\n", " optimizer.zero_grad()\n", " loss = exp_h_gbs_fock(angles)\n", - " loss.backward() # backpropagetion\n", - " optimizer.step() # update parameters\n", + " loss.backward() # backpropagetion\n", + " optimizer.step() # update parameters\n", " energy_gbs.append(loss)\n", - " print(idx,loss,end='\\r')" + " print(idx, loss, end='\\r')" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "id": "bf803423", "metadata": { "ExecuteTime": { @@ -914,7 +920,7 @@ } ], "source": [ - "energy_gbs" + "print(energy_gbs)" ] }, { @@ -949,14 +955,14 @@ ], "source": [ "R_values = np.linspace(0.1, 3, 50)\n", - "hartree_dis = R_values/0.529177 # using Bohr radius\n", + "hartree_dis = R_values / 0.529177 # using Bohr radius\n", "openfermion_h2 = np.load('openfermion_h2_v3.npy')\n", "openfermion_h2_fci = np.load('openfermion_h2_fci.npy')\n", "# %matplotlib notebook\n", - "fig = plt.figure()\n", - "nuclear_v = 1/hartree_dis\n", - "plt.plot(R_values, torch.stack(energy_bs).mT[0].detach().numpy() + nuclear_v,lw=1.5, label='vqe_bs')\n", - "plt.plot(R_values, torch.stack(energy_gbs).detach().numpy() + nuclear_v,lw=1.5,ls='--', label='vqe_gbs')\n", + "fig = plt.figure()\n", + "nuclear_v = 1 / hartree_dis\n", + "plt.plot(R_values, torch.stack(energy_bs).mT[0].detach().numpy() + nuclear_v, lw=1.5, label='vqe_bs')\n", + "plt.plot(R_values, torch.stack(energy_gbs).detach().numpy() + nuclear_v, lw=1.5, ls='--', label='vqe_gbs')\n", "plt.ylabel('Hartree energy')\n", "plt.xlabel('nuclear distance(A)')\n", "plt.title('Ground energy for $H_2$')\n", diff --git a/examples/demos/gbs/vqe_ground_energy_for_H2_alg/vqe_ground_energy_h2.py b/examples/demos/gbs/vqe_ground_energy_for_H2_alg/vqe_ground_energy_h2.py new file mode 100644 index 00000000..45702dfc --- /dev/null +++ b/examples/demos/gbs/vqe_ground_energy_for_H2_alg/vqe_ground_energy_h2.py @@ -0,0 +1,579 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.19.1 +# kernelspec: +# display_name: dq_v3 +# language: python +# name: dq_v3 +# --- + +# %% [markdown] +# # 氢分子基态能量求解的基础理论 + +# %% [markdown] +# ## 波恩-奥本海默近似 +# + +# %% [markdown] +# 原子单位制中$H_2$分子的哈密顿量如下图所示: +# $$ +# \mathcal{H}=-\frac{1}{2} \sum_{i} \nabla_{i}^{2}-\frac{1}{2} \sum_{A} \frac{\nabla_{A}^{2}}{M_{A}}-\sum_{i}\sum_{A} \frac{Z_{A}}{r_{iA}}+\sum_{i}\sum_{j>i} \frac{1}{r_{ij}}+\sum_{A}\sum_{B>A} \frac{Z_{A}Z_{B}}{R_{AB}} +# $$ +# +# 在原子单位制中,单位电荷 $e$ 被归一化为1,电子质量 $m_e$ 被归一化为1,真空电容率$4\pi\epsilon_0$被归一化为1, +# 同时所有的长度单位都以玻尔半径 $a_0$ 为单位,$a_0 \approx 5.29177 \times 10^{-11}m$。 +# +# 上式中$i,j$ 表示电子指标,$A, B$ 表示原子核指标,$M_A$ 和 $Z_A$ 表示原子核 $A$ 的原子序数和质量。 +# $r_{ij}$ 表示电子之间的距离,$R_{AB}$ 表示原子核之间的距离。 +# 上式中从左到右每一项分别对应着电子的动能项, 原子核的动能项,电子和原子核之间的库伦引力,电子之间的库伦斥力, +# 原子核之间的库伦斥力。 +# +# 在波恩-奥本海默近似中,假设电子在固定不动的原子核产生的电场中运动, 那么原子核的动能项可以忽略, 原子核的势能项是一个常数, +# $H_2$分子的哈密顿量可以简化为下图所示: +# +# $$ +# \mathcal{H}_{\mathrm{elec}}=-\frac12\sum_i \nabla_i^2-\sum_i\sum_A \frac{Z_A}{r_{iA}}+\sum_i\sum_{j>i} \frac1{r_{ij}} +# $$ +# +# 求解$H_2$分子的基态能量问题就变成了求解下面薛定谔方程的本征值问题: +# $$ +# \mathcal{H}_{\mathrm{elec}}\Psi_\mu(r) = E_\mu\Psi_\mu(r) +# $$ +# $\Psi_\mu(r)$ 是电子波函数,对应着本征能量 $E_\mu$。 + +# %% [markdown] +# ## Hartree-Fock 方法 + +# %% [markdown] +# ### Slater 行列式 + +# %% [markdown] +# Hartree-Fock 方法引入一组单电子轨道来构造多电子体系波函数,从而将多体问题简化为单体问题。 +# 对于二电子体系, 引入两个空间轨道 $\phi_1(r_1)、\phi_2(r_2)$ 来表示电子的波函数。一般的有 +# $$\Psi(r_1, r_2) = \phi_1(r_1)\phi_2(r_2)$$ +# 但是对于费米子体系而言需要满足交换反对称性, 因此电子波函数表示应该写成 +# $$\Psi(r_1, r_2) = \frac{1}{\sqrt{2}}(\phi_1(r_1)\phi_2(r_2)- \phi_2(r_1)\phi_1(r_2))$$ +# 可以引入Slater 行列式来表示 +# $$ \Psi(r_1, r_2) = \frac{1}{\sqrt{2}} \left|\begin{array}{cc} +# \phi_1(r_1) & \phi_2(r_1)\\ +# \phi_1(r_2) & \phi_2(r_2) +# \end{array} +# \right| $$ +# 但是考虑更一般的情况,将电子的自旋(spin)考虑进去, 对应的两个空间轨道波函数扩展为四个自旋轨道波函数, 它们的对应如下: +# $$ +# \chi_0(x) = \phi_1(r)\alpha(\omega) +# $$ +# $$ +# \chi_1(x) = \phi_1(r)\beta(\omega) +# $$ +# $$ +# \chi_2(x) = \phi_2(r)\alpha(\omega) +# $$ +# $$ +# \chi_3(x) = \phi_2(r)\beta(\omega) \\ +# $$ +# $\chi_0(x), \chi_1(x), \chi_2(x), \chi_3(x)$ 对应四个自旋轨道波函数, $\alpha(\omega), \beta(\omega)$ 分别对应自旋向上波函数和自旋向下波函数。 +# 那么更一般的情况下Slater 行列式如下 +# +# $$ \Psi(x_1,..., x_N) = \frac{1}{\sqrt{N!}} \left|\begin{array}{ccc} +# \chi_1(x_1) & ...& \chi_N(x_1)\\ +# ... & ...& ...\\ +# \chi_1(x_N) & ...& \chi_N(x_N)\\ +# \end{array} +# \right| $$ + +# %% [markdown] +# ### 二次量子化 + +# %% [markdown] +# $H_2$ 分子的两个电子对应四个自旋轨道波函数 $\chi_0(x), \chi_1(x), \chi_2(x), \chi_3(x)$ 共有6种组合方式,那么对应的基矢量有6个。 +# 我们采用类似玻色系统中的Fock态表示, 但是对应的物理含义不同, 比如量子态 $|0,1 \rangle_F$ 表示两个电子分别占据了前两个轨道。 +# $H_2$分子的双电子波函数可以表示为 +# +# $$ +# |\Psi\rangle_F = \lambda_1|0,1 \rangle_F + \lambda_2|0,2 \rangle_F + \lambda_3|0,3 \rangle_F + \lambda_4|1,2 \rangle_F +# + \lambda_5|1,3 \rangle_F + \lambda_6|2,3 \rangle_F +# $$ +# +# 类似玻色系统中的处理, 这里可以引入对应的生成算符 $f^\dagger_p$ 和湮灭算符 $f_p$, 他们满足下面的代数关系: +# +# $$ +# \{f^\dagger_p, f^\dagger_q\} = f^\dagger_pf^\dagger_q + f^\dagger_qf^\dagger_p = 0 \\ +# \{f_p, f^\dagger_q\} = f_pf^\dagger_q + f^\dagger_qf_p = \delta_{pq} +# $$ +# +# 对应的量子态可以通过生成算符作用到真空态 $|-\rangle_F$ 表示 +# +# $$ +# |0,1 \rangle_F = f^\dagger_0f^\dagger_1|-\rangle_F, \ \ +# |0,2 \rangle_F = f^\dagger_0f^\dagger_2|-\rangle_F, \ \ +# |0,3 \rangle_F = f^\dagger_0f^\dagger_3|-\rangle_F \\ +# |1,2 \rangle_F = f^\dagger_1f^\dagger_2|-\rangle_F, \ \ +# |1,3 \rangle_F = f^\dagger_1f^\dagger_3|-\rangle_F, \ \ +# |2,3 \rangle_F = f^\dagger_2f^\dagger_3|-\rangle_F \\ +# $$ +# +# 二电子体系的哈密顿可以用生成算符和湮灭算符表示[1]: +# +# $$ +# H_{\mathrm{elec}}=\sum_{pq}h_q^p f_p^\dagger f_q+\frac12 \sum_{pqrs}v_{rs}^{pq} f_p^\dagger f_q^\dagger f_rf_s +# $$ +# +# $p, q, r, s$ 表示自旋轨道,$h^p_q, v^{pq}_{rs}$ 对应单电子积分和双电子积分,它们的数值可以通过计算化学库 `openfermion` 得到。 +# +# $$ +# h_{q}^{p}=\int d\mathbf{x} \chi_{p}^{*}(\mathbf{x}) \Big(-\frac{1}{2}\nabla^{2}-\sum_{A} \frac{Z_{A}}{r_{A}}\Big) \chi_{q}(\mathbf{x}),\\v_{rs}^{pq}=\int d\mathbf{x}_1 d\mathbf{x}_2 \frac{\chi_p^*(\mathbf{x}_1)\chi_q^*(\mathbf{x}_2) \chi_r(\mathbf{x}_2)\chi_s(\mathbf{x}_1)}{r_{12}} +# $$ +# +# Hartree-Fock方法只考虑哈密顿量中电子非相互作用的部分, +# +# $$ +# H_{F}=\sum_{pq}h_{q}^{p} f_{p}^{\dagger}f_{q} +# $$ +# +# 因此得到的基态能量比真实的基态能量要高。 + +# %% [markdown] +# # 费米子体系到玻色子体系的映射 + +# %% [markdown] +# ## 费米子量子态到玻色子量子态的映射 + +# %% [markdown] +# 文献[1] 中详细介绍了如何将量子态从费米子系统映射到玻色子系统。首先对与玻色子系统的湮灭算符 $b$ 和生成算符 $b^\dagger$ 满足下面的对易关系 +# +# $$[b_p^\dagger,b_q^\dagger]=b_p^\dagger b_q^\dagger-b_q^\dagger b_p^\dagger=0 \\ +# [b_p,b_q^\dagger]=b_pb_q^\dagger-b_q^\dagger b_p=\delta_{pq}$$ +# +# 对应的Fock态表示如下: +# +# $$ +# |q_1,\cdots,q_N\rangle_B\equiv\frac{(b_1^\dagger)^{q_1}\cdots(b_N^\dagger)^{q_N}}{\sqrt{q_1!\cdots q_N!}}\left|0,\cdots,0\right\rangle_B +# $$ +# +# 文献[1] 给出了一个单射实现量子态从费米子系统到玻色子系统的映射 +# +# $$ +# \left|p_1,\cdots,p_N\right\rangle_F\leftrightarrow\left|q_1,\cdots,q_N\right\rangle_B +# $$ +# +# 具体的映射规则如下: +# +# $$ q_j = \begin{cases} +# p_1, \ \ if \ \ j=N \\ +# p_{N-j+1} - p_{N-j} - 1, \ \ if \ \ j\neq N +# \end{cases} +# $$ +# +# 对于 $H_2$ 分子的四个自旋轨道的6个基矢, 对应到玻色系统的Fock态如下: +# +# $$ +# |0,1\rangle_{F}\leftrightarrow|0,0\rangle_{B} ,\quad|0,2\rangle_{F}\leftrightarrow|1,0\rangle_{B} ,\quad|0,3\rangle_{F}\leftrightarrow|2,0\rangle_{B} ,\\|1,2\rangle_{F}\leftrightarrow|0,1\rangle_{B} ,\quad|1,3\rangle_{F}\leftrightarrow|1,1\rangle_{B} ,\quad|2,3\rangle_{F}\leftrightarrow|0,2\rangle_{B} +# $$ +# +# 对于费米子系统能量最低的基矢是两个电子分别占据最低的两个轨道, 对应着量子态 $|0, 1\rangle_F$, 映射到玻色系统中对应着真空态 $|0, 0\rangle_B$。 + +# %% [markdown] +# ## 费米子算符到玻色子算符的映射 + +# %% [markdown] +# 1. 在费米子系统中可以类比粒子数算符引入一个费米算符 $E^p_q$, +# $$ +# E^p_q = f^\dagger_p f_q = (E^q_p)^\dagger +# $$ +# 当 $p=q$ 时,$E^p_p$ 等价于粒子数算符, $p\neq q$ 时$E^p_q$ 可以理解为激发算符。 +# +# 二次量子化后的哈密顿量可以用算符 $E^p_q$ 表示, +# +# $$ +# \begin{aligned} +# H_{\mathrm{elec}}& =\Big[\frac{1}{2} \sum_{p}h_{p}^{p} E_{p}^{p}+\sum_{p>q}\Big(h_{q}^{p} E_{q}^{p}+\frac{1}{2} \tau_{qp}^{pq} E_{p}^{p}E_{q}^{q}\Big) \\ +# &+\sum_{p>q>r}\left(\tau_{rp}^{pq} E_p^pE_r^q+\tau_{qr}^{pq} E_q^qE_r^p+\tau_{rq}^{pr} E_r^rE_q^p\right) \\ +# &+\sum_{p>q>r>s}\left(\tau_{sr}^{pq} E_{r}^{p}E_{s}^{q}+\tau_{sq}^{pr} E_{q}^{p}E_{s}^{r}+\tau_{rq}^{ps} E_{q}^{p}(E_{s}^{r})^{\dagger}\right)\Big]+\mathrm{h.c.}, +# \end{aligned} +# $$ +# +# 2. 下面考虑将费米算符 $E^p_q$ 映射到玻色系统,来完成哈密顿量的映射。 +# +# 2.1 首先考虑单电子的情况 $(N=1)$, 对应的量子态映射如下 +# $$ +# |j\rangle_F \leftrightarrow |j\rangle_B +# $$ +# $E_{p}^{p}$ 作用到 $|j\rangle_F$ 结果如下 +# $$ +# E_{p}^{p}|j\rangle_{\mathrm{F}}=f_{p}^{+}f_{p}|j\rangle_{\mathrm{F}}=\delta_{pj}|j\rangle_{F} +# $$ +# 映射到玻色子体系中 $O_B$ 的作用如下 +# $$ +# O_B |j\rangle_B = \delta_{pj} |j\rangle_B +# $$ +# 可以得到 $O_B = |p\rangle_B \langle p|_B $, 那么有下面的映射关系 +# $$ +# E_p^p \leftrightarrow |p\rangle_B \langle p|_B +# $$ +# $E_{q+p}^{p}$ 作用到 $|j\rangle_F$ 结果如下: +# $$ +# \begin{aligned}E_{q}^{q+p}|j\rangle_{\mathrm{F}} +# &=f_{q+p}^{+}f_{q}f_{j}^{+}|-\rangle_{\mathrm{F}}\\ +# &=\delta_{qj}f_{q+p}^{+}|-\rangle_{F}\\ +# &=\delta_{qj}|j+p\rangle_{F}\end{aligned} +# $$ +# 对应到玻色系统的映射如下: +# $$ +# E_{q+p}^p \leftrightarrow (\sigma^\dagger)^p|q\rangle_B \langle q|_B +# $$ +# $\sigma^\dagger$ 是归一化玻色子生成算符。 +# +# 2.2 考虑二电子的情况 $(N=2)$ +# $$ +# E_r^r\left|p,q\right\rangle_F=\left(\delta_{p,r}+\delta_{q,r}\right)\left|p,q\right\rangle_F +# $$ +# +# $$ +# E_p^p\leftrightarrow{I}\otimes|p\rangle \langle p|+\sum_{a+b=p-1}|a,b\rangle \langle a,b| +# $$ +# +# $$ +# \begin{aligned} +# E_q^{q+p}& \leftrightarrow\sigma_{1}^{p} (\sigma_{2}^{\dagger})^{p} \sum_{a=0}^{\infty} |p+a,q\rangle \langle p+a,q|-\sum_{a=0}^{p-2} (\sigma_{1}^{\dagger})^{p-2-a} \sigma_{1}^{a} (\sigma_{2}^{\dagger})^{a+1} |a,q\rangle \langle a,q| \\ +# &+(\sigma_1^\dagger)^p\sum_{a+b=q-1}|a,b\rangle \langle a,b| . +# \end{aligned}$$ +# 下面是几个简单的例子: +# $$\begin{aligned} +# &E_0^0 \leftrightarrow{I}\otimes|0\rangle\left\langle0\right|, \\ +# &E_1^1\leftrightarrow{I}\otimes\left|1\right\rangle\left\langle1\right|+\left|0,0\right\rangle\left\langle0,0\right| \\ +# &E_0^1 \leftrightarrow\sum_{j=1}^{L} |j-1,1\rangle \langle j,0| , \\ +# &\text{E}_0 ^{2}\leftrightarrow\Big(\sum^{L-1}|j-1,2\rangle \langle j+1,0| \Big)-|0,1\rangle \langle0,0| , +# \end{aligned}$$ +# 因为这里只涉及到二电子体系,对于更大的体系可以参考文献[1],这里不再讨论。 +# +# 得到上述的映射关系之后, 只需要将二次量子化后的二电子哈密顿量映射到玻色子体系中,就可以将求解费米子体系基态问题过渡到玻色子体系的基态问题。 +# $$ +# \begin{aligned}H_{F}&=(h_{0}^{0}+v_{10}^{01}+v_{30}^{03}+v_{20}^{02}-v_{02}^{02}) E_{0}^{0}+(h_{1}^{1}+v_{21}^{12}+v_{31}^{13}-v_{13}^{13}) E_{1}^{1}+(h_{2}^{2}+v_{32}^{23}) E_{2}^{2}+h_{3}^{3} E_{3}^{3}\\&-v_{10}^{01} E_{1}^{0}E_{0}^{1}-v_{32}^{23} E_{2}^{2}E_{2}^{3}-v_{30}^{03} E_{3}^{0}E_{0}^{3}-v_{21}^{12} E_{2}^{1}E_{1}^{2}-(v_{20}^{02}-v_{02}^{02}) E_{2}^{0}E_{0}^{2}-(v_{31}^{13}-v_{13}^{13}) E_{3}^{1}E_{1}^{3}\\&-v_{12}^{03} (E_{1}^{0}E_{2}^{3}+\mathrm{h.c.})-v_{32}^{01} (E_{3}^{0}E_{2}^{1}+\mathrm{h.c.})\end{aligned} +# $$ +# +# $$ +# \begin{aligned} +# H_{F}\leftrightarrow H_{B}& =g_{1}\left|0,0\right\rangle\left\langle0,0\right|+g_{2}\left|0,2\right\rangle\left\langle0,2\right|+g_{3}\left(\left|0,1\right\rangle\left\langle0,1\right|+\left|2,0\right\rangle\left\langle2,0\right|\right) \\ +# &+g_4\big( |1,0\rangle \langle1,0|+|1,1\rangle \langle1,1| \big)+g_5 \big( |0,0\rangle \langle0,2|+\mathrm{h.c.}\big) \\ +# &- g_{5} ( |2,0\rangle \langle0,1|+\mathrm{h.c.}), +# \end{aligned} +# $$ +# +# $g_1, g_2, g_3, g_4, g_5$ 用单电子积分和双电子积分表示如下 +# $$ +# \begin{array}{ll}\hline\text{Coefficient}&\text{Definition}\\\hline g_1&h_0^0+h_1^1+v_{10}^{01}\\g_2&2h_2^2+v_{32}^{23}\\g_3&h_0^0+h_2^2+v_{20}^{02}\\g_4&h_0^0+h_2^2+v_{20}^{02}-v_{02}^{02}\\g_5&v_{02}^{02}\\\hline\end{array} +# $$ + +# %% [markdown] +# 对应的数值随原子核距离变化如下 + +# %% +import deepquantum as dq +import matplotlib.pyplot as plt +import numpy as np +import torch +from scipy import io + +dic = io.loadmat('boson_coeff2.mat') +g1 = dic['g1'][0] +g2 = dic['g2'][0] +g3 = dic['g3'][0] +g4 = dic['g4'][0] +g5 = dic['g5'][0] + +####################### +# # %matplotlib notebook +fig = plt.figure() +# R_values = np.linspace(0.1, 3, 50) +R_values = np.linspace(0.1, 10, 50 * 4) +plt.plot(R_values, dic['g1'][0], label='g1', color='red') +plt.plot(R_values, dic['g2'][0], label='g2', color='blue') +plt.plot(R_values, dic['g3'][0], label='g3', color='black', ls='--') +plt.plot(R_values, dic['g4'][0], label='g4', color='green') +plt.plot(R_values, dic['g5'][0], label='g5', color='orange', ls='--') + +plt.xlabel('bond distance') +plt.ylabel('bosonic coeffs') +plt.tight_layout() +plt.legend() + +# %% [markdown] +# # 变分量子求解器(VQE) + +# %% [markdown] +# 变分量子求解器(VQE) 是一种混合量子-经典算法[2],广泛应用于计算化学和物理中的量子态能量问题。 +# VQE利用量子计算机处理量子态的叠加和纠缠,通过变分原理逼近系统的基态能量,其中基态能量通过在量子计算机上测量得到, +# 变分过程在经典计算机上完成。 +# 量子测量得到的基态能量和真实的能量关系如下: +# $$ +# E_\mathrm{vqe} = \frac{\langle \Phi_t |H|\Phi_t \rangle}{\langle \Phi_t |\Phi_t \rangle}\geq E_\mathrm{real} +# $$ +# $|\Phi_t \rangle$ 表示试探波函数。 +# +# 在变分过程中,使用参数化波函数 $|\Phi_t (\theta) \rangle$,不断迭代变分参数 +# $\theta$ 使得 $|\Phi_t (\theta) \rangle$ 接近真实的波函数,从而得到更准确的基态能量值。 +# +# VQE的一般步骤如 +# 下: +# 1. 构造哈密顿量,将量子化学问题对应的哈密顿量映射成可变分的量子线路。 +# 2. 选择参数化量子态和量子线路, 得到一个参数化量子态 $|\Phi_t (\theta) \rangle$,参数 $\theta$ 可以对应到量子门或者光量子门的参数等。 +# 3. 算符基矢的期望值测量,对于各个算符基矢测量并计算对应的平均值。 +# 4. 哈密顿量的期望值测量,通过各个算符基矢的测量值计算当前波函数下的哈密顿量平均值 $E(\theta)$。 +# 5. 经典变分算法迭代优化哈密顿量平均值:通过梯度下降等算法更新参数$\theta$,得到能量更低的 $E(\theta)$。 + +# %% [markdown] +# # 变分量子算法的光量子线路实现 + +# %% [markdown] +# 1. 根据前面的讨论可以知道,考虑四个自旋轨道的二电子体系波函数映射到玻色系统中对应的波函数表示如下: +# +# $$ +# |\Psi\rangle_B = \lambda_1|0,0 \rangle_B + \lambda_2|1,0 \rangle_B + \lambda_3|2,0 \rangle_B + \lambda_4|0,1 \rangle_B +# + \lambda_5|1,1 \rangle_B + \lambda_6|0,2 \rangle_B +# $$ +# +# 上面6个基矢的按照光子数之和可以分为3组,$(|0,0 \rangle_B)$, $(|1,0 \rangle_B, |0,1 \rangle_B)$, $(|1,1 \rangle_B, |2,0 \rangle_B, |0,2 \rangle_B)$, 为了构造出一组完备的基矢, 因此需要用3个玻色采样线路来实现, 如下图所示。 +# +#
+# +#

+# +#

+#
+# +# 3个玻色采样线路输出分别对应上面的3组基矢, 同时为了归一化给每个玻色采样线路加入权重$w_1, w_2, w_3$。 +# $$ +# w_1^2 + w_2^2 + w_3^2 = 1 +# $$ +# 最后将输出的量子态组合为: +# $$ +# |\Psi\rangle_B = w_1|0,0 \rangle_B + w_2 (a_1|1,0 \rangle_B+a_2|0,1 \rangle_B) + w_3(b_1|1,1 \rangle_B + b_2|2,0 \rangle_B+ b_3|0,2 \rangle_B) +# $$ +# 可以验证对应的振幅是归一化的。 +# +# 2. 玻色采样线路的实现 +# +# 对于单个玻色采样线路,只需要下图的两模光量子线路即可, +#
+# +#

+# +#

+#
+# +# 三个线路的结构是相同的,不同之处在于更换不同的输入只需要更新不同的线路参数即可, 同时对于输入为 $|0,0 \rangle_B$ 的线路,只有一个确定的结果输出(不考虑损耗),不需要进行变分,因此只需要通过变分优化两个玻色采样线路参数以及3个权重即可。 + +# %% [markdown] +# # 代码实现 + +# %% [markdown] +# ## 两模玻色采样线路的VQE实现 + +# %% +state_01 = dq.FockState([0, 1]) +state_10 = dq.FockState([1, 0]) +state_11 = dq.FockState([1, 1]) +state_20 = dq.FockState([2, 0]) +state_02 = dq.FockState([0, 2]) + + +# %% +def exp_h(paras): + w1, w2, w3 = torch.nn.functional.normalize(abs(paras[0:3]), dim=0) # 归一化 + amp_00 = w1 + ############################ + nmode = 2 + cir2 = dq.QumodeCircuit(nmode=nmode, init_state=[0, 1], cutoff=3, backend='fock', basis=True) + cir2.ps(0, inputs=paras[3]) + cir2.ps(1, inputs=paras[4]) + cir2.bs([0, 1], inputs=[paras[5], paras[6]]) + cir2.ps(0, inputs=paras[7]) + cir2.ps(1, inputs=[8]) + state2 = cir2(is_prob=False) + amp_01 = w2 * state2[state_01] + amp_10 = w2 * state2[state_10] + ############################ + cir3 = dq.QumodeCircuit(nmode=nmode, init_state=[1, 1], cutoff=3, backend='fock', basis=True) + cir3.ps(0, inputs=paras[9]) + cir3.ps(1, inputs=paras[10]) + cir3.bs([0, 1], inputs=[paras[11], paras[12]]) + cir3.ps(0, inputs=paras[13]) + cir3.ps(1, inputs=[14]) + state3 = cir3(is_prob=False) + amp_11 = w3 * state3[state_11] + amp_20 = w3 * state3[state_20] + amp_02 = w3 * state3[state_02] + exp_h = ( + g_1 * abs(amp_00) ** 2 + + g_2 * abs(amp_02) ** 2 + + g_3 * (abs(amp_01) ** 2 + abs(amp_20) ** 2) + + g_4 * (abs(amp_10) ** 2 + abs(amp_11) ** 2) + + g_5 * (amp_00.conj() * amp_02 + amp_00 * amp_02.conj()) + - g_5 * (amp_20.conj() * amp_01 + amp_20 * amp_01.conj()) + ) # see + + return (exp_h).real + + +# %% +energy_bs = [] +for idx in range(50): + g_1 = g1[idx] + g_2 = g2[idx] + g_3 = g3[idx] + g_4 = g4[idx] + g_5 = g5[idx] + + w123 = torch.tensor([0.5, 0.5, 0.4], requires_grad=True) + angles = torch.nn.Parameter(torch.randn(12)) + paras = torch.cat([w123, angles]) + optimizer = torch.optim.Adam([w123, angles], lr=0.1) + + for _ in range(150): + optimizer.zero_grad() + paras = torch.cat([w123, angles]) + loss = exp_h(paras) + loss.backward() # backpropagetion + optimizer.step() # update parameters + energy_bs.append(loss) + print(idx, loss, end='\r') + +# %% [markdown] +# ## 基态能量随原子核距离的变化 + +# %% [markdown] +# 这里采用Hartree能量表示基态能量,一个Hartree能量等价于为 $27.2ev$,纵坐标表示Hartree能量, 横坐标为原子核的距离,对应的的单位为埃($10^{-10}m$)。 +# +# 对比数据为考虑两个空间轨道的近似能量的HF方法,以及完全活性空间配置相互作用(Full Configuration Interaction,FCI),FCI方法是量子化学中最精确的电子结构计算方法之一。它通过在给定基组内考虑所有可能的电子配置(即所有可能的电子占据状态),以尽可能准确地描述多电子系统的波函数,尽管FCI因为计算成本太高而无法应用于大多数实际化学系统,但它在小体系中被用作“金标准”来验证其他近似方法(如CC、MP2、DFT)的准确性。 + +# %% +R_values = R_values[0:50] +hartree_dis = R_values / 0.529177 # using Bohr radius +openfermion_h2 = np.load('openfermion_h2_v3.npy') +openfermion_h2_fci = np.load('openfermion_h2_fci.npy') +# # %matplotlib notebook +fig = plt.figure() +nuclear_v = 1 / hartree_dis +plt.plot(R_values, torch.stack(energy_bs).mT[0].detach().numpy() + nuclear_v, lw=4, label='vqe') +plt.plot(R_values, openfermion_h2[0:50], ls='--', label='openfermion_hf_2_orbitals') +plt.plot(R_values, openfermion_h2_fci[0:50], ls='--', label='openfermion_fci', color='black') +plt.ylabel('Hartree energy') +plt.xlabel('nuclear distance(A)') +plt.title('Ground energy for $H_2$') +plt.legend() +plt.tight_layout() + +# %% [markdown] +# # 两模高斯玻色采样线路的VQE实现 + +# %% [markdown] +# 同时我们也可以用高斯玻色采样做VQE变分求解 $H_2$的基态能量, 但是不同之处在于高斯玻色采样线路输出的高斯态对应的Fock基矢叠加一般是无穷多项,因此需要在我们需要的希尔伯特空间做截断然后归一化, 这一步相当于实验上的后选择操作。 与此同时带来的好处是只需要一张光量子芯片结合压缩光源就可以完成变分任务。下面演示通过加入压缩门和位移门来构造高斯玻色采样线路进行变分。 + +# %% +nmode = 2 +cir = dq.QumodeCircuit(nmode=nmode, init_state='vac', cutoff=3, backend='fock', basis=False) +cir.s(0, r=1) +cir.s(1, r=1) +cir.d(0, r=1) +cir.d(1, r=1) +cir.ps(0) +cir.ps(1) +cir.bs([0, 1]) +cir.d(0) +cir.d(1) +cir.draw() +# state = cir() + + +# %% +def exp_h_gbs_fock(paras): + s1, s2 = torch.nn.functional.normalize(abs(paras[0:2]), dim=0) # 归一化 + nmode = 2 + cir = dq.QumodeCircuit(nmode=nmode, init_state='vac', cutoff=3, backend='fock', basis=False) + cir.s(0, r=s1) + cir.s(1, r=s2) + cir.d(0, r=paras[2]) + cir.d(1, r=paras[3]) + cir.ps(0, paras[4]) + cir.ps(1, paras[5]) + cir.bs([0, 1], inputs=[paras[6], paras[7]]) + cir.d(0, r=paras[8]) + cir.d(1, r=paras[9]) + # cir.to(torch.double) + state = cir() + p_00 = state[0][0, 0] + p_01 = state[0][0, 1] + p_10 = state[0][1, 0] + p_11 = state[0][1, 1] + p_20 = state[0][2, 0] + p_02 = state[0][0, 2] + p_list = torch.stack([p_00, p_01, p_10, p_11, p_20, p_02]) + p_00_, p_01_, p_10_, p_11_, p_20_, p_02_ = torch.nn.functional.normalize(p_list, dim=0) + + exp_h = ( + g_1 * abs(p_00_) ** 2 + + g_2 * abs(p_02_) ** 2 + + g_3 * (abs(p_01_) ** 2 + abs(p_20_) ** 2) + + g_4 * (abs(p_10_) ** 2 + abs(p_11_) ** 2) + + g_5 * (p_00_.conj() * p_02_ + p_00_ * p_02_.conj()) + - g_5 * (p_20_.conj() * p_01_ + p_20_ * p_01_.conj()) + ) # see + + return (exp_h).real + + +# %% +energy_gbs = [] +for idx in range(50): + g_1 = g1[idx] + g_2 = g2[idx] + g_3 = g3[idx] + g_4 = g4[idx] + g_5 = g5[idx] + + angles = torch.nn.Parameter(torch.randn(10)) + optimizer = torch.optim.Adam([angles], lr=0.1) + for _ in range(150): + optimizer.zero_grad() + loss = exp_h_gbs_fock(angles) + loss.backward() # backpropagetion + optimizer.step() # update parameters + energy_gbs.append(loss) + print(idx, loss, end='\r') + +# %% +print(energy_gbs) + +# %% [markdown] +# 下面可以看到通过高斯玻色采样线路的变分结果也是非常接近玻色采样线路的变分结果的。 + +# %% +R_values = np.linspace(0.1, 3, 50) +hartree_dis = R_values / 0.529177 # using Bohr radius +openfermion_h2 = np.load('openfermion_h2_v3.npy') +openfermion_h2_fci = np.load('openfermion_h2_fci.npy') +# # %matplotlib notebook +fig = plt.figure() +nuclear_v = 1 / hartree_dis +plt.plot(R_values, torch.stack(energy_bs).mT[0].detach().numpy() + nuclear_v, lw=1.5, label='vqe_bs') +plt.plot(R_values, torch.stack(energy_gbs).detach().numpy() + nuclear_v, lw=1.5, ls='--', label='vqe_gbs') +plt.ylabel('Hartree energy') +plt.xlabel('nuclear distance(A)') +plt.title('Ground energy for $H_2$') +plt.legend() +plt.tight_layout() + +# %% [markdown] +# # 参考文献 + +# %% [markdown] +# [1] Dutta R, Vu N P, Lyu N, et al. Simulating electronic structure on bosonic quantum computers[J]. arXiv preprint arXiv:2404.10222, 2024. +# +# [2] FEDOROV D A, PENG B, GOVIND N, et al. Vqe method: a short survey and recent +# developments[J]. Materials Theory, 2022, 6(1):2. diff --git a/examples/demos/gbs/vqe_ground_energy_for_H2_hardware/VQE_H2_ground_energy.ipynb b/examples/demos/gbs/vqe_ground_energy_for_H2_hardware/VQE_H2_ground_energy.ipynb index 38d21f3d..8f64a06b 100644 --- a/examples/demos/gbs/vqe_ground_energy_for_H2_hardware/VQE_H2_ground_energy.ipynb +++ b/examples/demos/gbs/vqe_ground_energy_for_H2_hardware/VQE_H2_ground_energy.ipynb @@ -16,9 +16,9 @@ "source": [ "*前置模块*\n", "\n", - "[关于玻色采样的介绍](https://github.com/TuringQ/deepquantum/blob/main/examples/gbs/boson_sampling/boson_sampling.ipynb)\n", + "[关于玻色采样的介绍](https://github.com/TuringQ/deepquantum/blob/main/examples/demos/gbs/boson_sampling/boson_sampling.ipynb)\n", "\n", - "[光量子实现VQE](https://github.com/TuringQ/deepquantum/blob/main/examples/gbs/vqe_ground_energy_for_H2_alg/vqe_ground_energy_h2.ipynb)\n", + "[光量子实现VQE](https://github.com/TuringQ/deepquantum/blob/main/examples/demos/gbs/vqe_ground_energy_for_H2_alg/vqe_ground_energy_h2.ipynb)\n", "\n", "引言\n", "------\n", @@ -46,7 +46,7 @@ "\n", "[变分量子算法案例](https://deepquantum.turingq.com/category/quantum-variational-algorithm/)\n", "\n", - "[变分高斯玻色采样的训练](https://github.com/TuringQ/deepquantum/blob/main/examples/gbs/variational_gbs/variational_gbs.ipynb)\n", + "[变分高斯玻色采样的训练](https://github.com/TuringQ/deepquantum/blob/main/examples/demos/gbs/variational_gbs/variational_gbs.ipynb)\n", "\n", "变分量子算法的关键流程,是构建一个参数化的量子线路(Ansatz) $ \\left |\\psi(\\theta) \\right \\rangle = U(\\theta) \\left |0 \\right \\rangle ^{\\otimes n} $,测量得到损失函数 $L(\\theta)$ , 通过找到使损失函数最小的最优参数 $\\theta$ 来得到问题的解。\n", "\n", @@ -72,8 +72,7 @@ "

\n", " \n", "

\n", - "\n", - "\n" + "" ] }, { @@ -118,7 +117,7 @@ "\n", "在此算法中,无需对线路参数进行变分,既硬件友好,也节省了单独测量每个参数梯度的时间(对于梯度优化的VQA),或节省了迭代次数(对于非梯度优化的VQA)。\n", "\n", - "接下来,我们将在案例 [光量子实现VQE](https://github.com/TuringQ/deepquantum/blob/main/examples/gbs/vqe_ground_energy_for_H2_alg/vqe_ground_energy_h2.ipynb) 同个问题中,展示该算法的有效性。" + "接下来,我们将在案例 [光量子实现VQE](https://github.com/TuringQ/deepquantum/blob/main/examples/demos/gbs/vqe_ground_energy_for_H2_alg/vqe_ground_energy_h2.ipynb) 同个问题中,展示该算法的有效性。" ] }, { @@ -157,9 +156,9 @@ "source": [ "import deepquantum as dq\n", "import matplotlib.pyplot as plt\n", - "from scipy import io\n", - "import torch\n", "import numpy as np\n", + "import torch\n", + "from scipy import io\n", "\n", "dic = io.loadmat('boson_coeff2.mat')\n", "g1 = dic['g1'][0]\n", @@ -203,13 +202,13 @@ "cir.d(1, r=1)\n", "cir.ps(0)\n", "cir.ps(1)\n", - "cir.bs([0,1])\n", + "cir.bs([0, 1])\n", "cir.draw()" ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "c59b17d4", "metadata": { "ExecuteTime": { @@ -220,7 +219,7 @@ "outputs": [], "source": [ "def exp_h_gbs_fock(paras):\n", - " s1, s2 = torch.nn.functional.normalize(abs(paras[0:2]), dim=0) # 归一化\n", + " s1, s2 = torch.nn.functional.normalize(abs(paras[0:2]), dim=0) # 归一化\n", " nmode = 2\n", " cir = dq.QumodeCircuit(nmode=nmode, init_state='vac', cutoff=3, backend='fock', basis=False)\n", " cir.s(0, r=s1)\n", @@ -229,29 +228,34 @@ " cir.d(1, r=paras[3])\n", " cir.ps(0, paras[4])\n", " cir.ps(1, paras[5])\n", - " cir.bs([0,1], inputs=[paras[6], paras[7]])\n", + " cir.bs([0, 1], inputs=[paras[6], paras[7]])\n", "\n", " state = cir()\n", - " p_00 = state[0][0,0]\n", - " p_01 = state[0][0,1]\n", - " p_10 = state[0][1,0]\n", - " p_11 = state[0][1,1]\n", - " p_20 = state[0][2,0]\n", - " p_02 = state[0][0,2]\n", + " p_00 = state[0][0, 0]\n", + " p_01 = state[0][0, 1]\n", + " p_10 = state[0][1, 0]\n", + " p_11 = state[0][1, 1]\n", + " p_20 = state[0][2, 0]\n", + " p_02 = state[0][0, 2]\n", "\n", " p_list = torch.stack([p_00, p_01, p_10, p_11, p_20, p_02])\n", - " p_00_,p_01_,p_10_,p_11_,p_20_,p_02_ = torch.nn.functional.normalize(p_list, dim=0)\n", + " p_00_, p_01_, p_10_, p_11_, p_20_, p_02_ = torch.nn.functional.normalize(p_list, dim=0)\n", "\n", - " exp_h = g_1*abs(p_00_)**2 + g_2*abs(p_02_)**2 + g_3*(abs(p_01_)**2+ abs(p_20_)**2) + \\\n", - " g_4 *(abs(p_10_)**2 + abs(p_11_)**2) + g_5*(p_00_.conj()*p_02_+p_00_*p_02_.conj()) - \\\n", - " g_5* (p_20_.conj()*p_01_+p_20_*p_01_.conj()) # see\n", + " exp_h = (\n", + " g_1 * abs(p_00_) ** 2\n", + " + g_2 * abs(p_02_) ** 2\n", + " + g_3 * (abs(p_01_) ** 2 + abs(p_20_) ** 2)\n", + " + g_4 * (abs(p_10_) ** 2 + abs(p_11_) ** 2)\n", + " + g_5 * (p_00_.conj() * p_02_ + p_00_ * p_02_.conj())\n", + " - g_5 * (p_20_.conj() * p_01_ + p_20_ * p_01_.conj())\n", + " ) # see\n", "\n", " return (exp_h).real" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "550cf730", "metadata": { "ExecuteTime": { @@ -269,7 +273,7 @@ } ], "source": [ - "loss_gbs3 = [ ]\n", + "loss_gbs3 = []\n", "for idx in range(50):\n", " g_1 = g1[idx]\n", " g_2 = g2[idx]\n", @@ -278,14 +282,14 @@ " g_5 = g5[idx]\n", "\n", " angles = torch.nn.Parameter(torch.randn(8))\n", - " optimizer = torch.optim.Adam([angles], lr=0.1)\n", - " for epoch in range(150):\n", + " optimizer = torch.optim.Adam([angles], lr=0.1)\n", + " for _ in range(150):\n", " optimizer.zero_grad()\n", " loss = exp_h_gbs_fock(angles)\n", - " loss.backward() # backpropagetion\n", - " optimizer.step() # update parameters\n", + " loss.backward() # backpropagetion\n", + " optimizer.step() # update parameters\n", " loss_gbs3.append(loss)\n", - " print(idx,loss,end='\\r')" + " print(idx, loss, end='\\r')" ] }, { @@ -312,11 +316,11 @@ ], "source": [ "R_values = np.linspace(0.1, 2.5, 50)\n", - "hartree_dis = R_values/0.529177 # using Bohr radius\n", - "fig = plt.figure()\n", - "nuclear_v = 1/hartree_dis\n", + "hartree_dis = R_values / 0.529177 # using Bohr radius\n", + "fig = plt.figure()\n", + "nuclear_v = 1 / hartree_dis\n", "openfermion_h2_fci = np.load('openfermion_h2_fci.npy')\n", - "plt.plot(R_values, torch.stack(loss_gbs3).detach().numpy() + nuclear_v,lw=2, label='vqe_GBS_shallow')\n", + "plt.plot(R_values, torch.stack(loss_gbs3).detach().numpy() + nuclear_v, lw=2, label='vqe_GBS_shallow')\n", "plt.plot(R_values, openfermion_h2_fci[0:50], ls='--', label='openfermion_fci', color='black')\n", "plt.ylabel('Hartree energy')\n", "plt.xlabel('nuclear distance(A)')\n", @@ -373,7 +377,7 @@ "cir.s(1, r=1)\n", "cir.ps(0)\n", "cir.ps(1)\n", - "cir.bs([0,1])\n", + "cir.bs([0, 1])\n", "cir.draw()" ] }, @@ -389,28 +393,33 @@ }, "outputs": [], "source": [ - "def exp_h_gbs_fock(paras,w):\n", + "def exp_h_gbs_fock(paras, w):\n", " # s1, s2 = torch.nn.functional.normalize(abs(paras[0:2]), dim=0) # 归一化\n", " nmode = 2\n", " cir = dq.QumodeCircuit(nmode=nmode, init_state='vac', cutoff=3, backend='fock', basis=False)\n", " cir.s(0, encode=True)\n", - " cir.s(1,encode=True)\n", - " cir.ps(0,encode=True)\n", - " cir.ps(1,encode=True)\n", - " cir.bs([0,1], encode=True)\n", - " state = cir(data = paras)\n", - "\n", - " p_00 = state[:,0,0]\n", - " p_01 = state[:,0,1]\n", - " p_10 = state[:,1,0]\n", - " p_11 = state[:,1,1]\n", - " p_20 = state[:,2,0]\n", - " p_02 = state[:,0,2]\n", + " cir.s(1, encode=True)\n", + " cir.ps(0, encode=True)\n", + " cir.ps(1, encode=True)\n", + " cir.bs([0, 1], encode=True)\n", + " state = cir(data=paras)\n", + "\n", + " p_00 = state[:, 0, 0]\n", + " p_01 = state[:, 0, 1]\n", + " p_10 = state[:, 1, 0]\n", + " p_11 = state[:, 1, 1]\n", + " p_20 = state[:, 2, 0]\n", + " p_02 = state[:, 0, 2]\n", " p_list = torch.stack([p_00, p_01, p_10, p_11, p_20, p_02])\n", - " p_00_,p_01_,p_10_,p_11_,p_20_,p_02_ = torch.nn.functional.normalize(p_list, dim=0)\n", - " exp_h = g_1*abs(p_00_)**2 + g_2*abs(p_02_)**2 + g_3*(abs(p_01_)**2+ abs(p_20_)**2) + \\\n", - " g_4 *(abs(p_10_)**2 + abs(p_11_)**2) + g_5*(p_00_.conj()*p_02_+p_00_*p_02_.conj()) - \\\n", - " g_5* (p_20_.conj()*p_01_+p_20_*p_01_.conj()) # see\n", + " p_00_, p_01_, p_10_, p_11_, p_20_, p_02_ = torch.nn.functional.normalize(p_list, dim=0)\n", + " exp_h = (\n", + " g_1 * abs(p_00_) ** 2\n", + " + g_2 * abs(p_02_) ** 2\n", + " + g_3 * (abs(p_01_) ** 2 + abs(p_20_) ** 2)\n", + " + g_4 * (abs(p_10_) ** 2 + abs(p_11_) ** 2)\n", + " + g_5 * (p_00_.conj() * p_02_ + p_00_ * p_02_.conj())\n", + " - g_5 * (p_20_.conj() * p_01_ + p_20_ * p_01_.conj())\n", + " ) # see\n", " w = torch.nn.functional.softmax(w, dim=0)\n", " return (exp_h).real @ w" ] @@ -425,7 +434,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "id": "64aabe09", "metadata": { "ExecuteTime": { @@ -444,7 +453,7 @@ ], "source": [ "batch = 100\n", - "loss_gbs_4 = [ ]\n", + "loss_gbs_4 = []\n", "for idx in range(50):\n", " idx = idx\n", " g_1 = g1[idx]\n", @@ -453,18 +462,18 @@ " g_4 = g4[idx]\n", " g_5 = g5[idx]\n", "\n", - " angles = torch.rand(8*batch).reshape(batch,8)\n", - " angles[:,1] = 0.\n", - " angles[:,3] = 0.\n", - " w = torch.nn.Parameter(torch.rand(batch,1))\n", - " optimizer = torch.optim.Adam([w], lr=0.1)\n", - " for epoch in range(150):\n", + " angles = torch.rand(8 * batch).reshape(batch, 8)\n", + " angles[:, 1] = 0.0\n", + " angles[:, 3] = 0.0\n", + " w = torch.nn.Parameter(torch.rand(batch, 1))\n", + " optimizer = torch.optim.Adam([w], lr=0.1)\n", + " for _ in range(150):\n", " optimizer.zero_grad()\n", - " loss = exp_h_gbs_fock(angles,w)\n", - " loss.backward() # backpropagetion\n", - " optimizer.step() # update parameters\n", + " loss = exp_h_gbs_fock(angles, w)\n", + " loss.backward() # backpropagetion\n", + " optimizer.step() # update parameters\n", " loss_gbs_4.append(loss)\n", - " print(idx,loss,end='\\r')" + " print(idx, loss, end='\\r')" ] }, { @@ -490,9 +499,11 @@ } ], "source": [ - "fig = plt.figure()\n", - "plt.plot(R_values, torch.stack(loss_gbs3).detach().numpy() + nuclear_v,lw=2, label='vqe_GBS_shallow')\n", - "plt.plot(R_values, torch.stack(loss_gbs_4).mT[0].detach().numpy() + nuclear_v,lw=2, label='vqe_GBS_random',color='red')\n", + "fig = plt.figure()\n", + "plt.plot(R_values, torch.stack(loss_gbs3).detach().numpy() + nuclear_v, lw=2, label='vqe_GBS_shallow')\n", + "plt.plot(\n", + " R_values, torch.stack(loss_gbs_4).mT[0].detach().numpy() + nuclear_v, lw=2, label='vqe_GBS_random', color='red'\n", + ")\n", "plt.plot(R_values, openfermion_h2_fci[0:50], ls='--', label='openfermion_fci', color='black')\n", "plt.ylabel('Hartree energy')\n", "plt.xlabel('nuclear distance(A)')\n", diff --git a/examples/demos/gbs/vqe_ground_energy_for_H2_hardware/VQE_H2_ground_energy.py b/examples/demos/gbs/vqe_ground_energy_for_H2_hardware/VQE_H2_ground_energy.py new file mode 100644 index 00000000..36102693 --- /dev/null +++ b/examples/demos/gbs/vqe_ground_energy_for_H2_hardware/VQE_H2_ground_energy.py @@ -0,0 +1,311 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.19.1 +# kernelspec: +# display_name: dq_v3 +# language: python +# name: dq_v3 +# --- + +# %% [markdown] +# 硬件友好的并行随机光量子芯片实现VQE +# ====================================== + +# %% [markdown] +# *前置模块* +# +# [关于玻色采样的介绍](https://github.com/TuringQ/deepquantum/blob/main/examples/demos/gbs/boson_sampling/boson_sampling.ipynb) +# +# [光量子实现VQE](https://github.com/TuringQ/deepquantum/blob/main/examples/demos/gbs/vqe_ground_energy_for_H2_alg/vqe_ground_energy_h2.ipynb) +# +# 引言 +# ------ +# 在案例*氢分子基态能量求解的基础理论*中,引入了费米子体系到玻色子体系的映射,并展示了在光量子芯片中,如何使用玻色采样和高斯玻色采样实现VQE。 +# 在实践中,ansatz的大小和变分的需求都为实验验证增添了难度。 +# 此案例借鉴了论文[1]变分蒙特卡洛算法VQCMC的思路:对于表达能力不强的ansatz,构建随机线路采样池,以实现扩大ansatz表达能力的效果。 +# 特别的,由于光量子芯片可片上集成、量产的特点,我们可以利用多张参数不同的芯片构建采样池,并行地计算任务需要的观测量,经典地优化权重函数以得到系统最优解。 + +# %% [markdown] +# +# 变分量子算法 +# ------ +# +# 在DeepQuantum中,我们已经展示了若干从简单、中级到困难的变分量子算法(VQA)的案例👇 +# +# [变分量子算法案例](https://deepquantum.turingq.com/category/quantum-variational-algorithm/) +# +# [变分高斯玻色采样的训练](https://github.com/TuringQ/deepquantum/blob/main/examples/demos/gbs/variational_gbs/variational_gbs.ipynb) +# +# 变分量子算法的关键流程,是构建一个参数化的量子线路(Ansatz) $ \left |\psi(\theta) \right \rangle = U(\theta) \left |0 \right \rangle ^{\otimes n} $,测量得到损失函数 $L(\theta)$ , 通过找到使损失函数最小的最优参数 $\theta$ 来得到问题的解。 +# +# +#
+# +#

+# +#

+#
+# +# VQA的成功与否取决于变分量子线路的表达能力,也就是变分波函数可表达的量子态集合(图中深蓝区域)的大小。 +# +# 如图所示,如果问题的解在对应的量子态在该集合中(如点a),则通过变分训练理论上能够成功找到解; +# +# 若解在集合外,但在集合外一定阈值范围内(如点b),则在置信范围内也能得到问题的解; +# +# 若解远离该集合,VQA则会失败。 +# +# +#
+# +#

+# +#

+#
+ +# %% [markdown] +# 随机线路采样的量子-经典循环反馈策略 +# ------ +# 通常我们可以通过复杂化变分量子线路的方式来提升表达能力,例如增加Ansatz内量子门数,增加Ansatz数量等。 +# 然而,量子门数在硬件端受到限制[2][3],而梯度消失、贫瘠高原的问题[4][5]给软件优化算法带来了瓶颈。 +# +# 有幸的是,通过优化量子-经典循环反馈中的策略来提高表达能力这一点仍有待探索的空间。 +# 论文[1]介绍了一种利用随机线路采样的思路:通过神经网络构建一个决定了采样概率分布的参数化引导函数 $\alpha(\theta; \lambda)\ $,通过马尔可夫链蒙特卡洛方法进行采样。与经典VQA算法不同,不通过优化 $\theta$,而是通过优化参数化的引导函数 $\alpha(\theta; \lambda)$ 来解决问题。 +# +#
+# +#

+# +#

+#
+# +# 研究团队将该变分量子线路蒙特卡洛(VQCMC)算法应用于六比特的反铁磁海森堡模型求解基态能量的问题中,得到了比传统VQE数值模拟结果更精确的解。 +# +# 代价是更高的时间成本,对于从样本空间中每一条被马尔科夫链采样的量子线路都需要被测量,因此假设样本数量为M,则时间成本至少被提高了M倍。 +# 根据论文[6]的简单测算,具有量子优势的有效VQA时间成本以年为单位,那么以此仍需提高M倍的耗时显然是难以被接受的。 + +# %% [markdown] +# NISQ时代光量子芯片的完美适配 +# ------ +# 以往在考虑量子算法的时间成本时,往往默认了量子线路只有一条,所有的测量无论有没有前后的依赖关系,都会被认为是串联式的时间叠加。 +# 这符合目前的硬件水平,无论是超导、离子阱、中性原子平台的量子计算芯片,都难以实现量产。 +# +# 暂时唯一有量产潜力的光量子计算芯片为并联式的计算提供了可能,但目前面临规模小且难以实时调控变分的问题。 +# +# 利用随机线路采样的量子-经典循环反馈策略,我们可以对于表达能力不强的Ansatz(单张浅层光量子芯片),构建大容量的样本空间以提升整体系统的可表达性(组合随机多芯片集群),测量和构建损失函数(并行对集群进行测量,根据权重得到期望),通过经典方法优化权重函数,得到问题的解。 +# +# 在此算法中,无需对线路参数进行变分,既硬件友好,也节省了单独测量每个参数梯度的时间(对于梯度优化的VQA),或节省了迭代次数(对于非梯度优化的VQA)。 +# +# 接下来,我们将在案例 [光量子实现VQE](https://github.com/TuringQ/deepquantum/blob/main/examples/demos/gbs/vqe_ground_energy_for_H2_alg/vqe_ground_energy_h2.ipynb) 同个问题中,展示该算法的有效性。 + +# %% [markdown] +# 随机线路采样的光量子VQE代码实现 +# ------ + +# %% [markdown] +# 在$H_2$基态能量的理论求解案例中已经展示了利用两模BS线路和GBS线路,对于求解$H_2$基态能量随原子核距离变化的问题,得到了和“金标准”FCI一致的解,但是在变分GBS线路用了8个单模门和1个BS门。如果硬件达不到该要求,只能实现如下6个单模门和1个BS门,则常规的变分VQE方法无法得到好的解。 + +# %% +import deepquantum as dq +import matplotlib.pyplot as plt +import numpy as np +import torch +from scipy import io + +dic = io.loadmat('boson_coeff2.mat') +g1 = dic['g1'][0] +g2 = dic['g2'][0] +g3 = dic['g3'][0] +g4 = dic['g4'][0] +g5 = dic['g5'][0] + +# %% +nmode = 2 +cir = dq.QumodeCircuit(nmode=nmode, init_state='vac', cutoff=3, backend='fock', basis=False) +cir.s(0, r=1) +cir.s(1, r=1) +cir.d(0, r=1) +cir.d(1, r=1) +cir.ps(0) +cir.ps(1) +cir.bs([0, 1]) +cir.draw() + + +# %% +def exp_h_gbs_fock(paras): + s1, s2 = torch.nn.functional.normalize(abs(paras[0:2]), dim=0) # 归一化 + nmode = 2 + cir = dq.QumodeCircuit(nmode=nmode, init_state='vac', cutoff=3, backend='fock', basis=False) + cir.s(0, r=s1) + cir.s(1, r=s2) + cir.d(0, r=paras[2]) + cir.d(1, r=paras[3]) + cir.ps(0, paras[4]) + cir.ps(1, paras[5]) + cir.bs([0, 1], inputs=[paras[6], paras[7]]) + + state = cir() + p_00 = state[0][0, 0] + p_01 = state[0][0, 1] + p_10 = state[0][1, 0] + p_11 = state[0][1, 1] + p_20 = state[0][2, 0] + p_02 = state[0][0, 2] + + p_list = torch.stack([p_00, p_01, p_10, p_11, p_20, p_02]) + p_00_, p_01_, p_10_, p_11_, p_20_, p_02_ = torch.nn.functional.normalize(p_list, dim=0) + + exp_h = ( + g_1 * abs(p_00_) ** 2 + + g_2 * abs(p_02_) ** 2 + + g_3 * (abs(p_01_) ** 2 + abs(p_20_) ** 2) + + g_4 * (abs(p_10_) ** 2 + abs(p_11_) ** 2) + + g_5 * (p_00_.conj() * p_02_ + p_00_ * p_02_.conj()) + - g_5 * (p_20_.conj() * p_01_ + p_20_ * p_01_.conj()) + ) # see + + return (exp_h).real + + +# %% +loss_gbs3 = [] +for idx in range(50): + g_1 = g1[idx] + g_2 = g2[idx] + g_3 = g3[idx] + g_4 = g4[idx] + g_5 = g5[idx] + + angles = torch.nn.Parameter(torch.randn(8)) + optimizer = torch.optim.Adam([angles], lr=0.1) + for _ in range(150): + optimizer.zero_grad() + loss = exp_h_gbs_fock(angles) + loss.backward() # backpropagetion + optimizer.step() # update parameters + loss_gbs3.append(loss) + print(idx, loss, end='\r') + +# %% +R_values = np.linspace(0.1, 2.5, 50) +hartree_dis = R_values / 0.529177 # using Bohr radius +fig = plt.figure() +nuclear_v = 1 / hartree_dis +openfermion_h2_fci = np.load('openfermion_h2_fci.npy') +plt.plot(R_values, torch.stack(loss_gbs3).detach().numpy() + nuclear_v, lw=2, label='vqe_GBS_shallow') +plt.plot(R_values, openfermion_h2_fci[0:50], ls='--', label='openfermion_fci', color='black') +plt.ylabel('Hartree energy') +plt.xlabel('nuclear distance(A)') +plt.title('Ground energy for $H_2$') +plt.legend() +plt.tight_layout() + +# %% [markdown] +# 如果应用并行随机线路的思想,我们使用相同甚至更浅的ansatz, 通过DeepQuantum的data batch输入功能模拟多张GBS芯片,模拟对整个ansatz池进行并行计算,通过训练每张芯片对应的权重,可以得到与FCI接近的结果: + +# %% [markdown] +# 每张芯片使用的线路如下,对应的参数是随机参数: + +# %% +nmode = 2 +cir = dq.QumodeCircuit(nmode=nmode, init_state='vac', cutoff=3, backend='fock', basis=False) +cir.s(0, r=1) +cir.s(1, r=1) +cir.ps(0) +cir.ps(1) +cir.bs([0, 1]) +cir.draw() + + +# %% +def exp_h_gbs_fock(paras, w): + # s1, s2 = torch.nn.functional.normalize(abs(paras[0:2]), dim=0) # 归一化 + nmode = 2 + cir = dq.QumodeCircuit(nmode=nmode, init_state='vac', cutoff=3, backend='fock', basis=False) + cir.s(0, encode=True) + cir.s(1, encode=True) + cir.ps(0, encode=True) + cir.ps(1, encode=True) + cir.bs([0, 1], encode=True) + state = cir(data=paras) + + p_00 = state[:, 0, 0] + p_01 = state[:, 0, 1] + p_10 = state[:, 1, 0] + p_11 = state[:, 1, 1] + p_20 = state[:, 2, 0] + p_02 = state[:, 0, 2] + p_list = torch.stack([p_00, p_01, p_10, p_11, p_20, p_02]) + p_00_, p_01_, p_10_, p_11_, p_20_, p_02_ = torch.nn.functional.normalize(p_list, dim=0) + exp_h = ( + g_1 * abs(p_00_) ** 2 + + g_2 * abs(p_02_) ** 2 + + g_3 * (abs(p_01_) ** 2 + abs(p_20_) ** 2) + + g_4 * (abs(p_10_) ** 2 + abs(p_11_) ** 2) + + g_5 * (p_00_.conj() * p_02_ + p_00_ * p_02_.conj()) + - g_5 * (p_20_.conj() * p_01_ + p_20_ * p_01_.conj()) + ) # see + w = torch.nn.functional.softmax(w, dim=0) + return (exp_h).real @ w + + +# %% [markdown] +# 这里设置batch=100来模拟100 张GBS芯片, 通过训练100个权重来做变分任务。 + +# %% +batch = 100 +loss_gbs_4 = [] +for idx in range(50): + idx = idx + g_1 = g1[idx] + g_2 = g2[idx] + g_3 = g3[idx] + g_4 = g4[idx] + g_5 = g5[idx] + + angles = torch.rand(8 * batch).reshape(batch, 8) + angles[:, 1] = 0.0 + angles[:, 3] = 0.0 + w = torch.nn.Parameter(torch.rand(batch, 1)) + optimizer = torch.optim.Adam([w], lr=0.1) + for _ in range(150): + optimizer.zero_grad() + loss = exp_h_gbs_fock(angles, w) + loss.backward() # backpropagetion + optimizer.step() # update parameters + loss_gbs_4.append(loss) + print(idx, loss, end='\r') + +# %% +fig = plt.figure() +plt.plot(R_values, torch.stack(loss_gbs3).detach().numpy() + nuclear_v, lw=2, label='vqe_GBS_shallow') +plt.plot( + R_values, torch.stack(loss_gbs_4).mT[0].detach().numpy() + nuclear_v, lw=2, label='vqe_GBS_random', color='red' +) +plt.plot(R_values, openfermion_h2_fci[0:50], ls='--', label='openfermion_fci', color='black') +plt.ylabel('Hartree energy') +plt.xlabel('nuclear distance(A)') +plt.title('Ground energy for $H_2$') +plt.legend() +plt.tight_layout() + +# %% [markdown] +# # 参考文献 + +# %% [markdown] +# [1] Yang Y, Zhang Z, Wang A, et al. Maximizing quantum-computing expressive power through randomized circuits[J]. Physical Review Research, 2024, 6(2): 023098. +# +# [2] Ostaszewski M, Grant E, Benedetti M. Structure optimization for parameterized quantum circuits[J]. Quantum, 2021, 5: 391. +# +# [3] Choquette A, Di Paolo A, Barkoutsos P K, et al. Quantum-optimal-control-inspired ansatz for variational quantum algorithms[J]. Physical Review Research, 2021, 3(2): 023092. +# +# [4] Pesah A, Cerezo M, Wang S, et al. Absence of barren plateaus in quantum convolutional neural networks[J]. Physical Review X, 2021, 11(4): 041011. +# +# [5] Patti T L, Najafi K, Gao X, et al. Entanglement devised barren plateau mitigation[J]. Physical Review Research, 2021, 3(3): 033090. +# +# [6] Liu H Y, Chen Z Y, Sun T P, et al. Can variational quantum algorithms demonstrate quantum advantages? Time really matters[J]. arXiv preprint arXiv:2307.04089, 2023. diff --git a/examples/demos/tdm/advanced_cluster_state/advanced_cluster_state.ipynb b/examples/demos/tdm/advanced_cluster_state/advanced_cluster_state.ipynb index ed3b050b..f0212ae4 100644 --- a/examples/demos/tdm/advanced_cluster_state/advanced_cluster_state.ipynb +++ b/examples/demos/tdm/advanced_cluster_state/advanced_cluster_state.ipynb @@ -84,14 +84,14 @@ "$$\n", "具体可以写成\n", "$$\n", - "\\Psi_1 =\\int_{-\\infty}^{+\\infty}\\int_{-\\infty}^{+\\infty}\\int_{...}|x=\\frac{a_1}{\\sqrt{2}}\\rangle^A_1 |x=\\frac{a_1}{\\sqrt{2}}\\rangle^B_1 \n", + "\\Psi_1 =\\int_{-\\infty}^{+\\infty}\\int_{-\\infty}^{+\\infty}\\int_{...}|x=\\frac{a_1}{\\sqrt{2}}\\rangle^A_1 |x=\\frac{a_1}{\\sqrt{2}}\\rangle^B_1\n", "|x=\\frac{a_2}{\\sqrt{2}}\\rangle^A_2 |x=\\frac{a_2}{\\sqrt{2}}\\rangle^B_2 da_1da_2......\n", "$$\n", "3. 延时线圈\n", "\n", "作用在模式B上的延时线圈仅仅起到延时作用,没有分束器起作用。\n", "$$\n", - "\\Psi_2 =\\int_{-\\infty}^{+\\infty}\\int_{-\\infty}^{+\\infty}\\int_{...}|x=\\frac{a_1}{\\sqrt{2}}\\rangle^A_1 |x=\\frac{a_1}{\\sqrt{2}}\\rangle^B_2 \n", + "\\Psi_2 =\\int_{-\\infty}^{+\\infty}\\int_{-\\infty}^{+\\infty}\\int_{...}|x=\\frac{a_1}{\\sqrt{2}}\\rangle^A_1 |x=\\frac{a_1}{\\sqrt{2}}\\rangle^B_2\n", "|x=\\frac{a_2}{\\sqrt{2}}\\rangle^A_2 |x=\\frac{a_2}{\\sqrt{2}}\\rangle^B_3 da_1da_2......\n", "$$\n", "\n", @@ -99,7 +99,7 @@ "\n", "经过第二个$1:1$ 分束器,相同时刻实现空间上的纠缠。\n", "$$\n", - "\\Psi_2 =\\int_{-\\infty}^{+\\infty}\\int_{-\\infty}^{+\\infty}\\int_{...}|x=\\frac{a_1}{\\sqrt{2}}\\rangle^B_1|x=\\frac{a_1}{\\sqrt{2}}\\rangle^A_1 |x=\\frac{1}{2}(a_2-a_1)\\rangle^B_2 \n", + "\\Psi_2 =\\int_{-\\infty}^{+\\infty}\\int_{-\\infty}^{+\\infty}\\int_{...}|x=\\frac{a_1}{\\sqrt{2}}\\rangle^B_1|x=\\frac{a_1}{\\sqrt{2}}\\rangle^A_1 |x=\\frac{1}{2}(a_2-a_1)\\rangle^B_2\n", "|x=\\frac{1}{2}(a_2+a_1)\\rangle^A_2 |x=\\frac{1}{2}(a_3-a_2)\\rangle^B_3\n", "|x=\\frac{1}{2}(a_3+a_2)\\rangle^A_3 da_1da_2da_3......\n", "$$\n", @@ -167,10 +167,10 @@ "cir = dq.QumodeCircuitTDM(nmode=nmode, init_state='vac', cutoff=3)\n", "cir.s(0, r=r)\n", "cir.s(1, r=r)\n", - "cir.r(0, np.pi/2)\n", - "cir.bs([0,1], [np.pi/4, 0])\n", - "cir.delay(1, ntau=1, inputs=[np.pi/2, 0])\n", - "cir.bs([0,1], [np.pi/4, 0])\n", + "cir.r(0, np.pi / 2)\n", + "cir.bs([0, 1], [np.pi / 4, 0])\n", + "cir.delay(1, ntau=1, inputs=[np.pi / 2, 0])\n", + "cir.bs([0, 1], [np.pi / 4, 0])\n", "cir.homodyne_x(0)\n", "cir.homodyne_x(1)\n", "cir.to(torch.double)\n", @@ -260,7 +260,7 @@ "shots = 20\n", "cir(nstep=20)\n", "samples = cir.samples\n", - "samples.mT" + "print(samples.mT)" ] }, { @@ -297,14 +297,14 @@ } ], "source": [ - "size = int(shots/2)\n", - "samples_reshape = samples.mT.reshape(size,2,2)\n", - "x_delta = [ ]\n", + "size = int(shots / 2)\n", + "samples_reshape = samples.mT.reshape(size, 2, 2)\n", + "x_delta = []\n", "for i in range(size):\n", - " temp = samples_reshape[i].sum() - 2*samples_reshape[i][1,0]\n", + " temp = samples_reshape[i].sum() - 2 * samples_reshape[i][1, 0]\n", " x_delta.append(temp)\n", "x_delta2 = torch.stack(x_delta)\n", - "print('variance:', x_delta2.std()**2)\n", + "print('variance:', x_delta2.std() ** 2)\n", "print('mean:', x_delta2.mean())" ] }, @@ -369,23 +369,6 @@ "### 代码演示" ] }, - { - "cell_type": "code", - "execution_count": 18, - "id": "cdc77e45", - "metadata": { - "ExecuteTime": { - "end_time": "2024-09-30T05:25:30.220952Z", - "start_time": "2024-09-30T05:25:30.205949Z" - } - }, - "outputs": [], - "source": [ - "import deepquantum as dq\n", - "import numpy as np\n", - "import torch" - ] - }, { "cell_type": "code", "execution_count": null, @@ -419,15 +402,15 @@ "cir.s(1, r=r)\n", "cir.s(2, r=r)\n", "cir.s(3, r=r)\n", - "cir.r(0, np.pi/2)\n", - "cir.r(2, np.pi/2)\n", - "cir.bs([0,1], [np.pi/4, 0])\n", - "cir.bs([2,3], [np.pi/4, 0])\n", - "cir.bs([1,2], [np.pi/4, 0])\n", - "cir.delay(1, ntau=1, inputs=[np.pi/2, np.pi])\n", - "cir.delay(2, ntau=5, inputs=[np.pi/2, np.pi])\n", - "cir.bs([0,1], [np.pi/4, 0])\n", - "cir.bs([2,3], [np.pi/4, 0])\n", + "cir.r(0, np.pi / 2)\n", + "cir.r(2, np.pi / 2)\n", + "cir.bs([0, 1], [np.pi / 4, 0])\n", + "cir.bs([2, 3], [np.pi / 4, 0])\n", + "cir.bs([1, 2], [np.pi / 4, 0])\n", + "cir.delay(1, ntau=1, inputs=[np.pi / 2, np.pi])\n", + "cir.delay(2, ntau=5, inputs=[np.pi / 2, np.pi])\n", + "cir.bs([0, 1], [np.pi / 4, 0])\n", + "cir.bs([2, 3], [np.pi / 4, 0])\n", "cir.homodyne_x(0, eps=1e-6)\n", "cir.homodyne_x(1, eps=1e-6)\n", "cir.homodyne_x(2, eps=1e-6)\n", @@ -525,12 +508,12 @@ "cir(nstep=100)\n", "samples = cir.samples\n", "samples = samples.mT\n", - "err = [ ]\n", + "err = []\n", "for i in range(90):\n", - " temp = samples[i][:2].sum()- 1/np.sqrt(2)*(-samples[i+1][0]+samples[i+1][1]+samples[i+5][2:].sum())\n", + " temp = samples[i][:2].sum() - 1 / np.sqrt(2) * (-samples[i + 1][0] + samples[i + 1][1] + samples[i + 5][2:].sum())\n", " err.append(temp)\n", "err = torch.tensor(err)\n", - "print('variance:', err.std()**2)\n", + "print('variance:', err.std() ** 2)\n", "print('mean:', err.mean())" ] }, diff --git a/examples/demos/tdm/advanced_cluster_state/advanced_cluster_state.py b/examples/demos/tdm/advanced_cluster_state/advanced_cluster_state.py new file mode 100644 index 00000000..62816ed7 --- /dev/null +++ b/examples/demos/tdm/advanced_cluster_state/advanced_cluster_state.py @@ -0,0 +1,230 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.19.1 +# kernelspec: +# display_name: dq_v3 +# language: python +# name: dq_v3 +# --- + +# %% [markdown] +# # 复杂纠缠态制备 + +# %% [markdown] +# 前面介绍了单模线路结合时域复用方法制备简单的纠缠态,包括EPR态和GHZ态,这里介绍多模线路结合时域复用方法制备复杂的纠缠态。 + +# %% [markdown] +# ## 案例一:扩展EPR的制备 + +# %% [markdown] +# 第一种纠缠态的制备线路如下图所示, 由东京大学组提出[1], 通过两模线路结合延时线圈,实现了四个测量结果的纠缠,即实现了相邻两个时刻的两组测量结果纠缠。 +# +#
+# +#

+# +#

+#
+# +# 首先两组连续变量压缩光经过一个 $1:1$ 的分束器形成纠缠,第二模的量子态经过延时线圈和第二个时刻的第一模中的量子态再通过分束器纠缠,从而使得 +# 输出的相邻两个时刻的两组测量结果纠缠起来。 + +# %% [markdown] +# 下面给出这种纠缠态制备线路的理论分析以及使用DeepQuantum代码复现。 + +# %% [markdown] +# ### 理论分析 +# 这里我们在薛定谔表象中研究量子态如何经过一系列光量子门后演化到纠缠态。 +# +# 1.初态 +# +# $k$ 时刻的初态可以表示为 +# $$ +# \psi_k = |x=0\rangle^A_k|p=0\rangle^B_k = \int_{-\infty}^{+\infty}|x=0\rangle^A_k|x=a_k\rangle^B_k da_k +# $$ +# 那么考虑多个时刻的量子态初态为 +# $$ +# \Psi_0 = \prod_k \int_{-\infty}^{+\infty}|x=0\rangle^A_k|x=a_k\rangle^B_k da_k +# $$ +# $k$ 表示不同时刻的指标。 +# +# 2.第一个分束器 +# +# 经过第一个$1:1$ 分束器,相同时刻实现空间上的纠缠 +# +# $$ +# \Psi_1 = \hat{BS} \Psi_0 = \prod_k \int_{-\infty}^{+\infty}|x=\frac{a_k}{\sqrt{2}}\rangle^A_k |x=\frac{a_k}{\sqrt{2}}\rangle^B_k da_k +# $$ +# 具体可以写成 +# $$ +# \Psi_1 =\int_{-\infty}^{+\infty}\int_{-\infty}^{+\infty}\int_{...}|x=\frac{a_1}{\sqrt{2}}\rangle^A_1 |x=\frac{a_1}{\sqrt{2}}\rangle^B_1 +# |x=\frac{a_2}{\sqrt{2}}\rangle^A_2 |x=\frac{a_2}{\sqrt{2}}\rangle^B_2 da_1da_2...... +# $$ +# 3. 延时线圈 +# +# 作用在模式B上的延时线圈仅仅起到延时作用,没有分束器起作用。 +# $$ +# \Psi_2 =\int_{-\infty}^{+\infty}\int_{-\infty}^{+\infty}\int_{...}|x=\frac{a_1}{\sqrt{2}}\rangle^A_1 |x=\frac{a_1}{\sqrt{2}}\rangle^B_2 +# |x=\frac{a_2}{\sqrt{2}}\rangle^A_2 |x=\frac{a_2}{\sqrt{2}}\rangle^B_3 da_1da_2...... +# $$ +# +# 4. 第二个分束器 +# +# 经过第二个$1:1$ 分束器,相同时刻实现空间上的纠缠。 +# $$ +# \Psi_2 =\int_{-\infty}^{+\infty}\int_{-\infty}^{+\infty}\int_{...}|x=\frac{a_1}{\sqrt{2}}\rangle^B_1|x=\frac{a_1}{\sqrt{2}}\rangle^A_1 |x=\frac{1}{2}(a_2-a_1)\rangle^B_2 +# |x=\frac{1}{2}(a_2+a_1)\rangle^A_2 |x=\frac{1}{2}(a_3-a_2)\rangle^B_3 +# |x=\frac{1}{2}(a_3+a_2)\rangle^A_3 da_1da_2da_3...... +# $$ +# +# 考虑相邻两次的测量结果, +# $$ +# \frac{1}{2}(a_k-a_{k-1}) + \frac{1}{2}(a_k+a_{k-1}) + \frac{1}{2}(a_{k+1}-a_{k}) - \frac{1}{2}(a_{k+1}+a_{k}) = 0 +# $$ +# 可以看到相邻的两次测量结果关联起来了,形成了四个量子态组成的纠缠态。 + +# %% [markdown] +# ### 代码演示 + +# %% +import deepquantum as dq +import numpy as np +import torch + +# %% +r = 6 +nmode = 2 +cir = dq.QumodeCircuitTDM(nmode=nmode, init_state='vac', cutoff=3) +cir.s(0, r=r) +cir.s(1, r=r) +cir.r(0, np.pi / 2) +cir.bs([0, 1], [np.pi / 4, 0]) +cir.delay(1, ntau=1, inputs=[np.pi / 2, 0]) +cir.bs([0, 1], [np.pi / 4, 0]) +cir.homodyne_x(0) +cir.homodyne_x(1) +cir.to(torch.double) +cir() +cir.draw() + +# %% [markdown] +# 完成线路前向运行之后可以查看等效的线路 + +# %% +cir.draw(unroll=True) + +# %% +shots = 20 +cir(nstep=20) +samples = cir.samples +print(samples.mT) + +# %% [markdown] +# 计算相邻两次测量结果共四个结果的误差, 可以看到误差很小,因此可以认为它们相互纠缠起来。 + +# %% +size = int(shots / 2) +samples_reshape = samples.mT.reshape(size, 2, 2) +x_delta = [] +for i in range(size): + temp = samples_reshape[i].sum() - 2 * samples_reshape[i][1, 0] + x_delta.append(temp) +x_delta2 = torch.stack(x_delta) +print('variance:', x_delta2.std() ** 2) +print('mean:', x_delta2.mean()) + +# %% [markdown] +# # 案例二:二维簇态的制备 + +# %% [markdown] +# 2019 年东京大学组通过下面的四模线路加上两个延时线圈实现了二维簇态的制备[2] +#
+# +#

+# +#

+#
+ +# %% [markdown] +# 图中的三个分束器都是 $1:1$ 分束, 同一时刻四路压缩光经过三个分束器之后在空间上形成纠缠, 然后第二路经过第一个延时线做一个周期的延时, 第三路第二个延时线圈做 $N$ 个周期的延时( 这里$N=5$),最后输出的四路量子态在时间和空间上形成纠缠。 具体来说 +# $$ +# x_k^A + x_k^B - \frac{1}{\sqrt{2}}(-x^A_{k+1} + x^B_{k+1} + x^C_{k+N} + x^D_{k+N}) = 0 +# $$ +# $k$ 对应的是时间指标。 +# +# 随时间的纠缠过程如下图所示, +#
+# +#

+# +#

+#
+# 从上面的等式的前四项可以看出 $k$ 时刻和 $k+1$ 时刻是纠缠在一起的,这里就形成一维的纠缠链, 同时后两项增加了 $k+N$ 时刻的纠缠, 这里就形成了二维的纠缠,具体细节可以参考[2]。 + +# %% [markdown] +# ### 代码演示 + +# %% +r = 8 +nmode = 4 +cir = dq.QumodeCircuitTDM(nmode=nmode, init_state='vac', cutoff=3) +cir.s(0, r=r) +cir.s(1, r=r) +cir.s(2, r=r) +cir.s(3, r=r) +cir.r(0, np.pi / 2) +cir.r(2, np.pi / 2) +cir.bs([0, 1], [np.pi / 4, 0]) +cir.bs([2, 3], [np.pi / 4, 0]) +cir.bs([1, 2], [np.pi / 4, 0]) +cir.delay(1, ntau=1, inputs=[np.pi / 2, np.pi]) +cir.delay(2, ntau=5, inputs=[np.pi / 2, np.pi]) +cir.bs([0, 1], [np.pi / 4, 0]) +cir.bs([2, 3], [np.pi / 4, 0]) +cir.homodyne_x(0, eps=1e-6) +cir.homodyne_x(1, eps=1e-6) +cir.homodyne_x(2, eps=1e-6) +cir.homodyne_x(3, eps=1e-6) +cir.to(torch.double) +cir() +cir.draw() + +# %% +cir(nstep=100) + +# %% [markdown] +# 查看等效的展开后的线路 + +# %% +cir.draw(unroll=True) + +# %% [markdown] +# 现在运行线路100次,对采样结果做统计分析来验证 +# $$ +# x_k^A + x_k^B - \frac{1}{\sqrt{2}}(-x^A_{k+1} + x^B_{k+1} + x^C_{k+N} + x^D_{k+N}) = 0 +# $$ + +# %% +cir(nstep=100) +samples = cir.samples +samples = samples.mT +err = [] +for i in range(90): + temp = samples[i][:2].sum() - 1 / np.sqrt(2) * (-samples[i + 1][0] + samples[i + 1][1] + samples[i + 5][2:].sum()) + err.append(temp) +err = torch.tensor(err) +print('variance:', err.std() ** 2) +print('mean:', err.mean()) + +# %% [markdown] +# # 附录 + +# %% [markdown] +# [1] Yokoyama S, Ukai R, Armstrong S C, et al. Ultra-large-scale continuous-variable cluster states multiplexed in the time domain[J]. Nature Photonics, 2013, 7(12): 982-986. +# +# [2] Asavanant W. Time-Domain Multiplexed 2-Dimensional Cluster State: Universal Quantum Computing Platform[J]. arxiv. org, Cornell University Library, 201. diff --git a/examples/demos/tdm/simple_cluster_state/simple_cluster_state.ipynb b/examples/demos/tdm/simple_cluster_state/simple_cluster_state.ipynb index 6f243a11..a0098be8 100644 --- a/examples/demos/tdm/simple_cluster_state/simple_cluster_state.ipynb +++ b/examples/demos/tdm/simple_cluster_state/simple_cluster_state.ipynb @@ -56,7 +56,7 @@ "对应两模输入的量子态分别是$|0\\rangle_p, |0\\rangle_x$, 其中$|0\\rangle_p$ 可以通过傅里叶变换用$x$ 基矢表示。\n", "$$\n", "|0\\rangle_p = \\int_{-\\infty}^{+\\infty} |a\\rangle_x d a\n", - "$$ \n", + "$$\n", "那么经过一个$1:1$的分束器之后量子态可以变为,\n", "$$\n", "BS \\int_{-\\infty}^{+\\infty} |a\\rangle_x^A |0\\rangle_x^Bd a = \\int_{-\\infty}^{+\\infty} |\\frac{a}{\\sqrt{2}}\\rangle_x^A |\\frac{a}{\\sqrt{2}}\\rangle_x^Bd a \\sim \\int_{-\\infty}^{+\\infty} |x\\rangle_x^A |x\\rangle_x^Bd x\n", @@ -86,14 +86,14 @@ "对应三模输入的量子态分别是$|0\\rangle_p^A, |0\\rangle_x^B, |0\\rangle_x^C$, 其中$|0\\rangle_p$ 可以通过傅里叶变换用$x$ 基矢表示, 初始系统的量子态如下,\n", "$$\n", "\\int_{-\\infty}^{+\\infty} |a\\rangle_x^A |0\\rangle_x^B |0\\rangle_x^C d a\n", - "$$ \n", + "$$\n", "那么经过第一个$1:2$的分束器之后量子态可以变为,\n", "$$\n", "BS_1 \\int_{-\\infty}^{+\\infty} |a\\rangle_x^A |0\\rangle_x^B |0\\rangle_x^C d a = \\int_{-\\infty}^{+\\infty} |\\sqrt{\\frac{1}{3}}a\\rangle_x^A |\\sqrt{\\frac{2}{3}}a\\rangle_x^B|0\\rangle_x^C d a\n", "$$\n", "经过第二个$1:1$的分束器之后量子态可以变为,\n", "$$\\int_{-\\infty}^{+\\infty} |\\sqrt{\\frac{1}{3}}a\\rangle_x^A |\\sqrt{\\frac{1}{3}}a\\rangle_x^B|\\sqrt{\\frac{1}{3}}a\\rangle_x^C da\\sim\n", - "\\int_{-\\infty}^{+\\infty} |x\\rangle_x^A |x\\rangle_x^B|x\\rangle_x^C dx \n", + "\\int_{-\\infty}^{+\\infty} |x\\rangle_x^A |x\\rangle_x^B|x\\rangle_x^C dx\n", "$$\n", "从最后的结果可看到当第一个模式进行Homodyne测量到$x_0$时, 第二、三个模式进行Homodyne测量同样会得到$x_0$。\n", "\n", @@ -267,7 +267,7 @@ "nmode = 1\n", "cir = dq.QumodeCircuitTDM(nmode=nmode, init_state='vac', cutoff=3)\n", "cir.s(0, r=r)\n", - "cir.delay(0, ntau=1, inputs=[np.pi/2, np.pi/2], encode=True) # 参数编码\n", + "cir.delay(0, ntau=1, inputs=[np.pi / 2, np.pi / 2], encode=True) # 参数编码\n", "cir.homodyne_x(0)\n", "cir.draw()" ] @@ -348,12 +348,11 @@ } ], "source": [ - "data1 = torch.tensor([[np.pi/2, np.pi/2],\n", - " [np.pi/4, 0]])\n", + "data1 = torch.tensor([[np.pi / 2, np.pi / 2], [np.pi / 4, 0]])\n", "data2 = data1.unsqueeze(0)\n", "cir(data=data2, nstep=13)\n", "sample = cir.samples\n", - "sample" + "print(sample)" ] }, { @@ -402,7 +401,7 @@ "data3 = torch.stack([data1, data1])\n", "cir(data=data3, nstep=13)\n", "sample = cir.samples\n", - "sample.mT" + "print(sample.mT)" ] }, { @@ -449,11 +448,11 @@ "source": [ "r = 6\n", "nmode = 1\n", - "theta1 = torch.arcsin(torch.sqrt(torch.tensor(1/3)))\n", - "theta2 = torch.arcsin(torch.sqrt(torch.tensor(1/2)))\n", + "theta1 = torch.arcsin(torch.sqrt(torch.tensor(1 / 3)))\n", + "theta2 = torch.arcsin(torch.sqrt(torch.tensor(1 / 2)))\n", "cir = dq.QumodeCircuitTDM(nmode=nmode, init_state='vac', cutoff=3)\n", "cir.s(0, r=r)\n", - "cir.delay(0, ntau=1, inputs=[np.pi/2, np.pi/2], encode=True)\n", + "cir.delay(0, ntau=1, inputs=[np.pi / 2, np.pi / 2], encode=True)\n", "cir.homodyne_x(0)\n", "cir.draw()" ] @@ -526,13 +525,11 @@ } ], "source": [ - "data1 = torch.tensor([[np.pi/2, np.pi/2],\n", - " [theta1, 0],\n", - " [theta2, 0]])\n", + "data1 = torch.tensor([[np.pi / 2, np.pi / 2], [theta1, 0], [theta2, 0]])\n", "data2 = data1.unsqueeze(0)\n", "cir(data=data2, nstep=13)\n", "samples = cir.samples\n", - "samples" + "print(samples)" ] }, { @@ -582,7 +579,7 @@ "data3 = torch.stack([data1, data1])\n", "cir(data=data3, nstep=14)\n", "samples = cir.samples\n", - "samples.mT" + "print(samples.mT)" ] }, { diff --git a/examples/demos/tdm/simple_cluster_state/simple_cluster_state.py b/examples/demos/tdm/simple_cluster_state/simple_cluster_state.py new file mode 100644 index 00000000..505e8018 --- /dev/null +++ b/examples/demos/tdm/simple_cluster_state/simple_cluster_state.py @@ -0,0 +1,219 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.19.1 +# kernelspec: +# display_name: dq_v3 +# language: python +# name: dq_v3 +# --- + +# %% [markdown] +# # 纠缠态制备 + +# %% [markdown] +# 在量子比特系统和连续变量系统中量子纠缠都是量子信息的关键资源,最常用的纠缠态是EPR态 和GHZ态,它们在量子通信和量子网络中都起到了关键作用。 +# 量子光学中制备这些纠缠态的方法是通过分束器混合两束压缩光,然后产生纠缠态。 +# 然而一般的制备方法都是针对特定的纠缠态设计特定的实验架构,这些方法不具有普遍性。 +# 2019年,日本东京大学Furusawa组提出一种基于延时线圈的时域复用方法[1],可以通过这种方法制备多种纠缠态,包括常见的EPR态 和GHZ态。 +# 自此以后研究者们通过时域复用技术构造了各种大规模一维或者二维的簇态[2,3],向实现通用量子计算迈进一大步。 +# 这里我们以一维的EPR和GHZ态为例,详细介绍如何通过DeepQuantum模拟实现小规模簇态的构造。 + +# %% [markdown] +# ## EPR 和GHZ 态简单介绍 + +# %% [markdown] +# 1. 两模线路制备EPR态的图示如下: +# +#
+# +#

+# +#

+#
+# +# 对应两模输入的量子态分别是$|0\rangle_p, |0\rangle_x$, 其中$|0\rangle_p$ 可以通过傅里叶变换用$x$ 基矢表示。 +# $$ +# |0\rangle_p = \int_{-\infty}^{+\infty} |a\rangle_x d a +# $$ +# 那么经过一个$1:1$的分束器之后量子态可以变为, +# $$ +# BS \int_{-\infty}^{+\infty} |a\rangle_x^A |0\rangle_x^Bd a = \int_{-\infty}^{+\infty} |\frac{a}{\sqrt{2}}\rangle_x^A |\frac{a}{\sqrt{2}}\rangle_x^Bd a \sim \int_{-\infty}^{+\infty} |x\rangle_x^A |x\rangle_x^Bd x +# $$ +# 从最后的结果可看到当第一个模式进行Homodyne测量到$x_0$时, 第二个模式进行Homodyne测量同样会得到$x_0$。 + +# %% [markdown] +# 2. 三模线路制备GHZ态的图示如下: +# +#
+# +#

+# +#

+#
+# +# 对应三模输入的量子态分别是$|0\rangle_p^A, |0\rangle_x^B, |0\rangle_x^C$, 其中$|0\rangle_p$ 可以通过傅里叶变换用$x$ 基矢表示, 初始系统的量子态如下, +# $$ +# \int_{-\infty}^{+\infty} |a\rangle_x^A |0\rangle_x^B |0\rangle_x^C d a +# $$ +# 那么经过第一个$1:2$的分束器之后量子态可以变为, +# $$ +# BS_1 \int_{-\infty}^{+\infty} |a\rangle_x^A |0\rangle_x^B |0\rangle_x^C d a = \int_{-\infty}^{+\infty} |\sqrt{\frac{1}{3}}a\rangle_x^A |\sqrt{\frac{2}{3}}a\rangle_x^B|0\rangle_x^C d a +# $$ +# 经过第二个$1:1$的分束器之后量子态可以变为, +# $$\int_{-\infty}^{+\infty} |\sqrt{\frac{1}{3}}a\rangle_x^A |\sqrt{\frac{1}{3}}a\rangle_x^B|\sqrt{\frac{1}{3}}a\rangle_x^C da\sim +# \int_{-\infty}^{+\infty} |x\rangle_x^A |x\rangle_x^B|x\rangle_x^C dx +# $$ +# 从最后的结果可看到当第一个模式进行Homodyne测量到$x_0$时, 第二、三个模式进行Homodyne测量同样会得到$x_0$。 +# +# 注意这里$|0\rangle_p, |0\rangle_x$ 都是理想情况下的连续变量量子态,分别表示$p$方向和$x$方向的完美压缩态, 一般实验中会采用压缩态来近似, 通过$\frac{\pi}{2}$的旋转门可以实现$|0\rangle_p$和$|0\rangle_x$ 的相互转换。 + +# %% [markdown] +# ## 时域复用核心模块介绍 + +# %% [markdown] +#
+# +#

+# +#

+#
+ +# %% [markdown] +# 单模线路的时域复用线路结构如上图,核心模块是可调分束器+延时线圈+可调移相器结构,初始状态下延时线中是真空态,分束器角度设为$\frac{\pi}{2}$,输入的压缩态经过可调分束器和延时线圈中的真空态形成置换,第一次输出真空态。 +# 然后延时线圈中变为压缩态,第二次输入时域束器角度设为特定值(比如$\frac{\pi}{4}$),将两个压缩态纠缠起来,然后其中的一部分通过Homodyne测量探测输出,另一部分继续通过延时线圈和下一个输入的压缩态形成干涉(或者置换),然后部分纠缠态继续输出,如此往复。 +# 通过设计可调分束器和可调移相器角度周期性变化,以及变化周期匹配输入光源频率,可以制备出多种形式的一维纠缠态,比如EPR态,GHZ态,星状纠缠态等等(见下图)。 + +# %% [markdown] +#
+# +#

+# +#

+#
+ +# %% [markdown] +# 通过时域复用模块来制备EPR态和GHZ态其实就是通过设置周期性分束器和移相器角度,使得时域复用线路可以等效1.1中的光量子线路。下面介绍如何通过时域复用线路制备EPR态和GHZ态。 +#
+# +#

+# +#

+#
+# 1.EPR 态的制备 +# +# 分束器周期性变化角度为$\frac{\pi}{2},\frac{\pi}{4}$, 旋转门对应的周期性变化角度为$\frac{\pi}{2}, 0$。第一个$X$压缩态输入通过$\frac{\pi}{2}$的分束器置换出真空态, 然后通过$\frac{\pi}{2}$旋转门变换成$P$压缩态。 第二个$X$压缩态输入通过$1:1$分束器和$P$压缩态形成纠缠,然后其中一部分输出经过Homodyne 探测, 剩余部分和第三个$X$压缩态置换输出然后经过Homodyne探测。因此相邻两次输出的一定是EPR纠缠态, 相邻两次探测结果也是纠缠的。 +# +# 2.GHZ 态的制备 +# +# 分束器周期性变化角度为$\frac{\pi}{2},\theta, \frac{\pi}{4}$ ($\theta$ 是 $1:2$分束对应的角度), 旋转门对应的周期性变化角度为$\frac{\pi}{2}, 0, 0$。 +# 第一个$X$压缩态输入通过$\frac{\pi}{2}$的分束器置换出真空态, 然后通过$\frac{\pi}{2}$旋转门变换成$P$压缩态。 +# 第二个$X$压缩态输入通过$1:2$分束器和$P$压缩态形成纠缠,然后其中一部分输出经过Homodyne 探测, 剩余部分和第三个$X$压缩态通过$1:1$分束器和$P$压缩态形成纠缠,然后其中一部分输出经过Homodyne 探测,剩余部分和第四个$X$压缩态置换输出然后经过Homodyne探测。 +# 因此相邻三次输出的一定是GHZ纠缠态, 相邻三次探测结果也是纠缠的。 +# + +# %% [markdown] +# # 代码演示 + +# %% [markdown] +# 下面演示如何通过DeepQuantum时域复用模块制备简单的纠缠态。 + +# %% [markdown] +# ## EPR 态 + +# %% +import deepquantum as dq +import numpy as np +import torch + +# %% [markdown] +# 先使用`QumodeCircuitTDM`模块搭建单模时域复用线路,这个模块使用高斯后端。延时线圈参数 $n_\tau=1$,对应着延时线里只能同时存在一个量子态。周期性参数编码为 $[\frac{\pi}{2}, \frac{\pi}{2}]$ 和 $[\frac{\pi}{4}, 0]$,对应着分束器周期性变化角度为 $\frac{\pi}{2}$、$\frac{\pi}{4}$,旋转门的周期性变化角度为 $\frac{\pi}{2}$、$0$。 + +# %% +r = 9 +nmode = 1 +cir = dq.QumodeCircuitTDM(nmode=nmode, init_state='vac', cutoff=3) +cir.s(0, r=r) +cir.delay(0, ntau=1, inputs=[np.pi / 2, np.pi / 2], encode=True) # 参数编码 +cir.homodyne_x(0) +cir.draw() + +# %% [markdown] +# 完成线路前向运行之后可以查看等效的线路 + +# %% +cir() +cir.draw(unroll=True) + +# %% [markdown] +# 现在编码周期性参数,然后运行线路同时进行Homodyne 测量采样, 可以看到采样结果是两两关联的(第一次是真空态的采样结果) + +# %% +data1 = torch.tensor([[np.pi / 2, np.pi / 2], [np.pi / 4, 0]]) +data2 = data1.unsqueeze(0) +cir(data=data2, nstep=13) +sample = cir.samples +print(sample) + +# %% [markdown] +# 同时还支持多个batch参数的编码, 输出对应的batch结果 + +# %% +data3 = torch.stack([data1, data1]) +cir(data=data3, nstep=13) +sample = cir.samples +print(sample.mT) + +# %% [markdown] +# ## GHZ态 + +# %% [markdown] +# 先使用`QumodeCircuitTDM`模块搭建单模时域复用线路,延时线圈参数 $n_\tau=1$,对应着延时线里只能同时存在一个量子态。周期性参数编码为 $[\frac{\pi}{2}, \frac{\pi}{2}]$、$[\theta_1, 0]$ 和 $[\theta_2, 0]$,对应着分束器周期性变化角度为 $\frac{\pi}{2}$、$\theta_1$、$\theta_2$,旋转门的周期性变化角度为 $\frac{\pi}{2}$、$0$、$0$。$\theta_1$ 和 $\theta_2$ 对应分束器分束比为 $1:2$ 和 $1:1$ 的角度。 + +# %% +r = 6 +nmode = 1 +theta1 = torch.arcsin(torch.sqrt(torch.tensor(1 / 3))) +theta2 = torch.arcsin(torch.sqrt(torch.tensor(1 / 2))) +cir = dq.QumodeCircuitTDM(nmode=nmode, init_state='vac', cutoff=3) +cir.s(0, r=r) +cir.delay(0, ntau=1, inputs=[np.pi / 2, np.pi / 2], encode=True) +cir.homodyne_x(0) +cir.draw() + +# %% +cir() +cir.draw(unroll=True) + +# %% [markdown] +# 现在编码周期性参数,然后运行线路同时进行Homodyne 测量采样, 可以看到相邻的三个采样结果是关联的(第一次采样是真空态的采样结果)。 + +# %% +data1 = torch.tensor([[np.pi / 2, np.pi / 2], [theta1, 0], [theta2, 0]]) +data2 = data1.unsqueeze(0) +cir(data=data2, nstep=13) +samples = cir.samples +print(samples) + +# %% [markdown] +# 同时还支持多个batch参数的编码, 输出对应的batch结果。 + +# %% +data3 = torch.stack([data1, data1]) +cir(data=data3, nstep=14) +samples = cir.samples +print(samples.mT) + +# %% [markdown] +# # 附录 + +# %% [markdown] +# [1] Takeda S, Takase K, Furusawa A. On-demand photonic entanglement synthesizer[J]. Science advances, 2019, 5(5): eaaw4530. +# +# [2] Yokoyama S, Ukai R, Armstrong S C, et al. Ultra-large-scale continuous-variable cluster states multiplexed in the time domain[J]. Nature Photonics, 2013, 7(12): 982-986. +# +# [3] Asavanant W. Time-Domain Multiplexed 2-Dimensional Cluster State: Universal Quantum Computing Platform[J]. arxiv. org, Cornell University Library, 201. diff --git a/examples/hhl.ipynb b/examples/hhl.ipynb index b95f637d..48107437 100644 --- a/examples/hhl.ipynb +++ b/examples/hhl.ipynb @@ -42,7 +42,7 @@ "metadata": {}, "outputs": [], "source": [ - "from typing import Any, List, Optional, Union\n", + "from typing import Any\n", "\n", "import deepquantum as dq\n", "import numpy as np\n", @@ -56,11 +56,11 @@ " nqubit: int,\n", " ncount: int,\n", " unitary: Any,\n", - " minmax: Optional[List[int]] = None,\n", + " minmax: list[int] | None = None,\n", " den_mat: bool = False,\n", " mps: bool = False,\n", - " chi: Optional[int] = None,\n", - " show_barrier: bool = False\n", + " chi: int | None = None,\n", + " show_barrier: bool = False,\n", " ) -> None:\n", " if not isinstance(unitary, torch.Tensor):\n", " unitary = torch.tensor(unitary, dtype=torch.cfloat)\n", @@ -70,9 +70,18 @@ " minmax = [0, ncount + nreg_i - 1]\n", " assert minmax[1] - minmax[0] == ncount + nreg_i - 1\n", " self.unitary = unitary\n", - " super().__init__(nqubit=nqubit, wires=None, minmax=minmax, ancilla=None, controls=None,\n", - " init_state='zeros', name='QuantumPhaseEstimation', den_mat=den_mat,\n", - " mps=mps, chi=chi)\n", + " super().__init__(\n", + " nqubit=nqubit,\n", + " wires=None,\n", + " minmax=minmax,\n", + " ancilla=None,\n", + " controls=None,\n", + " init_state='zeros',\n", + " name='QuantumPhaseEstimation',\n", + " den_mat=den_mat,\n", + " mps=mps,\n", + " chi=chi,\n", + " )\n", " wires_c = list(range(minmax[0], minmax[0] + ncount))\n", " wires_i = list(range(minmax[0] + ncount, minmax[1] + 1))\n", " self.hlayer(wires_c)\n", @@ -83,8 +92,14 @@ " self.any(unitary=u, wires=wires_i, controls=wire)\n", " if show_barrier:\n", " self.barrier()\n", - " iqft = dq.QuantumFourierTransform(nqubit=nqubit, minmax=[wires_c[0], wires_c[-1]], den_mat=self.den_mat,\n", - " mps=self.mps, chi=self.chi, show_barrier=show_barrier).inverse()\n", + " iqft = dq.QuantumFourierTransform(\n", + " nqubit=nqubit,\n", + " minmax=[wires_c[0], wires_c[-1]],\n", + " den_mat=self.den_mat,\n", + " mps=self.mps,\n", + " chi=self.chi,\n", + " show_barrier=show_barrier,\n", + " ).inverse()\n", " self.add(iqft)\n", "\n", "\n", @@ -96,34 +111,52 @@ " t0: float = 1,\n", " den_mat: bool = False,\n", " mps: bool = False,\n", - " chi: Optional[int] = None,\n", - " show_barrier: bool = False\n", + " chi: int | None = None,\n", + " show_barrier: bool = False,\n", " ) -> None:\n", " if not isinstance(mat, torch.Tensor):\n", " mat = torch.tensor(mat)\n", " t0 *= 2 * torch.pi\n", - " unitary = torch.linalg.matrix_exp(1j * mat * t0 / 2 ** ncount)\n", + " unitary = torch.linalg.matrix_exp(1j * mat * t0 / 2**ncount)\n", " assert is_unitary(unitary)\n", " nreg_i = int(np.log2(len(unitary)))\n", " nqubit = 1 + ncount + nreg_i\n", " self.unitary = unitary\n", - " super().__init__(nqubit=nqubit, wires=None, minmax=None, ancilla=None, controls=None,\n", - " init_state='zeros', name='HHL', den_mat=den_mat, mps=mps, chi=chi)\n", - " qpe = QuantumPhaseEstimation(nqubit=nqubit, ncount=ncount, unitary=unitary, minmax=[1, nqubit-1],\n", - " den_mat=self.den_mat, mps=self.mps, chi=self.chi, show_barrier=show_barrier)\n", + " super().__init__(\n", + " nqubit=nqubit,\n", + " wires=None,\n", + " minmax=None,\n", + " ancilla=None,\n", + " controls=None,\n", + " init_state='zeros',\n", + " name='HHL',\n", + " den_mat=den_mat,\n", + " mps=mps,\n", + " chi=chi,\n", + " )\n", + " qpe = QuantumPhaseEstimation(\n", + " nqubit=nqubit,\n", + " ncount=ncount,\n", + " unitary=unitary,\n", + " minmax=[1, nqubit - 1],\n", + " den_mat=self.den_mat,\n", + " mps=self.mps,\n", + " chi=self.chi,\n", + " show_barrier=show_barrier,\n", + " )\n", " self.add(qpe)\n", " if show_barrier:\n", " self.barrier()\n", "\n", " # 旋转线路需要根据矩阵的特征值确定\n", - " for i in range(2 ** ncount):\n", + " for i in range(2**ncount):\n", " for j in range(ncount):\n", - " if format(i, '0' + str(ncount) + 'b')[ncount-j-1] == '0':\n", + " if format(i, '0' + str(ncount) + 'b')[ncount - j - 1] == '0':\n", " self.x(1 + j)\n", - " theta = 2 * torch.pi * i / 2 ** ncount\n", - " self.ry(0, inputs=theta, controls=list(range(1, ncount+1)))\n", + " theta = 2 * torch.pi * i / 2**ncount\n", + " self.ry(0, inputs=theta, controls=list(range(1, ncount + 1)))\n", " for j in range(ncount):\n", - " if format(i, '0' + str(ncount) + 'b')[ncount-j-1] == '0':\n", + " if format(i, '0' + str(ncount) + 'b')[ncount - j - 1] == '0':\n", " self.x(1 + j)\n", " if show_barrier:\n", " self.barrier()\n", @@ -151,15 +184,12 @@ "source": [ "# 该网络模型可用于特征值为整数的厄米矩阵A,如果特征值不是整数,需要调节合适的t0值,用于U演化。\n", "# 例子1\n", - "ncount = 4 # 定义测量比特数,测量比特的数量影响测量相位的精度。\n", - "nancilla = 2 # 定义辅助比特数,辅助比特的数量对应测量的酉矩阵大小。\n", - "nqubit = 1 + ncount + nancilla # 总比特数\n", - "mat = 1/4 * torch.tensor([[15, 9, 5, -3],\n", - " [9, 15, 3, -5],\n", - " [5, 3, 15, -9],\n", - " [-3, -5, -9, 15]]) # A矩阵,特征值为8,4,2,1\n", - "b = [0,0,0,1] # b向量\n", - "t0 = 1 # 设置t0时,要保证(2^ncount)*(A的所有特征值)*t0 为整数。\n", + "ncount = 4 # 定义测量比特数,测量比特的数量影响测量相位的精度。\n", + "nancilla = 2 # 定义辅助比特数,辅助比特的数量对应测量的酉矩阵大小。\n", + "nqubit = 1 + ncount + nancilla # 总比特数\n", + "mat = 1 / 4 * torch.tensor([[15, 9, 5, -3], [9, 15, 3, -5], [5, 3, 15, -9], [-3, -5, -9, 15]]) # A矩阵,特征值为8,4,2,1\n", + "b = [0, 0, 0, 1] # b向量\n", + "t0 = 1 # 设置t0时,要保证(2^ncount)*(A的所有特征值)*t0 为整数。\n", "\n", "\n", "# # 例子2\n", @@ -179,7 +209,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "a13e1aa7-b29e-45bc-b72e-b0d6b090c466", "metadata": {}, "outputs": [ @@ -202,11 +232,10 @@ "source": [ "# 计算投点的保真度结果\n", "p = np.zeros([len(b), 1])\n", + "prefix = '1' + '0' * ncount\n", "for i in range(len(b)):\n", - " try:\n", - " p[i] = counts['1' + '0' * ncount + format(i, '0' + str(nancilla) + 'b')]\n", - " except:\n", - " p[i] = 0\n", + " key = f'{prefix}{i:0{nancilla}b}'\n", + " p[i] = counts.get(key, 0)\n", "ss = sum(p)\n", "p = p / ss\n", "print('量子投点概率计算:' + str(p))\n", @@ -215,13 +244,13 @@ "x = np.linalg.inv(mat) @ np.mat(b).T\n", "x = np.array(x)\n", "# 将准确的解\n", - "x = x ** 2 / sum(x ** 2)\n", + "x = x**2 / sum(x**2)\n", "print('准确值:' + str(x))\n", "\n", "fidelity = 0\n", - "for i in range(2 ** nancilla):\n", + "for i in range(2**nancilla):\n", " fidelity = np.sqrt(x[i] * p[i]) + fidelity\n", - "fidelity = fidelity ** 2\n", + "fidelity = fidelity**2\n", "print('保真度:' + str(fidelity))" ] }, @@ -260,7 +289,7 @@ "x = np.linalg.inv(mat) @ np.mat(b).T\n", "x = np.array(x)\n", "# 将准确的解\n", - "x = x ** 2 / sum(x ** 2)\n", + "x = x**2 / sum(x**2)\n", "# x = x ** 2\n", "# x = x / sum(x ** 2) ** 0.5\n", "x = torch.tensor(x)\n", @@ -7841,7 +7870,7 @@ } ], "source": [ - "hhl.draw() # 画出线路" + "hhl.draw() # 画出线路" ] } ], diff --git a/examples/hhl.py b/examples/hhl.py new file mode 100644 index 00000000..55b414f9 --- /dev/null +++ b/examples/hhl.py @@ -0,0 +1,222 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.19.1 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# %% [markdown] +# HHL算法解的x为负值,量子测量该怎么表示? +# 在量子计算中,我们通常使用复数来表示量子态。因此,负数可以直接在量子态中表示。然而,当我们进行测量时,我们只能得到量子态的概率分布,而不能直接得到负数。在HHL算法中,我们通常通过测量得到的结果来估计解向量的各个元素。如果解向量的元素为负数,我们可以通过调整我们的测量策略来解决这个问题。例如,我们可以将解向量的所有元素加上一个大的正数,使得所有的元素都变为正数。然后,我们可以使用HHL算法来求解调整后的线性方程组。最后,我们可以将得到的解向量的所有元素减去之前加上的那个大的正数,从而得到原始线性方程组的解。这种方法可以处理解向量中包含负数的情况。然而,这种方法可能会增加问题的复杂性,因为我们需要处理更大的数。在实际应用中,我们需要根据具体的问题和量子硬件的性能来选择合适的策略。 + +# %% [markdown] +# ![image.png](attachment:3e0e0f56-ffdf-45ec-bc43-85b97ff2647e.png) + +# %% [markdown] +# ![image.png](attachment:eba58114-05cb-4807-86be-a2b1c45440e6.png) + +# %% +from typing import Any + +import deepquantum as dq +import numpy as np +import torch +from deepquantum.qmath import is_unitary + + +class QuantumPhaseEstimation(dq.Ansatz): + def __init__( + self, + nqubit: int, + ncount: int, + unitary: Any, + minmax: list[int] | None = None, + den_mat: bool = False, + mps: bool = False, + chi: int | None = None, + show_barrier: bool = False, + ) -> None: + if not isinstance(unitary, torch.Tensor): + unitary = torch.tensor(unitary, dtype=torch.cfloat) + assert is_unitary(unitary) + nreg_i = int(np.log2(len(unitary))) + if minmax is None: + minmax = [0, ncount + nreg_i - 1] + assert minmax[1] - minmax[0] == ncount + nreg_i - 1 + self.unitary = unitary + super().__init__( + nqubit=nqubit, + wires=None, + minmax=minmax, + ancilla=None, + controls=None, + init_state='zeros', + name='QuantumPhaseEstimation', + den_mat=den_mat, + mps=mps, + chi=chi, + ) + wires_c = list(range(minmax[0], minmax[0] + ncount)) + wires_i = list(range(minmax[0] + ncount, minmax[1] + 1)) + self.hlayer(wires_c) + if show_barrier: + self.barrier() + for i, wire in enumerate(wires_c): + u = torch.linalg.matrix_power(self.unitary, 2 ** (ncount - 1 - i)) + self.any(unitary=u, wires=wires_i, controls=wire) + if show_barrier: + self.barrier() + iqft = dq.QuantumFourierTransform( + nqubit=nqubit, + minmax=[wires_c[0], wires_c[-1]], + den_mat=self.den_mat, + mps=self.mps, + chi=self.chi, + show_barrier=show_barrier, + ).inverse() + self.add(iqft) + + +class HHL(dq.Ansatz): + def __init__( + self, + ncount: int, + mat: Any, + t0: float = 1, + den_mat: bool = False, + mps: bool = False, + chi: int | None = None, + show_barrier: bool = False, + ) -> None: + if not isinstance(mat, torch.Tensor): + mat = torch.tensor(mat) + t0 *= 2 * torch.pi + unitary = torch.linalg.matrix_exp(1j * mat * t0 / 2**ncount) + assert is_unitary(unitary) + nreg_i = int(np.log2(len(unitary))) + nqubit = 1 + ncount + nreg_i + self.unitary = unitary + super().__init__( + nqubit=nqubit, + wires=None, + minmax=None, + ancilla=None, + controls=None, + init_state='zeros', + name='HHL', + den_mat=den_mat, + mps=mps, + chi=chi, + ) + qpe = QuantumPhaseEstimation( + nqubit=nqubit, + ncount=ncount, + unitary=unitary, + minmax=[1, nqubit - 1], + den_mat=self.den_mat, + mps=self.mps, + chi=self.chi, + show_barrier=show_barrier, + ) + self.add(qpe) + if show_barrier: + self.barrier() + + # 旋转线路需要根据矩阵的特征值确定 + for i in range(2**ncount): + for j in range(ncount): + if format(i, '0' + str(ncount) + 'b')[ncount - j - 1] == '0': + self.x(1 + j) + theta = 2 * torch.pi * i / 2**ncount + self.ry(0, inputs=theta, controls=list(range(1, ncount + 1))) + for j in range(ncount): + if format(i, '0' + str(ncount) + 'b')[ncount - j - 1] == '0': + self.x(1 + j) + if show_barrier: + self.barrier() + + iqpe = qpe.inverse() + self.add(iqpe) + if show_barrier: + self.barrier() + + +# %% +# 该网络模型可用于特征值为整数的厄米矩阵A,如果特征值不是整数,需要调节合适的t0值,用于U演化。 +# 例子1 +ncount = 4 # 定义测量比特数,测量比特的数量影响测量相位的精度。 +nancilla = 2 # 定义辅助比特数,辅助比特的数量对应测量的酉矩阵大小。 +nqubit = 1 + ncount + nancilla # 总比特数 +mat = 1 / 4 * torch.tensor([[15, 9, 5, -3], [9, 15, 3, -5], [5, 3, 15, -9], [-3, -5, -9, 15]]) # A矩阵,特征值为8,4,2,1 +b = [0, 0, 0, 1] # b向量 +t0 = 1 # 设置t0时,要保证(2^ncount)*(A的所有特征值)*t0 为整数。 + + +# # 例子2 +# ncount = 4 # 定义测量比特数,测量比特的数量影响测量相位的精度。 +# nancilla = 1 # 定义辅助比特数,辅助比特的数量对应测量的酉矩阵大小。 +# nqubit = 1 + ncount + nancilla # 总比特数 +# mat = torch.tensor([[4.,1], [1,4]]) # A矩阵,特征值为5,3 +# b = [1, 1] # b向量 +# t0 = 1 # 设置t0时,要保证(2^ncount)*(A的所有特征值)*t0 为整数。 + +hhl = HHL(ncount=ncount, mat=mat, t0=t0, show_barrier=True) +state = dq.QubitState(nqubit, state=b).state +res = hhl(state=state) +counts = hhl.measure(shots=100000) +print(counts) + +# %% +# 计算投点的保真度结果 +p = np.zeros([len(b), 1]) +prefix = '1' + '0' * ncount +for i in range(len(b)): + key = f'{prefix}{i:0{nancilla}b}' + p[i] = counts.get(key, 0) +ss = sum(p) +p = p / ss +print('量子投点概率计算:' + str(p)) + +# 计算准确的解 +x = np.linalg.inv(mat) @ np.mat(b).T +x = np.array(x) +# 将准确的解 +x = x**2 / sum(x**2) +print('准确值:' + str(x)) + +fidelity = 0 +for i in range(2**nancilla): + fidelity = np.sqrt(x[i] * p[i]) + fidelity +fidelity = fidelity**2 +print('保真度:' + str(fidelity)) + +# %% +# 计算结果与正确答案的余弦相似度 +p = res[len(res) // 2 : len(res) // 2 + len(b)] +# p = hhl.tensor_rep(res)[0][tuple([1] + [0] * ncount)] +p = abs(p) ** 2 +ss = sum(p) +p = p / ss +print('量子概率计算:' + str(p)) + +# 计算准确的解 +x = np.linalg.inv(mat) @ np.mat(b).T +x = np.array(x) +# 将准确的解 +x = x**2 / sum(x**2) +# x = x ** 2 +# x = x / sum(x ** 2) ** 0.5 +x = torch.tensor(x) +print('准确值:' + str(x)) + +print('余弦相似度:' + str(sum(x * p) / (sum(abs(x) ** 2) ** 0.5 * sum(abs(p) ** 2) ** 0.5))) + +# %% +hhl.draw() # 画出线路 diff --git a/examples/qaoa.ipynb b/examples/qaoa.ipynb index 4637285c..ed4377fc 100644 --- a/examples/qaoa.ipynb +++ b/examples/qaoa.ipynb @@ -11,6 +11,7 @@ "import torch\n", "import torch.nn as nn\n", "\n", + "\n", "class QAOA(nn.Module):\n", " def __init__(self, nqubit, pairs, coefs, step):\n", " super().__init__()\n", @@ -24,7 +25,7 @@ " self.cir = dq.QubitCircuit(nqubit)\n", " self.cir.hlayer()\n", " self.cir.barrier()\n", - " for _ in range(step): #step量子网络演化步数,步数越高计算越精确\n", + " for _ in range(step): # step量子网络演化步数,步数越高计算越精确\n", " for wires in pairs: # Hp项\n", " self.cir.cnot(wires[0], wires[1])\n", " self.cir.rz(wires[1], encode=True)\n", @@ -33,7 +34,7 @@ " for i in range(nqubit): # Hb项\n", " self.cir.rx(i, encode=True)\n", " self.cir.barrier()\n", - " for wires in pairs: # 测量每一项Hp\n", + " for wires in pairs: # 测量每一项Hp\n", " self.cir.observable(wires)\n", "\n", " @property\n", @@ -49,12 +50,12 @@ " self.cir.encode(self.params)\n", " return self.cir.draw()\n", "\n", - " def measure(self): # 测量线路\n", + " def measure(self): # 测量线路\n", " return self.cir.measure()\n", "\n", " def forward(self):\n", " self.cir(self.params)\n", - " return sum(self.coefs * self.cir.expectation()) # 测量的哈密顿量可能需要根据具体的问题进行一定的修改" + " return sum(self.coefs * self.cir.expectation()) # 测量的哈密顿量可能需要根据具体的问题进行一定的修改" ] }, { @@ -68,7 +69,7 @@ " # 选择优化器\n", " optimizer = torch.optim.Adam(model.parameters(), lr=lr)\n", " # optimizer = torch.optim.RMSprop(model.parameters(), lr, alpha=0.99, eps=1e-08, weight_decay=0, momentum=0, centered=False)\n", - " train_loss_list=[]\n", + " train_loss_list = []\n", " for e in range(epoch):\n", " y_pred = model()\n", " loss = y_pred\n", @@ -78,8 +79,7 @@ " train_loss_list.append(loss.detach().numpy())\n", " print(f'Iteration: {e} loss {loss.item()}')\n", " # torch.save(model.state_dict(), save_path+model_type+'.pt') # 保存训练好的模型参数,用于后续的推理或测试\n", - " metrics = {'epoch': list(range(1, len(train_loss_list) + 1)),\n", - " 'train_loss': train_loss_list}\n", + " metrics = {'epoch': list(range(1, len(train_loss_list) + 1)), 'train_loss': train_loss_list}\n", " return model, metrics" ] }, @@ -100,8 +100,17 @@ ], "source": [ "# 定义问题,有若干个元素,它们如果放在一起,则两两之间有一定的矛盾值,求将他们分为两组,使总矛盾值最小。\n", - "problem = {'Z0 Z4': 0.73, 'Z0 Z5': 0.33, 'Z0 Z6': 0.5, 'Z1 Z4': 0.69, 'Z1 Z5': 0.36,\n", - " 'Z2 Z5': 0.88, 'Z2 Z6': 0.58, 'Z3 Z5': 0.67, 'Z3 Z6': 0.43}\n", + "problem = {\n", + " 'Z0 Z4': 0.73,\n", + " 'Z0 Z5': 0.33,\n", + " 'Z0 Z6': 0.5,\n", + " 'Z1 Z4': 0.69,\n", + " 'Z1 Z5': 0.36,\n", + " 'Z2 Z5': 0.88,\n", + " 'Z2 Z6': 0.58,\n", + " 'Z3 Z5': 0.67,\n", + " 'Z3 Z6': 0.43,\n", + "}\n", "\n", "pairs = []\n", "coefs = []\n", @@ -119,9 +128,9 @@ "print('coefs:', coefs)\n", "\n", "nqubit = 7 # 比特的总个数,应与problem中的比特数保持一致\n", - "step = 4 # 量子网络演化步数,步数越高计算越精确\n", - "lr = 0.01 # 定义学习率\n", - "epoch = 100 # 定义迭代次数\n", + "step = 4 # 量子网络演化步数,步数越高计算越精确\n", + "lr = 0.01 # 定义学习率\n", + "epoch = 100 # 定义迭代次数\n", "\n", "qaoa_model = QAOA(nqubit, pairs, coefs, step)" ] diff --git a/examples/qaoa.py b/examples/qaoa.py new file mode 100644 index 00000000..f71f5b84 --- /dev/null +++ b/examples/qaoa.py @@ -0,0 +1,132 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.19.1 +# kernelspec: +# display_name: dq +# language: python +# name: python3 +# --- + +# %% +import deepquantum as dq +import torch +import torch.nn as nn + + +class QAOA(nn.Module): + def __init__(self, nqubit, pairs, coefs, step): + super().__init__() + self.nqubit = nqubit + self.pairs = pairs + self.coefs = torch.tensor(coefs) + self.step = step + self.gamma = nn.Parameter(0.1 * torch.ones(step)) + self.beta = nn.Parameter(torch.ones(step)) + + self.cir = dq.QubitCircuit(nqubit) + self.cir.hlayer() + self.cir.barrier() + for _ in range(step): # step量子网络演化步数,步数越高计算越精确 + for wires in pairs: # Hp项 + self.cir.cnot(wires[0], wires[1]) + self.cir.rz(wires[1], encode=True) + self.cir.cnot(wires[0], wires[1]) + self.cir.barrier() + for i in range(nqubit): # Hb项 + self.cir.rx(i, encode=True) + self.cir.barrier() + for wires in pairs: # 测量每一项Hp + self.cir.observable(wires) + + @property + def params(self): + params = torch.empty(0) + for i in range(self.step): + gammas = 2 * self.gamma[i] * self.coefs + betas = 2 * self.beta[i].repeat(self.nqubit) + params = torch.cat([params, gammas, betas]) + return params + + def draw(self): # 画出量子线路图 + self.cir.encode(self.params) + return self.cir.draw() + + def measure(self): # 测量线路 + return self.cir.measure() + + def forward(self): + self.cir(self.params) + return sum(self.coefs * self.cir.expectation()) # 测量的哈密顿量可能需要根据具体的问题进行一定的修改 + + +# %% +def trainer(model, epoch, lr): + # 选择优化器 + optimizer = torch.optim.Adam(model.parameters(), lr=lr) + # optimizer = torch.optim.RMSprop(model.parameters(), lr, alpha=0.99, eps=1e-08, weight_decay=0, momentum=0, centered=False) + train_loss_list = [] + for e in range(epoch): + y_pred = model() + loss = y_pred + optimizer.zero_grad() + loss.backward(retain_graph=True) + optimizer.step() + train_loss_list.append(loss.detach().numpy()) + print(f'Iteration: {e} loss {loss.item()}') + # torch.save(model.state_dict(), save_path+model_type+'.pt') # 保存训练好的模型参数,用于后续的推理或测试 + metrics = {'epoch': list(range(1, len(train_loss_list) + 1)), 'train_loss': train_loss_list} + return model, metrics + + +# %% +# 定义问题,有若干个元素,它们如果放在一起,则两两之间有一定的矛盾值,求将他们分为两组,使总矛盾值最小。 +problem = { + 'Z0 Z4': 0.73, + 'Z0 Z5': 0.33, + 'Z0 Z6': 0.5, + 'Z1 Z4': 0.69, + 'Z1 Z5': 0.36, + 'Z2 Z5': 0.88, + 'Z2 Z6': 0.58, + 'Z3 Z5': 0.67, + 'Z3 Z6': 0.43, +} + +pairs = [] +coefs = [] + +for key, value in problem.items(): + temp = [] + for item in key.split(): + if item[0] == 'Z': + temp.append(int(item[1:])) + if len(temp) == 2: + pairs.append(temp) + coefs.append(value) + +print('pairs:', pairs) +print('coefs:', coefs) + +nqubit = 7 # 比特的总个数,应与problem中的比特数保持一致 +step = 4 # 量子网络演化步数,步数越高计算越精确 +lr = 0.01 # 定义学习率 +epoch = 100 # 定义迭代次数 + +qaoa_model = QAOA(nqubit, pairs, coefs, step) + +# %% +optim_model, metrics = trainer(qaoa_model, epoch, lr) + +# %% +res = qaoa_model.measure() +print(res) +max_key = max(res, key=res.get) +print('最佳分割方案' + str(max_key)) + +# %% +qaoa_model.draw() diff --git a/examples/qresnets.ipynb b/examples/qresnets.ipynb index 8da31040..15779e5b 100644 --- a/examples/qresnets.ipynb +++ b/examples/qresnets.ipynb @@ -19,22 +19,21 @@ "source": [ "import random\n", "\n", - "import numpy as np\n", + "import deepquantum as dq\n", "import matplotlib.pyplot as plt\n", + "import numpy as np\n", "import torch\n", - "from torch import pi, nn\n", - "from torch.utils.data import Dataset, DataLoader, random_split\n", - "import deepquantum as dq\n", + "from torch import nn\n", + "from torch.utils.data import DataLoader, Dataset, random_split\n", "\n", "random.seed(43)\n", "np.random.seed(43)\n", - "torch.manual_seed(43)\n", - "\n" + "torch.manual_seed(43)" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -61,9 +60,6 @@ " return self.x_values[idx], self.y_values[idx]\n", "\n", "\n", - "\n", - "\n", - "\n", "class Y2Dataset(Dataset):\n", " def __init__(self, omega, amps):\n", " \"\"\"\n", @@ -76,7 +72,7 @@ "\n", " def y2(self, x, omega, amps):\n", " sum_result = torch.zeros_like(x, dtype=torch.complex64)\n", - " for w, a in zip(omega, amps):\n", + " for w, a in zip(omega, amps, strict=True):\n", " sum_result += a * torch.exp(1j * w * x) + torch.conj(a) * torch.exp(-1j * w * x)\n", " return sum_result.real\n", "\n", @@ -105,14 +101,19 @@ ], "source": [ "# Example usage:\n", - "omega1 = torch.tensor([0.0, 1.0, -1.0]) \n", + "omega1 = torch.tensor([0.0, 1.0, -1.0])\n", "a = torch.tensor(0.1)\n", "dataset_y1_omega1 = Y1Dataset(omega1, a)\n", "\n", "\n", "# Plot\n", "plt.figure(figsize=(10, 6))\n", - "plt.plot(dataset_y1_omega1.x_values.flatten().numpy(), dataset_y1_omega1.y_values.flatten().numpy(), 'k--', label='y1(x) with Ω1')\n", + "plt.plot(\n", + " dataset_y1_omega1.x_values.flatten().numpy(),\n", + " dataset_y1_omega1.y_values.flatten().numpy(),\n", + " 'k--',\n", + " label='y1(x) with Ω1',\n", + ")\n", "plt.xlabel('x')\n", "plt.legend()\n", "plt.grid(True)\n", @@ -137,14 +138,19 @@ ], "source": [ "# Example usage:\n", - "omega2 = torch.tensor([0.0, 1.0, 0.5]) \n", + "omega2 = torch.tensor([0.0, 1.0, 0.5])\n", "a = torch.tensor(0.1j)\n", "dataset_y1_omega2 = Y1Dataset(omega2, a)\n", "\n", "\n", "# Plot\n", "plt.figure(figsize=(10, 6))\n", - "plt.plot(dataset_y1_omega2.x_values.flatten().numpy(), dataset_y1_omega2.y_values.flatten().numpy(), 'k--', label='y1(x) with Ω2')\n", + "plt.plot(\n", + " dataset_y1_omega2.x_values.flatten().numpy(),\n", + " dataset_y1_omega2.y_values.flatten().numpy(),\n", + " 'k--',\n", + " label='y1(x) with Ω2',\n", + ")\n", "plt.xlabel('x')\n", "plt.legend()\n", "plt.grid(True)\n", @@ -169,14 +175,19 @@ ], "source": [ "# Example usage:\n", - "omega2 = torch.tensor([0.0, 1.0, 0.5]) \n", - "amps = torch.tensor([0.1j,0.15, 0.12]) # 并非所有的系数都能拟合,取决于变分线路\n", + "omega2 = torch.tensor([0.0, 1.0, 0.5])\n", + "amps = torch.tensor([0.1j, 0.15, 0.12]) # 并非所有的系数都能拟合,取决于变分线路\n", "dataset_y2_omega2 = Y2Dataset(omega2, amps)\n", "\n", "\n", "# Plot\n", "plt.figure(figsize=(10, 6))\n", - "plt.plot(dataset_y2_omega2.x_values.flatten().numpy(), dataset_y2_omega2.y_values.flatten().numpy(), 'k--', label='y2(x) with Ω2')\n", + "plt.plot(\n", + " dataset_y2_omega2.x_values.flatten().numpy(),\n", + " dataset_y2_omega2.y_values.flatten().numpy(),\n", + " 'k--',\n", + " label='y2(x) with Ω2',\n", + ")\n", "plt.xlabel('x')\n", "plt.legend()\n", "plt.grid(True)\n", @@ -224,7 +235,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -247,19 +258,18 @@ " self.qnn.ry(wires=0, encode=True)\n", " self.qnn.u3(wires=0)\n", " self.qnn.observable(wires=0, basis='z')\n", + "\n", " def forward(self, x):\n", - " state = self.qnn(data=x)\n", + " self.qnn(data=x)\n", " exp = self.qnn.expectation()\n", " if self.residual:\n", - " exp = (exp[:, [0]] + exp[:, [1]])/2 \n", - " else:\n", - " exp = exp\n", + " exp = (exp[:, [0]] + exp[:, [1]]) / 2\n", " return exp" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -270,52 +280,57 @@ " for epoch in range(epochs):\n", " # Training phase\n", " model.train() # Set the model to training mode\n", - " for j, (x, y) in enumerate(train_loader):\n", + " for x, y in train_loader:\n", " yhat = model(x)\n", " loss = torch.nn.functional.mse_loss(yhat, y)\n", " opt.zero_grad()\n", " loss.backward()\n", " opt.step()\n", - " if epoch==0: print(loss.item())\n", + " if epoch == 0:\n", + " print(loss.item())\n", "\n", " # Validation phase\n", " model.eval() # Set the model to evaluation mode\n", " val_loss = 0.0\n", " with torch.no_grad(): # Disable gradient calculation\n", - " for j, (x, y) in enumerate(val_loader):\n", + " for x, y in val_loader:\n", " yhat = model(x)\n", " loss = torch.nn.functional.mse_loss(yhat, y)\n", " val_loss += loss.item()\n", "\n", " avg_val_loss = val_loss / len(val_loader)\n", " print('Validation - Epoch', epoch, 'Average Valid Loss:', avg_val_loss)\n", - " \n", - " return model\n" + "\n", + " return model" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def plot_predictions(dataset, label_dataset, trained_model, label_model):\n", " if not isinstance(trained_model, list):\n", " # Generate predictions using the model\n", - " trained_model.eval() \n", + " trained_model.eval()\n", " with torch.no_grad():\n", " predictions = trained_model(dataset.x_values).flatten().numpy()\n", "\n", " # Plot the original y(x) and the predictions\n", " plt.figure(figsize=(10, 6))\n", - " plt.plot(dataset.x_values.flatten().numpy(), dataset.y_values.flatten().numpy(), 'k--', label=label_dataset, zorder=2)\n", + " plt.plot(\n", + " dataset.x_values.flatten().numpy(), dataset.y_values.flatten().numpy(), 'k--', label=label_dataset, zorder=2\n", + " )\n", " plt.plot(dataset.x_values.flatten().numpy(), predictions, 'gray', linewidth=6, label=label_model, zorder=1)\n", " else:\n", " # Plot the original y(x) and the predictions\n", " plt.figure(figsize=(10, 6))\n", - " plt.plot(dataset.x_values.flatten().numpy(), dataset.y_values.flatten().numpy(), 'k--', label=label_dataset, zorder=2)\n", - " for model, label in zip(trained_model, label_model):\n", - " model.eval() \n", + " plt.plot(\n", + " dataset.x_values.flatten().numpy(), dataset.y_values.flatten().numpy(), 'k--', label=label_dataset, zorder=2\n", + " )\n", + " for model, label in zip(trained_model, label_model, strict=True):\n", + " model.eval()\n", " with torch.no_grad():\n", " predictions = model(dataset.x_values).flatten().numpy()\n", " if 'residual' in label:\n", @@ -324,8 +339,6 @@ " color = 'gray'\n", " plt.plot(dataset.x_values.flatten().numpy(), predictions, color, linewidth=6, label=label, zorder=1)\n", "\n", - "\n", - " \n", " plt.xlabel('x')\n", " plt.legend()\n", " plt.grid(True)\n", @@ -358,7 +371,7 @@ ], "source": [ "qnn_traditional = QNN(residual=False)\n", - "qnn_traditional.qnn.draw()\n" + "qnn_traditional.qnn.draw()" ] }, { @@ -612,9 +625,12 @@ } ], "source": [ - "plot_predictions(dataset_y1_omega1, label_dataset='y1(x) with Ω1', \n", - " trained_model=[trained_qnn_traditional, trained_qnn_residual], \n", - " label_model=['qnn_traditional', 'qnn_residual'])\n" + "plot_predictions(\n", + " dataset_y1_omega1,\n", + " label_dataset='y1(x) with Ω1',\n", + " trained_model=[trained_qnn_traditional, trained_qnn_residual],\n", + " label_model=['qnn_traditional', 'qnn_residual'],\n", + ")" ] }, { @@ -643,7 +659,7 @@ ], "source": [ "qnn_traditional = QNN(residual=False)\n", - "qnn_traditional.qnn.draw()\n" + "qnn_traditional.qnn.draw()" ] }, { @@ -770,7 +786,7 @@ } ], "source": [ - "qnn_residual = QNN(residual=True) # 参数初始化敏感,需要多实例化几次\n", + "qnn_residual = QNN(residual=True) # 参数初始化敏感,需要多实例化几次\n", "qnn_residual.qnn.draw()" ] }, @@ -897,9 +913,12 @@ } ], "source": [ - "plot_predictions(dataset_y1_omega2, label_dataset='y1(x) with Ω2', \n", - " trained_model=[trained_qnn_traditional, trained_qnn_residual], \n", - " label_model=['qnn_traditional', 'qnn_residual'])\n" + "plot_predictions(\n", + " dataset_y1_omega2,\n", + " label_dataset='y1(x) with Ω2',\n", + " trained_model=[trained_qnn_traditional, trained_qnn_residual],\n", + " label_model=['qnn_traditional', 'qnn_residual'],\n", + ")" ] }, { @@ -933,7 +952,7 @@ ], "source": [ "qnn_traditional = QNN(residual=False)\n", - "qnn_traditional.qnn.draw()\n" + "qnn_traditional.qnn.draw()" ] }, { @@ -1060,7 +1079,7 @@ } ], "source": [ - "qnn_residual = QNN(residual=True) # 参数初始化敏感,需要多实例化几次\n", + "qnn_residual = QNN(residual=True) # 参数初始化敏感,需要多实例化几次\n", "qnn_residual.qnn.draw()" ] }, @@ -1187,9 +1206,12 @@ } ], "source": [ - "plot_predictions(dataset_y2_omega2, label_dataset='y2(x) with Ω2', \n", - " trained_model=[trained_qnn_traditional, trained_qnn_residual], \n", - " label_model=['qnn_traditional', 'qnn_residual'])\n" + "plot_predictions(\n", + " dataset_y2_omega2,\n", + " label_dataset='y2(x) with Ω2',\n", + " trained_model=[trained_qnn_traditional, trained_qnn_residual],\n", + " label_model=['qnn_traditional', 'qnn_residual'],\n", + ")" ] }, { diff --git a/examples/qresnets.py b/examples/qresnets.py new file mode 100644 index 00000000..68ce8c8c --- /dev/null +++ b/examples/qresnets.py @@ -0,0 +1,336 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.19.1 +# kernelspec: +# display_name: dq +# language: python +# name: python3 +# --- + +# %% +import random + +import deepquantum as dq +import matplotlib.pyplot as plt +import numpy as np +import torch +from torch import nn +from torch.utils.data import DataLoader, Dataset, random_split + +random.seed(43) +np.random.seed(43) +torch.manual_seed(43) + + +# %% +class Y1Dataset(Dataset): + def __init__(self, omega, a): + """ + Args: + omega (tensor): A tensor of omega values. + a (tensor): A complex number representing the amplitude. + """ + self.x_values = torch.linspace(-6, 6, 600).view(-1, 1) # 2D tensor with shape (num_samples, 1) + self.y_values = self.y1(self.x_values, omega, a) + + def y1(self, x, omega, a): + sum_result = torch.zeros_like(x, dtype=torch.complex64) + for w in omega: + sum_result += a * torch.exp(1j * w * x) + torch.conj(a) * torch.exp(-1j * w * x) + return sum_result.real + + def __len__(self): + return len(self.x_values) + + def __getitem__(self, idx): + return self.x_values[idx], self.y_values[idx] + + +class Y2Dataset(Dataset): + def __init__(self, omega, amps): + """ + Args: + omega (tensor): A tensor of omega values. + amps (tensor): A tensor of complex numbers representing the amplitudes. + """ + self.x_values = torch.linspace(-6, 6, 600).view(-1, 1) # 2D tensor with shape (num_samples, 1) + self.y_values = self.y2(self.x_values, omega, amps) + + def y2(self, x, omega, amps): + sum_result = torch.zeros_like(x, dtype=torch.complex64) + for w, a in zip(omega, amps, strict=True): + sum_result += a * torch.exp(1j * w * x) + torch.conj(a) * torch.exp(-1j * w * x) + return sum_result.real + + def __len__(self): + return len(self.x_values) + + def __getitem__(self, idx): + return self.x_values[idx], self.y_values[idx] + + +# %% +# Example usage: +omega1 = torch.tensor([0.0, 1.0, -1.0]) +a = torch.tensor(0.1) +dataset_y1_omega1 = Y1Dataset(omega1, a) + + +# Plot +plt.figure(figsize=(10, 6)) +plt.plot( + dataset_y1_omega1.x_values.flatten().numpy(), + dataset_y1_omega1.y_values.flatten().numpy(), + 'k--', + label='y1(x) with Ω1', +) +plt.xlabel('x') +plt.legend() +plt.grid(True) +plt.show() + +# %% +# Example usage: +omega2 = torch.tensor([0.0, 1.0, 0.5]) +a = torch.tensor(0.1j) +dataset_y1_omega2 = Y1Dataset(omega2, a) + + +# Plot +plt.figure(figsize=(10, 6)) +plt.plot( + dataset_y1_omega2.x_values.flatten().numpy(), + dataset_y1_omega2.y_values.flatten().numpy(), + 'k--', + label='y1(x) with Ω2', +) +plt.xlabel('x') +plt.legend() +plt.grid(True) +plt.show() + +# %% +# Example usage: +omega2 = torch.tensor([0.0, 1.0, 0.5]) +amps = torch.tensor([0.1j, 0.15, 0.12]) # 并非所有的系数都能拟合,取决于变分线路 +dataset_y2_omega2 = Y2Dataset(omega2, amps) + + +# Plot +plt.figure(figsize=(10, 6)) +plt.plot( + dataset_y2_omega2.x_values.flatten().numpy(), + dataset_y2_omega2.y_values.flatten().numpy(), + 'k--', + label='y2(x) with Ω2', +) +plt.xlabel('x') +plt.legend() +plt.grid(True) +plt.show() + + +# %% +def get_loader(dataset, split_ratio=0.8, batch_size=8): + # Splitting the dataset into training and validation datasets + train_size = int(split_ratio * len(dataset)) + val_size = len(dataset) - train_size + train_dataset, val_dataset = random_split(dataset, [train_size, val_size]) + + # You can now use DataLoader to load these datasets if needed + train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) + val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False) + return train_loader, val_loader + + +# %% +get_loader(dataset_y1_omega1) + + +# %% +class QNN(nn.Module): + def __init__(self, residual=False): + super().__init__() + self.residual = residual + if residual: + self.qnn = dq.QubitCircuit(2) + self.qnn.u3(wires=1) + self.qnn.ry(wires=0) + self.qnn.ry(wires=1, encode=True, controls=0) + self.qnn.ry(wires=0) + self.qnn.u3(wires=1) + self.qnn.observable(wires=1, basis='z') + self.qnn.observable(wires=[0, 1], basis='zz') + else: + self.qnn = dq.QubitCircuit(1) + self.qnn.u3(wires=0) + self.qnn.ry(wires=0, encode=True) + self.qnn.u3(wires=0) + self.qnn.observable(wires=0, basis='z') + + def forward(self, x): + self.qnn(data=x) + exp = self.qnn.expectation() + if self.residual: + exp = (exp[:, [0]] + exp[:, [1]]) / 2 + return exp + + +# %% +def train_model(epochs, model, dataset): + train_loader, val_loader = get_loader(dataset) + opt = torch.optim.Adam(model.parameters(), lr=0.01) + + for epoch in range(epochs): + # Training phase + model.train() # Set the model to training mode + for x, y in train_loader: + yhat = model(x) + loss = torch.nn.functional.mse_loss(yhat, y) + opt.zero_grad() + loss.backward() + opt.step() + if epoch == 0: + print(loss.item()) + + # Validation phase + model.eval() # Set the model to evaluation mode + val_loss = 0.0 + with torch.no_grad(): # Disable gradient calculation + for x, y in val_loader: + yhat = model(x) + loss = torch.nn.functional.mse_loss(yhat, y) + val_loss += loss.item() + + avg_val_loss = val_loss / len(val_loader) + print('Validation - Epoch', epoch, 'Average Valid Loss:', avg_val_loss) + + return model + + +# %% +def plot_predictions(dataset, label_dataset, trained_model, label_model): + if not isinstance(trained_model, list): + # Generate predictions using the model + trained_model.eval() + with torch.no_grad(): + predictions = trained_model(dataset.x_values).flatten().numpy() + + # Plot the original y(x) and the predictions + plt.figure(figsize=(10, 6)) + plt.plot( + dataset.x_values.flatten().numpy(), dataset.y_values.flatten().numpy(), 'k--', label=label_dataset, zorder=2 + ) + plt.plot(dataset.x_values.flatten().numpy(), predictions, 'gray', linewidth=6, label=label_model, zorder=1) + else: + # Plot the original y(x) and the predictions + plt.figure(figsize=(10, 6)) + plt.plot( + dataset.x_values.flatten().numpy(), dataset.y_values.flatten().numpy(), 'k--', label=label_dataset, zorder=2 + ) + for model, label in zip(trained_model, label_model, strict=True): + model.eval() + with torch.no_grad(): + predictions = model(dataset.x_values).flatten().numpy() + if 'residual' in label: + color = 'red' + elif 'traditional' in label: + color = 'gray' + plt.plot(dataset.x_values.flatten().numpy(), predictions, color, linewidth=6, label=label, zorder=1) + + plt.xlabel('x') + plt.legend() + plt.grid(True) + plt.show() + + +# %% [markdown] +# # 实验1(dataset_y1_omega1) + +# %% +qnn_traditional = QNN(residual=False) +qnn_traditional.qnn.draw() + +# %% +trained_qnn_traditional = train_model(epochs=30, model=qnn_traditional, dataset=dataset_y1_omega1) + +# %% +qnn_residual = QNN(residual=True) +qnn_residual.qnn.draw() + +# %% +trained_qnn_residual = train_model(epochs=30, model=qnn_residual, dataset=dataset_y1_omega1) + +# %% +plot_predictions( + dataset_y1_omega1, + label_dataset='y1(x) with Ω1', + trained_model=[trained_qnn_traditional, trained_qnn_residual], + label_model=['qnn_traditional', 'qnn_residual'], +) + +# %% [markdown] +# # 实验2(dataset_y1_omega2) + +# %% +qnn_traditional = QNN(residual=False) +qnn_traditional.qnn.draw() + +# %% +trained_qnn_traditional = train_model(epochs=30, model=qnn_traditional, dataset=dataset_y1_omega2) + +# %% +qnn_residual = QNN(residual=True) # 参数初始化敏感,需要多实例化几次 +qnn_residual.qnn.draw() + +# %% +trained_qnn_residual = train_model(epochs=30, model=qnn_residual, dataset=dataset_y1_omega2) + +# %% +plot_predictions( + dataset_y1_omega2, + label_dataset='y1(x) with Ω2', + trained_model=[trained_qnn_traditional, trained_qnn_residual], + label_model=['qnn_traditional', 'qnn_residual'], +) + +# %% [markdown] +# # 实验3(dataset_y2_omega2) + +# %% [markdown] +# + +# %% +qnn_traditional = QNN(residual=False) +qnn_traditional.qnn.draw() + +# %% +trained_qnn_traditional = train_model(epochs=30, model=qnn_traditional, dataset=dataset_y2_omega2) + +# %% +qnn_residual = QNN(residual=True) # 参数初始化敏感,需要多实例化几次 +qnn_residual.qnn.draw() + +# %% +trained_qnn_residual = train_model(epochs=30, model=qnn_residual, dataset=dataset_y2_omega2) + +# %% +plot_predictions( + dataset_y2_omega2, + label_dataset='y2(x) with Ω2', + trained_model=[trained_qnn_traditional, trained_qnn_residual], + label_model=['qnn_traditional', 'qnn_residual'], +) + +# %% + +# %% + +# %% + +# %% diff --git a/examples/quantum_state_transfer_1d.ipynb b/examples/quantum_state_transfer_1d.ipynb index a2718661..2205f26a 100644 --- a/examples/quantum_state_transfer_1d.ipynb +++ b/examples/quantum_state_transfer_1d.ipynb @@ -25,16 +25,17 @@ "outputs": [], "source": [ "import deepquantum as dq\n", - "import torch\n", - "import torch.nn as nn\n", + "import matplotlib.pyplot as plt\n", "import numpy as np\n", - "from numpy import linalg as la\n", - "import matplotlib.pyplot as plt" + "import torch\n", + "from numpy import linalg as la" ] }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "lines_to_next_cell": 2 + }, "source": [ "首先考虑简单的两个比特情况,写出给定哈密顿量的矩阵形式,将时间演化算符进行特征值分解,得到由特征向量组成的矩阵u和对角矩阵u_diag,再通过自定义这两个量子门进行线路演化,最后对每个比特测量相应的概率。" ] @@ -50,7 +51,7 @@ " input: expectation values of sigmaz, type: torch.Tensor\n", " output: prob for |0> and |1> for each qubit, size: (num_qubits, batch, 2)\n", " \"\"\"\n", - " probs_all = torch.stack(((sigmaz_exp+1)/2, (1-sigmaz_exp)/2)).transpose(0,-1)\n", + " probs_all = torch.stack(((sigmaz_exp + 1) / 2, (1 - sigmaz_exp) / 2)).transpose(0, -1)\n", "\n", " return probs_all" ] @@ -62,8 +63,8 @@ "outputs": [], "source": [ "sigmax_d2 = np.matrix([[0, 1], [1, 0]])\n", - "sigmax_d4 = np.kron(sigmax_d2, sigmax_d2) # H\n", - "u = la.eig(sigmax_d4)[1] # 特征值分解\n", + "sigmax_d4 = np.kron(sigmax_d2, sigmax_d2) # H\n", + "u = la.eig(sigmax_d4)[1] # 特征值分解\n", "udag = u.transpose()\n", "eig_val = la.eig(sigmax_d4)[0]" ] @@ -89,22 +90,21 @@ "# batch = 1\n", "nqubit = 2\n", "\n", - "tlist = np.linspace(0, 2*np.pi, 100)\n", + "tlist = np.linspace(0, 2 * np.pi, 100)\n", "probs_q0 = []\n", "probs_qend = []\n", - "for i in range(100) :\n", + "for i in range(100):\n", " t = tlist[i]\n", "\n", - " u_exp = np.diag(np.exp(eig_val*(-1j)*t)) # 构造对角矩阵Lambda\n", + " u_exp = np.diag(np.exp(eig_val * (-1j) * t)) # 构造对角矩阵Lambda\n", "\n", - " cir = dq.QubitCircuit(nqubit, mps=True, chi=2) # mps=True 使用张量网络 \n", - " cir.x(0) # initial state 10\n", - " cir.any(unitary=udag, wires=[0,1], name=\"udag_x\") # u*exp(-i*Lambda*t)*u^dag\n", - " cir.any(unitary=u_exp, wires=[0,1], name=\"diag_t\")\n", - " cir.any(unitary=u, wires=[0,1], name=\"u_x\")\n", + " cir = dq.QubitCircuit(nqubit, mps=True, chi=2) # mps=True 使用张量网络\n", + " cir.x(0) # initial state 10\n", + " cir.any(unitary=udag, wires=[0, 1], name='udag_x') # u*exp(-i*Lambda*t)*u^dag\n", + " cir.any(unitary=u_exp, wires=[0, 1], name='diag_t')\n", + " cir.any(unitary=u, wires=[0, 1], name='u_x')\n", "\n", - "\n", - " for i in range(nqubit): # sigmaz 平均值\n", + " for i in range(nqubit): # sigmaz 平均值\n", " cir.observable(i)\n", " cir()\n", "\n", @@ -135,17 +135,15 @@ "probs_qend = np.matrix(probs_qend)\n", "fig = plt.figure(figsize=(8, 5))\n", "ax = fig.add_subplot(111)\n", - "plt.xlim(0, 2*np.pi)\n", + "plt.xlim(0, 2 * np.pi)\n", "plt.ylim(-0.01, 1.01)\n", - "plt.plot(tlist, probs_q0[:,1], label=\"$q_0=|1>$\", color=\"dodgerblue\")\n", - "plt.plot(tlist, probs_qend[:,1], label=\"$q_1=|1>$\", color=\"red\")\n", - "plt.vlines(np.pi/2, -0.1, 1.1, ls=\"--\")\n", - "plt.text(1.6, 0.5, \"$t=\\pi/2$\")\n", - "plt.xlabel(\"Time\")\n", - "plt.ylabel(\"Probability\")\n", - "plt.xticks([0, np.pi/2, np.pi, np.pi/2*3, 2*np.pi], \n", - " [0, \"$\\pi/2$\", \"$\\pi$\", \"$3\\pi/2$\", \"$2\\pi$\"]\n", - " )\n", + "plt.plot(tlist, probs_q0[:, 1], label='$q_0=|1>$', color='dodgerblue')\n", + "plt.plot(tlist, probs_qend[:, 1], label='$q_1=|1>$', color='red')\n", + "plt.vlines(np.pi / 2, -0.1, 1.1, ls='--')\n", + "plt.text(1.6, 0.5, r'$t=\\pi/2$')\n", + "plt.xlabel('Time')\n", + "plt.ylabel('Probability')\n", + "plt.xticks([0, np.pi / 2, np.pi, np.pi / 2 * 3, 2 * np.pi], [0, r'$\\pi/2$', r'$\\pi$', r'$3\\pi/2$', r'$2\\pi$'])\n", "plt.legend()\n", "plt.show()" ] @@ -178,23 +176,23 @@ "# batch = 1\n", "nqubit = 20\n", "\n", - "tlist = np.linspace(0, 2*np.pi, 100) # time slicing \n", + "tlist = np.linspace(0, 2 * np.pi, 100) # time slicing\n", "allprobs_q0 = []\n", "allprobs_q1 = []\n", "allprobs_qend = []\n", "allprobs_qend2 = []\n", - "for i in range(100) :\n", + "for i in range(100):\n", " t = tlist[i]\n", - " cir = dq.QubitCircuit(nqubit, mps=True, chi=2) # mps=True using Tensornetwork\n", - " cir.x(0) # initial state 10\n", - " \n", - " for q in range(nqubit-1):\n", - " u_exp = np.diag(np.exp(eig_val*(-1j)*t)) # 构造对角矩阵Lambda \n", - " cir.any(unitary=udag, wires=[q,q+1], name=\"udag_x\") # u*exp(-i*Lambda*t)*u^dag\n", - " cir.any(unitary=u_exp, wires=[q,q+1], name=\"diag_t\")\n", - " cir.any(unitary=u, wires=[q,q+1], name=\"u_x\")\n", + " cir = dq.QubitCircuit(nqubit, mps=True, chi=2) # mps=True using Tensornetwork\n", + " cir.x(0) # initial state 10\n", + "\n", + " for q in range(nqubit - 1):\n", + " u_exp = np.diag(np.exp(eig_val * (-1j) * t)) # 构造对角矩阵Lambda\n", + " cir.any(unitary=udag, wires=[q, q + 1], name='udag_x') # u*exp(-i*Lambda*t)*u^dag\n", + " cir.any(unitary=u_exp, wires=[q, q + 1], name='diag_t')\n", + " cir.any(unitary=u, wires=[q, q + 1], name='u_x')\n", "\n", - " for i in range(nqubit): # sigmaz 平均值\n", + " for i in range(nqubit): # sigmaz 平均值\n", " cir.observable(i)\n", " cir()\n", "\n", @@ -230,19 +228,17 @@ "\n", "fig = plt.figure(figsize=(8, 5))\n", "ax = fig.add_subplot(111)\n", - "plt.xlim(0, 2*np.pi)\n", + "plt.xlim(0, 2 * np.pi)\n", "plt.ylim(-0.01, 1.01)\n", - "plt.plot(tlist, allprobs_q0[:,1], label=\"$q_0=|1>$\", color=\"dodgerblue\")\n", - "plt.plot(tlist, allprobs_q1[:,1], label=\"$q_1=|1>$\", color=\"green\")\n", - "plt.plot(tlist, allprobs_qend[:,1], label=\"$q_{end}=|1>$\", color=\"red\")\n", - "plt.plot(tlist, allprobs_qend2[:,1], label=\"$q_{end-1}=|1>$\")\n", - "plt.vlines(np.pi/2, -0.1, 1.1, ls=\"--\")\n", - "plt.text(1.6, 0.5, \"$t=\\pi/2$\")\n", - "plt.xlabel(\"Time\")\n", - "plt.ylabel(\"Probability\")\n", - "plt.xticks([0, np.pi/2, np.pi, np.pi/2*3, 2*np.pi], \n", - " [0, \"$\\pi/2$\", \"$\\pi$\", \"$3\\pi/2$\", \"$2\\pi$\"]\n", - " )\n", + "plt.plot(tlist, allprobs_q0[:, 1], label='$q_0=|1>$', color='dodgerblue')\n", + "plt.plot(tlist, allprobs_q1[:, 1], label='$q_1=|1>$', color='green')\n", + "plt.plot(tlist, allprobs_qend[:, 1], label='$q_{end}=|1>$', color='red')\n", + "plt.plot(tlist, allprobs_qend2[:, 1], label='$q_{end-1}=|1>$')\n", + "plt.vlines(np.pi / 2, -0.1, 1.1, ls='--')\n", + "plt.text(1.6, 0.5, r'$t=\\pi/2$')\n", + "plt.xlabel('Time')\n", + "plt.ylabel('Probability')\n", + "plt.xticks([0, np.pi / 2, np.pi, np.pi / 2 * 3, 2 * np.pi], [0, r'$\\pi/2$', r'$\\pi$', r'$3\\pi/2$', r'$2\\pi$'])\n", "plt.legend()\n", "plt.show()" ] @@ -265,8 +261,7 @@ "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.9.17" - }, - "orig_nbformat": 4 + } }, "nbformat": 4, "nbformat_minor": 2 diff --git a/examples/quantum_state_transfer_1d.py b/examples/quantum_state_transfer_1d.py new file mode 100644 index 00000000..014ea319 --- /dev/null +++ b/examples/quantum_state_transfer_1d.py @@ -0,0 +1,152 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.19.1 +# kernelspec: +# display_name: test +# language: python +# name: python3 +# --- + +# %% [markdown] +# # 一维量子态传输 + +# %% [markdown] +# DeepQuantum 模拟一维简单量子态传输,通过给定的哈密顿量得到时间演化算符,对其进行特征值分解,得到对应的量子门,从而构造相应的量子线路进行求解。 +# DeepQuantum 支持大规模的多比特模拟,通过设置mps=True可以使用张量网络求解。 \ +# 考虑简单的N个比特一维链体系,给定的哈密顿量如下:$$H =\sum_n\sigma_n^x\sigma_{n+1}^x$$ \ +# $\sigma_n^x$ 代表作用在第n个比特的pauli x 算符,n取值为1,...,N-1。 \ +# 因为哈密顿量中同时加入$\sigma^x$和$\sigma^y$时会出现不对易的情况,为了方便分解多比特的时间演化算符为多个两比特时间演化算符,此处只考虑$\sigma^x_n \cdot \sigma^x_{n+1}$部分,因此是文献中的模型的简化(https://arxiv.org/pdf/quant-ph/0309131)。 + +# %% +import deepquantum as dq +import matplotlib.pyplot as plt +import numpy as np +import torch +from numpy import linalg as la + +# %% [markdown] +# 首先考虑简单的两个比特情况,写出给定哈密顿量的矩阵形式,将时间演化算符进行特征值分解,得到由特征向量组成的矩阵u和对角矩阵u_diag,再通过自定义这两个量子门进行线路演化,最后对每个比特测量相应的概率。 + + +# %% +def get_probs_(sigmaz_exp): + """Obtain probs of |0> and |1> for each qubit + input: expectation values of sigmaz, type: torch.Tensor + output: prob for |0> and |1> for each qubit, size: (num_qubits, batch, 2) + """ + probs_all = torch.stack(((sigmaz_exp + 1) / 2, (1 - sigmaz_exp) / 2)).transpose(0, -1) + + return probs_all + + +# %% +sigmax_d2 = np.matrix([[0, 1], [1, 0]]) +sigmax_d4 = np.kron(sigmax_d2, sigmax_d2) # H +u = la.eig(sigmax_d4)[1] # 特征值分解 +udag = u.transpose() +eig_val = la.eig(sigmax_d4)[0] + +# %% +# batch = 1 +nqubit = 2 + +tlist = np.linspace(0, 2 * np.pi, 100) +probs_q0 = [] +probs_qend = [] +for i in range(100): + t = tlist[i] + + u_exp = np.diag(np.exp(eig_val * (-1j) * t)) # 构造对角矩阵Lambda + + cir = dq.QubitCircuit(nqubit, mps=True, chi=2) # mps=True 使用张量网络 + cir.x(0) # initial state 10 + cir.any(unitary=udag, wires=[0, 1], name='udag_x') # u*exp(-i*Lambda*t)*u^dag + cir.any(unitary=u_exp, wires=[0, 1], name='diag_t') + cir.any(unitary=u, wires=[0, 1], name='u_x') + + for i in range(nqubit): # sigmaz 平均值 + cir.observable(i) + cir() + + probs_q0.append((get_probs_(cir.expectation()).tolist())[0]) + probs_qend.append((get_probs_(cir.expectation()).tolist())[1]) + +cir.draw() + +# %% +probs_q0 = np.matrix(probs_q0) +probs_qend = np.matrix(probs_qend) +fig = plt.figure(figsize=(8, 5)) +ax = fig.add_subplot(111) +plt.xlim(0, 2 * np.pi) +plt.ylim(-0.01, 1.01) +plt.plot(tlist, probs_q0[:, 1], label='$q_0=|1>$', color='dodgerblue') +plt.plot(tlist, probs_qend[:, 1], label='$q_1=|1>$', color='red') +plt.vlines(np.pi / 2, -0.1, 1.1, ls='--') +plt.text(1.6, 0.5, r'$t=\pi/2$') +plt.xlabel('Time') +plt.ylabel('Probability') +plt.xticks([0, np.pi / 2, np.pi, np.pi / 2 * 3, 2 * np.pi], [0, r'$\pi/2$', r'$\pi$', r'$3\pi/2$', r'$2\pi$']) +plt.legend() +plt.show() + +# %% [markdown] +# 对于多比特情况,哈密顿量的矩阵维度随比特数增多呈指数增长,但是容易证明其对应的时间演化算符特征值分解之后的结果可以写成张量积的形式,这说明时间演化算符可以分解成多个量子门的组合。下面给出任意多个比特的模拟,nqubit的值可以任意选择。 + +# %% +# batch = 1 +nqubit = 20 + +tlist = np.linspace(0, 2 * np.pi, 100) # time slicing +allprobs_q0 = [] +allprobs_q1 = [] +allprobs_qend = [] +allprobs_qend2 = [] +for i in range(100): + t = tlist[i] + cir = dq.QubitCircuit(nqubit, mps=True, chi=2) # mps=True using Tensornetwork + cir.x(0) # initial state 10 + + for q in range(nqubit - 1): + u_exp = np.diag(np.exp(eig_val * (-1j) * t)) # 构造对角矩阵Lambda + cir.any(unitary=udag, wires=[q, q + 1], name='udag_x') # u*exp(-i*Lambda*t)*u^dag + cir.any(unitary=u_exp, wires=[q, q + 1], name='diag_t') + cir.any(unitary=u, wires=[q, q + 1], name='u_x') + + for i in range(nqubit): # sigmaz 平均值 + cir.observable(i) + cir() + + allprobs_q0.append((get_probs_(cir.expectation()).tolist())[0]) + allprobs_q1.append((get_probs_(cir.expectation()).tolist())[1]) + allprobs_qend.append((get_probs_(cir.expectation()).tolist())[-1]) + allprobs_qend2.append((get_probs_(cir.expectation()).tolist())[-2]) + +cir.draw() + +# %% +allprobs_q0 = np.matrix(allprobs_q0) +allprobs_q1 = np.matrix(allprobs_q1) +allprobs_qend = np.matrix(allprobs_qend) +allprobs_qend2 = np.matrix(allprobs_qend2) + +fig = plt.figure(figsize=(8, 5)) +ax = fig.add_subplot(111) +plt.xlim(0, 2 * np.pi) +plt.ylim(-0.01, 1.01) +plt.plot(tlist, allprobs_q0[:, 1], label='$q_0=|1>$', color='dodgerblue') +plt.plot(tlist, allprobs_q1[:, 1], label='$q_1=|1>$', color='green') +plt.plot(tlist, allprobs_qend[:, 1], label='$q_{end}=|1>$', color='red') +plt.plot(tlist, allprobs_qend2[:, 1], label='$q_{end-1}=|1>$') +plt.vlines(np.pi / 2, -0.1, 1.1, ls='--') +plt.text(1.6, 0.5, r'$t=\pi/2$') +plt.xlabel('Time') +plt.ylabel('Probability') +plt.xticks([0, np.pi / 2, np.pi, np.pi / 2 * 3, 2 * np.pi], [0, r'$\pi/2$', r'$\pi$', r'$3\pi/2$', r'$2\pi$']) +plt.legend() +plt.show() diff --git a/examples/ruff.toml b/examples/ruff.toml index 95963ec0..54daf952 100644 --- a/examples/ruff.toml +++ b/examples/ruff.toml @@ -1,3 +1,8 @@ +extend = '../pyproject.toml' + +[lint] +extend-ignore = ['E501'] + [lint.isort] known-first-party = [] known-third-party = ['deepquantum'] diff --git a/examples/test_for_onchip_optimizer.ipynb b/examples/test_for_onchip_optimizer.ipynb index e53a938a..8c5dc1d2 100644 --- a/examples/test_for_onchip_optimizer.ipynb +++ b/examples/test_for_onchip_optimizer.ipynb @@ -15,22 +15,20 @@ "metadata": {}, "outputs": [], "source": [ - "import deepquantum as dq\n", "import deepquantum.photonic.circuit as circuit\n", - "import matplotlib.pyplot as plt\n", "import numpy as np\n", - "\n", - "from deepquantum.optimizer import *\n", - "from deepquantum.photonic.decompose import *\n", + "from deepquantum.optimizer import OptimizerBayesian, OptimizerFourier, OptimizerSPSA\n", + "from deepquantum.photonic.decompose import UnitaryDecomposer\n", "from scipy.stats import unitary_group\n", "\n", - "np.set_printoptions(precision=8, floatmode='fixed', suppress=True) # to make the print info aligned\n", + "np.set_printoptions(precision=8, floatmode='fixed', suppress=True) # to make the print info aligned\n", "\n", "N = 8\n", "u8x8 = unitary_group.rvs(N)\n", - "decomp_rlt = UnitaryDecomposer(u8x8, \"cssr\").decomp()\n", + "decomp_rlt = UnitaryDecomposer(u8x8, 'cssr').decomp()\n", "mzi_info = decomp_rlt[0]\n", "\n", + "\n", "def zero_init(mzi_info):\n", " for i in range(len(mzi_info['MZI_list'])):\n", " mzi_info['MZI_list'][i][2] = 0.0\n", @@ -38,6 +36,7 @@ " mzi_info['phase_angle_ori'] = np.zeros(N)\n", " mzi_info['phase_angle'] = np.zeros(N)\n", "\n", + "\n", "def bar_securing(mzi_info):\n", " adjustable_ids = []\n", " for i in range(len(mzi_info['MZI_list'])):\n", @@ -45,34 +44,37 @@ " mzi_info['MZI_list'][i][-1] = np.pi\n", " mzi_info['MZI_list'][i][-2] = np.pi\n", "\n", + "\n", "def xx_planting(mzi_info):\n", " for i in [20, 24]:\n", - " mzi_info['MZI_list'][i][-1] = np.pi/2\n", + " mzi_info['MZI_list'][i][-1] = np.pi / 2\n", + "\n", "\n", "def yy_planting(mzi_info):\n", " for i in [20, 24]:\n", - " mzi_info['MZI_list'][i][-1] = np.pi/2\n", + " mzi_info['MZI_list'][i][-1] = np.pi / 2\n", " mzi_info['MZI_list'][i][-2] = np.pi\n", "\n", + "\n", "def zz_planting(mzi_info):\n", " for i in [20, 24]:\n", " mzi_info['MZI_list'][i][-1] = np.pi\n", " mzi_info['MZI_list'][i][-2] = np.pi\n", "\n", + "\n", "def zx_planting(mzi_info):\n", " mzi_info['MZI_list'][20][-1] = np.pi\n", " mzi_info['MZI_list'][20][-2] = np.pi\n", - " mzi_info['MZI_list'][24][-1] = np.pi/2\n", + " mzi_info['MZI_list'][24][-1] = np.pi / 2\n", "\n", "\n", - "def trial_planting(mzi_info,angle_list):\n", - " change_list = [[1,-1],\n", - " [4,-1],\n", - " [5,-1]]\n", + "def trial_planting(mzi_info, angle_list):\n", + " change_list = [[1, -1], [4, -1], [5, -1]]\n", " for i in range(len(angle_list)):\n", - " a,b = change_list[i]\n", + " a, b = change_list[i]\n", " mzi_info['MZI_list'][a][b] = angle_list[i]\n", "\n", + "\n", "zero_init(mzi_info)\n", "bar_securing(mzi_info)\n", "zz_planting(mzi_info)\n", @@ -83,33 +85,33 @@ " label_list = ['|00101000>', '|00100100>', '|00011000>', '|00010100>']\n", " value = np.zeros(4)\n", " for i in range(4):\n", - " try:\n", - " value[i] = rlt_strkey[label_list[i]]\n", - " except:\n", - " pass\n", + " value[i] = rlt_strkey.get(label_list[i], 0.0)\n", " return value\n", "\n", "\n", - "def estimate_energy(post_selection_rlt,type='ZZ'):\n", - " norm = post_selection_rlt/(post_selection_rlt.sum()+1e-16)\n", - " if type=='ZZ': # ZZ basis\n", - " return norm[0]+norm[3]-norm[1]-norm[2]\n", - " if type=='ZI': # ZI basis\n", - " return norm[0]+norm[1]-norm[2]-norm[3]\n", - " if type=='IZ': # IZ basis\n", - " return -norm[0]-norm[1]+norm[2]+norm[3]\n", + "def estimate_energy(post_selection_rlt, basis='ZZ'):\n", + " norm = post_selection_rlt / (post_selection_rlt.sum() + 1e-16)\n", + " if basis == 'ZZ': # ZZ basis\n", + " return norm[0] + norm[3] - norm[1] - norm[2]\n", + " if basis == 'ZI': # ZI basis\n", + " return norm[0] + norm[1] - norm[2] - norm[3]\n", + " if basis == 'IZ': # IZ basis\n", + " return -norm[0] - norm[1] + norm[2] + norm[3]\n", + "\n", "\n", "def compute_process(mzi_info):\n", - " cir = circuit.QumodeCircuit(nmode=8,init_state=[0, 0, 0, 1, 0, 1, 0, 0], name=\"test\", cutoff = 3, basis = True) # basis=True, using state list\n", + " cir = circuit.QumodeCircuit(\n", + " nmode=8, init_state=[0, 0, 0, 1, 0, 1, 0, 0], name='test', cutoff=3, basis=True\n", + " ) # basis=True, using state list\n", " for info in mzi_info['MZI_list']:\n", - " cir.ps(inputs=info[2],wires=[info[0]])\n", - " cir.bs_theta(inputs=np.pi/4,wires=[info[0],info[1]])\n", - " cir.ps(inputs=info[3],wires=[info[0]])\n", - " cir.bs_theta(inputs=np.pi/4,wires=[info[0],info[1]])\n", + " cir.ps(inputs=info[2], wires=[info[0]])\n", + " cir.bs_theta(inputs=np.pi / 4, wires=[info[0], info[1]])\n", + " cir.ps(inputs=info[3], wires=[info[0]])\n", + " cir.bs_theta(inputs=np.pi / 4, wires=[info[0], info[1]])\n", " rlt = cir(is_prob=True)\n", " rlt_strkey = dict()\n", - " for key in rlt.keys():\n", - " rlt_strkey[str(key)] = np.abs(rlt[key].detach().numpy().squeeze())**2\n", + " for key in rlt:\n", + " rlt_strkey[str(key)] = np.abs(rlt[key].detach().numpy().squeeze()) ** 2\n", "\n", " # rltm = cir.measure(shots=10000)[0]\n", " # rltm_strkey = dict()\n", @@ -119,11 +121,12 @@ "\n", " return post_selection(rlt_strkey)\n", "\n", + "\n", "def estimate_eigval(angle_list):\n", " # to minimize\n", " zero_init(mzi_info)\n", " bar_securing(mzi_info)\n", - " trial_planting(mzi_info,angle_list)\n", + " trial_planting(mzi_info, angle_list)\n", "\n", " zz_planting(mzi_info)\n", " post_selection_rlt = compute_process(mzi_info)\n", @@ -141,15 +144,15 @@ "\n", " # zz_planting(mzi_info)\n", " # post_selection_rlt = compute_process(mzi_info)\n", - " # e_zi = estimate_energy(post_selection_rlt,type='ZI')\n", + " # e_zi = estimate_energy(post_selection_rlt,basis='ZI')\n", "\n", " # xx_planting(mzi_info)\n", " # post_selection_rlt = compute_process(mzi_info)\n", - " # e_ix = estimate_energy(post_selection_rlt,type='IZ')\n", + " # e_ix = estimate_energy(post_selection_rlt,basis='IZ')\n", "\n", " # zx_planting(mzi_info)\n", " # post_selection_rlt = compute_process(mzi_info)\n", - " # e_zx = estimate_energy(post_selection_rlt,type='ZZ')\n", + " # e_zx = estimate_energy(post_selection_rlt,basis='ZZ')\n", "\n", " # return (1 + e_zi + e_ix - e_zx) / 2\n", "\n", @@ -158,6 +161,7 @@ " # to maximize\n", " return -estimate_eigval(angle_list)\n", "\n", + "\n", "def void_target():\n", " return -1" ] @@ -171,7 +175,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -287,30 +291,27 @@ ], "source": [ "# 测试BO算法\n", - "print(\"=\"*50)\n", - "print(\"运行BO\")\n", - "param_init = {'t1':0, 't2':0, 't3':0}\n", - "bo_optimizer = OptimizerBayesian(target_func=void_target,param_init=param_init,random_state=0)\n", + "print('=' * 50)\n", + "print('运行BO')\n", + "param_init = {'t1': 0, 't2': 0, 't3': 0}\n", + "bo_optimizer = OptimizerBayesian(target_func=void_target, param_init=param_init, random_state=0)\n", "\n", - "print(\"轮次\",\"\\t\",\"参数值\",\"\\t\",\" \"*31,\"目标值\")\n", + "print('轮次', '\\t', '参数值', '\\t', ' ' * 31, '目标值')\n", "for _ in range(100):\n", " # 待片上计算的参数\n", " p1 = bo_optimizer.param_suggest()\n", " # 模拟芯片计算过程,之后需要替换成接口\n", - " f1 = -estimate_eigval(p1.tolist()) # BO 内置用法是最大化目标;但是接下来打印时再添一个符号即可\n", + " f1 = -estimate_eigval(p1.tolist()) # BO 内置用法是最大化目标;但是接下来打印时再添一个符号即可\n", " # 测试结果传回,更新param_dict\n", - " bo_optimizer.param_register([p1],[f1])\n", + " bo_optimizer.param_register([p1], [f1])\n", " f1_to_print = -f1\n", - " if bo_optimizer.best_target == f1:\n", - " mark = \"*\"\n", - " else:\n", - " mark = \" \"\n", - " if f1_to_print<0:\n", - " print(bo_optimizer.iter,\"\\t\",p1,\"\\t\",f\"{f1_to_print:8.10f} \",mark)\n", + " mark = '*' if bo_optimizer.best_target == f1 else ' '\n", + " if f1_to_print < 0:\n", + " print(bo_optimizer.iter, '\\t', p1, '\\t', f'{f1_to_print:8.10f} ', mark)\n", " else:\n", - " print(bo_optimizer.iter,\"\\t\",p1,\"\\t \",f\"{f1_to_print:8.10f} \",mark)\n", + " print(bo_optimizer.iter, '\\t', p1, '\\t ', f'{f1_to_print:8.10f} ', mark)\n", "\n", - "print(\"BO结果:\",estimate_eigval(list(bo_optimizer.best_param_dict.values())))" + "print('BO结果:', estimate_eigval(list(bo_optimizer.best_param_dict.values())))" ] }, { @@ -731,40 +732,39 @@ ], "source": [ "# 测试SPSA算法\n", - "print(\"=\"*50)\n", - "print(\"运行SPSA\")\n", - "param_init = {'t1':np.random.randn(), 't2':np.random.randn(), 't3':np.random.randn()}\n", - "spsa_optimizer = OptimizerSPSA(target_func=void_target,param_init=param_init,random_state=0)\n", + "print('=' * 50)\n", + "print('运行SPSA')\n", + "param_init = {'t1': np.random.randn(), 't2': np.random.randn(), 't3': np.random.randn()}\n", + "spsa_optimizer = OptimizerSPSA(target_func=void_target, param_init=param_init, random_state=0)\n", "\n", - "print(\"轮次\",\"\\t\",\"参数值\",\"\\t\",\" \"*31,\"目标值\")\n", + "print('轮次', '\\t', '参数值', '\\t', ' ' * 31, '目标值')\n", "for _ in range(200):\n", " # 待片上计算的两组参数\n", - " p1,p2 = spsa_optimizer.param_suggest()\n", + " p1, p2 = spsa_optimizer.param_suggest()\n", " # 以下两行模拟芯片计算过程,之后需要替换成接口\n", " f1 = estimate_eigval(p1.tolist())\n", " f2 = estimate_eigval(p2.tolist())\n", " # 测试结果传回,更新param_dict\n", - " spsa_optimizer.param_register([p1,p2],[f1,f2])\n", + " spsa_optimizer.param_register([p1, p2], [f1, f2])\n", " # spsa_optimizer.param_register(np.array([p1,p2]),np.array([f1,f2]))\n", "\n", - "\n", - " if f1<0:\n", - " print(spsa_optimizer.iter,\"\\t\",p1,\"\\t\",f\"{f1:8.10f} \")\n", + " if f1 < 0:\n", + " print(spsa_optimizer.iter, '\\t', p1, '\\t', f'{f1:8.10f} ')\n", " else:\n", - " print(spsa_optimizer.iter,\"\\t\",p1,\"\\t \",f\"{f1:8.10f} \")\n", - " if f2<0:\n", - " print(spsa_optimizer.iter,\"\\t\",p2,\"\\t\",f\"{f2:8.10f} \")\n", + " print(spsa_optimizer.iter, '\\t', p1, '\\t ', f'{f1:8.10f} ')\n", + " if f2 < 0:\n", + " print(spsa_optimizer.iter, '\\t', p2, '\\t', f'{f2:8.10f} ')\n", " else:\n", - " print(spsa_optimizer.iter,\"\\t\",p2,\"\\t \",f\"{f2:8.10f} \")\n", + " print(spsa_optimizer.iter, '\\t', p2, '\\t ', f'{f2:8.10f} ')\n", " # print(spsa_optimizer.hyperparam['c']/(1+spsa_optimizer.iter)**spsa_optimizer.hyperparam['gamma'])\n", "\n", - " if (f1<-0.92) and (f2<-0.92):\n", + " if (f1 < -0.92) and (f2 < -0.92):\n", " # print(\"!!!进入步长减小阶段!!!\")\n", " spsa_optimizer.hyperparam['c'] = 0.001\n", - " elif (f1<-0.999) and (f2<-0.999):\n", + " elif (f1 < -0.999) and (f2 < -0.999):\n", " spsa_optimizer.hyperparam['c'] = 1e-4\n", "\n", - "print(\"SPSA结果:\",estimate_eigval(list(spsa_optimizer.best_param_dict.values())))" + "print('SPSA结果:', estimate_eigval(list(spsa_optimizer.best_param_dict.values())))" ] }, { @@ -835,12 +835,12 @@ ], "source": [ "# 测试Fourier级数方法\n", - "print(\"=\"*50)\n", - "print(\"运行Fourier级数法\")\n", - "param_init = {'t1':np.random.randn(), 't2':np.random.randn(), 't3':np.random.randn()}\n", + "print('=' * 50)\n", + "print('运行Fourier级数法')\n", + "param_init = {'t1': np.random.randn(), 't2': np.random.randn(), 't3': np.random.randn()}\n", "# param_init = dict(zip(param_init.keys(),np.random.randn(3)))\n", - "fourier_optimizer = OptimizerFourier(target_func=void_target,param_init=param_init,random_state=0,order=3,lr=0.1)\n", - "print(\"轮次\",\"\\t\",\"参数值\",\"\\t\",\" \"*31,\"目标值\")\n", + "fourier_optimizer = OptimizerFourier(target_func=void_target, param_init=param_init, random_state=0, order=3, lr=0.1)\n", + "print('轮次', '\\t', '参数值', '\\t', ' ' * 31, '目标值')\n", "for _ in range(50):\n", " # 待片上计算的两组参数\n", " param_array = fourier_optimizer.param_suggest()\n", @@ -850,15 +850,15 @@ " target[i] = estimate_eigval(param_array[i])\n", "\n", " # 测试结果传回,更新param_dict\n", - " fourier_optimizer.param_register(param_array,target)\n", + " fourier_optimizer.param_register(param_array, target)\n", " p1 = np.array(list(fourier_optimizer.best_param_dict.values()))\n", " f1 = fourier_optimizer.best_target\n", - " if f1<0:\n", - " print(fourier_optimizer.iter,\"\\t\",p1,\"\\t\",f\"{f1:8.10f} \")\n", + " if f1 < 0:\n", + " print(fourier_optimizer.iter, '\\t', p1, '\\t', f'{f1:8.10f} ')\n", " else:\n", - " print(fourier_optimizer.iter,\"\\t\",p1,\"\\t \",f\"{f1:8.10f} \")\n", + " print(fourier_optimizer.iter, '\\t', p1, '\\t ', f'{f1:8.10f} ')\n", "\n", - "print(\"Fourier结果:\",estimate_eigval(list(fourier_optimizer.best_param_dict.values())))" + "print('Fourier结果:', estimate_eigval(list(fourier_optimizer.best_param_dict.values())))" ] } ], diff --git a/examples/test_for_onchip_optimizer.py b/examples/test_for_onchip_optimizer.py new file mode 100644 index 00000000..f80b7814 --- /dev/null +++ b/examples/test_for_onchip_optimizer.py @@ -0,0 +1,257 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.19.1 +# kernelspec: +# display_name: dq_cuda_251 +# language: python +# name: python3 +# --- + +# %% +# # !pip install bayesian-optimization + +# %% +import deepquantum.photonic.circuit as circuit +import numpy as np +from deepquantum.optimizer import OptimizerBayesian, OptimizerFourier, OptimizerSPSA +from deepquantum.photonic.decompose import UnitaryDecomposer +from scipy.stats import unitary_group + +np.set_printoptions(precision=8, floatmode='fixed', suppress=True) # to make the print info aligned + +N = 8 +u8x8 = unitary_group.rvs(N) +decomp_rlt = UnitaryDecomposer(u8x8, 'cssr').decomp() +mzi_info = decomp_rlt[0] + + +def zero_init(mzi_info): + for i in range(len(mzi_info['MZI_list'])): + mzi_info['MZI_list'][i][2] = 0.0 + mzi_info['MZI_list'][i][3] = 0.0 + mzi_info['phase_angle_ori'] = np.zeros(N) + mzi_info['phase_angle'] = np.zeros(N) + + +def bar_securing(mzi_info): + adjustable_ids = [] + for i in range(len(mzi_info['MZI_list'])): + if i not in adjustable_ids: + mzi_info['MZI_list'][i][-1] = np.pi + mzi_info['MZI_list'][i][-2] = np.pi + + +def xx_planting(mzi_info): + for i in [20, 24]: + mzi_info['MZI_list'][i][-1] = np.pi / 2 + + +def yy_planting(mzi_info): + for i in [20, 24]: + mzi_info['MZI_list'][i][-1] = np.pi / 2 + mzi_info['MZI_list'][i][-2] = np.pi + + +def zz_planting(mzi_info): + for i in [20, 24]: + mzi_info['MZI_list'][i][-1] = np.pi + mzi_info['MZI_list'][i][-2] = np.pi + + +def zx_planting(mzi_info): + mzi_info['MZI_list'][20][-1] = np.pi + mzi_info['MZI_list'][20][-2] = np.pi + mzi_info['MZI_list'][24][-1] = np.pi / 2 + + +def trial_planting(mzi_info, angle_list): + change_list = [[1, -1], [4, -1], [5, -1]] + for i in range(len(angle_list)): + a, b = change_list[i] + mzi_info['MZI_list'][a][b] = angle_list[i] + + +zero_init(mzi_info) +bar_securing(mzi_info) +zz_planting(mzi_info) + + +def post_selection(rlt_strkey): + # print(rlt_strkey) + label_list = ['|00101000>', '|00100100>', '|00011000>', '|00010100>'] + value = np.zeros(4) + for i in range(4): + value[i] = rlt_strkey.get(label_list[i], 0.0) + return value + + +def estimate_energy(post_selection_rlt, basis='ZZ'): + norm = post_selection_rlt / (post_selection_rlt.sum() + 1e-16) + if basis == 'ZZ': # ZZ basis + return norm[0] + norm[3] - norm[1] - norm[2] + if basis == 'ZI': # ZI basis + return norm[0] + norm[1] - norm[2] - norm[3] + if basis == 'IZ': # IZ basis + return -norm[0] - norm[1] + norm[2] + norm[3] + + +def compute_process(mzi_info): + cir = circuit.QumodeCircuit( + nmode=8, init_state=[0, 0, 0, 1, 0, 1, 0, 0], name='test', cutoff=3, basis=True + ) # basis=True, using state list + for info in mzi_info['MZI_list']: + cir.ps(inputs=info[2], wires=[info[0]]) + cir.bs_theta(inputs=np.pi / 4, wires=[info[0], info[1]]) + cir.ps(inputs=info[3], wires=[info[0]]) + cir.bs_theta(inputs=np.pi / 4, wires=[info[0], info[1]]) + rlt = cir(is_prob=True) + rlt_strkey = dict() + for key in rlt: + rlt_strkey[str(key)] = np.abs(rlt[key].detach().numpy().squeeze()) ** 2 + + # rltm = cir.measure(shots=10000)[0] + # rltm_strkey = dict() + # for key in rltm.keys(): + # rltm_strkey[str(key)] = rltm[key] + # return post_selection(rltm_strkey) + + return post_selection(rlt_strkey) + + +def estimate_eigval(angle_list): + # to minimize + zero_init(mzi_info) + bar_securing(mzi_info) + trial_planting(mzi_info, angle_list) + + zz_planting(mzi_info) + post_selection_rlt = compute_process(mzi_info) + e_zz = estimate_energy(post_selection_rlt) + + xx_planting(mzi_info) + post_selection_rlt = compute_process(mzi_info) + e_xx = estimate_energy(post_selection_rlt) + + yy_planting(mzi_info) + post_selection_rlt = compute_process(mzi_info) + e_yy = estimate_energy(post_selection_rlt) + + return (1 + e_zz - e_xx - e_yy) / 2 + + # zz_planting(mzi_info) + # post_selection_rlt = compute_process(mzi_info) + # e_zi = estimate_energy(post_selection_rlt,basis='ZI') + + # xx_planting(mzi_info) + # post_selection_rlt = compute_process(mzi_info) + # e_ix = estimate_energy(post_selection_rlt,basis='IZ') + + # zx_planting(mzi_info) + # post_selection_rlt = compute_process(mzi_info) + # e_zx = estimate_energy(post_selection_rlt,basis='ZZ') + + # return (1 + e_zi + e_ix - e_zx) / 2 + + +def target_function(angle_list): + # to maximize + return -estimate_eigval(angle_list) + + +def void_target(): + return -1 + + +# %% + +# %% +# 测试BO算法 +print('=' * 50) +print('运行BO') +param_init = {'t1': 0, 't2': 0, 't3': 0} +bo_optimizer = OptimizerBayesian(target_func=void_target, param_init=param_init, random_state=0) + +print('轮次', '\t', '参数值', '\t', ' ' * 31, '目标值') +for _ in range(100): + # 待片上计算的参数 + p1 = bo_optimizer.param_suggest() + # 模拟芯片计算过程,之后需要替换成接口 + f1 = -estimate_eigval(p1.tolist()) # BO 内置用法是最大化目标;但是接下来打印时再添一个符号即可 + # 测试结果传回,更新param_dict + bo_optimizer.param_register([p1], [f1]) + f1_to_print = -f1 + mark = '*' if bo_optimizer.best_target == f1 else ' ' + if f1_to_print < 0: + print(bo_optimizer.iter, '\t', p1, '\t', f'{f1_to_print:8.10f} ', mark) + else: + print(bo_optimizer.iter, '\t', p1, '\t ', f'{f1_to_print:8.10f} ', mark) + +print('BO结果:', estimate_eigval(list(bo_optimizer.best_param_dict.values()))) + +# %% +# 测试SPSA算法 +print('=' * 50) +print('运行SPSA') +param_init = {'t1': np.random.randn(), 't2': np.random.randn(), 't3': np.random.randn()} +spsa_optimizer = OptimizerSPSA(target_func=void_target, param_init=param_init, random_state=0) + +print('轮次', '\t', '参数值', '\t', ' ' * 31, '目标值') +for _ in range(200): + # 待片上计算的两组参数 + p1, p2 = spsa_optimizer.param_suggest() + # 以下两行模拟芯片计算过程,之后需要替换成接口 + f1 = estimate_eigval(p1.tolist()) + f2 = estimate_eigval(p2.tolist()) + # 测试结果传回,更新param_dict + spsa_optimizer.param_register([p1, p2], [f1, f2]) + # spsa_optimizer.param_register(np.array([p1,p2]),np.array([f1,f2])) + + if f1 < 0: + print(spsa_optimizer.iter, '\t', p1, '\t', f'{f1:8.10f} ') + else: + print(spsa_optimizer.iter, '\t', p1, '\t ', f'{f1:8.10f} ') + if f2 < 0: + print(spsa_optimizer.iter, '\t', p2, '\t', f'{f2:8.10f} ') + else: + print(spsa_optimizer.iter, '\t', p2, '\t ', f'{f2:8.10f} ') + # print(spsa_optimizer.hyperparam['c']/(1+spsa_optimizer.iter)**spsa_optimizer.hyperparam['gamma']) + + if (f1 < -0.92) and (f2 < -0.92): + # print("!!!进入步长减小阶段!!!") + spsa_optimizer.hyperparam['c'] = 0.001 + elif (f1 < -0.999) and (f2 < -0.999): + spsa_optimizer.hyperparam['c'] = 1e-4 + +print('SPSA结果:', estimate_eigval(list(spsa_optimizer.best_param_dict.values()))) + +# %% +# 测试Fourier级数方法 +print('=' * 50) +print('运行Fourier级数法') +param_init = {'t1': np.random.randn(), 't2': np.random.randn(), 't3': np.random.randn()} +# param_init = dict(zip(param_init.keys(),np.random.randn(3))) +fourier_optimizer = OptimizerFourier(target_func=void_target, param_init=param_init, random_state=0, order=3, lr=0.1) +print('轮次', '\t', '参数值', '\t', ' ' * 31, '目标值') +for _ in range(50): + # 待片上计算的两组参数 + param_array = fourier_optimizer.param_suggest() + # 模拟芯片计算过程,之后需要替换成接口 + target = np.zeros(len(param_array)) + for i in range(len(param_array)): + target[i] = estimate_eigval(param_array[i]) + + # 测试结果传回,更新param_dict + fourier_optimizer.param_register(param_array, target) + p1 = np.array(list(fourier_optimizer.best_param_dict.values())) + f1 = fourier_optimizer.best_target + if f1 < 0: + print(fourier_optimizer.iter, '\t', p1, '\t', f'{f1:8.10f} ') + else: + print(fourier_optimizer.iter, '\t', p1, '\t ', f'{f1:8.10f} ') + +print('Fourier结果:', estimate_eigval(list(fourier_optimizer.best_param_dict.values()))) diff --git a/examples/unitary_mapper.ipynb b/examples/unitary_mapper.ipynb index 3d4b1a80..0a878ac6 100644 --- a/examples/unitary_mapper.ipynb +++ b/examples/unitary_mapper.ipynb @@ -13,8 +13,7 @@ "outputs": [], "source": [ "import deepquantum as dq\n", - "import numpy as np\n", - "import torch" + "import numpy as np" ] }, { @@ -33,7 +32,7 @@ " Use `UnitaryMapper` for mapping the quantum gate to photonic quantum circuit. \\\n", " nmode: 2*n_qubits + 2 \\\n", " auxiliary modes: [0,0] or [1,0] in the last 2modes \\\n", - "succcess probability: preferred 1/3 for 2qubtis, 1/4 for 3 qubits " + "succcess probability: preferred 1/3 for 2qubtis, 1/4 for 3 qubits" ] }, { @@ -56,18 +55,9 @@ }, "outputs": [], "source": [ - "swap = np.array([[1,0,0,0],\n", - " [0,0,1,0],\n", - " [0,1,0,0],\n", - " [0,0,0,1]])\n", - "iswap = np.array([[1,0,0,0],\n", - " [0,0,1j,0],\n", - " [0,1j,0,0],\n", - " [0,0,0,1]])\n", - "cnot = np.array([[1,0,0,0],\n", - " [0,1,0,0],\n", - " [0,0,0,1],\n", - " [0,0,1,0]])" + "swap = np.array([[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]])\n", + "iswap = np.array([[1, 0, 0, 0], [0, 0, 1j, 0], [0, 1j, 0, 0], [0, 0, 0, 1]])\n", + "cnot = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]])" ] }, { @@ -85,15 +75,15 @@ "nqubit = 2\n", "nmode = 6\n", "ugate = cnot\n", - "aux = [0,0]\n", - "aux_pos = [4,5]\n", - "success = 1/3\n", + "aux = [0, 0]\n", + "aux_pos = [4, 5]\n", + "success = 1 / 3\n", "umap = dq.UnitaryMapper(nqubit=nqubit, nmode=nmode, ugate=ugate, success=success, aux=aux, aux_pos=aux_pos)" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "869aad66", "metadata": { "ExecuteTime": { @@ -123,13 +113,13 @@ ], "source": [ "umap.ugate = cnot\n", - "umap.success = 1/3\n", - "Re3 = umap.solve_eqs_real(total_trials=1, trials=10, precision=1e-5) # for real solution" + "umap.success = 1 / 3\n", + "re = umap.solve_eqs_real(total_trials=1, trials=10, precision=1e-5) # for real solution" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "ac5c373c", "metadata": { "ExecuteTime": { @@ -161,7 +151,7 @@ ], "source": [ "## check result\n", - "result = (Re3[0][0][0])\n", + "result = re[0][0][0]\n", "transfer_mat = umap.get_transfer_mat(result)\n", "umap.plot_u(transfer_mat, vmin=-1)\n", "# np.save(\"cnot_test.npy\", re) # save the first result" @@ -207,7 +197,7 @@ ], "source": [ "cnot_test = result\n", - "init_state = [1,0,1,0,0,0]\n", + "init_state = [1, 0, 1, 0, 0, 0]\n", "test_circuit = dq.QumodeCircuit(nmode=6, init_state=init_state, basis=True)\n", "test_circuit.any(cnot_test, list(range(6)))\n", "test_circuit.draw()" @@ -225,12 +215,12 @@ }, "outputs": [], "source": [ - "re = test_circuit(state=[1,0,0,1,0,0])" + "re = test_circuit(state=[1, 0, 0, 1, 0, 0])" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "id": "a83102ce", "metadata": { "ExecuteTime": { @@ -271,7 +261,7 @@ } ], "source": [ - "re" + "print(re)" ] }, { @@ -310,12 +300,16 @@ }, "outputs": [], "source": [ - "u6x6 = np.array([[1, 0, 1, -1, 0, 0],\n", - " [0, 1, 0 ,0, 0, np.sqrt(2)],\n", - " [1, 0, 0, 1, 1, 0],\n", - " [-1, 0, 1, 0, 1, 0],\n", - " [0, 0, 1, 1, -1,0],\n", - " [0, np.sqrt(2), 0,0,0,-1]])/np.sqrt(3)" + "u6x6 = np.array(\n", + " [\n", + " [1, 0, 1, -1, 0, 0],\n", + " [0, 1, 0, 0, 0, np.sqrt(2)],\n", + " [1, 0, 0, 1, 1, 0],\n", + " [-1, 0, 1, 0, 1, 0],\n", + " [0, 0, 1, 1, -1, 0],\n", + " [0, np.sqrt(2), 0, 0, 0, -1],\n", + " ]\n", + ") / np.sqrt(3)" ] }, { @@ -385,7 +379,7 @@ } ], "source": [ - "p_mzi = dq.DrawClements(6,mzi_info[0])\n", + "p_mzi = dq.DrawClements(6, mzi_info[0])\n", "p_mzi.plotting_clements()" ] }, @@ -409,7 +403,7 @@ }, "outputs": [], "source": [ - "u8x8 = np.eye(8,8)\n", + "u8x8 = np.eye(8, 8)\n", "ud = dq.UnitaryDecomposer(u8x8)\n", "mzi_info = ud.decomp()" ] @@ -437,13 +431,13 @@ } ], "source": [ - "p_mzi = dq.DrawClements(8,mzi_info[0])\n", + "p_mzi = dq.DrawClements(8, mzi_info[0])\n", "p_mzi.plotting_clements()" ] }, { "cell_type": "code", - "execution_count": 17, + "execution_count": null, "id": "1962412d", "metadata": { "ExecuteTime": { @@ -528,7 +522,7 @@ } ], "source": [ - "p_mzi.ps_position" + "print(p_mzi.ps_position)" ] }, { @@ -556,7 +550,7 @@ }, "outputs": [], "source": [ - "clements = dq.Clements(nmode=6, init_state=[1,0,1,0,0,0], cutoff=3)" + "clements = dq.Clements(nmode=6, init_state=[1, 0, 1, 0, 0, 0], cutoff=3)" ] }, { @@ -590,7 +584,7 @@ }, { "cell_type": "code", - "execution_count": 47, + "execution_count": null, "id": "7e4e0c32", "metadata": { "ExecuteTime": { @@ -631,9 +625,9 @@ } ], "source": [ - "data = clements.dict2data(mzi_info[2]) # encoding the 6x6 data\n", + "data = clements.dict2data(mzi_info[2]) # encoding the 6x6 data\n", "re = clements(data=data)\n", - "re" + "print(re)" ] }, { diff --git a/examples/unitary_mapper.py b/examples/unitary_mapper.py new file mode 100644 index 00000000..bf33ca6d --- /dev/null +++ b/examples/unitary_mapper.py @@ -0,0 +1,133 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.19.1 +# kernelspec: +# display_name: dqp +# language: python +# name: dqp +# --- + +# %% +import deepquantum as dq +import numpy as np + +# %% [markdown] +# # quantum u gate map + +# %% [markdown] +# Use `UnitaryMapper` for mapping the quantum gate to photonic quantum circuit. \ +# nmode: 2*n_qubits + 2 \ +# auxiliary modes: [0,0] or [1,0] in the last 2modes \ +# succcess probability: preferred 1/3 for 2qubtis, 1/4 for 3 qubits + +# %% [markdown] +# ## map the quantum gate + +# %% +swap = np.array([[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]]) +iswap = np.array([[1, 0, 0, 0], [0, 0, 1j, 0], [0, 1j, 0, 0], [0, 0, 0, 1]]) +cnot = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]]) + +# %% +nqubit = 2 +nmode = 6 +ugate = cnot +aux = [0, 0] +aux_pos = [4, 5] +success = 1 / 3 +umap = dq.UnitaryMapper(nqubit=nqubit, nmode=nmode, ugate=ugate, success=success, aux=aux, aux_pos=aux_pos) + +# %% +umap.ugate = cnot +umap.success = 1 / 3 +re = umap.solve_eqs_real(total_trials=1, trials=10, precision=1e-5) # for real solution + +# %% +## check result +result = re[0][0][0] +transfer_mat = umap.get_transfer_mat(result) +umap.plot_u(transfer_mat, vmin=-1) +# np.save("cnot_test.npy", re) # save the first result + +# %% [markdown] +# ## check results + +# %% +cnot_test = result +init_state = [1, 0, 1, 0, 0, 0] +test_circuit = dq.QumodeCircuit(nmode=6, init_state=init_state, basis=True) +test_circuit.any(cnot_test, list(range(6))) +test_circuit.draw() + +# %% +re = test_circuit(state=[1, 0, 0, 1, 0, 0]) + +# %% +print(re) + +# %% [markdown] +# # decompose clements + +# %% [markdown] +# decomposing the optical qunatum circuit(unitary matrix) to clements structure + +# %% [markdown] +# ## 6x6 case + +# %% +u6x6 = np.array( + [ + [1, 0, 1, -1, 0, 0], + [0, 1, 0, 0, 0, np.sqrt(2)], + [1, 0, 0, 1, 1, 0], + [-1, 0, 1, 0, 1, 0], + [0, 0, 1, 1, -1, 0], + [0, np.sqrt(2), 0, 0, 0, -1], + ] +) / np.sqrt(3) + +# %% +ud = dq.UnitaryDecomposer(u6x6) +mzi_info = ud.decomp() +mzi_info[1] + +# %% +p_mzi = dq.DrawClements(6, mzi_info[0]) +p_mzi.plotting_clements() + +# %% [markdown] +# ## 8x8 case + +# %% +u8x8 = np.eye(8, 8) +ud = dq.UnitaryDecomposer(u8x8) +mzi_info = ud.decomp() + +# %% +p_mzi = dq.DrawClements(8, mzi_info[0]) +p_mzi.plotting_clements() + +# %% +print(p_mzi.ps_position) + +# %% [markdown] +# ## Clements in ansatz + +# %% +clements = dq.Clements(nmode=6, init_state=[1, 0, 1, 0, 0, 0], cutoff=3) + +# %% +clements.draw() + +# %% +data = clements.dict2data(mzi_info[2]) # encoding the 6x6 data +re = clements(data=data) +print(re) + +# %% +clements.draw() # 6x6 CNOT Clements structure parameters diff --git a/examples/vqe_for_CRW.ipynb b/examples/vqe_for_CRW.ipynb index 4a48bb5e..fa7eb3cd 100644 --- a/examples/vqe_for_CRW.ipynb +++ b/examples/vqe_for_CRW.ipynb @@ -59,7 +59,7 @@ "### Decomposition in terms of Pauli operators\n", "\n", "\n", - "By calculation, it can be decomposed as follows: \n", + "By calculation, it can be decomposed as follows:\n", "\n", "$$\n", "U = \\frac{A \\text{$\\sigma $i}\\otimes \\text{$\\sigma $x}\\otimes \\text{$\\sigma $z}}{2 \\sqrt{2}}+\\frac{A \\text{$\\sigma $i}\\otimes \\text{$\\sigma $z}\\otimes \\text{$\\sigma $x}}{2\n", @@ -74,7 +74,7 @@ " \\text{$\\sigma $z}\\otimes \\text{$\\sigma $z}-\\frac{1}{4} \\Delta \\text{$\\sigma $z}\\otimes \\text{$\\sigma $i}\\otimes \\text{$\\sigma $i}-\\frac{1}{4} \\Delta \\text{$\\sigma\n", " $z}\\otimes \\text{$\\sigma $i}\\otimes \\text{$\\sigma $z}\\\\\n", " -\\frac{1}{4} \\Delta \\text{$\\sigma $z}\\otimes \\text{$\\sigma $z}\\otimes \\text{$\\sigma $i}+\\\\\n", - " \\frac{3}{4} \\Delta \n", + " \\frac{3}{4} \\Delta\n", " \\text{$\\sigma $i}\\otimes \\text{$\\sigma $i}\\otimes \\text{$\\sigma $i}+\\frac{1}{4} \\Delta \\text{$\\sigma $z}\\otimes \\text{$\\sigma $z}\\otimes \\text{$\\sigma\n", " $z}-\\frac{\\text{$\\sigma $x}\\otimes \\text{$\\sigma $i}\\otimes \\text{$\\sigma $z}}{4}\\\\\n", " -\\frac{\\text{$\\sigma $x}\\otimes \\text{$\\sigma $z}\\otimes \\text{$\\sigma\n", @@ -95,16 +95,16 @@ " \\sqrt{2}}-\\frac{1}{4}\\right) \\text{$\\sigma $y}\\otimes \\text{$\\sigma $z}\\otimes \\text{$\\sigma $y}\n", "$$\n", "\n", - "that is, we found the desired decomposition of matrix $U$ in terms of Pauli operators. Since in VQE algorithms one usually deals with the minimization of the energy of a system, from now on we call the unitary matrix *Hamiltonian* $U \\rightarrow \\mathcal{H}$ of the system, and *energy* its mean value when evaluated on a given state $|\\psi(\\theta)\\rangle$, that is $E(\\theta) = \\langle \\mathcal{H} \\rangle_{\\theta}=\\langle \\psi(\\theta) | \\mathcal{H}|\\psi(\\theta)\\rangle$. \n", - "Notice that, $|\\psi(\\theta)$ is an *eigenvector* of the unitary $\\mathcal{H}$, with the energy $E(\\theta)$ being the corresponding *eigenvalue*. Our task is then to find the lowest eigenvalue of $\\mathcal{H}$. \n", + "that is, we found the desired decomposition of matrix $U$ in terms of Pauli operators. Since in VQE algorithms one usually deals with the minimization of the energy of a system, from now on we call the unitary matrix *Hamiltonian* $U \\rightarrow \\mathcal{H}$ of the system, and *energy* its mean value when evaluated on a given state $|\\psi(\\theta)\\rangle$, that is $E(\\theta) = \\langle \\mathcal{H} \\rangle_{\\theta}=\\langle \\psi(\\theta) | \\mathcal{H}|\\psi(\\theta)\\rangle$.\n", + "Notice that, $|\\psi(\\theta)$ is an *eigenvector* of the unitary $\\mathcal{H}$, with the energy $E(\\theta)$ being the corresponding *eigenvalue*. Our task is then to find the lowest eigenvalue of $\\mathcal{H}$.\n", "\n", "### Variational Quantum Eigensolver\n", "\n", - "The idea behind VQE, is to use a quantum computer to evaluate the mean value of the *Hamiltonian* on a trial state $|\\psi(\\theta)\\rangle$ parametrized by $\\theta$, and then slowly change this parameter in order to find lower and lower values for the energy $E(\\theta)$. \n", + "The idea behind VQE, is to use a quantum computer to evaluate the mean value of the *Hamiltonian* on a trial state $|\\psi(\\theta)\\rangle$ parametrized by $\\theta$, and then slowly change this parameter in order to find lower and lower values for the energy $E(\\theta)$.\n", "\n", - " \n", "\n", - "***Measurements in quantum computers generally happens along the $Z$ basis (known as *computational basis*), which means that we can only measure eigevectors and eigenvalues of $Z$.*** In order to measure other different observables, we need to change basis, and this can be done by introducing some gate before the measurement happens. \n", + "\n", + "***Measurements in quantum computers generally happens along the $Z$ basis (known as *computational basis*), which means that we can only measure eigevectors and eigenvalues of $Z$.*** In order to measure other different observables, we need to change basis, and this can be done by introducing some gate before the measurement happens.\n", "\n", "### Change of basis\n", "\n", @@ -112,13 +112,13 @@ "$$\n", "X=HZH\\quad Y=(HS^\\dagger)^\\dagger Z(HS^\\dagger)\n", "$$\n", - "we can measure along the X basis by introducing an Hadamard $H$ gate before the measurement. Same happens with $Y$, by using a combination of Hadamard and Phase gates $HS^\\dagger$. \n", + "we can measure along the X basis by introducing an Hadamard $H$ gate before the measurement. Same happens with $Y$, by using a combination of Hadamard and Phase gates $HS^\\dagger$.\n", "\n", - "However, in our case we wish to measure two-qubits observables. \n", + "However, in our case we wish to measure two-qubits observables.\n", "\n", "#### Observable $Z_1 Z_2 Z_3$\n", - " \n", - "The operator $Z_1 Z_2 Z_3$ acts like: \n", + "\n", + "The operator $Z_1 Z_2 Z_3$ acts like:\n", "$$\n", "Z_{1}Z_{2}Z_{3}\\left|000\\right\\rangle \t=+1\\\\\n", "Z_{1}Z_{2}Z_{3}\\left|011\\right\\rangle \t=+1\\\\\n", @@ -132,7 +132,7 @@ "In calculation, we firstly checks whether the first two qubits are in the same state (both $0$ or both $1$), in which case it has eigenvalue $1$, otherwise it has eigenvalue $-1$. A similar action can be implemented using a CNOT, in fact this gates loads on the second qubit the binary sum of the two qubits $\\text{CNOT}|q_1\\rangle|q_2\\rangle=|q_1\\rangle|q_1\\oplus q_2\\rangle$. Next, we will perform the same calculation on the second and third bits.\n", "\n", "In fact, if\n", - "* $q_1 = q_2$ (qubits are in the same state), it holds that $|q_1\\oplus q_2\\rangle = |0\\rangle$ and a measurement of the second qubit in the computational basis ($Z$ basis) yields result $+1$, \n", + "* $q_1 = q_2$ (qubits are in the same state), it holds that $|q_1\\oplus q_2\\rangle = |0\\rangle$ and a measurement of the second qubit in the computational basis ($Z$ basis) yields result $+1$,\n", "* $q_1\\neq q_2$, then $|q_1\\oplus q_2\\rangle=|1\\rangle$ and a measurement would yield result $-1$, as desired.\n", "Then, for the second and third qubits, we perform the same operations.\n", "\n", @@ -143,17 +143,16 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ + "import cma\n", "import deepquantum as dq\n", "import numpy as np\n", - "import cma\n", - "from cma.fitness_functions import elli # cannot be an instance method\n", - "from cma.optimization_tools import EvalParallel2" + "from numpy import kron" ] }, { @@ -191,13 +190,13 @@ "\n", "qc.barrier()\n", "\n", - "qc.cx(0,1)\n", - "qc.cx(1,2)\n", + "qc.cx(0, 1)\n", + "qc.cx(1, 2)\n", "qc()\n", "\n", "counts = qc.measure(wires=2)\n", "\n", - "print(\"Measurement in the ZZZ basis\")\n", + "print('Measurement in the ZZZ basis')\n", "qc.draw()" ] }, @@ -274,12 +273,12 @@ "qc.h(1)\n", "qc.h(2)\n", "\n", - "qc.cx(0,1)\n", - "qc.cx(1,2)\n", + "qc.cx(0, 1)\n", + "qc.cx(1, 2)\n", "\n", "qc.measure(wires=2)\n", "\n", - "print(\"Measurement in the XXX basis\")\n", + "print('Measurement in the XXX basis')\n", "qc.draw()" ] }, @@ -300,6 +299,7 @@ "end_time": "2023-05-30T07:35:10.089460Z", "start_time": "2023-05-30T07:35:09.981944Z" }, + "lines_to_next_cell": 2, "tags": [] }, "outputs": [ @@ -334,11 +334,11 @@ "qc.h(1)\n", "qc.h(2)\n", "\n", - "qc.cx(0,1)\n", - "qc.cx(1,2)\n", + "qc.cx(0, 1)\n", + "qc.cx(1, 2)\n", "qc.measure(wires=2)\n", - "print(\"Measurement in the YYY basis\")\n", - "qc.draw(output=\"mpl\")" + "print('Measurement in the YYY basis')\n", + "qc.draw(output='mpl')" ] }, { @@ -379,7 +379,9 @@ }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "lines_to_next_cell": 2 + }, "source": [ "### Builds the trial state using the ansatz:\n", "\n", @@ -389,7 +391,7 @@ "\n", "Return\n", "- qc: returns the input quantum circuit added with the gates creating the trial state\n", - " \n" + "\n" ] }, { @@ -403,24 +405,24 @@ "def ansatz(qc, theta):\n", " # qc = dq.QubitCircuit(3)\n", "\n", - " [theta_1,theta_2,theta_3,theta_4,theta_5,theta_6,theta_7,theta_8,theta_9] = theta\n", + " [theta_1, theta_2, theta_3, theta_4, theta_5, theta_6, theta_7, theta_8, theta_9] = theta\n", "\n", - " qc.rx(0,theta_1)\n", - " qc.rx(1,theta_2)\n", - " qc.rx(2,theta_3)\n", + " qc.rx(0, theta_1)\n", + " qc.rx(1, theta_2)\n", + " qc.rx(2, theta_3)\n", "\n", - " qc.rz(0,theta_4)\n", - " qc.rz(1,theta_5)\n", - " qc.rz(2,theta_6)\n", + " qc.rz(0, theta_4)\n", + " qc.rz(1, theta_5)\n", + " qc.rz(2, theta_6)\n", "\n", " qc.barrier()\n", "\n", - " qc.rxx([0,1],theta_7)\n", - " qc.rxx([0,2],theta_8)\n", + " qc.rxx([0, 1], theta_7)\n", + " qc.rxx([0, 2], theta_8)\n", "\n", " qc.barrier()\n", "\n", - " qc.rxx([1,2],theta_9)\n", + " qc.rxx([1, 2], theta_9)\n", "\n", " return qc" ] @@ -445,7 +447,7 @@ "source": [ "qc = dq.QubitCircuit(3)\n", "theta = np.random.random(9)\n", - "qc = ansatz(qc,theta)\n", + "qc = ansatz(qc, theta)\n", "qc.draw()" ] }, @@ -493,38 +495,37 @@ }, { "cell_type": "code", - "execution_count": 9, - "metadata": {}, + "execution_count": null, + "metadata": { + "lines_to_next_cell": 2 + }, "outputs": [], "source": [ "def measurements(qc, op, shots):\n", - "\n", " if op.count('I') == 0:\n", " for i in range(len(op)):\n", " # Change of basis, since X = HZH\n", - " if op[i] == \"X\":\n", + " if op[i] == 'X':\n", " qc.h(i)\n", "\n", " # Change of basis, since Y = (HS†)Z(HS†)\n", - " elif op[i] == \"Y\":\n", + " elif op[i] == 'Y':\n", " qc.sdg(i)\n", " qc.h(i)\n", "\n", " # CNOT used to measure ZZ operator\n", - " for i in range(len(op)-1):\n", - " qc.cx(i,i+1)\n", + " for i in range(len(op) - 1):\n", + " qc.cx(i, i + 1)\n", "\n", " qc()\n", "\n", - " counts = qc.measure(shots=shots, wires=len(op)-1)\n", + " counts = qc.measure(shots=shots, wires=len(op) - 1)\n", "\n", " elif op.count('I') == 1:\n", - "\n", " index = []\n", "\n", " for i in range(len(op)):\n", " if op[i] != 'I':\n", - "\n", " index.append(i)\n", "\n", " if op[i] == 'X':\n", @@ -533,10 +534,10 @@ " qc.sdg(i)\n", " qc.h(i)\n", "\n", - " qc.cx(index[0],index[1])\n", + " qc.cx(index[0], index[1])\n", "\n", " qc()\n", - " counts = qc.measure(shots=shots, wires=index[1])\n", + " counts = qc.measure(shots=shots, wires=index[1])\n", "\n", " elif op.count('I') == 2:\n", " for i in range(len(op)):\n", @@ -548,30 +549,31 @@ " qc.h(i)\n", "\n", " qc()\n", - " counts = qc.measure(shots=shots, wires=i)\n", + " counts = qc.measure(shots=shots, wires=i)\n", "\n", " else:\n", " counts = {'0': shots}\n", "\n", - "\n", " # Check the results, and evaluate the mean value dividing by the number of shots\n", " if len(counts) == 1:\n", " try:\n", " counts['0']\n", " mean_val = 1\n", - " except:\n", + " except KeyError:\n", " mean_val = -1\n", " else:\n", " # Evaluates the mean value of Z operator, as the difference in the number of\n", " # 0s and 1s in the measurement outcomes\n", - " mean_val = (counts['0']-counts['1'])/shots\n", + " mean_val = (counts['0'] - counts['1']) / shots\n", "\n", - " return mean_val\n" + " return mean_val" ] }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "lines_to_next_cell": 2 + }, "source": [ "### Evaulates the Energy of the trial state using the mean values of the operators.\n", "\n", @@ -584,7 +586,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2023-05-30T07:35:24.112471Z", @@ -628,57 +630,55 @@ } ], "source": [ - "import numpy as np\n", - "from numpy import kron\n", + "def hilbert_schmidt(m1, m2):\n", + " \"\"\"Hilbert-Schmidt-Product of two matrices m1, m2\"\"\"\n", + " return (np.dot(m1.conjugate().transpose(), m2)).trace()\n", "\n", - "def HS(M1, M2):\n", - " \"\"\"Hilbert-Schmidt-Product of two matrices M1, M2\"\"\"\n", - " return (np.dot(M1.conjugate().transpose(), M2)).trace()\n", "\n", - "def decompose(H):\n", - " \"\"\"Decompose Hermitian matrix H into Pauli matrices\"\"\"\n", + "def decompose(h):\n", + " \"\"\"Decompose Hermitian matrix into Pauli matrices\"\"\"\n", "\n", - " sx = np.array([[0, 1], [ 1, 0]], dtype=np.complex128)\n", - " sy = np.array([[0, -1j],[1j, 0]], dtype=np.complex128)\n", - " sz = np.array([[1, 0], [0, -1]], dtype=np.complex128)\n", - " id = np.array([[1, 0], [ 0, 1]], dtype=np.complex128)\n", - " S = [sx, sy, sz, id]\n", + " sx = np.array([[0, 1], [1, 0]], dtype=np.complex128)\n", + " sy = np.array([[0, -1j], [1j, 0]], dtype=np.complex128)\n", + " sz = np.array([[1, 0], [0, -1]], dtype=np.complex128)\n", + " mat_i = np.array([[1, 0], [0, 1]], dtype=np.complex128)\n", + " s_lst = [sx, sy, sz, mat_i]\n", " labels = ['X', 'Y', 'Z', 'I']\n", "\n", - " decomposed_H = {}\n", + " decomposed_h = {}\n", " for i in range(4):\n", " for j in range(4):\n", " for k in range(4):\n", - "\n", " label = labels[i] + labels[j] + labels[k]\n", - " a_ij = 0.125 * HS( kron(kron(S[i], S[j]), S[k]), H)\n", + " a_ij = 0.125 * hilbert_schmidt(kron(kron(s_lst[i], s_lst[j]), s_lst[k]), h)\n", "\n", " if abs(a_ij) > 1e-6:\n", - " decomposed_H[label] = a_ij.real\n", - "\n", - " # print(f'{label:<10} {a_ij}')\n", - " return decomposed_H\n", - "\n", - "\n", - "A, delta = 0,1\n", - "\n", - "H = np.array([ [0, A * np.sqrt(2), A * np.sqrt(2), 0, 0, 0, 1, 1],\n", - " [A * np.sqrt(2), 0, 0, 0, np.sqrt(2), 0, 0, 0],\n", - " [A * np.sqrt(2), 0, 0, 0, 0, np.sqrt(2), 0, 0],\n", - " [0, 0, 0, 2 * delta, 0, 0, 1, 1],\n", - " [0, np.sqrt(2), 0, 0, delta, 0, A, 0],\n", - " [0, 0, np.sqrt(2), 0, 0, delta, 0, A],\n", - " [1, 0, 0, 1, A, 0, delta, 0],\n", - " [1, 0, 0, 1, 0, A, 0, delta]])\n", - "\n", - "# A = 0, delta = 1\n", - "decomposed_H = decompose(H)\n", - "decomposed_H" + " decomposed_h[label] = a_ij.real\n", + " return decomposed_h\n", + "\n", + "\n", + "a, delta = 0, 1\n", + "\n", + "h = np.array(\n", + " [\n", + " [0, a * np.sqrt(2), a * np.sqrt(2), 0, 0, 0, 1, 1],\n", + " [a * np.sqrt(2), 0, 0, 0, np.sqrt(2), 0, 0, 0],\n", + " [a * np.sqrt(2), 0, 0, 0, 0, np.sqrt(2), 0, 0],\n", + " [0, 0, 0, 2 * delta, 0, 0, 1, 1],\n", + " [0, np.sqrt(2), 0, 0, delta, 0, a, 0],\n", + " [0, 0, np.sqrt(2), 0, 0, delta, 0, a],\n", + " [1, 0, 0, 1, a, 0, delta, 0],\n", + " [1, 0, 0, 1, 0, a, 0, delta],\n", + " ]\n", + ")\n", + "\n", + "decomposed_h = decompose(h)\n", + "print(decomposed_h)" ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2023-05-30T07:35:26.130657Z", @@ -689,9 +689,9 @@ "outputs": [], "source": [ "def hamiltonian(vqe_res):\n", - " en = 0.\n", - " for key in decomposed_H.keys():\n", - " en += vqe_res[key] * decomposed_H[key]\n", + " en = 0.0\n", + " for key in decomposed_h:\n", + " en += vqe_res[key] * decomposed_h[key]\n", " return en" ] }, @@ -699,7 +699,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Executes the VQE algorithm. \n", + "### Executes the VQE algorithm.\n", "Creates and executes three quantum circuits, then evaluates the energy.\n", "\n", "Arguments\n", @@ -722,7 +722,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "metadata": { "tags": [] }, @@ -749,18 +749,17 @@ ], "source": [ "vqe_res = dict()\n", - "shots = 10 ** 2\n", - "\n", - "for op in decomposed_H.keys():\n", + "shots = 10**2\n", "\n", + "for op in decomposed_h:\n", " qc = dq.QubitCircuit(3)\n", "\n", " qc = ansatz(qc, theta)\n", " qc.barrier()\n", "\n", - " vqe_res[op] = measurements(qc,op,shots)\n", + " vqe_res[op] = measurements(qc, op, shots)\n", "\n", - "print(len(vqe_res),vqe_res)\n", + "print(len(vqe_res), vqe_res)\n", "energy = hamiltonian(vqe_res)\n", "print(energy.real)\n", "qc.draw()" @@ -768,7 +767,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2023-05-30T07:35:28.840731Z", @@ -779,15 +778,13 @@ "outputs": [], "source": [ "def vqe_step(theta):\n", - "\n", " # Number of executions for each quantum circuit\n", " shots = 2**10\n", "\n", " vqe_res = dict()\n", " # qc_list = dict()\n", "\n", - " for op in decomposed_H.keys():\n", - "\n", + " for op in decomposed_h:\n", " qc = dq.QubitCircuit(3)\n", "\n", " # Implementation of the ansatz\n", @@ -817,7 +814,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2023-05-30T07:35:32.086275Z", @@ -844,14 +841,14 @@ "\n", "# Run the VQE step to evaluate the energy (eigenvalue of the Hamiltonian) of the state with given theta\n", "energy = vqe_step(theta)\n", - "energy" + "print(energy)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Our aim is to find a value for the parameter that yields the lowest possible energy, and that is the desired lowest eigenvalue for the Task. \n", + "Our aim is to find a value for the parameter that yields the lowest possible energy, and that is the desired lowest eigenvalue for the Task.\n", "\n", "\n", "##### Using an optimizator" @@ -862,10 +859,7 @@ "execution_count": 16, "metadata": {}, "outputs": [], - "source": [ - "from cma.fitness_functions import elli # cannot be an instance method\n", - "from cma.optimization_tools import EvalParallel2" - ] + "source": [] }, { "cell_type": "code", @@ -935,9 +929,9 @@ } ], "source": [ - "options = {'seed':123, 'popsize':30, 'maxiter':100, 'verb_disp': 50}\n", + "options = {'seed': 123, 'popsize': 30, 'maxiter': 100, 'verb_disp': 50}\n", "\n", - "es = cma.CMAEvolutionStrategy(9 * [0], .6, options)\n", + "es = cma.CMAEvolutionStrategy(9 * [0], 0.6, options)\n", "# with EvalParallel2(elli, es.popsize + 1) as eval_all:\n", "# while not es.stop():\n", "# solutions = es.ask()\n", diff --git a/examples/vqe_for_CRW.py b/examples/vqe_for_CRW.py new file mode 100644 index 00000000..6c56b032 --- /dev/null +++ b/examples/vqe_for_CRW.py @@ -0,0 +1,550 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.19.1 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# %% [markdown] +# # Variational Quantum Eigensolver + +# %% [markdown] +# The aim of the task is to find the lowest eigenvalue of the following unitary matrix +# +# $$ +# \left(\begin{array}{cccccccc} +# 0 & \sqrt{2}A & \sqrt{2}A & 0 & 0 & 0 & 1 & 1\\ +# \sqrt{2}A & 0 & 0 & 0 & \sqrt{2} & 0 & 0 & 0\\ +# \sqrt{2}A & 0 & 0 & 0 & 0 & \sqrt{2} & 0 & 0\\ +# 0 & 0 & 0 & 2\Delta & 0 & 0 & 1 & 1\\ +# 0 & \sqrt{2} & 0 & 0 & \Delta & 0 & A & 0\\ +# 0 & 0 & \sqrt{2} & 0 & 0 & \Delta & 0 & A\\ +# 1 & 0 & 0 & 1 & A & 0 & \Delta & 0\\ +# 1 & 0 & 0 & 1 & 0 & A & 0 & \Delta +# \end{array}\right) +# $$ +# +# using VQE-like circuits. In order to do so, one must find a decomposition of the unitary $U$ in terms of Pauli operators $\{I, X, Y, Z\}$: +# +# $$ +# I = +# \begin{bmatrix} +# 1 & 0 \\ +# 0 & 1 +# \end{bmatrix}\quad +# +# X = +# \begin{bmatrix} +# 0 & 1 \\ +# 1& 0 +# \end{bmatrix}\quad +# +# Y = +# \begin{bmatrix} +# 0 & -i \\ +# i & 0 +# \end{bmatrix}\quad +# +# Z = +# \begin{bmatrix} +# 1 & 0 \\ +# 0 & -1 +# \end{bmatrix} +# $$ +# +# in order to be implemented on a quantum computer. In particular, since we are dealing with a $8\times8$ matrix, three qubits are needed, thus the desired decomposition will be made of *tensor products* of three Pauli operators (e.g. $X_1 \otimes Y_2 \otimes Z_3$, where the subscript denotes the system on which the operator acts). +# +# ### Decomposition in terms of Pauli operators +# +# +# By calculation, it can be decomposed as follows: +# +# $$ +# U = \frac{A \text{$\sigma $i}\otimes \text{$\sigma $x}\otimes \text{$\sigma $z}}{2 \sqrt{2}}+\frac{A \text{$\sigma $i}\otimes \text{$\sigma $z}\otimes \text{$\sigma $x}}{2 +# \sqrt{2}}+\frac{A \text{$\sigma $z}\otimes \text{$\sigma $i}\otimes \text{$\sigma $x}}{2 \sqrt{2}}\\ +# +\left(\frac{A}{2 \sqrt{2}}-\frac{A}{2}\right) \text{$\sigma $z}\otimes +# \text{$\sigma $x}\otimes \text{$\sigma $i}+\frac{A \text{$\sigma $i}\otimes \text{$\sigma $i}\otimes \text{$\sigma $x}}{2 \sqrt{2}}+\left(\frac{A}{2 +# \sqrt{2}}+\frac{A}{2}\right) \text{$\sigma $i}\otimes \text{$\sigma $x}\otimes \text{$\sigma $i}\\ +# +\frac{A \text{$\sigma $z}\otimes \text{$\sigma $x}\otimes \text{$\sigma +# $z}}{2 \sqrt{2}}+\frac{A \text{$\sigma $z}\otimes \text{$\sigma $z}\otimes \text{$\sigma $x}}{2 \sqrt{2}}-\frac{1}{4} \Delta \text{$\sigma $i}\otimes \text{$\sigma +# $i}\otimes \text{$\sigma $z}-\frac{1}{4} \Delta \text{$\sigma $i}\otimes \text{$\sigma $z}\otimes \text{$\sigma $i}\\ +# +\frac{1}{4} \Delta \text{$\sigma $i}\otimes +# \text{$\sigma $z}\otimes \text{$\sigma $z}-\frac{1}{4} \Delta \text{$\sigma $z}\otimes \text{$\sigma $i}\otimes \text{$\sigma $i}-\frac{1}{4} \Delta \text{$\sigma +# $z}\otimes \text{$\sigma $i}\otimes \text{$\sigma $z}\\ +# -\frac{1}{4} \Delta \text{$\sigma $z}\otimes \text{$\sigma $z}\otimes \text{$\sigma $i}+\\ +# \frac{3}{4} \Delta +# \text{$\sigma $i}\otimes \text{$\sigma $i}\otimes \text{$\sigma $i}+\frac{1}{4} \Delta \text{$\sigma $z}\otimes \text{$\sigma $z}\otimes \text{$\sigma +# $z}-\frac{\text{$\sigma $x}\otimes \text{$\sigma $i}\otimes \text{$\sigma $z}}{4}\\ +# -\frac{\text{$\sigma $x}\otimes \text{$\sigma $z}\otimes \text{$\sigma +# $i}}{4}+\frac{\text{$\sigma $x}\otimes \text{$\sigma $i}\otimes \text{$\sigma $i}}{4}+\left(\frac{1}{4}+\frac{1}{2 \sqrt{2}}\right) \text{$\sigma $x}\otimes \text{$\sigma +# $i}\otimes \text{$\sigma $x}\\ +# +\frac{\text{$\sigma $x}\otimes \text{$\sigma $x}\otimes \text{$\sigma $i}}{4}+\left(\frac{1}{4}+\frac{1}{2 \sqrt{2}}\right) \text{$\sigma +# $y}\otimes \text{$\sigma $i}\otimes \text{$\sigma $y}\\ +# -\frac{\text{$\sigma $y}\otimes \text{$\sigma $y}\otimes \text{$\sigma $i}}{4}+\left(\frac{1}{2 +# \sqrt{2}}-\frac{1}{4}\right) \text{$\sigma $x}\otimes \text{$\sigma $y}\otimes \text{$\sigma $y}\\ +# +\left(-\frac{1}{4}-\frac{1}{2 \sqrt{2}}\right) \text{$\sigma $y}\otimes +# \text{$\sigma $x}\otimes \text{$\sigma $y}+\left(\frac{1}{2 \sqrt{2}}-\frac{1}{4}\right) \text{$\sigma $y}\otimes \text{$\sigma $y}\otimes \text{$\sigma +# $x}\\ +# +\frac{\text{$\sigma $x}\otimes \text{$\sigma $x}\otimes \text{$\sigma $z}}{4}+\left(\frac{1}{2 \sqrt{2}}-\frac{1}{4}\right) \text{$\sigma $x}\otimes \text{$\sigma +# $z}\otimes \text{$\sigma $x}\\ +# +\frac{\text{$\sigma $x}\otimes \text{$\sigma $z}\otimes \text{$\sigma $z}}{4}+\left(\frac{1}{4}+\frac{1}{2 \sqrt{2}}\right) \text{$\sigma +# $x}\otimes \text{$\sigma $x}\otimes \text{$\sigma $x}\\ +# -\frac{\text{$\sigma $y}\otimes \text{$\sigma $y}\otimes \text{$\sigma $z}}{4}+\left(\frac{1}{2 +# \sqrt{2}}-\frac{1}{4}\right) \text{$\sigma $y}\otimes \text{$\sigma $z}\otimes \text{$\sigma $y} +# $$ +# +# that is, we found the desired decomposition of matrix $U$ in terms of Pauli operators. Since in VQE algorithms one usually deals with the minimization of the energy of a system, from now on we call the unitary matrix *Hamiltonian* $U \rightarrow \mathcal{H}$ of the system, and *energy* its mean value when evaluated on a given state $|\psi(\theta)\rangle$, that is $E(\theta) = \langle \mathcal{H} \rangle_{\theta}=\langle \psi(\theta) | \mathcal{H}|\psi(\theta)\rangle$. +# Notice that, $|\psi(\theta)$ is an *eigenvector* of the unitary $\mathcal{H}$, with the energy $E(\theta)$ being the corresponding *eigenvalue*. Our task is then to find the lowest eigenvalue of $\mathcal{H}$. +# +# ### Variational Quantum Eigensolver +# +# The idea behind VQE, is to use a quantum computer to evaluate the mean value of the *Hamiltonian* on a trial state $|\psi(\theta)\rangle$ parametrized by $\theta$, and then slowly change this parameter in order to find lower and lower values for the energy $E(\theta)$. +# +# +# +# ***Measurements in quantum computers generally happens along the $Z$ basis (known as *computational basis*), which means that we can only measure eigevectors and eigenvalues of $Z$.*** In order to measure other different observables, we need to change basis, and this can be done by introducing some gate before the measurement happens. +# +# ### Change of basis +# +# Noticing that: +# $$ +# X=HZH\quad Y=(HS^\dagger)^\dagger Z(HS^\dagger) +# $$ +# we can measure along the X basis by introducing an Hadamard $H$ gate before the measurement. Same happens with $Y$, by using a combination of Hadamard and Phase gates $HS^\dagger$. +# +# However, in our case we wish to measure two-qubits observables. +# +# #### Observable $Z_1 Z_2 Z_3$ +# +# The operator $Z_1 Z_2 Z_3$ acts like: +# $$ +# Z_{1}Z_{2}Z_{3}\left|000\right\rangle =+1\\ +# Z_{1}Z_{2}Z_{3}\left|011\right\rangle =+1\\ +# Z_{1}Z_{2}Z_{3}\left|101\right\rangle =+1\\ +# Z_{1}Z_{2}Z_{3}\left|110\right\rangle =+1\\ +# Z_{1}Z_{2}Z_{3}\left|001\right\rangle =-1\\ +# Z_{1}Z_{2}Z_{3}\left|010\right\rangle =-1\\ +# Z_{1}Z_{2}Z_{3}\left|100\right\rangle =-1\\ +# Z_{1}Z_{2}Z_{3}\left|111\right\rangle =-1\\ +# $$ +# In calculation, we firstly checks whether the first two qubits are in the same state (both $0$ or both $1$), in which case it has eigenvalue $1$, otherwise it has eigenvalue $-1$. A similar action can be implemented using a CNOT, in fact this gates loads on the second qubit the binary sum of the two qubits $\text{CNOT}|q_1\rangle|q_2\rangle=|q_1\rangle|q_1\oplus q_2\rangle$. Next, we will perform the same calculation on the second and third bits. +# +# In fact, if +# * $q_1 = q_2$ (qubits are in the same state), it holds that $|q_1\oplus q_2\rangle = |0\rangle$ and a measurement of the second qubit in the computational basis ($Z$ basis) yields result $+1$, +# * $q_1\neq q_2$, then $|q_1\oplus q_2\rangle=|1\rangle$ and a measurement would yield result $-1$, as desired. +# Then, for the second and third qubits, we perform the same operations. +# +# > Remember that, given a general qubit state $|\psi\rangle=\alpha |0\rangle + \beta |1\rangle$, it holds $\langle Z \rangle = \langle \psi | Z | \psi \rangle = |\alpha|^2-|\beta|^2$. +# +# In this way, we can measure the observable $Z_1 Z_2 Z_3$ by introducing two CNOT gates and then measuring the third qubit in the usual $Z$ basis, as reported in the following circuit: + +# %% +import cma +import deepquantum as dq +import numpy as np +from numpy import kron + +# %% +qc = dq.QubitCircuit(3) + +qc.barrier() + +qc.cx(0, 1) +qc.cx(1, 2) +qc() + +counts = qc.measure(wires=2) + +print('Measurement in the ZZZ basis') +qc.draw() + +# %% +print(counts) + +# %% [markdown] +# Other observables can be reduced to a measurement of $Z_1 Z_2 Z_3$ by means of appropriate unitary transformations (change of basis). + +# %% [markdown] +# #### Observable $X_1 X_2 X_3$ +# +# Using $X=HZH$ and the CNOT gates, we have that the measurement in the $X_1 X_2 X_3$ can be achieved using the following circuit: + +# %% +qc = dq.QubitCircuit(3) +qc.barrier() + +qc.h(0) +qc.h(1) +qc.h(2) + +qc.cx(0, 1) +qc.cx(1, 2) + +qc.measure(wires=2) + +print('Measurement in the XXX basis') +qc.draw() + +# %% [markdown] +# #### Observable $Y_1 Y_2 Y_3$ +# +# Using $Y=(HS^\dagger)^\dagger Z(HS^\dagger)$ and the CNOT gates, we have that the measurement in the $Y_1 Y_2 Y_3$ can be achieved using the following circuit: + +# %% +qc = dq.QubitCircuit(3) +qc.barrier() + +qc.sdg(0) +qc.sdg(1) +qc.sdg(2) + +qc.h(0) +qc.h(1) +qc.h(2) + +qc.cx(0, 1) +qc.cx(1, 2) +qc.measure(wires=2) +print('Measurement in the YYY basis') +qc.draw(output='mpl') + + +# %% [markdown] +# ## VQE + +# %% [markdown] +# We now proceed implementing the VQE architecure, which works as follows: +# * choose an ansatz for a trial state $|\psi(\theta)\rangle$, parametrized by the parameter $\theta$ +# * use quantum circuits to estimate the mean values +# * compute the energy $E(\theta)=\langle \mathcal{H} \rangle_\theta$ +# * change $\theta$ in order to reach a lower energy +# + +# %% [markdown] +# VQE can be used for many things. The most popular application of VQE is for the quantum chemistry problem, as in this paper (https://arxiv.org/abs/1701.02691), where they are trying to find the ground state wavefunction of a molecular Hamiltonian (i.e. the VQE is trying to find the eigenvector with the smallest eigenvalue/energy). Here you can see that they suggest a unitary coupled cluster (UCC) ansatz. The reason they choose UCC is because it is well-known that coupled cluster already gives a very good approximation of the ground state wavefunction, in fact it is the basis for what chemists call the "gold standard of quantum chemistry". +# +# Remember VQE is a heuristic. The better the ansatz that you start with, the more likely your VQE will perform well. We can use intuition, or trial-and-error, or just use any knowledge you have of the problem to come up with something that you believe will work well (as in the case of using a coupled cluster ansatz for the problem where coupled cluster is already considered "the gold standard" for people solving the problem on classical computers). +# +# There is no general recipe for how to come up with the ansatz for VQE which will universally work well on every VQE problem, and that is why VQE is called a "heuristic". + +# %% [markdown] +# ## Functions declaration + +# %% [markdown] +# ### Builds the trial state using the ansatz: +# +# Arguments +# * qc: is a QubitCircuit object from deepquantum +# * theta (real): is the parameter parametrizing the trial state +# +# Return +# - qc: returns the input quantum circuit added with the gates creating the trial state +# +# + + +# %% +def ansatz(qc, theta): + # qc = dq.QubitCircuit(3) + + [theta_1, theta_2, theta_3, theta_4, theta_5, theta_6, theta_7, theta_8, theta_9] = theta + + qc.rx(0, theta_1) + qc.rx(1, theta_2) + qc.rx(2, theta_3) + + qc.rz(0, theta_4) + qc.rz(1, theta_5) + qc.rz(2, theta_6) + + qc.barrier() + + qc.rxx([0, 1], theta_7) + qc.rxx([0, 2], theta_8) + + qc.barrier() + + qc.rxx([1, 2], theta_9) + + return qc + + +# %% +qc = dq.QubitCircuit(3) +theta = np.random.random(9) +qc = ansatz(qc, theta) +qc.draw() + +# %% [markdown] +# In general, we need a total of 64 different circuits for measurement. + +# %% [markdown] +# ### Implements the quantum measurements in different 64 basis: +# +# Arguments +# * qc: is a QubitCircuit object from deepquantum +# * op (str): is a string with possible values. +# +# Return +# - qc: returns the input quantum circuit added with the appropriate gates to measure in the selected basis. +# + +# %% +op = 'XXXYYZZZIII' +len(op), op.count('I') + + +# %% +def measurements(qc, op, shots): + if op.count('I') == 0: + for i in range(len(op)): + # Change of basis, since X = HZH + if op[i] == 'X': + qc.h(i) + + # Change of basis, since Y = (HS†)Z(HS†) + elif op[i] == 'Y': + qc.sdg(i) + qc.h(i) + + # CNOT used to measure ZZ operator + for i in range(len(op) - 1): + qc.cx(i, i + 1) + + qc() + + counts = qc.measure(shots=shots, wires=len(op) - 1) + + elif op.count('I') == 1: + index = [] + + for i in range(len(op)): + if op[i] != 'I': + index.append(i) + + if op[i] == 'X': + qc.h(i) + elif op[i] == 'Y': + qc.sdg(i) + qc.h(i) + + qc.cx(index[0], index[1]) + + qc() + counts = qc.measure(shots=shots, wires=index[1]) + + elif op.count('I') == 2: + for i in range(len(op)): + if op[i] != 'I': + if op[i] == 'X': + qc.h(i) + elif op[i] == 'Y': + qc.sdg(i) + qc.h(i) + + qc() + counts = qc.measure(shots=shots, wires=i) + + else: + counts = {'0': shots} + + # Check the results, and evaluate the mean value dividing by the number of shots + if len(counts) == 1: + try: + counts['0'] + mean_val = 1 + except KeyError: + mean_val = -1 + else: + # Evaluates the mean value of Z operator, as the difference in the number of + # 0s and 1s in the measurement outcomes + mean_val = (counts['0'] - counts['1']) / shots + + return mean_val + + +# %% [markdown] +# ### Evaulates the Energy of the trial state using the mean values of the operators. +# +# Arguments +# * params (dict): is an dictionary containing the mean values form the measurements of the operators +# +# Return +# - en (real): energy of the system + + +# %% +def hilbert_schmidt(m1, m2): + """Hilbert-Schmidt-Product of two matrices m1, m2""" + return (np.dot(m1.conjugate().transpose(), m2)).trace() + + +def decompose(h): + """Decompose Hermitian matrix into Pauli matrices""" + + sx = np.array([[0, 1], [1, 0]], dtype=np.complex128) + sy = np.array([[0, -1j], [1j, 0]], dtype=np.complex128) + sz = np.array([[1, 0], [0, -1]], dtype=np.complex128) + mat_i = np.array([[1, 0], [0, 1]], dtype=np.complex128) + s_lst = [sx, sy, sz, mat_i] + labels = ['X', 'Y', 'Z', 'I'] + + decomposed_h = {} + for i in range(4): + for j in range(4): + for k in range(4): + label = labels[i] + labels[j] + labels[k] + a_ij = 0.125 * hilbert_schmidt(kron(kron(s_lst[i], s_lst[j]), s_lst[k]), h) + + if abs(a_ij) > 1e-6: + decomposed_h[label] = a_ij.real + return decomposed_h + + +a, delta = 0, 1 + +h = np.array( + [ + [0, a * np.sqrt(2), a * np.sqrt(2), 0, 0, 0, 1, 1], + [a * np.sqrt(2), 0, 0, 0, np.sqrt(2), 0, 0, 0], + [a * np.sqrt(2), 0, 0, 0, 0, np.sqrt(2), 0, 0], + [0, 0, 0, 2 * delta, 0, 0, 1, 1], + [0, np.sqrt(2), 0, 0, delta, 0, a, 0], + [0, 0, np.sqrt(2), 0, 0, delta, 0, a], + [1, 0, 0, 1, a, 0, delta, 0], + [1, 0, 0, 1, 0, a, 0, delta], + ] +) + +decomposed_h = decompose(h) +print(decomposed_h) + + +# %% +def hamiltonian(vqe_res): + en = 0.0 + for key in decomposed_h: + en += vqe_res[key] * decomposed_h[key] + return en + + +# %% [markdown] +# ### Executes the VQE algorithm. +# Creates and executes three quantum circuits, then evaluates the energy. +# +# Arguments +# * theta (real): is the parameters parametrizing the trial state +# +# Return +# - energy (real): the energy of the system +# +# + +# %% +np.random.seed(123) +theta = np.random.random(9) + +# %% +vqe_res = dict() +shots = 10**2 + +for op in decomposed_h: + qc = dq.QubitCircuit(3) + + qc = ansatz(qc, theta) + qc.barrier() + + vqe_res[op] = measurements(qc, op, shots) + +print(len(vqe_res), vqe_res) +energy = hamiltonian(vqe_res) +print(energy.real) +qc.draw() + + +# %% +def vqe_step(theta): + # Number of executions for each quantum circuit + shots = 2**10 + + vqe_res = dict() + # qc_list = dict() + + for op in decomposed_h: + qc = dq.QubitCircuit(3) + + # Implementation of the ansatz + qc = ansatz(qc, theta) + + # Just for plotting purposes + qc.barrier() + + # Measurements in the appropriate basis are implemented + + # Get the measurements results + + vqe_res[op] = measurements(qc, op, shots) + # qc_list[op] = qc + + energy = hamiltonian(vqe_res) + # print(theta, abs(energy + 1.23607)) + return energy + + +# %% [markdown] +# Let's try if it all works properly: + +# %% +# Set the value of theta +theta = np.random.random(9) + +# Run the VQE step to evaluate the energy (eigenvalue of the Hamiltonian) of the state with given theta +energy = vqe_step(theta) +print(energy) + +# %% [markdown] +# Our aim is to find a value for the parameter that yields the lowest possible energy, and that is the desired lowest eigenvalue for the Task. +# +# +# ##### Using an optimizator + +# %% + +# %% +# cma.s.pprint(cma.CMAOptions()) + +# %% +options = {'seed': 123, 'popsize': 30, 'maxiter': 100, 'verb_disp': 50} + +es = cma.CMAEvolutionStrategy(9 * [0], 0.6, options) +# with EvalParallel2(elli, es.popsize + 1) as eval_all: +# while not es.stop(): +# solutions = es.ask() +# # print(len(solutions)) +# es.tell(solutions, [vqe_step(x) for x in solutions]) +# es.manage_plateaus() +# es.logger.add() # write data to disc to be plotted +# es.disp() + + +while not es.stop(): + solutions = es.ask() + es.tell(solutions, [vqe_step(x) for x in solutions]) + # es.manage_plateaus() + es.logger.add() # write data to disc to be plotted + es.disp() + +es.result_pretty() + +# %% +print(es.result.fbest) + +# %% +cma.plot() diff --git a/pyproject.toml b/pyproject.toml index d5cd1584..e35b5452 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,16 +47,17 @@ classifiers = [ ] [project.optional-dependencies] -dev = ['pytest', +dev = ['graphix==0.3.1', + 'jupytext', + 'pennylane-sf', 'perceval-quandela', - 'strawberryfields', - 'thewalrus', - 'graphix==0.3.1', + 'pre-commit', + 'pytest', 'qutip', - 'pennylane-sf', - 'jupytext', 'ruff', - 'pre-commit'] + 'strawberryfields', + 'thewalrus' +] [project.urls] Homepage = 'https://deepquantum.turingq.com/' @@ -80,8 +81,11 @@ line-length = 120 quote-style = 'single' [tool.ruff.lint] -select = ['E', 'W', 'F', 'I', 'B', 'UP'] +select = ['E', 'W', 'F', 'I', 'B', 'UP', 'N', 'A', 'SIM'] [tool.ruff.lint.extend-per-file-ignores] '__init__.py' = ['F401'] -'tutorials/*.ipynb' = ['E402'] + +[tool.ruff.lint.isort] +order-by-type = false +case-sensitive = true diff --git a/requirements-dev.txt b/requirements-dev.txt index 45fc1ef8..5b75548b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,10 +1,10 @@ -pytest -perceval-quandela -strawberryfields @ git+https://github.com/XanaduAI/strawberryfields.git@master -thewalrus @ git+https://github.com/XanaduAI/thewalrus.git@master graphix==0.3.1 -qutip -pennylane-sf jupytext -ruff +pennylane-sf +perceval-quandela pre-commit +pytest +qutip +ruff +strawberryfields @ git+https://github.com/XanaduAI/strawberryfields.git@master +thewalrus @ git+https://github.com/XanaduAI/thewalrus.git@master diff --git a/src/deepquantum/__init__.py b/src/deepquantum/__init__.py index 1b0bdd7d..3b968ef1 100644 --- a/src/deepquantum/__init__.py +++ b/src/deepquantum/__init__.py @@ -1,32 +1,29 @@ -""" -This is the top level module from which all basic functions and classes of -DeepQuantum can be directly imported. -""" +"""This is the top level module from which all basic functions and classes of DeepQuantum can be directly imported.""" __version__ = '4.4.0' -from . import adjoint -from . import ansatz -from . import bitmath -from . import channel -from . import circuit -from . import communication -from . import cutting -from . import distributed -from . import gate -from . import layer -from . import operation -from . import optimizer -from . import qasm3 -from . import qmath -from . import qpd -from . import state -from . import utils - -from . import mbqc -from . import photonic - +from . import ( + adjoint, + ansatz, + bitmath, + channel, + circuit, + communication, + cutting, + distributed, + gate, + layer, + mbqc, + operation, + optimizer, + photonic, + qasm3, + qmath, + qpd, + state, + utils, +) from .ansatz import ( Ansatz, ControlledMultiplier, @@ -40,29 +37,75 @@ QuantumPhaseEstimationSingleQubit, RandomCircuitG3, ShorCircuit, - ShorCircuitFor15 + ShorCircuitFor15, +) +from .channel import ( + AmplitudeDamping, + BitFlip, + Depolarizing, + GeneralizedAmplitudeDamping, + Pauli, + PhaseDamping, + PhaseFlip, +) +from .circuit import DistributedQubitCircuit, QubitCircuit +from .communication import cleanup_distributed, setup_distributed +from .gate import ( + Barrier, + CNOT, + CombinedSingleGate, + Fredkin, + Hadamard, + HamiltonianGate, + Identity, + ImaginarySwap, + LatentGate, + PauliX, + PauliY, + PauliZ, + PhaseShift, + ProjectionJ, + ReconfigurableBeamSplitter, + Rx, + Rxx, + Rxy, + Ry, + Ryy, + Rz, + Rzz, + SDaggerGate, + SGate, + Swap, + TDaggerGate, + TGate, + Toffoli, + U3Gate, + UAnyGate, +) +from .layer import CnotLayer, CnotRing, HLayer, Observable, RxLayer, RyLayer, RzLayer, U3Layer, XLayer, YLayer, ZLayer +from .mbqc import GraphState, Pattern, SubGraphState +from .photonic import ( + BosonicState, + CatState, + Clements, + DistributedFockState, + DistributedQumodeCircuit, + DrawClements, + FockState, + FockStateBosonic, + GKPState, + GaussianBosonSampling, + GaussianState, + QumodeCircuit, + QumodeCircuitTDM, + UnitaryDecomposer, + UnitaryMapper, + hafnian, + permanent, + takagi, + torontonian, + williamson, ) -from .channel import BitFlip, PhaseFlip, Depolarizing, Pauli, AmplitudeDamping, PhaseDamping -from .channel import GeneralizedAmplitudeDamping -from .circuit import QubitCircuit, DistributedQubitCircuit -from .communication import setup_distributed, cleanup_distributed -from .gate import U3Gate, PhaseShift, Identity, PauliX, PauliY, PauliZ, Hadamard -from .gate import SGate, SDaggerGate, TGate, TDaggerGate -from .gate import Rx, Ry, Rz, ProjectionJ, CombinedSingleGate -from .gate import CNOT, Swap, ImaginarySwap, Rxx, Ryy, Rzz, Rxy, ReconfigurableBeamSplitter, Toffoli, Fredkin -from .gate import UAnyGate, LatentGate, HamiltonianGate, Barrier -from .layer import Observable, U3Layer, XLayer, YLayer, ZLayer, HLayer, RxLayer, RyLayer, RzLayer -from .layer import CnotLayer, CnotRing from .qasm3 import cir_to_qasm3, qasm3_to_cir -from .qmath import multi_kron, partial_trace, amplitude_encoding, measure, expectation -from .qmath import meyer_wallach_measure -from .state import QubitState, MatrixProductState, DistributedQubitState - -from .mbqc import SubGraphState, GraphState -from .mbqc import Pattern - -from .photonic import permanent, takagi, hafnian, torontonian, williamson -from .photonic import FockState, GaussianState, BosonicState, CatState, GKPState, FockStateBosonic -from .photonic import QumodeCircuit, QumodeCircuitTDM, Clements, GaussianBosonSampling -from .photonic import UnitaryMapper, UnitaryDecomposer, DrawClements -from .photonic import DistributedFockState, DistributedQumodeCircuit +from .qmath import amplitude_encoding, expectation, measure, meyer_wallach_measure, multi_kron, partial_trace +from .state import DistributedQubitState, MatrixProductState, QubitState diff --git a/src/deepquantum/adjoint.py b/src/deepquantum/adjoint.py index 035eebbc..6705492b 100644 --- a/src/deepquantum/adjoint.py +++ b/src/deepquantum/adjoint.py @@ -1,17 +1,14 @@ -""" -Adjoint differentiation -""" +"""Adjoint differentiation""" from copy import deepcopy -from typing import Tuple from typing import TYPE_CHECKING import torch from torch import nn from torch.autograd import Function -from .distributed import dist_one_targ_gate, dist_many_ctrl_one_targ_gate, dist_many_targ_gate, inner_product_dist -from .gate import SingleGate, CombinedSingleGate +from .distributed import dist_many_ctrl_one_targ_gate, dist_many_targ_gate, dist_one_targ_gate, inner_product_dist +from .gate import CombinedSingleGate, SingleGate from .operation import Gate from .state import DistributedQubitState @@ -30,13 +27,10 @@ class AdjointExpectation(Function): observable (Observable): The observable. *parameters (torch.Tensor): The parameters of the quantum circuit. """ + @staticmethod def forward( - ctx, - state: DistributedQubitState, - operators: nn.Sequential, - observable: 'Observable', - *parameters: torch.Tensor + ctx, state: DistributedQubitState, operators: nn.Sequential, observable: 'Observable', *parameters: torch.Tensor ) -> torch.Tensor: ctx.state_phi = state ctx.operators = operators @@ -46,7 +40,7 @@ def forward( return inner_product_dist(ctx.state_lambda, ctx.state_phi).real @staticmethod - def backward(ctx, grad_out: torch.Tensor) -> Tuple[None, ...]: + def backward(ctx, grad_out: torch.Tensor) -> tuple[None, ...]: parameters = [*ctx.saved_tensors] grads = [] idx = 1 @@ -62,7 +56,7 @@ def backward(ctx, grad_out: torch.Tensor) -> Tuple[None, ...]: ctx.state_phi = gate_dagger(ctx.state_phi) if gate.npara > 0: if parameters[-idx].requires_grad: - du_dx = gate.get_derivative(parameters[-idx]).unsqueeze(0).flatten(0, -3) # (npara, 2**n, 2**n) + du_dx = gate.get_derivative(parameters[-idx]).unsqueeze(0).flatten(0, -3) # (npara, 2**n, 2**n) wires = gate.controls + gate.wires targets = [gate.nqubit - wire - 1 for wire in wires] grads_gate = [] @@ -72,8 +66,9 @@ def backward(ctx, grad_out: torch.Tensor) -> Tuple[None, ...]: if len(gate.controls) == 0: state_mu = dist_one_targ_gate(state_mu, targets[0], mat) else: - state_mu = dist_many_ctrl_one_targ_gate(state_mu, targets[:-1], targets[-1], mat, - True) + state_mu = dist_many_ctrl_one_targ_gate( + state_mu, targets[:-1], targets[-1], mat, True + ) else: zeros = mat.new_zeros(2 ** len(wires) - 2 ** len(gate.wires)).diag_embed() matrix = torch.block_diag(zeros, mat) diff --git a/src/deepquantum/ansatz.py b/src/deepquantum/ansatz.py index a0eb0eb7..48a1fc50 100644 --- a/src/deepquantum/ansatz.py +++ b/src/deepquantum/ansatz.py @@ -1,16 +1,14 @@ -""" -Ansatze: various quantum circuits -""" +"""Ansatze: various quantum circuits""" import random -from typing import Any, List, Optional, Union +from typing import Any import numpy as np import torch -from .qmath import int_to_bitstring, is_unitary # avoid circular import from .circuit import QubitCircuit -from .gate import U3Gate, Rxx, Ryy, Rzz +from .gate import Rxx, Ryy, Rzz, U3Gate +from .qmath import int_to_bitstring, is_unitary # avoid circular import class Ansatz(QubitCircuit): @@ -32,22 +30,24 @@ class Ansatz(QubitCircuit): chi (int or None, optional): The bond dimension for matrix product state representation. Default: ``None`` """ + def __init__( self, nqubit: int, - wires: Union[int, List[int], None] = None, - minmax: Optional[List[int]] = None, - ancilla: Union[int, List[int], None] = None, - controls: Union[int, List[int], None] = None, + wires: int | list[int] | None = None, + minmax: list[int] | None = None, + ancilla: int | list[int] | None = None, + controls: int | list[int] | None = None, init_state: Any = 'zeros', - name: Optional[str] = None, + name: str | None = None, den_mat: bool = False, reupload: bool = False, mps: bool = False, - chi: Optional[int] = None + chi: int | None = None, ) -> None: - super().__init__(nqubit=nqubit, init_state=init_state, name=name, den_mat=den_mat, - reupload=reupload, mps=mps, chi=chi) + super().__init__( + nqubit=nqubit, init_state=init_state, name=name, den_mat=den_mat, reupload=reupload, mps=mps, chi=chi + ) if wires is None: if minmax is None: minmax = [0, nqubit - 1] @@ -88,19 +88,20 @@ class ControlledMultiplier(Ansatz): Default: ``None`` debug (bool, optional): Whether to print the debug information. Default: ``False`` """ + def __init__( self, nqubit: int, a: int, mod: int, - minmax: Optional[List[int]] = None, - nqubitx: Optional[int] = None, - ancilla: Union[int, List[int], None] = None, - controls: Union[int, List[int], None] = None, + minmax: list[int] | None = None, + nqubitx: int | None = None, + ancilla: int | list[int] | None = None, + controls: int | list[int] | None = None, den_mat: bool = False, mps: bool = False, - chi: Optional[int] = None, - debug: bool = False + chi: int | None = None, + debug: bool = False, ) -> None: assert isinstance(a, int) assert isinstance(mod, int) @@ -110,25 +111,43 @@ def __init__( nqubitx = len(bin(mod)) - 2 if ancilla is None: ancilla = [minmax[1] + 1] - super().__init__(nqubit=nqubit, wires=None, minmax=minmax, ancilla=ancilla, controls=controls, - init_state='zeros', name='ControlledMultiplier', den_mat=den_mat, mps=mps, chi=chi) + super().__init__( + nqubit=nqubit, + wires=None, + minmax=minmax, + ancilla=ancilla, + controls=controls, + init_state='zeros', + name='ControlledMultiplier', + den_mat=den_mat, + mps=mps, + chi=chi, + ) # one extra qubit to prevent overflow assert len(self.wires) >= nqubitx + len(bin(mod)) - 1, 'Quantum register is not enough.' minmax1 = [self.minmax[0], self.minmax[0] + nqubitx - 1] minmax2 = [minmax1[1] + 1, minmax[1]] - qft = QuantumFourierTransform(nqubit=nqubit, minmax=minmax2, reverse=True, - den_mat=self.den_mat, mps=self.mps, chi=self.chi) + qft = QuantumFourierTransform( + nqubit=nqubit, minmax=minmax2, reverse=True, den_mat=self.den_mat, mps=self.mps, chi=self.chi + ) iqft = qft.inverse() self.add(qft) - k = 0 - for i in range(minmax1[1], minmax1[0] - 1, -1): # the significant bit in |x> is reversed in Fig.6 + for k, i in enumerate(range(minmax1[1], minmax1[0] - 1, -1)): # the significant bit in |x> is reversed in Fig.6 if debug and 2**k * a >= 2 * mod: print(f'The number 2^{k}*{a} in {self.name} may be too large, unless the control qubit {i} is 0.') - pma = PhiModularAdder(nqubit=nqubit, number=2**k * a, mod=mod, minmax=minmax2, - ancilla=self.ancilla, controls=self.controls + [i], - den_mat=self.den_mat, mps=self.mps, chi=self.chi, debug=debug) + pma = PhiModularAdder( + nqubit=nqubit, + number=2**k * a, + mod=mod, + minmax=minmax2, + ancilla=self.ancilla, + controls=self.controls + [i], + den_mat=self.den_mat, + mps=self.mps, + chi=self.chi, + debug=debug, + ) self.add(pma) - k += 1 self.add(iqft) @@ -152,18 +171,19 @@ class ControlledUa(Ansatz): Default: ``None`` debug (bool, optional): Whether to print the debug information. Default: ``False`` """ + def __init__( self, nqubit: int, a: int, mod: int, - minmax: Optional[List[int]] = None, - ancilla: Union[int, List[int], None] = None, - controls: Union[int, List[int], None] = None, + minmax: list[int] | None = None, + ancilla: int | list[int] | None = None, + controls: int | list[int] | None = None, den_mat: bool = False, mps: bool = False, - chi: Optional[int] = None, - debug: bool = False + chi: int | None = None, + debug: bool = False, ) -> None: # |x> with n bits, |0> with n+1 bits and one extra ancilla bit nregister = len(bin(mod)) - 2 @@ -172,20 +192,50 @@ def __init__( minmax = [0, nregister - 1] if ancilla is None: ancilla = list(range(minmax[1] + 1, minmax[1] + 1 + nancilla)) - super().__init__(nqubit=nqubit, wires=None, minmax=minmax, ancilla=ancilla, controls=controls, - init_state='zeros', name='ControlledUa', den_mat=den_mat, mps=mps, chi=chi) + super().__init__( + nqubit=nqubit, + wires=None, + minmax=minmax, + ancilla=ancilla, + controls=controls, + init_state='zeros', + name='ControlledUa', + den_mat=den_mat, + mps=mps, + chi=chi, + ) assert len(self.wires) == nregister assert len(self.ancilla) == nancilla - cmult = ControlledMultiplier(nqubit=nqubit, a=a, mod=mod, minmax=[self.minmax[0], self.ancilla[-2]], - nqubitx=nregister, ancilla=self.ancilla[-1], controls=self.controls, - den_mat=self.den_mat, mps=self.mps, chi=self.chi, debug=debug) + cmult = ControlledMultiplier( + nqubit=nqubit, + a=a, + mod=mod, + minmax=[self.minmax[0], self.ancilla[-2]], + nqubitx=nregister, + ancilla=self.ancilla[-1], + controls=self.controls, + den_mat=self.den_mat, + mps=self.mps, + chi=self.chi, + debug=debug, + ) self.add(cmult) for i in range(len(self.wires)): self.swap([self.wires[i], self.ancilla[i + 1]], controls=self.controls) a_inv = pow(a, -1, mod) - cmult_inv = ControlledMultiplier(nqubit=nqubit, a=a_inv, mod=mod, minmax=[self.minmax[0], self.ancilla[-2]], - nqubitx=nregister, ancilla=self.ancilla[-1], controls=self.controls, - den_mat=self.den_mat, mps=self.mps, chi=self.chi, debug=debug).inverse() + cmult_inv = ControlledMultiplier( + nqubit=nqubit, + a=a_inv, + mod=mod, + minmax=[self.minmax[0], self.ancilla[-2]], + nqubitx=nregister, + ancilla=self.ancilla[-1], + controls=self.controls, + den_mat=self.den_mat, + mps=self.mps, + chi=self.chi, + debug=debug, + ).inverse() self.add(cmult_inv) @@ -203,6 +253,7 @@ class HHL(Ansatz): Default: ``None`` show_barrier (bool, optional): Whether to show the barriers in the circuit. Default: ``False`` """ + def __init__( self, ncount: int, @@ -210,33 +261,51 @@ def __init__( t0: float = 1, den_mat: bool = False, mps: bool = False, - chi: Optional[int] = None, - show_barrier: bool = False + chi: int | None = None, + show_barrier: bool = False, ) -> None: if not isinstance(mat, torch.Tensor): mat = torch.tensor(mat) t0 *= 2 * torch.pi - unitary = torch.linalg.matrix_exp(1j * mat * t0 / 2 ** ncount) + unitary = torch.linalg.matrix_exp(1j * mat * t0 / 2**ncount) assert is_unitary(unitary) nreg_i = int(np.log2(len(unitary))) nqubit = 1 + ncount + nreg_i self.unitary = unitary - super().__init__(nqubit=nqubit, wires=None, minmax=None, ancilla=None, controls=None, - init_state='zeros', name='HHL', den_mat=den_mat, mps=mps, chi=chi) - qpe = QuantumPhaseEstimation(nqubit=nqubit, ncount=ncount, unitary=unitary, minmax=[1, nqubit-1], - den_mat=self.den_mat, mps=self.mps, chi=self.chi, show_barrier=show_barrier) + super().__init__( + nqubit=nqubit, + wires=None, + minmax=None, + ancilla=None, + controls=None, + init_state='zeros', + name='HHL', + den_mat=den_mat, + mps=mps, + chi=chi, + ) + qpe = QuantumPhaseEstimation( + nqubit=nqubit, + ncount=ncount, + unitary=unitary, + minmax=[1, nqubit - 1], + den_mat=self.den_mat, + mps=self.mps, + chi=self.chi, + show_barrier=show_barrier, + ) self.add(qpe) if show_barrier: self.barrier() - for i in range(2 ** ncount): + for i in range(2**ncount): for j in range(ncount): - if format(i, '0' + str(ncount) + 'b')[ncount-j-1] == '0': + if format(i, '0' + str(ncount) + 'b')[ncount - j - 1] == '0': self.x(1 + j) - theta = 2 * torch.pi * i / 2 ** ncount - self.ry(0, inputs=theta, controls=list(range(1, ncount+1))) + theta = 2 * torch.pi * i / 2**ncount + self.ry(0, inputs=theta, controls=list(range(1, ncount + 1))) for j in range(ncount): - if format(i, '0' + str(ncount) + 'b')[ncount-j-1] == '0': + if format(i, '0' + str(ncount) + 'b')[ncount - j - 1] == '0': self.x(1 + j) if show_barrier: self.barrier() @@ -260,17 +329,28 @@ class NumberEncoder(Ansatz): chi (int or None, optional): The bond dimension for matrix product state representation. Default: ``None`` """ + def __init__( self, nqubit: int, number: int, - minmax: Optional[List[int]] = None, + minmax: list[int] | None = None, den_mat: bool = False, mps: bool = False, - chi: Optional[int] = None + chi: int | None = None, ) -> None: - super().__init__(nqubit=nqubit, wires=None, minmax=minmax, ancilla=None, controls=None, - init_state='zeros', name='NumberEncoder', den_mat=den_mat, mps=mps, chi=chi) + super().__init__( + nqubit=nqubit, + wires=None, + minmax=minmax, + ancilla=None, + controls=None, + init_state='zeros', + name='NumberEncoder', + den_mat=den_mat, + mps=mps, + chi=chi, + ) bits = int_to_bitstring(number, len(self.wires)) for i, wire in enumerate(self.wires): if bits[i] == '1': @@ -294,27 +374,36 @@ class PhiAdder(Ansatz): Default: ``None`` debug (bool, optional): Whether to print the debug information. Default: ``False`` """ + def __init__( self, nqubit: int, number: int, - minmax: Optional[List[int]] = None, - controls: Union[int, List[int], None] = None, + minmax: list[int] | None = None, + controls: int | list[int] | None = None, den_mat: bool = False, mps: bool = False, - chi: Optional[int] = None, - debug: bool = False + chi: int | None = None, + debug: bool = False, ) -> None: - super().__init__(nqubit=nqubit, wires=None, minmax=minmax, ancilla=None, controls=controls, - init_state='zeros', name='PhiAdder', den_mat=den_mat, mps=mps, chi=chi) + super().__init__( + nqubit=nqubit, + wires=None, + minmax=minmax, + ancilla=None, + controls=controls, + init_state='zeros', + name='PhiAdder', + den_mat=den_mat, + mps=mps, + chi=chi, + ) bits = int_to_bitstring(number, len(self.wires), debug=debug) for i, wire in enumerate(self.wires): phi = 0 - k = 0 - for j in range(i, len(bits)): + for k, j in enumerate(range(i, len(bits))): if bits[j] == '1': - phi += torch.pi / 2 ** k - k += 1 + phi += torch.pi / 2**k if phi != 0: self.p(wires=wire, inputs=phi, controls=self.controls) @@ -338,36 +427,65 @@ class PhiModularAdder(Ansatz): Default: ``None`` debug (bool, optional): Whether to print the debug information. Default: ``False`` """ + def __init__( self, nqubit: int, number: int, mod: int, - minmax: Optional[List[int]] = None, - ancilla: Union[int, List[int], None] = None, - controls: Union[int, List[int], None] = None, + minmax: list[int] | None = None, + ancilla: int | list[int] | None = None, + controls: int | list[int] | None = None, den_mat: bool = False, mps: bool = False, - chi: Optional[int] = None, - debug: bool = False + chi: int | None = None, + debug: bool = False, ) -> None: if minmax is None: minmax = [0, nqubit - 2] if ancilla is None: ancilla = [minmax[1] + 1] - super().__init__(nqubit=nqubit, wires=None, minmax=minmax, ancilla=ancilla, controls=controls, - init_state='zeros', name='PhiModularAdder', den_mat=den_mat, mps=mps, chi=chi) + super().__init__( + nqubit=nqubit, + wires=None, + minmax=minmax, + ancilla=ancilla, + controls=controls, + init_state='zeros', + name='PhiModularAdder', + den_mat=den_mat, + mps=mps, + chi=chi, + ) if debug and number >= 2 * mod: print(f'The number {number} in {self.name} is too large.') - phi_add_number = PhiAdder(nqubit=nqubit, number=number, minmax=self.minmax, controls=self.controls, - den_mat=self.den_mat, mps=self.mps, chi=self.chi, debug=debug) + phi_add_number = PhiAdder( + nqubit=nqubit, + number=number, + minmax=self.minmax, + controls=self.controls, + den_mat=self.den_mat, + mps=self.mps, + chi=self.chi, + debug=debug, + ) phi_sub_number = phi_add_number.inverse() - phi_add_mod = PhiAdder(nqubit=nqubit, number=mod, minmax=self.minmax, controls=self.ancilla, - den_mat=self.den_mat, mps=self.mps, chi=self.chi, debug=debug) - phi_sub_mod = PhiAdder(nqubit=nqubit, number=mod, minmax=self.minmax, - den_mat=self.den_mat, mps=self.mps, chi=self.chi, debug=debug).inverse() - qft = QuantumFourierTransform(nqubit=nqubit, minmax=self.minmax, reverse=True, - den_mat=self.den_mat, mps=self.mps, chi=self.chi) + phi_add_mod = PhiAdder( + nqubit=nqubit, + number=mod, + minmax=self.minmax, + controls=self.ancilla, + den_mat=self.den_mat, + mps=self.mps, + chi=self.chi, + debug=debug, + ) + phi_sub_mod = PhiAdder( + nqubit=nqubit, number=mod, minmax=self.minmax, den_mat=self.den_mat, mps=self.mps, chi=self.chi, debug=debug + ).inverse() + qft = QuantumFourierTransform( + nqubit=nqubit, minmax=self.minmax, reverse=True, den_mat=self.den_mat, mps=self.mps, chi=self.chi + ) iqft = qft.inverse() self.add(phi_add_number) self.add(phi_sub_mod) @@ -403,20 +521,30 @@ class QuantumConvolutionalNeuralNetwork(Ansatz): chi (int or None, optional): The bond dimension for matrix product state representation. Default: ``None`` """ + def __init__( self, nqubit: int, nlayer: int, - minmax: Optional[List[int]] = None, + minmax: list[int] | None = None, init_state: Any = 'zeros', den_mat: bool = False, requires_grad: bool = True, mps: bool = False, - chi: Optional[int] = None + chi: int | None = None, ) -> None: - super().__init__(nqubit=nqubit, wires=None, minmax=minmax, ancilla=None, controls=None, - init_state=init_state, name='QuantumConvolutionalNeuralNetwork', den_mat=den_mat, - mps=mps, chi=chi) + super().__init__( + nqubit=nqubit, + wires=None, + minmax=minmax, + ancilla=None, + controls=None, + init_state=init_state, + name='QuantumConvolutionalNeuralNetwork', + den_mat=den_mat, + mps=mps, + chi=chi, + ) wires = self.wires self.requires_grad = requires_grad u1 = U3Gate(nqubit=nqubit, den_mat=den_mat, requires_grad=requires_grad) @@ -430,7 +558,7 @@ def __init__( wires = wires[::2] self.latent(wires=wires) - def conv(self, wires: List[int]) -> None: + def conv(self, wires: list[int]) -> None: rxx = Rxx(nqubit=self.nqubit, den_mat=self.den_mat, requires_grad=self.requires_grad) ryy = Ryy(nqubit=self.nqubit, den_mat=self.den_mat, requires_grad=self.requires_grad) rzz = Rzz(nqubit=self.nqubit, den_mat=self.den_mat, requires_grad=self.requires_grad) @@ -444,7 +572,7 @@ def conv(self, wires: List[int]) -> None: self.add(u1, wires=wires[2 * i + start - 1]) self.add(u2, wires=wire) - def pool(self, wires: List[int]) -> None: + def pool(self, wires: list[int]) -> None: cu = U3Gate(nqubit=self.nqubit, den_mat=self.den_mat, requires_grad=self.requires_grad) for i, wire in enumerate(wires[1::2]): self.add(cu, wires=wires[2 * i], controls=wire) @@ -467,20 +595,30 @@ class QuantumFourierTransform(Ansatz): Default: ``None`` show_barrier (bool, optional): Whether to show the barriers in the circuit. Default: ``False`` """ + def __init__( self, nqubit: int, - minmax: Optional[List[int]] = None, + minmax: list[int] | None = None, reverse: bool = False, init_state: Any = 'zeros', den_mat: bool = False, mps: bool = False, - chi: Optional[int] = None, - show_barrier: bool = False + chi: int | None = None, + show_barrier: bool = False, ) -> None: - super().__init__(nqubit=nqubit, wires=None, minmax=minmax, ancilla=None, controls=None, - init_state=init_state, name='QuantumFourierTransform', den_mat=den_mat, - mps=mps, chi=chi) + super().__init__( + nqubit=nqubit, + wires=None, + minmax=minmax, + ancilla=None, + controls=None, + init_state=init_state, + name='QuantumFourierTransform', + den_mat=den_mat, + mps=mps, + chi=chi, + ) self.reverse = reverse for i in self.wires: self.qft_block(i) @@ -513,16 +651,17 @@ class QuantumPhaseEstimation(Ansatz): Default: ``None`` show_barrier (bool, optional): Whether to show the barriers in the circuit. Default: ``False`` """ + def __init__( self, nqubit: int, ncount: int, unitary: Any, - minmax: Optional[List[int]] = None, + minmax: list[int] | None = None, den_mat: bool = False, mps: bool = False, - chi: Optional[int] = None, - show_barrier: bool = False + chi: int | None = None, + show_barrier: bool = False, ) -> None: if not isinstance(unitary, torch.Tensor): unitary = torch.tensor(unitary, dtype=torch.cfloat) @@ -532,9 +671,18 @@ def __init__( minmax = [0, ncount + nreg_i - 1] assert minmax[1] - minmax[0] == ncount + nreg_i - 1 self.unitary = unitary - super().__init__(nqubit=nqubit, wires=None, minmax=minmax, ancilla=None, controls=None, - init_state='zeros', name='QuantumPhaseEstimation', den_mat=den_mat, - mps=mps, chi=chi) + super().__init__( + nqubit=nqubit, + wires=None, + minmax=minmax, + ancilla=None, + controls=None, + init_state='zeros', + name='QuantumPhaseEstimation', + den_mat=den_mat, + mps=mps, + chi=chi, + ) wires_c = list(range(minmax[0], minmax[0] + ncount)) wires_i = list(range(minmax[0] + ncount, minmax[1] + 1)) self.hlayer(wires_c) @@ -545,8 +693,14 @@ def __init__( self.any(unitary=u, wires=wires_i, controls=wire) if show_barrier: self.barrier() - iqft = QuantumFourierTransform(nqubit=nqubit, minmax=[wires_c[0], wires_c[-1]], den_mat=self.den_mat, - mps=self.mps, chi=self.chi, show_barrier=show_barrier).inverse() + iqft = QuantumFourierTransform( + nqubit=nqubit, + minmax=[wires_c[0], wires_c[-1]], + den_mat=self.den_mat, + mps=self.mps, + chi=self.chi, + show_barrier=show_barrier, + ).inverse() self.add(iqft) @@ -561,25 +715,29 @@ class QuantumPhaseEstimationSingleQubit(Ansatz): chi (int or None, optional): The bond dimension for matrix product state representation. Default: ``None`` """ - def __init__( - self, - t: int, - phase: Any, - den_mat: bool = False, - mps: bool = False, - chi: Optional[int] = None - ) -> None: + + def __init__(self, t: int, phase: Any, den_mat: bool = False, mps: bool = False, chi: int | None = None) -> None: nqubit = t + 1 self.phase = phase - super().__init__(nqubit=nqubit, wires=None, minmax=None, ancilla=None, controls=None, - init_state='zeros', name='QuantumPhaseEstimationSingleQubit', den_mat=den_mat, - mps=mps, chi=chi) + super().__init__( + nqubit=nqubit, + wires=None, + minmax=None, + ancilla=None, + controls=None, + init_state='zeros', + name='QuantumPhaseEstimationSingleQubit', + den_mat=den_mat, + mps=mps, + chi=chi, + ) self.hlayer(list(range(t))) self.x(t) for i in range(t): self.cp(i, t, torch.pi * phase * (2 ** (t - i))) - iqft = QuantumFourierTransform(nqubit=nqubit, minmax=[0, t - 1], - den_mat=self.den_mat, mps=self.mps, chi=self.chi).inverse() + iqft = QuantumFourierTransform( + nqubit=nqubit, minmax=[0, t - 1], den_mat=self.den_mat, mps=self.mps, chi=self.chi + ).inverse() self.add(iqft) @@ -599,27 +757,35 @@ class RandomCircuitG3(Ansatz): chi (int or None, optional): The bond dimension for matrix product state representation. Default: ``None`` """ + def __init__( self, nqubit: int, ngate: int, - wires: Optional[List[int]] = None, - minmax: Optional[List[int]] = None, + wires: list[int] | None = None, + minmax: list[int] | None = None, init_state: Any = 'zeros', den_mat: bool = False, mps: bool = False, - chi: Optional[int] = None + chi: int | None = None, ) -> None: - super().__init__(nqubit=nqubit, wires=wires, minmax=minmax, ancilla=None, controls=None, - init_state=init_state, name='RandomCircuitG3', den_mat=den_mat, mps=mps, chi=chi) + super().__init__( + nqubit=nqubit, + wires=wires, + minmax=minmax, + ancilla=None, + controls=None, + init_state=init_state, + name='RandomCircuitG3', + den_mat=den_mat, + mps=mps, + chi=chi, + ) self.ngate = ngate self.gate_set = ['CNOT', 'H', 'T'] for _ in range(ngate): gate = random.sample(self.gate_set, 1)[0] - if gate == 'CNOT': - wire = random.sample(self.wires, 2) - else: - wire = random.sample(self.wires, 1) + wire = random.sample(self.wires, 2) if gate == 'CNOT' else random.sample(self.wires, 1) if gate == 'CNOT': self.cnot(wire[0], wire[1]) elif gate == 'H': @@ -641,6 +807,7 @@ class ShorCircuit(Ansatz): Default: ``None`` debug (bool, optional): Whether to print the debug information. Default: ``False`` """ + def __init__( self, mod: int, @@ -648,30 +815,49 @@ def __init__( a: int, den_mat: bool = False, mps: bool = False, - chi: Optional[int] = None, - debug: bool = False + chi: int | None = None, + debug: bool = False, ) -> None: nreg = len(bin(mod)) - 2 nqubit = ncount + 2 * nreg + 2 - super().__init__(nqubit=nqubit, wires=None, minmax=None, ancilla=None, controls=None, - init_state='zeros', name='ShorCircuit', den_mat=den_mat, mps=mps, chi=chi) + super().__init__( + nqubit=nqubit, + wires=None, + minmax=None, + ancilla=None, + controls=None, + init_state='zeros', + name='ShorCircuit', + den_mat=den_mat, + mps=mps, + chi=chi, + ) minmax1 = [0, ncount - 1] minmax2 = [ncount, ncount + nreg - 1] ancilla = list(range(ncount + nreg, nqubit)) self.hlayer(list(range(ncount))) self.x(ncount + nreg - 1) - n = 0 - for i in range(ncount - 1, -1, -1): + for n, i in enumerate(range(ncount - 1, -1, -1)): # Compute a^{2^n} (mod N) by repeated squaring an = a for _ in range(n): - an = an ** 2 % mod - cua = ControlledUa(nqubit=nqubit, a=an, mod=mod, minmax=minmax2, ancilla=ancilla, controls=[i], - den_mat=self.den_mat, mps=self.mps, chi=self.chi, debug=debug) + an = an**2 % mod + cua = ControlledUa( + nqubit=nqubit, + a=an, + mod=mod, + minmax=minmax2, + ancilla=ancilla, + controls=[i], + den_mat=self.den_mat, + mps=self.mps, + chi=self.chi, + debug=debug, + ) self.add(cua) - n += 1 - iqft = QuantumFourierTransform(nqubit=nqubit, minmax=minmax1, - den_mat=self.den_mat, mps=self.mps, chi=self.chi).inverse() + iqft = QuantumFourierTransform( + nqubit=nqubit, minmax=minmax1, den_mat=self.den_mat, mps=self.mps, chi=self.chi + ).inverse() self.add(iqft) @@ -688,32 +874,35 @@ class ShorCircuitFor15(Ansatz): chi (int or None, optional): The bond dimension for matrix product state representation. Default: ``None`` """ - def __init__( - self, - ncount: int, - a: int, - den_mat: bool = False, - mps: bool = False, - chi: Optional[int] = None - ) -> None: + + def __init__(self, ncount: int, a: int, den_mat: bool = False, mps: bool = False, chi: int | None = None) -> None: mod = 15 nreg = len(bin(mod)) - 2 nqubit = ncount + nreg self.ncount = ncount - super().__init__(nqubit=nqubit, wires=None, minmax=None, ancilla=None, controls=None, - init_state='zeros', name='ShorCircuitFor15', den_mat=den_mat, mps=mps, chi=chi) + super().__init__( + nqubit=nqubit, + wires=None, + minmax=None, + ancilla=None, + controls=None, + init_state='zeros', + name='ShorCircuitFor15', + den_mat=den_mat, + mps=mps, + chi=chi, + ) minmax = [0, ncount - 1] self.hlayer(list(range(ncount))) self.x(ncount + nreg - 1) - n = 0 - for i in range(ncount - 1, -1, -1): - self.cua(a, 2 ** n, i) - n += 1 - iqft = QuantumFourierTransform(nqubit=nqubit, minmax=minmax, - den_mat=self.den_mat, mps=self.mps, chi=self.chi).inverse() + for n, i in enumerate(range(ncount - 1, -1, -1)): + self.cua(a, 2**n, i) + iqft = QuantumFourierTransform( + nqubit=nqubit, minmax=minmax, den_mat=self.den_mat, mps=self.mps, chi=self.chi + ).inverse() self.add(iqft) - def cua(self, a: int, power: int, controls: Union[int, List[int], None]) -> None: + def cua(self, a: int, power: int, controls: int | list[int] | None) -> None: assert a in [2, 4, 7, 8, 11, 13] for _ in range(power): if a in [2, 13]: diff --git a/src/deepquantum/bitmath.py b/src/deepquantum/bitmath.py index c441d94b..53a544ee 100644 --- a/src/deepquantum/bitmath.py +++ b/src/deepquantum/bitmath.py @@ -1,53 +1,55 @@ -""" -Bit-twiddling functions of unsigned integers -""" - -from typing import List, Union +"""Bit-twiddling functions of unsigned integers""" import torch def power_of_2(exp: int) -> int: + """Calculate 2 raised to the power of the given exponent.""" return 1 << exp def is_power_of_2(number: int) -> bool: + """Check if a number is a power of 2.""" return (number > 0) and (number & (number - 1) == 0) def log_base2(number: int) -> int: + """Calculate the base-2 logarithm of a power-of-2 number.""" assert is_power_of_2(number) return number.bit_length() - 1 # See https://arxiv.org/abs/2311.01512 Alg.1 -def get_bit(number: Union[int, torch.Tensor], bit_index: int) -> Union[int, torch.Tensor]: +def get_bit(number: int | torch.Tensor, bit_index: int) -> int | torch.Tensor: + """Get the value of a specific bit in an integer.""" return (number >> bit_index) & 1 -def flip_bit(number: Union[int, torch.Tensor], bit_index: int) -> Union[int, torch.Tensor]: +def flip_bit(number: int | torch.Tensor, bit_index: int) -> int | torch.Tensor: + """Flip the value of a specific bit in an integer.""" return number ^ (1 << bit_index) -def flip_bits(number: Union[int, torch.Tensor], bit_indices: List[int]) -> Union[int, torch.Tensor]: +def flip_bits(number: int | torch.Tensor, bit_indices: list[int]) -> int | torch.Tensor: + """Flip the values of multiple specific bits in an integer.""" for bit_index in bit_indices: number = flip_bit(number, bit_index) return number -def insert_bit(number: Union[int, torch.Tensor], bit_index: int, bit_value: int) -> Union[int, torch.Tensor]: +def insert_bit(number: int | torch.Tensor, bit_index: int, bit_value: int) -> int | torch.Tensor: + """Insert a bit value at a specific index in an integer.""" left = (number >> bit_index) << (bit_index + 1) middle = bit_value << bit_index right = number & ((1 << bit_index) - 1) return left | middle | right -def all_bits_are_one(number: int, bit_indices: List[int]) -> bool: - for i in bit_indices: - if not get_bit(number, i): - return False - return True +def all_bits_are_one(number: int, bit_indices: list[int]) -> bool: + """Check if all specified bits in an integer are set to 1.""" + return all(get_bit(number, i) for i in bit_indices) -def get_bit_mask(bit_indices: List[int]) -> int: +def get_bit_mask(bit_indices: list[int]) -> int: + """Get the integer representation of a bitmask with bits set at the specified indices.""" return flip_bits(0, bit_indices) diff --git a/src/deepquantum/channel.py b/src/deepquantum/channel.py index afe53b27..9edada2a 100644 --- a/src/deepquantum/channel.py +++ b/src/deepquantum/channel.py @@ -1,15 +1,12 @@ -""" -Quantum channels -""" +"""Quantum channels""" -from typing import Any, List, Union +from typing import Any import torch from .gate import Identity, PauliX, PauliY, PauliZ from .operation import Channel - mat_i = Identity().matrix mat_x = PauliX().matrix mat_y = PauliY().matrix @@ -34,16 +31,18 @@ class BitFlip(Channel): requires_grad (bool, optional): Whether the parameter is ``nn.Parameter`` or ``buffer``. Default: ``False`` (which means ``buffer``) """ + def __init__( self, inputs: Any = None, nqubit: int = 1, - wires: Union[int, List[int], None] = None, + wires: int | list[int] | None = None, tsr_mode: bool = False, - requires_grad: bool = False + requires_grad: bool = False, ) -> None: - super().__init__(inputs=inputs, name='BitFlip', nqubit=nqubit, wires=wires, tsr_mode=tsr_mode, - requires_grad=requires_grad) + super().__init__( + inputs=inputs, name='BitFlip', nqubit=nqubit, wires=wires, tsr_mode=tsr_mode, requires_grad=requires_grad + ) def get_matrix(self, theta: Any) -> torch.Tensor: """Get the local Kraus matrices acting on density matrices.""" @@ -75,16 +74,18 @@ class PhaseFlip(Channel): requires_grad (bool, optional): Whether the parameter is ``nn.Parameter`` or ``buffer``. Default: ``False`` (which means ``buffer``) """ + def __init__( self, inputs: Any = None, nqubit: int = 1, - wires: Union[int, List[int], None] = None, + wires: int | list[int] | None = None, tsr_mode: bool = False, - requires_grad: bool = False + requires_grad: bool = False, ) -> None: - super().__init__(inputs=inputs, name='PhaseFlip', nqubit=nqubit, wires=wires, tsr_mode=tsr_mode, - requires_grad=requires_grad) + super().__init__( + inputs=inputs, name='PhaseFlip', nqubit=nqubit, wires=wires, tsr_mode=tsr_mode, requires_grad=requires_grad + ) def get_matrix(self, theta: Any) -> torch.Tensor: """Get the local Kraus matrices acting on density matrices.""" @@ -119,16 +120,23 @@ class Depolarizing(Channel): requires_grad (bool, optional): Whether the parameter is ``nn.Parameter`` or ``buffer``. Default: ``False`` (which means ``buffer``) """ + def __init__( self, inputs: Any = None, nqubit: int = 1, - wires: Union[int, List[int], None] = None, + wires: int | list[int] | None = None, tsr_mode: bool = False, - requires_grad: bool = False + requires_grad: bool = False, ) -> None: - super().__init__(inputs=inputs, name='Depolarizing', nqubit=nqubit, wires=wires, tsr_mode=tsr_mode, - requires_grad=requires_grad) + super().__init__( + inputs=inputs, + name='Depolarizing', + nqubit=nqubit, + wires=wires, + tsr_mode=tsr_mode, + requires_grad=requires_grad, + ) def get_matrix(self, theta: Any) -> torch.Tensor: """Get the local Kraus matrices acting on density matrices.""" @@ -165,16 +173,18 @@ class Pauli(Channel): requires_grad (bool, optional): Whether the parameter is ``nn.Parameter`` or ``buffer``. Default: ``False`` (which means ``buffer``) """ + def __init__( self, inputs: Any = None, nqubit: int = 1, - wires: Union[int, List[int], None] = None, + wires: int | list[int] | None = None, tsr_mode: bool = False, - requires_grad: bool = False + requires_grad: bool = False, ) -> None: - super().__init__(inputs=inputs, name='Pauli', nqubit=nqubit, wires=wires, tsr_mode=tsr_mode, - requires_grad=requires_grad) + super().__init__( + inputs=inputs, name='Pauli', nqubit=nqubit, wires=wires, tsr_mode=tsr_mode, requires_grad=requires_grad + ) self.npara = 4 @property @@ -196,7 +206,7 @@ def get_matrix(self, theta: Any) -> torch.Tensor: theta = self.inputs_to_tensor(theta).reshape(-1) prob = torch.sin(theta) ** 2 prob = prob / prob.sum() - mat1 = torch.sqrt(prob[0:1]) * mat_i.to(prob.device) # avoid scalar Tensor + mat1 = torch.sqrt(prob[0:1]) * mat_i.to(prob.device) # avoid scalar Tensor mat2 = torch.sqrt(prob[1:2]) * mat_x.to(prob.device) mat3 = torch.sqrt(prob[2:3]) * mat_y.to(prob.device) mat4 = torch.sqrt(prob[3:4]) * mat_z.to(prob.device) @@ -226,23 +236,30 @@ class AmplitudeDamping(Channel): requires_grad (bool, optional): Whether the parameter is ``nn.Parameter`` or ``buffer``. Default: ``False`` (which means ``buffer``) """ + def __init__( self, inputs: Any = None, nqubit: int = 1, - wires: Union[int, List[int], None] = None, + wires: int | list[int] | None = None, tsr_mode: bool = False, - requires_grad: bool = False + requires_grad: bool = False, ) -> None: - super().__init__(inputs=inputs, name='AmplitudeDamping', nqubit=nqubit, wires=wires, tsr_mode=tsr_mode, - requires_grad=requires_grad) + super().__init__( + inputs=inputs, + name='AmplitudeDamping', + nqubit=nqubit, + wires=wires, + tsr_mode=tsr_mode, + requires_grad=requires_grad, + ) def get_matrix(self, theta: Any) -> torch.Tensor: """Get the local Kraus matrices acting on density matrices.""" theta = self.inputs_to_tensor(theta).reshape(-1) prob = torch.sin(theta) ** 2 - m0 = torch.tensor([0.], dtype=prob.dtype, device=prob.device) - m1 = torch.tensor([1.], dtype=prob.dtype, device=prob.device) + m0 = torch.tensor([0.0], dtype=prob.dtype, device=prob.device) + m1 = torch.tensor([1.0], dtype=prob.dtype, device=prob.device) mat1 = torch.stack([m1, m0, m0, torch.sqrt(1 - prob)]).reshape(2, 2) mat2 = torch.stack([m0, torch.sqrt(prob), m0, m0]).reshape(2, 2) return torch.stack([mat1, mat2]) + 0j @@ -271,23 +288,30 @@ class PhaseDamping(Channel): requires_grad (bool, optional): Whether the parameter is ``nn.Parameter`` or ``buffer``. Default: ``False`` (which means ``buffer``) """ + def __init__( self, inputs: Any = None, nqubit: int = 1, - wires: Union[int, List[int], None] = None, + wires: int | list[int] | None = None, tsr_mode: bool = False, - requires_grad: bool = False + requires_grad: bool = False, ) -> None: - super().__init__(inputs=inputs, name='PhaseDamping', nqubit=nqubit, wires=wires, tsr_mode=tsr_mode, - requires_grad=requires_grad) + super().__init__( + inputs=inputs, + name='PhaseDamping', + nqubit=nqubit, + wires=wires, + tsr_mode=tsr_mode, + requires_grad=requires_grad, + ) def get_matrix(self, theta: Any) -> torch.Tensor: """Get the local Kraus matrices acting on density matrices.""" theta = self.inputs_to_tensor(theta).reshape(-1) prob = torch.sin(theta) ** 2 - m0 = torch.tensor([0.], dtype=prob.dtype, device=prob.device) - m1 = torch.tensor([1.], dtype=prob.dtype, device=prob.device) + m0 = torch.tensor([0.0], dtype=prob.dtype, device=prob.device) + m1 = torch.tensor([1.0], dtype=prob.dtype, device=prob.device) mat1 = torch.stack([m1, m0, m0, torch.sqrt(1 - prob)]).reshape(2, 2) mat2 = torch.stack([m0, m0, m0, torch.sqrt(prob)]).reshape(2, 2) return torch.stack([mat1, mat2]) + 0j @@ -320,16 +344,23 @@ class GeneralizedAmplitudeDamping(Channel): requires_grad (bool, optional): Whether the parameter is ``nn.Parameter`` or ``buffer``. Default: ``False`` (which means ``buffer``) """ + def __init__( self, inputs: Any = None, nqubit: int = 1, - wires: Union[int, List[int], None] = None, + wires: int | list[int] | None = None, tsr_mode: bool = False, - requires_grad: bool = False + requires_grad: bool = False, ) -> None: - super().__init__(inputs=inputs, name='GeneralizedAmplitudeDamping', nqubit=nqubit, wires=wires, - tsr_mode=tsr_mode, requires_grad=requires_grad) + super().__init__( + inputs=inputs, + name='GeneralizedAmplitudeDamping', + nqubit=nqubit, + wires=wires, + tsr_mode=tsr_mode, + requires_grad=requires_grad, + ) self.npara = 2 def inputs_to_tensor(self, inputs: Any = None) -> torch.Tensor: @@ -344,8 +375,8 @@ def get_matrix(self, theta: Any) -> torch.Tensor: """Get the local Kraus matrices acting on density matrices.""" theta = self.inputs_to_tensor(theta).reshape(-1) prob = torch.sin(theta) ** 2 - m0 = torch.tensor(0., dtype=prob.dtype, device=prob.device) - m1 = torch.tensor(1., dtype=prob.dtype, device=prob.device) + m0 = torch.tensor(0.0, dtype=prob.dtype, device=prob.device) + m1 = torch.tensor(1.0, dtype=prob.dtype, device=prob.device) mat1 = torch.sqrt(prob[0]) * torch.stack([m1, m0, m0, torch.sqrt(1 - prob[1])]).reshape(2, 2) mat2 = torch.sqrt(prob[0]) * torch.stack([m0, torch.sqrt(prob[1]), m0, m0]).reshape(2, 2) mat3 = torch.sqrt(1 - prob[0]) * torch.stack([torch.sqrt(1 - prob[1]), m0, m0, m1]).reshape(2, 2) diff --git a/src/deepquantum/circuit.py b/src/deepquantum/circuit.py index 31fea859..24759ccf 100644 --- a/src/deepquantum/circuit.py +++ b/src/deepquantum/circuit.py @@ -1,13 +1,10 @@ -""" -Quantum circuit -""" +"""Quantum circuit""" -from copy import copy, deepcopy from collections import defaultdict -from collections.abc import Sequence, Hashable +from collections.abc import Hashable, Sequence +from copy import copy, deepcopy from itertools import product -from typing import Any, Dict, List, Optional, Tuple, Union -from typing import TYPE_CHECKING +from typing import Any, TYPE_CHECKING import numpy as np import torch @@ -15,21 +12,66 @@ from torch import nn, vmap from .adjoint import AdjointExpectation -from .channel import BitFlip, PhaseFlip, Depolarizing, Pauli, AmplitudeDamping, PhaseDamping -from .channel import GeneralizedAmplitudeDamping -from .cutting import transform_cut2move, partition_problem +from .channel import ( + AmplitudeDamping, + BitFlip, + Depolarizing, + GeneralizedAmplitudeDamping, + Pauli, + PhaseDamping, + PhaseFlip, +) +from .cutting import partition_problem, transform_cut2move from .distributed import measure_dist -from .gate import ParametricSingleGate -from .gate import U3Gate, PhaseShift, PauliX, PauliY, PauliZ, Hadamard, SGate, SDaggerGate, TGate, TDaggerGate -from .gate import Rx, Ry, Rz, ProjectionJ, CNOT, Swap, ImaginarySwap, Rxx, Ryy, Rzz, Rxy, ReconfigurableBeamSplitter -from .gate import Toffoli, Fredkin -from .gate import CombinedSingleGate, UAnyGate, LatentGate, HamiltonianGate, Reset, Barrier, WireCut, Move -from .layer import Observable, U3Layer, XLayer, YLayer, ZLayer, HLayer, RxLayer, RyLayer, RzLayer, CnotLayer, CnotRing -from .operation import Operation, Gate, Layer, Channel, MeasureQPD -from .qmath import amplitude_encoding, measure, expectation, sample_sc_mcmc, sample2expval -from .qmath import slice_state_vector, inner_product_mps, get_prob_mps +from .gate import ( + Barrier, + CNOT, + CombinedSingleGate, + Fredkin, + Hadamard, + HamiltonianGate, + ImaginarySwap, + LatentGate, + Move, + ParametricSingleGate, + PauliX, + PauliY, + PauliZ, + PhaseShift, + ProjectionJ, + ReconfigurableBeamSplitter, + Reset, + Rx, + Rxx, + Rxy, + Ry, + Ryy, + Rz, + Rzz, + SDaggerGate, + SGate, + Swap, + TDaggerGate, + TGate, + Toffoli, + U3Gate, + UAnyGate, + WireCut, +) +from .layer import CnotLayer, CnotRing, HLayer, Observable, RxLayer, RyLayer, RzLayer, U3Layer, XLayer, YLayer, ZLayer +from .operation import Channel, Gate, Layer, MeasureQPD, Operation +from .qmath import ( + amplitude_encoding, + expectation, + get_prob_mps, + inner_product_mps, + measure, + sample2expval, + sample_sc_mcmc, + slice_state_vector, +) from .qpd import SingleGateQPD -from .state import QubitState, MatrixProductState, DistributedQubitState +from .state import DistributedQubitState, MatrixProductState, QubitState if TYPE_CHECKING: from .mbqc import Pattern @@ -55,16 +97,17 @@ class QubitCircuit(Operation): Raises: AssertionError: If the type or dimension of ``init_state`` does not match ``nqubit`` or ``den_mat``. """ + def __init__( self, nqubit: int, init_state: Any = 'zeros', - name: Optional[str] = None, + name: str | None = None, den_mat: bool = False, reupload: bool = False, mps: bool = False, - chi: Optional[int] = None, - shots: int = 1024 + chi: int | None = None, + shots: int = 1024, ) -> None: super().__init__(name=name, nqubit=nqubit, wires=None, den_mat=den_mat) self.reupload = reupload @@ -78,7 +121,7 @@ def __init__( self.state = None self.ndata = 0 self.depth = np.array([0] * nqubit) - self._cut_lst = [] # [(index of cutting, wire of cutting), ...] + self._cut_lst = [] # [(index of cutting, wire of cutting), ...] self.wires_measure = [] self.wires_condition = [] # MBQC @@ -111,8 +154,15 @@ def __add__(self, rhs: 'QubitCircuit') -> 'QubitCircuit': The information of observables and measurements is the same as the second ``QubitCircuit``. """ assert self.nqubit == rhs.nqubit - cir = QubitCircuit(nqubit=self.nqubit, init_state=self.init_state, name=self.name, den_mat=self.den_mat, - reupload=self.reupload, mps=self.mps, chi=self.chi) + cir = QubitCircuit( + nqubit=self.nqubit, + init_state=self.init_state, + name=self.name, + den_mat=self.den_mat, + reupload=self.reupload, + mps=self.mps, + chi=self.chi, + ) shift = len(self.operators) cir.operators = self.operators + rhs.operators cir.encoders = self.encoders + rhs.encoders @@ -140,12 +190,11 @@ def to(self, arg: Any) -> 'QubitCircuit': self.observables.to(arg) return self - # pylint: disable=arguments-renamed def forward( self, - data: Optional[torch.Tensor] = None, - state: Union[torch.Tensor, QubitState, List[torch.Tensor], MatrixProductState, None] = None - ) -> Union[torch.Tensor, List[torch.Tensor]]: + data: torch.Tensor | None = None, + state: torch.Tensor | QubitState | list[torch.Tensor] | MatrixProductState | None = None, + ) -> torch.Tensor | list[torch.Tensor]: """Perform a forward pass of the quantum circuit and return the final state. This method applies the ``operators`` of the quantum circuit to the initial state or the given state @@ -196,28 +245,26 @@ def forward( def _forward_helper( self, - data: Optional[torch.Tensor] = None, - state: Union[torch.Tensor, QubitState, List[torch.Tensor], MatrixProductState, None] = None - ) -> Union[torch.Tensor, List[torch.Tensor]]: + data: torch.Tensor | None = None, + state: torch.Tensor | QubitState | list[torch.Tensor] | MatrixProductState | None = None, + ) -> torch.Tensor | list[torch.Tensor]: """Perform a forward pass for one sample.""" self.encode(data) if state is None: state = self.init_state if self.mps: if not isinstance(state, MatrixProductState): - state = MatrixProductState(nsite=self.nqubit, state=state, chi=self.chi, - normalize=self.init_state.normalize) + state = MatrixProductState( + nsite=self.nqubit, state=state, chi=self.chi, normalize=self.init_state.normalize + ) return self.operators(state).tensors if isinstance(state, QubitState): state = state.state x = self.operators(self.tensor_rep(state)) - if self.den_mat: - x = self.matrix_rep(x) - else: - x = self.vector_rep(x) + x = self.matrix_rep(x) if self.den_mat else self.vector_rep(x) return x.squeeze(0) - def encode(self, data: Optional[torch.Tensor]) -> None: + def encode(self, data: torch.Tensor | None) -> None: """Encode the input data into the quantum circuit parameters. This method iterates over the ``encoders`` of the quantum circuit and initializes their parameters @@ -252,7 +299,7 @@ def init_para(self) -> None: for op in self.operators: op.init_para() - def init_encoder(self) -> None: # deal with the problem of state_dict() with vmap + def init_encoder(self) -> None: # deal with the problem of state_dict() with vmap """Initialize the parameters of the ``encoders``.""" for op in self.encoders: op.init_para() @@ -275,7 +322,7 @@ def amplitude_encoding(self, data: Any) -> torch.Tensor: """Encode data into quantum states using amplitude encoding.""" return amplitude_encoding(data, self.nqubit) - def observable(self, wires: Union[int, List[int], None] = None, basis: str = 'z') -> None: + def observable(self, wires: int | list[int] | None = None, basis: str = 'z') -> None: """Add an ``Observable``. Args: @@ -284,8 +331,7 @@ def observable(self, wires: Union[int, List[int], None] = None, basis: str = 'z' basis (str, optional): The measurement basis for each wire. It can be ``'x'``, ``'y'``, or ``'z'``. If only one character is given, it is repeated for all wires. Default: ``'z'`` """ - observable = Observable(nqubit=self.nqubit, wires=wires, basis=basis, - den_mat=self.den_mat, tsr_mode=False) + observable = Observable(nqubit=self.nqubit, wires=wires, basis=basis, den_mat=self.den_mat, tsr_mode=False) self.observables.append(observable) def reset_observable(self) -> None: @@ -294,11 +340,11 @@ def reset_observable(self) -> None: def measure( self, - shots: Optional[int] = None, + shots: int | None = None, with_prob: bool = False, - wires: Union[int, List[int], None] = None, - block_size: int = 2 ** 24 - ) -> Union[Dict, List[Dict], None]: + wires: int | list[int] | None = None, + block_size: int = 2**24, + ) -> dict | list[dict] | None: """Measure the final state. Args: @@ -316,10 +362,9 @@ def measure( wires = list(range(self.nqubit)) self.wires_measure = self._convert_indices(wires) if self.mps: - samples = sample_sc_mcmc(prob_func=self._get_prob, - proposal_sampler=self._proposal_sampler, - shots=shots, - num_chain=5) + samples = sample_sc_mcmc( + prob_func=self._get_prob, proposal_sampler=self._proposal_sampler, shots=shots, num_chain=5 + ) result = dict(samples) if with_prob: for k in result: @@ -328,10 +373,16 @@ def measure( if self.state is None: return else: - return measure(self.state, shots=shots, with_prob=with_prob, wires=self.wires_measure, - den_mat=self.den_mat, block_size=block_size) - - def expectation(self, shots: Optional[int] = None) -> torch.Tensor: + return measure( + self.state, + shots=shots, + with_prob=with_prob, + wires=self.wires_measure, + den_mat=self.den_mat, + block_size=block_size, + ) + + def expectation(self, shots: int | None = None) -> torch.Tensor: """Get the expectation value according to the final state and ``observables``. Args: @@ -352,11 +403,11 @@ def expectation(self, shots: Optional[int] = None) -> torch.Tensor: out.append(expval) else: self.shots = shots - dtype = self.state[0].real.dtype # in order to be compatible with MPS + dtype = self.state[0].real.dtype # in order to be compatible with MPS device = self.state[0].device for observable in self.observables: cir_basis = QubitCircuit(nqubit=self.nqubit, den_mat=self.den_mat, mps=self.mps, chi=self.chi) - for wire, basis in zip(observable.wires, observable.basis): + for wire, basis in zip(observable.wires, observable.basis, strict=True): if basis == 'x': cir_basis.h(wire) elif basis == 'y': @@ -380,7 +431,7 @@ def expectation(self, shots: Optional[int] = None) -> torch.Tensor: out = torch.stack(out, dim=-1) return out - def defer_measure(self, with_prob: bool = False) -> Union[torch.Tensor, Tuple[torch.Tensor, List, List]]: + def defer_measure(self, with_prob: bool = False) -> torch.Tensor | tuple[torch.Tensor, list, list]: """Get the state vectors and the measurement results after deferred measurement.""" assert not self.den_mat assert not self.mps @@ -423,12 +474,9 @@ def get_unitary(self) -> torch.Tensor: for op in self.operators: if isinstance(op, Barrier): continue - if u is None: - u = op.get_unitary() - else: - u = op.get_unitary() @ u + u = op.get_unitary() if u is None else op.get_unitary() @ u if u is None: - return torch.eye(2 ** self.nqubit, dtype=torch.cfloat) + return torch.eye(2**self.nqubit, dtype=torch.cfloat) else: return u @@ -450,7 +498,7 @@ def get_amplitude(self, bits: str) -> torch.Tensor: amp = state.squeeze() return amp - def get_prob(self, bits: str, wires: Union[int, List[int], None] = None) -> torch.Tensor: + def get_prob(self, bits: str, wires: int | list[int] | None = None) -> torch.Tensor: """Get the probability for the given bit string. Args: @@ -463,11 +511,9 @@ def get_prob(self, bits: str, wires: Union[int, List[int], None] = None) -> torc if len(wires) != self.nqubit: if self.mps: assert len(bits) == len(wires) - idx = 0 state = copy(self.state) - for i in wires: - state[i] = state[i][:, [int(bits[idx])], :] - idx += 1 + for i, wire in enumerate(wires): + state[wire] = state[wire][:, [int(bits[i])], :] return inner_product_mps(state, state).real else: state = self.state.reshape(-1) @@ -493,17 +539,12 @@ def inverse(self, encode: bool = False) -> 'QubitCircuit': You should ONLY encode data onto the original circuit. If you want to encode data onto the inversed circuit, set ``encode`` to be ``True``. """ - if isinstance(self.name, str): - name = self.name + '_inverse' - else: - name = self.name - cir = QubitCircuit(nqubit=self.nqubit, name=name, den_mat=self.den_mat, reupload=self.reupload, - mps=self.mps, chi=self.chi) + name = self.name + '_inverse' if isinstance(self.name, str) else self.name + cir = QubitCircuit( + nqubit=self.nqubit, name=name, den_mat=self.den_mat, reupload=self.reupload, mps=self.mps, chi=self.chi + ) for op in reversed(self.operators): - if isinstance(op, Channel): - op_inv = op - else: - op_inv = op.inverse() + op_inv = op if isinstance(op, Channel) else op.inverse() cir.add(op_inv) if encode and op in self.encoders: cir.encoders.append(op_inv) @@ -522,11 +563,7 @@ def max_depth(self) -> int: return max(self.depth) def _slice_state_vector( - self, - state: torch.Tensor, - wires: Union[int, List[int]], - bits: str, - normalize: bool = True + self, state: torch.Tensor, wires: int | list[int], bits: str, normalize: bool = True ) -> torch.Tensor: """Get the sliced state vectors according to ``wires`` and ``bits``.""" assert not self.den_mat @@ -536,8 +573,30 @@ def _slice_state_vector( def qasm(self) -> str: """Get QASM of the quantum circuit.""" - allowed_ops = (U3Gate, PhaseShift, PauliX, PauliY, PauliZ, Hadamard, SGate, SDaggerGate, TGate, - TDaggerGate, Rx, Ry, Rz, CNOT, Swap, Rxx, Ryy, Rzz, Toffoli, Fredkin, Barrier, Layer) + allowed_ops = ( + U3Gate, + PhaseShift, + PauliX, + PauliY, + PauliZ, + Hadamard, + SGate, + SDaggerGate, + TGate, + TDaggerGate, + Rx, + Ry, + Rz, + CNOT, + Swap, + Rxx, + Ryy, + Rzz, + Toffoli, + Fredkin, + Barrier, + Layer, + ) without_control_gates = (TGate, TDaggerGate, CNOT, Rxx, Ryy, Rzz, Toffoli, Fredkin, Barrier) single_control_gates = (U3Gate, PhaseShift, PauliY, PauliZ, Hadamard, SGate, SDaggerGate, Rx, Ry, Rz, Swap) qasm_lst = ['OPENQASM 2.0;\n' + 'include "qelib1.inc";\n'] @@ -561,10 +620,9 @@ def qasm(self) -> str: if len(op.controls) > 0: Gate._reset_qasm_new_gate() raise ValueError(f'Too many control bits for {op.name}') - elif isinstance(op, single_control_gates): - if len(op.controls) > 1: - Gate._reset_qasm_new_gate() - raise ValueError(f'Too many control bits for {op.name}') + elif isinstance(op, single_control_gates) and len(op.controls) > 1: + Gate._reset_qasm_new_gate() + raise ValueError(f'Too many control bits for {op.name}') qasm_lst.append(op._qasm()) for wire in self.wires_measure: qasm_lst.append(f'measure q[{wire}] -> c[{wire}];\n') @@ -573,7 +631,6 @@ def qasm(self) -> str: def _qasm(self): """Get QASM of the quantum circuit for visualization.""" - # pylint: disable=protected-access qasm_lst = ['OPENQASM 2.0;\n' + 'include "qelib1.inc";\n'] if self.wires_measure == self.wires_condition == []: qasm_lst.append(f'qreg q[{self.nqubit}];\n') @@ -601,8 +658,29 @@ def pattern(self) -> 'Pattern': """Get the MBQC pattern.""" assert not self.den_mat and not self.mps, 'Currently NOT supported' from .mbqc import Pattern - allowed_ops = (PauliX, PauliY, PauliZ, Hadamard, SGate, Rx, Ry, Rz, CNOT, Toffoli, Barrier, - XLayer, YLayer, ZLayer, HLayer, RxLayer, RyLayer, RzLayer, CnotLayer, CnotRing) + + allowed_ops = ( + PauliX, + PauliY, + PauliZ, + Hadamard, + SGate, + Rx, + Ry, + Rz, + CNOT, + Toffoli, + Barrier, + XLayer, + YLayer, + ZLayer, + HLayer, + RxLayer, + RyLayer, + RzLayer, + CnotLayer, + CnotRing, + ) for i in range(self.nqubit): self.wire2node_dict[i] = i state_zero = torch.zeros_like(self.init_state.state) @@ -632,8 +710,8 @@ def pattern(self) -> 'Pattern': return pattern def _update_pattern(self, pattern: 'Pattern', gate: Gate, node_next: int, encode: bool = False) -> 'Pattern': - assert len(gate.controls) == 0, f'Control bits are NOT supported for MBQC pattern transpiler' - assert not gate.condition, f'Conditional mode is NOT supported for MBQC pattern transpiler' + assert len(gate.controls) == 0, 'Control bits are NOT supported for MBQC pattern transpiler' + assert not gate.condition, 'Conditional mode is NOT supported for MBQC pattern transpiler' nodes = [self.wire2node_dict[i] for i in gate.wires] ancilla = [node_next + i for i in range(gate.nancilla)] if isinstance(gate, ParametricSingleGate): @@ -648,36 +726,36 @@ def _update_pattern(self, pattern: 'Pattern', gate: Gate, node_next: int, encode pattern.ndata += len(gate.idx_enc) else: pattern.npara += gate.nancilla - for wire, node in zip(gate.wires, gate.nodes): + for wire, node in zip(gate.wires, gate.nodes, strict=True): self.wire2node_dict[wire] = node return pattern def transform_cut2move(self) -> 'QubitCircuit': """Transform ``WireCut`` to ``Move`` and expand the observables accordingly.""" operators = deepcopy(self.operators) - if len(self.observables) == 0: - observables = None - else: - observables = deepcopy(self.observables) + observables = None if len(self.observables) == 0 else deepcopy(self.observables) operators, observables = transform_cut2move(operators, self._cut_lst, observables, False) - cir = QubitCircuit(operators[0].nqubit, den_mat=self.den_mat, reupload=self.reupload, - mps=self.mps, chi=self.chi, shots=self.shots) + cir = QubitCircuit( + operators[0].nqubit, + den_mat=self.den_mat, + reupload=self.reupload, + mps=self.mps, + chi=self.chi, + shots=self.shots, + ) for op in operators: cir.add(op) if observables is not None: cir.observables = observables return cir - def get_subexperiments(self, qubit_labels: Optional[Sequence[Hashable]] = None) -> Tuple[Dict, List[float]]: + def get_subexperiments(self, qubit_labels: Sequence[Hashable] | None = None) -> tuple[dict, list[float]]: """Generate cutting subexperiments and their associated coefficients.""" operators = deepcopy(self.operators) - if len(self.observables) == 0: - observables = None - else: - observables = deepcopy(self.observables) + observables = None if len(self.observables) == 0 else deepcopy(self.observables) operators, observables = transform_cut2move(operators, self._cut_lst, observables, True) label2sub_dict, label2obs_dict = partition_problem(operators, qubit_labels, observables) - label2qpd_dict = defaultdict(list) # {label: [idx, ...]} + label2qpd_dict = defaultdict(list) # {label: [idx, ...]} gate_label_lst = [] gate_coeff_lst = [] nbasis_lst = [] @@ -699,8 +777,9 @@ def get_subexperiments(self, qubit_labels: Optional[Sequence[Hashable]] = None) for combination in product(*ranges): for label, sub_ops in label2sub_dict.items(): nqubit = sub_ops[0].nqubit - cir = QubitCircuit(nqubit, den_mat=self.den_mat, reupload=self.reupload, mps=self.mps, chi=self.chi, - shots=self.shots) + cir = QubitCircuit( + nqubit, den_mat=self.den_mat, reupload=self.reupload, mps=self.mps, chi=self.chi, shots=self.shots + ) if observables is not None: obs = nn.ModuleList() for ob in label2obs_dict[label]: @@ -710,25 +789,28 @@ def get_subexperiments(self, qubit_labels: Optional[Sequence[Hashable]] = None) new_ob.gates.extend(ob.gates) obs.append(new_ob) for op in sub_ops: - if isinstance(op, SingleGateQPD): - for i, idx in enumerate(combination): - if op.label == gate_label_lst_sorted[i]: - for ops in op.bases[idx]: - for gate in ops: - cir.add(gate) - if observables is not None and len(op.bases[idx][0]) > 0: - if isinstance(op.bases[idx][0][-1], MeasureQPD): - pauliz = PauliZ(nqubit, op.wires, den_mat=op.den_mat, tsr_mode=True) - for new_ob in obs: - new_ob.wires = new_ob.wires + [op.wires] - new_ob.basis = new_ob.basis + 'z' - new_ob.gates.append(pauliz) - else: + if not isinstance(op, SingleGateQPD): cir.add(op) + continue + for i, idx in enumerate(combination): + if op.label != gate_label_lst_sorted[i]: + continue + for ops in op.bases[idx]: + for gate in ops: + cir.add(gate) + if observables is None: + continue + if len(op.bases[idx][0]) == 0 or not isinstance(op.bases[idx][0][-1], MeasureQPD): + continue + pauliz = PauliZ(nqubit, op.wires, den_mat=op.den_mat, tsr_mode=True) + for new_ob in obs: + new_ob.wires = new_ob.wires + [op.wires] + new_ob.basis = new_ob.basis + 'z' + new_ob.gates.append(pauliz) if observables is not None: cir.observables = obs subexperiments[label].append(cir) - coeff = 1. + coeff = 1.0 for i, idx in enumerate(combination): coeff *= gate_coeff_lst_sorted[i][idx] coefficients.append(coeff) @@ -743,8 +825,8 @@ def add( self, op: Operation, encode: bool = False, - wires: Union[int, List[int], None] = None, - controls: Union[int, List[int], None] = None + wires: int | list[int] | None = None, + controls: int | list[int] | None = None, ) -> None: """A method that adds an operation to the quantum circuit. @@ -782,7 +864,7 @@ def add( assert self.nqubit == op.nqubit shift = len(self.operators) self.operators += op.operators - self.encoders += op.encoders + self.encoders += op.encoders self.observables = op.observables self.npara += op.npara self.ndata += op.ndata @@ -823,16 +905,23 @@ def u3( self, wires: int, inputs: Any = None, - controls: Union[int, List[int], None] = None, + controls: int | list[int] | None = None, condition: bool = False, - encode: bool = False + encode: bool = False, ) -> None: """Add a U3 gate.""" requires_grad = not encode if inputs is not None: requires_grad = False - u3 = U3Gate(inputs=inputs, nqubit=self.nqubit, wires=wires, controls=controls, - condition=condition, den_mat=self.den_mat, requires_grad=requires_grad) + u3 = U3Gate( + inputs=inputs, + nqubit=self.nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=self.den_mat, + requires_grad=requires_grad, + ) self.add(u3, encode=encode) def cu(self, control: int, target: int, inputs: Any = None, encode: bool = False) -> None: @@ -840,24 +929,37 @@ def cu(self, control: int, target: int, inputs: Any = None, encode: bool = False requires_grad = not encode if inputs is not None: requires_grad = False - cu = U3Gate(inputs=inputs, nqubit=self.nqubit, wires=[target], controls=[control], - den_mat=self.den_mat, requires_grad=requires_grad) + cu = U3Gate( + inputs=inputs, + nqubit=self.nqubit, + wires=[target], + controls=[control], + den_mat=self.den_mat, + requires_grad=requires_grad, + ) self.add(cu, encode=encode) def p( self, wires: int, inputs: Any = None, - controls: Union[int, List[int], None] = None, + controls: int | list[int] | None = None, condition: bool = False, - encode: bool = False + encode: bool = False, ) -> None: """Add a phase shift gate.""" requires_grad = not encode if inputs is not None: requires_grad = False - p = PhaseShift(inputs=inputs, nqubit=self.nqubit, wires=wires, controls=controls, - condition=condition, den_mat=self.den_mat, requires_grad=requires_grad) + p = PhaseShift( + inputs=inputs, + nqubit=self.nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=self.den_mat, + requires_grad=requires_grad, + ) self.add(p, encode=encode) def cp(self, control: int, target: int, inputs: Any = None, encode: bool = False) -> None: @@ -865,50 +967,54 @@ def cp(self, control: int, target: int, inputs: Any = None, encode: bool = False requires_grad = not encode if inputs is not None: requires_grad = False - cp = PhaseShift(inputs=inputs, nqubit=self.nqubit, wires=[target], controls=[control], - den_mat=self.den_mat, requires_grad=requires_grad) + cp = PhaseShift( + inputs=inputs, + nqubit=self.nqubit, + wires=[target], + controls=[control], + den_mat=self.den_mat, + requires_grad=requires_grad, + ) self.add(cp, encode=encode) - def x(self, wires: int, controls: Union[int, List[int], None] = None, condition: bool = False) -> None: + def x(self, wires: int, controls: int | list[int] | None = None, condition: bool = False) -> None: """Add a Pauli-X gate.""" x = PauliX(nqubit=self.nqubit, wires=wires, controls=controls, condition=condition, den_mat=self.den_mat) self.add(x) - def y(self, wires: int, controls: Union[int, List[int], None] = None, condition: bool = False) -> None: + def y(self, wires: int, controls: int | list[int] | None = None, condition: bool = False) -> None: """Add a Pauli-Y gate.""" y = PauliY(nqubit=self.nqubit, wires=wires, controls=controls, condition=condition, den_mat=self.den_mat) self.add(y) - def z(self, wires: int, controls: Union[int, List[int], None] = None, condition: bool = False) -> None: + def z(self, wires: int, controls: int | list[int] | None = None, condition: bool = False) -> None: """Add a Pauli-Z gate.""" z = PauliZ(nqubit=self.nqubit, wires=wires, controls=controls, condition=condition, den_mat=self.den_mat) self.add(z) - def h(self, wires: int, controls: Union[int, List[int], None] = None, condition: bool = False) -> None: + def h(self, wires: int, controls: int | list[int] | None = None, condition: bool = False) -> None: """Add a Hadamard gate.""" h = Hadamard(nqubit=self.nqubit, wires=wires, controls=controls, condition=condition, den_mat=self.den_mat) self.add(h) - def s(self, wires: int, controls: Union[int, List[int], None] = None, condition: bool = False) -> None: + def s(self, wires: int, controls: int | list[int] | None = None, condition: bool = False) -> None: """Add an S gate.""" s = SGate(nqubit=self.nqubit, wires=wires, controls=controls, condition=condition, den_mat=self.den_mat) self.add(s) - def sdg(self, wires: int, controls: Union[int, List[int], None] = None, condition: bool = False) -> None: + def sdg(self, wires: int, controls: int | list[int] | None = None, condition: bool = False) -> None: """Add an S dagger gate.""" - sdg = SDaggerGate(nqubit=self.nqubit, wires=wires, controls=controls, condition=condition, - den_mat=self.den_mat) + sdg = SDaggerGate(nqubit=self.nqubit, wires=wires, controls=controls, condition=condition, den_mat=self.den_mat) self.add(sdg) - def t(self, wires: int, controls: Union[int, List[int], None] = None, condition: bool = False) -> None: + def t(self, wires: int, controls: int | list[int] | None = None, condition: bool = False) -> None: """Add a T gate.""" t = TGate(nqubit=self.nqubit, wires=wires, controls=controls, condition=condition, den_mat=self.den_mat) self.add(t) - def tdg(self, wires: int, controls: Union[int, List[int], None] = None, condition: bool = False) -> None: + def tdg(self, wires: int, controls: int | list[int] | None = None, condition: bool = False) -> None: """Add a T dagger gate.""" - tdg = TDaggerGate(nqubit=self.nqubit, wires=wires, controls=controls, condition=condition, - den_mat=self.den_mat) + tdg = TDaggerGate(nqubit=self.nqubit, wires=wires, controls=controls, condition=condition, den_mat=self.den_mat) self.add(tdg) def ch(self, control: int, target: int) -> None: @@ -940,48 +1046,69 @@ def rx( self, wires: int, inputs: Any = None, - controls: Union[int, List[int], None] = None, + controls: int | list[int] | None = None, condition: bool = False, - encode: bool = False + encode: bool = False, ) -> None: """Add an Rx gate.""" requires_grad = not encode if inputs is not None: requires_grad = False - rx = Rx(inputs=inputs, nqubit=self.nqubit, wires=wires, controls=controls, condition=condition, - den_mat=self.den_mat, requires_grad=requires_grad) + rx = Rx( + inputs=inputs, + nqubit=self.nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=self.den_mat, + requires_grad=requires_grad, + ) self.add(rx, encode=encode) def ry( self, wires: int, inputs: Any = None, - controls: Union[int, List[int], None] = None, + controls: int | list[int] | None = None, condition: bool = False, - encode: bool = False + encode: bool = False, ) -> None: """Add an Ry gate.""" requires_grad = not encode if inputs is not None: requires_grad = False - ry = Ry(inputs=inputs, nqubit=self.nqubit, wires=wires, controls=controls, condition=condition, - den_mat=self.den_mat, requires_grad=requires_grad) + ry = Ry( + inputs=inputs, + nqubit=self.nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=self.den_mat, + requires_grad=requires_grad, + ) self.add(ry, encode=encode) def rz( self, wires: int, inputs: Any = None, - controls: Union[int, List[int], None] = None, + controls: int | list[int] | None = None, condition: bool = False, - encode: bool = False + encode: bool = False, ) -> None: """Add an Rz gate.""" requires_grad = not encode if inputs is not None: requires_grad = False - rz = Rz(inputs=inputs, nqubit=self.nqubit, wires=wires, controls=controls, condition=condition, - den_mat=self.den_mat, requires_grad=requires_grad) + rz = Rz( + inputs=inputs, + nqubit=self.nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=self.den_mat, + requires_grad=requires_grad, + ) self.add(rz, encode=encode) def crx(self, control: int, target: int, inputs: Any = None, encode: bool = False) -> None: @@ -989,8 +1116,14 @@ def crx(self, control: int, target: int, inputs: Any = None, encode: bool = Fals requires_grad = not encode if inputs is not None: requires_grad = False - crx = Rx(inputs=inputs, nqubit=self.nqubit, wires=[target], controls=[control], - den_mat=self.den_mat, requires_grad=requires_grad) + crx = Rx( + inputs=inputs, + nqubit=self.nqubit, + wires=[target], + controls=[control], + den_mat=self.den_mat, + requires_grad=requires_grad, + ) self.add(crx, encode=encode) def cry(self, control: int, target: int, inputs: Any = None, encode: bool = False) -> None: @@ -998,8 +1131,14 @@ def cry(self, control: int, target: int, inputs: Any = None, encode: bool = Fals requires_grad = not encode if inputs is not None: requires_grad = False - cry = Ry(inputs=inputs, nqubit=self.nqubit, wires=[target], controls=[control], - den_mat=self.den_mat, requires_grad=requires_grad) + cry = Ry( + inputs=inputs, + nqubit=self.nqubit, + wires=[target], + controls=[control], + den_mat=self.den_mat, + requires_grad=requires_grad, + ) self.add(cry, encode=encode) def crz(self, control: int, target: int, inputs: Any = None, encode: bool = False) -> None: @@ -1007,8 +1146,14 @@ def crz(self, control: int, target: int, inputs: Any = None, encode: bool = Fals requires_grad = not encode if inputs is not None: requires_grad = False - crz = Rz(inputs=inputs, nqubit=self.nqubit, wires=[target], controls=[control], - den_mat=self.den_mat, requires_grad=requires_grad) + crz = Rz( + inputs=inputs, + nqubit=self.nqubit, + wires=[target], + controls=[control], + den_mat=self.den_mat, + requires_grad=requires_grad, + ) self.add(crz, encode=encode) def j( @@ -1016,16 +1161,24 @@ def j( wires: int, inputs: Any = None, plane: str = 'xy', - controls: Union[int, List[int], None] = None, + controls: int | list[int] | None = None, condition: bool = False, - encode: bool = False + encode: bool = False, ) -> None: """Add a projection matrix J.""" requires_grad = not encode if inputs is not None: requires_grad = False - j = ProjectionJ(inputs=inputs, nqubit=self.nqubit, wires=wires, plane=plane, controls=controls, - condition=condition, den_mat=self.den_mat, requires_grad=requires_grad) + j = ProjectionJ( + inputs=inputs, + nqubit=self.nqubit, + wires=wires, + plane=plane, + controls=controls, + condition=condition, + den_mat=self.den_mat, + requires_grad=requires_grad, + ) self.add(j, encode=encode) def cnot(self, control: int, target: int) -> None: @@ -1048,95 +1201,131 @@ def cz(self, control: int, target: int) -> None: cz = PauliZ(nqubit=self.nqubit, wires=[target], controls=[control], den_mat=self.den_mat) self.add(cz) - def swap(self, wires: List[int], controls: Union[int, List[int], None] = None, condition: bool = False) -> None: + def swap(self, wires: list[int], controls: int | list[int] | None = None, condition: bool = False) -> None: """Add a SWAP gate.""" swap = Swap(nqubit=self.nqubit, wires=wires, controls=controls, condition=condition, den_mat=self.den_mat) self.add(swap) - def iswap(self, wires: List[int], controls: Union[int, List[int], None] = None, condition: bool = False) -> None: + def iswap(self, wires: list[int], controls: int | list[int] | None = None, condition: bool = False) -> None: """Add an imaginary SWAP gate.""" - iswap = ImaginarySwap(nqubit=self.nqubit, wires=wires, controls=controls, condition=condition, - den_mat=self.den_mat) + iswap = ImaginarySwap( + nqubit=self.nqubit, wires=wires, controls=controls, condition=condition, den_mat=self.den_mat + ) self.add(iswap) def rxx( self, - wires: List[int], + wires: list[int], inputs: Any = None, - controls: Union[int, List[int], None] = None, + controls: int | list[int] | None = None, condition: bool = False, - encode: bool = False + encode: bool = False, ) -> None: """Add an Rxx gate.""" requires_grad = not encode if inputs is not None: requires_grad = False - rxx = Rxx(inputs=inputs, nqubit=self.nqubit, wires=wires, controls=controls, condition=condition, - den_mat=self.den_mat, requires_grad=requires_grad) + rxx = Rxx( + inputs=inputs, + nqubit=self.nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=self.den_mat, + requires_grad=requires_grad, + ) self.add(rxx, encode=encode) def ryy( self, - wires: List[int], + wires: list[int], inputs: Any = None, - controls: Union[int, List[int], None] = None, + controls: int | list[int] | None = None, condition: bool = False, - encode: bool = False + encode: bool = False, ) -> None: """Add an Ryy gate.""" requires_grad = not encode if inputs is not None: requires_grad = False - ryy = Ryy(inputs=inputs, nqubit=self.nqubit, wires=wires, controls=controls, condition=condition, - den_mat=self.den_mat, requires_grad=requires_grad) + ryy = Ryy( + inputs=inputs, + nqubit=self.nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=self.den_mat, + requires_grad=requires_grad, + ) self.add(ryy, encode=encode) def rzz( self, - wires: List[int], + wires: list[int], inputs: Any = None, - controls: Union[int, List[int], None] = None, + controls: int | list[int] | None = None, condition: bool = False, - encode: bool = False + encode: bool = False, ) -> None: """Add an Rzz gate.""" requires_grad = not encode if inputs is not None: requires_grad = False - rzz = Rzz(inputs=inputs, nqubit=self.nqubit, wires=wires, controls=controls, condition=condition, - den_mat=self.den_mat, requires_grad=requires_grad) + rzz = Rzz( + inputs=inputs, + nqubit=self.nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=self.den_mat, + requires_grad=requires_grad, + ) self.add(rzz, encode=encode) def rxy( self, - wires: List[int], + wires: list[int], inputs: Any = None, - controls: Union[int, List[int], None] = None, + controls: int | list[int] | None = None, condition: bool = False, - encode: bool = False + encode: bool = False, ) -> None: """Add an Rxy gate.""" requires_grad = not encode if inputs is not None: requires_grad = False - rxy = Rxy(inputs=inputs, nqubit=self.nqubit, wires=wires, controls=controls, condition=condition, - den_mat=self.den_mat, requires_grad=requires_grad) + rxy = Rxy( + inputs=inputs, + nqubit=self.nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=self.den_mat, + requires_grad=requires_grad, + ) self.add(rxy, encode=encode) def rbs( self, - wires: List[int], + wires: list[int], inputs: Any = None, - controls: Union[int, List[int], None] = None, + controls: int | list[int] | None = None, condition: bool = False, - encode: bool = False + encode: bool = False, ) -> None: """Add a Reconfigurable Beam Splitter gate.""" requires_grad = not encode if inputs is not None: requires_grad = False - rbs = ReconfigurableBeamSplitter(inputs=inputs, nqubit=self.nqubit, wires=wires, controls=controls, - condition=condition, den_mat=self.den_mat, requires_grad=requires_grad) + rbs = ReconfigurableBeamSplitter( + inputs=inputs, + nqubit=self.nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=self.den_mat, + requires_grad=requires_grad, + ) self.add(rbs, encode=encode) def crxx(self, control: int, target1: int, target2: int, inputs: Any = None, encode: bool = False) -> None: @@ -1144,8 +1333,14 @@ def crxx(self, control: int, target1: int, target2: int, inputs: Any = None, enc requires_grad = not encode if inputs is not None: requires_grad = False - crxx = Rxx(inputs=inputs, nqubit=self.nqubit, wires=[target1, target2], controls=[control], - den_mat=self.den_mat, requires_grad=requires_grad) + crxx = Rxx( + inputs=inputs, + nqubit=self.nqubit, + wires=[target1, target2], + controls=[control], + den_mat=self.den_mat, + requires_grad=requires_grad, + ) self.add(crxx, encode=encode) def cryy(self, control: int, target1: int, target2: int, inputs: Any = None, encode: bool = False) -> None: @@ -1153,8 +1348,14 @@ def cryy(self, control: int, target1: int, target2: int, inputs: Any = None, enc requires_grad = not encode if inputs is not None: requires_grad = False - cryy = Ryy(inputs=inputs, nqubit=self.nqubit, wires=[target1, target2], controls=[control], - den_mat=self.den_mat, requires_grad=requires_grad) + cryy = Ryy( + inputs=inputs, + nqubit=self.nqubit, + wires=[target1, target2], + controls=[control], + den_mat=self.den_mat, + requires_grad=requires_grad, + ) self.add(cryy, encode=encode) def crzz(self, control: int, target1: int, target2: int, inputs: Any = None, encode: bool = False) -> None: @@ -1162,8 +1363,14 @@ def crzz(self, control: int, target1: int, target2: int, inputs: Any = None, enc requires_grad = not encode if inputs is not None: requires_grad = False - crzz = Rzz(inputs=inputs, nqubit=self.nqubit, wires=[target1, target2], controls=[control], - den_mat=self.den_mat, requires_grad=requires_grad) + crzz = Rzz( + inputs=inputs, + nqubit=self.nqubit, + wires=[target1, target2], + controls=[control], + den_mat=self.den_mat, + requires_grad=requires_grad, + ) self.add(crzz, encode=encode) def crxy(self, control: int, target1: int, target2: int, inputs: Any = None, encode: bool = False) -> None: @@ -1171,8 +1378,14 @@ def crxy(self, control: int, target1: int, target2: int, inputs: Any = None, enc requires_grad = not encode if inputs is not None: requires_grad = False - crxy = Rxy(inputs=inputs, nqubit=self.nqubit, wires=[target1, target2], controls=[control], - den_mat=self.den_mat, requires_grad=requires_grad) + crxy = Rxy( + inputs=inputs, + nqubit=self.nqubit, + wires=[target1, target2], + controls=[control], + den_mat=self.den_mat, + requires_grad=requires_grad, + ) self.add(crxy, encode=encode) def toffoli(self, control1: int, control2: int, target: int) -> None: @@ -1198,113 +1411,133 @@ def cswap(self, control: int, target1: int, target2: int) -> None: def any( self, unitary: Any, - wires: Union[int, List[int], None] = None, - minmax: Optional[List[int]] = None, - controls: Union[int, List[int], None] = None, - name: str = 'uany' + wires: int | list[int] | None = None, + minmax: list[int] | None = None, + controls: int | list[int] | None = None, + name: str = 'uany', ) -> None: """Add an arbitrary unitary gate.""" - uany = UAnyGate(unitary=unitary, nqubit=self.nqubit, wires=wires, minmax=minmax, controls=controls, - name=name, den_mat=self.den_mat) + uany = UAnyGate( + unitary=unitary, + nqubit=self.nqubit, + wires=wires, + minmax=minmax, + controls=controls, + name=name, + den_mat=self.den_mat, + ) self.add(uany) def latent( self, - wires: Union[int, List[int], None] = None, - minmax: Optional[List[int]] = None, + wires: int | list[int] | None = None, + minmax: list[int] | None = None, inputs: Any = None, - controls: Union[int, List[int], None] = None, + controls: int | list[int] | None = None, encode: bool = False, - name: str = 'latent' + name: str = 'latent', ) -> None: """Add a latent gate.""" requires_grad = not encode if inputs is not None: requires_grad = False - latent = LatentGate(inputs=inputs, nqubit=self.nqubit, wires=wires, minmax=minmax, controls=controls, - name=name, den_mat=self.den_mat, requires_grad=requires_grad) + latent = LatentGate( + inputs=inputs, + nqubit=self.nqubit, + wires=wires, + minmax=minmax, + controls=controls, + name=name, + den_mat=self.den_mat, + requires_grad=requires_grad, + ) self.add(latent, encode=encode) def hamiltonian( self, hamiltonian: Any, t: Any = None, - wires: Union[int, List[int], None] = None, - minmax: Optional[List[int]] = None, - controls: Union[int, List[int], None] = None, + wires: int | list[int] | None = None, + minmax: list[int] | None = None, + controls: int | list[int] | None = None, encode: bool = False, - name: str = 'hamiltonian' + name: str = 'hamiltonian', ) -> None: """Add a Hamiltonian gate.""" requires_grad = not encode if t is not None: requires_grad = False - ham = HamiltonianGate(hamiltonian=hamiltonian, t=t, nqubit=self.nqubit, wires=wires, minmax=minmax, - controls=controls, name=name, den_mat=self.den_mat, requires_grad=requires_grad) + ham = HamiltonianGate( + hamiltonian=hamiltonian, + t=t, + nqubit=self.nqubit, + wires=wires, + minmax=minmax, + controls=controls, + name=name, + den_mat=self.den_mat, + requires_grad=requires_grad, + ) self.add(ham, encode=encode) - def xlayer(self, wires: Union[int, List[int], None] = None) -> None: + def xlayer(self, wires: int | list[int] | None = None) -> None: """Add a layer of Pauli-X gates.""" xl = XLayer(nqubit=self.nqubit, wires=wires, den_mat=self.den_mat) self.add(xl) - def ylayer(self, wires: Union[int, List[int], None] = None) -> None: + def ylayer(self, wires: int | list[int] | None = None) -> None: """Add a layer of Pauli-Y gates.""" yl = YLayer(nqubit=self.nqubit, wires=wires, den_mat=self.den_mat) self.add(yl) - def zlayer(self, wires: Union[int, List[int], None] = None) -> None: + def zlayer(self, wires: int | list[int] | None = None) -> None: """Add a layer of Pauli-Z gates.""" zl = ZLayer(nqubit=self.nqubit, wires=wires, den_mat=self.den_mat) self.add(zl) - def hlayer(self, wires: Union[int, List[int], None] = None) -> None: + def hlayer(self, wires: int | list[int] | None = None) -> None: """Add a layer of Hadamard gates.""" hl = HLayer(nqubit=self.nqubit, wires=wires, den_mat=self.den_mat) self.add(hl) - def rxlayer(self, wires: Union[int, List[int], None] = None, inputs: Any = None, encode: bool = False) -> None: + def rxlayer(self, wires: int | list[int] | None = None, inputs: Any = None, encode: bool = False) -> None: """Add a layer of Rx gates.""" requires_grad = not encode if inputs is not None: requires_grad = False - rxl = RxLayer(nqubit=self.nqubit, wires=wires, inputs=inputs, den_mat=self.den_mat, - requires_grad=requires_grad) + rxl = RxLayer(nqubit=self.nqubit, wires=wires, inputs=inputs, den_mat=self.den_mat, requires_grad=requires_grad) self.add(rxl, encode=encode) - def rylayer(self, wires: Union[int, List[int], None] = None, inputs: Any = None, encode: bool = False) -> None: + def rylayer(self, wires: int | list[int] | None = None, inputs: Any = None, encode: bool = False) -> None: """Add a layer of Ry gates.""" requires_grad = not encode if inputs is not None: requires_grad = False - ryl = RyLayer(nqubit=self.nqubit, wires=wires, inputs=inputs, den_mat=self.den_mat, - requires_grad=requires_grad) + ryl = RyLayer(nqubit=self.nqubit, wires=wires, inputs=inputs, den_mat=self.den_mat, requires_grad=requires_grad) self.add(ryl, encode=encode) - def rzlayer(self, wires: Union[int, List[int], None] = None, inputs: Any = None, encode: bool = False) -> None: + def rzlayer(self, wires: int | list[int] | None = None, inputs: Any = None, encode: bool = False) -> None: """Add a layer of Rz gates.""" requires_grad = not encode if inputs is not None: requires_grad = False - rzl = RzLayer(nqubit=self.nqubit, wires=wires, inputs=inputs, den_mat=self.den_mat, - requires_grad=requires_grad) + rzl = RzLayer(nqubit=self.nqubit, wires=wires, inputs=inputs, den_mat=self.den_mat, requires_grad=requires_grad) self.add(rzl, encode=encode) - def u3layer(self, wires: Union[int, List[int], None] = None, inputs: Any = None, encode: bool = False) -> None: + def u3layer(self, wires: int | list[int] | None = None, inputs: Any = None, encode: bool = False) -> None: """Add a layer of U3 gates.""" requires_grad = not encode if inputs is not None: requires_grad = False - u3l = U3Layer(nqubit=self.nqubit, wires=wires, inputs=inputs, den_mat=self.den_mat, - requires_grad=requires_grad) + u3l = U3Layer(nqubit=self.nqubit, wires=wires, inputs=inputs, den_mat=self.den_mat, requires_grad=requires_grad) self.add(u3l, encode=encode) - def cxlayer(self, wires: Optional[List[List[int]]] = None) -> None: + def cxlayer(self, wires: list[list[int]] | None = None) -> None: """Add a layer of CNOT gates.""" cxl = CnotLayer(nqubit=self.nqubit, wires=wires, den_mat=self.den_mat) self.add(cxl) - def cnot_ring(self, minmax: Optional[List[int]] = None, step: int = 1, reverse: bool = False) -> None: + def cnot_ring(self, minmax: list[int] | None = None, step: int = 1, reverse: bool = False) -> None: """Add a layer of CNOT gates in a cyclic way.""" cxr = CnotRing(nqubit=self.nqubit, minmax=minmax, step=step, reverse=reverse, den_mat=self.den_mat) self.add(cxr) @@ -1372,23 +1605,23 @@ def gen_amp_damp(self, wires: int, inputs: Any = None, encode: bool = False) -> gad = GeneralizedAmplitudeDamping(inputs=inputs, nqubit=self.nqubit, wires=wires, requires_grad=requires_grad) self.add(gad, encode=encode) - def reset(self, wires: Union[int, List[int], None] = None, postselect: Optional[int] = 0) -> None: + def reset(self, wires: int | list[int] | None = None, postselect: int | None = 0) -> None: """Add a reset operation.""" assert not self.den_mat and not self.mps, 'Currently NOT supported' rs = Reset(nqubit=self.nqubit, wires=wires, postselect=postselect) self.add(rs) - def barrier(self, wires: Union[int, List[int], None] = None) -> None: + def barrier(self, wires: int | list[int] | None = None) -> None: """Add a barrier.""" br = Barrier(nqubit=self.nqubit, wires=wires) self.add(br) - def cut(self, wires: Union[int, List[int]]) -> None: + def cut(self, wires: int | list[int]) -> None: """Add a wire-cut operation.""" wc = WireCut(nqubit=self.nqubit, wires=wires) self.add(wc) - def move(self, wire1: int, wire2: int, postselect: Optional[int] = 0) -> None: + def move(self, wire1: int, wire2: int, postselect: int | None = 0) -> None: """Add a move operation.""" mv = Move(nqubit=self.nqubit, wires=[wire1, wire2], postselect=postselect) self.add(mv) @@ -1403,23 +1636,29 @@ class DistributedQubitCircuit(QubitCircuit): reupload (bool, optional): Whether to use data re-uploading. Default: ``False`` shots (int, optional): The number of shots for the measurement. Default: 1024 """ - def __init__(self, nqubit, name = None, reupload = False, shots = 1024) -> None: - super().__init__(nqubit=nqubit, init_state='zeros', name=name, den_mat=False, - reupload=reupload, mps=False, chi=None, shots=shots) - def set_init_state(self, init_state: Union[str, DistributedQubitState] = 'zeros') -> None: + def __init__(self, nqubit, name=None, reupload=False, shots=1024) -> None: + super().__init__( + nqubit=nqubit, + init_state='zeros', + name=name, + den_mat=False, + reupload=reupload, + mps=False, + chi=None, + shots=shots, + ) + + def set_init_state(self, init_state: str | DistributedQubitState = 'zeros') -> None: """Set the initial state of the circuit.""" if init_state == 'zeros': self.init_state = DistributedQubitState(self.nqubit) elif isinstance(init_state, DistributedQubitState): self.init_state = init_state - # pylint: disable=arguments-renamed @torch.no_grad() def forward( - self, - data: Optional[torch.Tensor] = None, - state: Optional[DistributedQubitState] = None + self, data: torch.Tensor | None = None, state: DistributedQubitState | None = None ) -> DistributedQubitState: """Perform a forward pass of the quantum circuit and return the final state. @@ -1443,11 +1682,11 @@ def forward( def measure( self, - shots: Optional[int] = None, + shots: int | None = None, with_prob: bool = False, - wires: Union[int, List[int], None] = None, - block_size: int = 2 ** 24 - ) -> Union[Dict, None]: + wires: int | list[int] | None = None, + block_size: int = 2**24, + ) -> dict | None: """Measure the final state. Args: @@ -1467,10 +1706,11 @@ def measure( if self.state is None: return else: - return measure_dist(self.state, shots=shots, with_prob=with_prob, wires=self.wires_measure, - block_size=block_size) + return measure_dist( + self.state, shots=shots, with_prob=with_prob, wires=self.wires_measure, block_size=block_size + ) - def expectation(self, shots: Optional[int] = None) -> torch.Tensor: + def expectation(self, shots: int | None = None) -> torch.Tensor: """Get the expectation value according to the final state and ``observables``. Args: @@ -1509,7 +1749,7 @@ def expectation(self, shots: Optional[int] = None) -> torch.Tensor: device = self.state.amps.device for observable in self.observables: cir_basis = DistributedQubitCircuit(self.nqubit) - for wire, basis in zip(observable.wires, observable.basis): + for wire, basis in zip(observable.wires, observable.basis, strict=True): if basis == 'x': cir_basis.h(wire) elif basis == 'y': diff --git a/src/deepquantum/communication.py b/src/deepquantum/communication.py index 17b2e28c..7f7c261a 100644 --- a/src/deepquantum/communication.py +++ b/src/deepquantum/communication.py @@ -1,21 +1,18 @@ -""" -Communication utilities -""" +"""Communication utilities""" import os -from typing import Optional, Tuple import torch import torch.distributed as dist -def setup_distributed(backend: str = 'nccl', port: str = '29500') -> Tuple[int, int, int]: - """Initialize torch.distributed.""" +def setup_distributed(backend: str = 'nccl', port: str = '29500') -> tuple[int, int, int]: + """Initialize ``torch.distributed``.""" try: # These should be set by the launch script (e.g., torchrun) rank = int(os.environ['RANK']) world_size = int(os.environ['WORLD_SIZE']) - local_rank = int(os.environ['LOCAL_RANK']) # GPU id on the current node + local_rank = int(os.environ['LOCAL_RANK']) # GPU id on the current node except KeyError: print('RANK, WORLD_SIZE, and LOCAL_RANK env vars must be set.') # Fallback for single-process testing (optional) @@ -45,30 +42,33 @@ def cleanup_distributed() -> None: def comm_get_rank() -> int: + """Get the rank of the current process.""" if not dist.is_initialized(): return 0 return dist.get_rank() def comm_get_world_size() -> int: + """Get the total number of processes.""" if not dist.is_initialized(): return 1 return dist.get_world_size() -def comm_exchange_arrays(send_data: torch.Tensor, recv_data: torch.Tensor, pair_rank: Optional[int]) -> None: - """Simulate a point-to-point exchange using dist.all_to_all_single - with output_split_sizes and input_split_sizes to minimize memory. - If pair_rank is None, this rank participates in the collective call - but sends/receives no actual data to/from other specific ranks in this logical P2P. +def comm_exchange_arrays(send_data: torch.Tensor, recv_data: torch.Tensor, pair_rank: int | None) -> None: + """Exchange tensor data with a peer rank using collective communication. + + This performs a point-to-point communication via ``dist.all_to_all_single`` and allows specific ranks + to participate in the collective call without active data transfer by setting ``pair_rank`` to ``None``. Args: - send_data (torch.Tensor): The data this rank wants to send to pair_rank. - If pair_rank is None, this can be an empty tensor with correct dtype and device. - recv_data (torch.Tensor): The Tensor where data received from pair_rank will be stored. - It MUST already be allocated with the correct size if pair_rank is not None. - If pair_rank is None, this can be an empty tensor. - pair_rank (int or None): The rank of the process to exchange data with, or None. + send_data (torch.Tensor): Data to be sent to the ``pair_rank``. + If ``pair_rank`` is ``None``, this can be an empty tensor with correct dtype and device. + recv_data (torch.Tensor): Pre-allocated buffer to store received data. Must match + ``send_data`` in shape and dtype if ``pair_rank`` is active. + If ``pair_rank`` is ``None``, this can be an empty tensor. + pair_rank (int or None): The target rank for exchange, or ``None`` to remain + quiescent during the collective call. """ world_size = comm_get_world_size() rank = comm_get_rank() diff --git a/src/deepquantum/cutting.py b/src/deepquantum/cutting.py index 80e12193..42706a29 100644 --- a/src/deepquantum/cutting.py +++ b/src/deepquantum/cutting.py @@ -1,17 +1,14 @@ -""" -Circuit cutting -""" +"""Circuit cutting""" import bisect from collections import defaultdict -from collections.abc import Sequence, Hashable -from typing import Callable, Dict, List, Optional, Tuple +from collections.abc import Callable, Hashable, Sequence from uuid import uuid4 from networkx import Graph, connected_components from torch import nn -from .gate import Barrier, WireCut, Move +from .gate import Barrier, Move, WireCut from .layer import Observable from .operation import GateQPD from .qpd import DoubleGateQPD @@ -19,16 +16,16 @@ def transform_cut2move( operators: nn.Sequential, - cut_lst: List[Tuple[int, int]], - observables: Optional[nn.ModuleList] = None, - qpd_form: bool = False -) -> Tuple[nn.Sequential, Optional[nn.ModuleList]]: + cut_lst: list[tuple[int, int]], + observables: nn.ModuleList | None = None, + qpd_form: bool = False, +) -> tuple[nn.Sequential, nn.ModuleList | None]: """Transform ``WireCut`` to ``Move`` and expand the observables accordingly.""" nqubit = operators[0].nqubit cuts_per_qubit = defaultdict(list) for idx, wire in cut_lst: cuts_per_qubit[wire].append(idx) - ncut_cum_lst = [] # ncut before the current qubit + ncut_cum_lst = [] # ncut before the current qubit ncut = 0 for i in range(nqubit + 1): ncut_cum_lst.append(ncut) @@ -48,10 +45,7 @@ def transform_cut2move( op.set_controls(new_controls) if isinstance(op, WireCut): move = Move(nqubit=new_nqubit, wires=[op.wires[0], op.wires[0] + 1], tsr_mode=op.tsr_mode) - if qpd_form: - operators[i] = move.qpd() - else: - operators[i] = move + operators[i] = move.qpd() if qpd_form else move if observables is not None: for ob in observables: ob.set_nqubit(new_nqubit) @@ -61,10 +55,8 @@ def transform_cut2move( def partition_labels( - operators: nn.Sequential, - ignore: Callable = lambda x: False, - keep_idle_wires: bool = False -) -> List[Optional[int]]: + operators: nn.Sequential, ignore: Callable = lambda x: False, keep_idle_wires: bool = False +) -> list[int | None]: """Generate partition labels from the connectivity of a quantum circuit.""" nqubit = operators[0].nqubit graph = Graph() @@ -74,7 +66,7 @@ def partition_labels( continue wires = op.wires + op.controls for i, wire1 in enumerate(wires): - for wire2 in wires[i+1:]: + for wire2 in wires[i + 1 :]: graph.add_edge(wire1, wire2) qubit_subsets = list(connected_components(graph)) qubit_subsets.sort(key=min) @@ -85,9 +77,7 @@ def partition_labels( for wire in wires: idle_wires.discard(wire) qubit_subsets = [ - subset - for subset in qubit_subsets - if not (len(subset) == 1 and next(iter(subset)) in idle_wires) + subset for subset in qubit_subsets if not (len(subset) == 1 and next(iter(subset)) in idle_wires) ] qubit_labels = [None] * nqubit for i, subset in enumerate(qubit_subsets): @@ -96,7 +86,7 @@ def partition_labels( return qubit_labels -def map_qubit(qubit_labels: Sequence[Hashable]) -> Tuple[List[Tuple], Dict[Hashable, List]]: +def map_qubit(qubit_labels: Sequence[Hashable]) -> tuple[list[tuple], dict[Hashable, list]]: """Generate a qubit map given a qubit partitioning.""" qubit_map = [] label2qubits_dict = defaultdict(list) @@ -110,7 +100,7 @@ def map_qubit(qubit_labels: Sequence[Hashable]) -> Tuple[List[Tuple], Dict[Hasha return qubit_map, dict(label2qubits_dict) -def label_operators(operators: nn.Sequential, qubit_map: Sequence[Tuple]) -> Dict[Hashable, List]: +def label_operators(operators: nn.Sequential, qubit_map: Sequence[tuple]) -> dict[Hashable, list]: """Generate a list of operators for each partition of the circuit.""" unique_labels = set([label for label, _ in qubit_map if label is not None]) label2ops_dict = {label: [] for label in unique_labels} @@ -133,7 +123,7 @@ def split_barriers(operators: nn.Sequential) -> nn.Sequential: for i, op in enumerate(operators): wires = op.wires + op.controls nwire = len(wires) - if nwire == 1 or (not type(op) is Barrier): + if nwire == 1 or (type(op) is not Barrier): continue barrier_uuid = f'Barrier_uuid={uuid4()}' operators[i] = Barrier(op.nqubit, wires[0], barrier_uuid) @@ -178,7 +168,7 @@ def get_qpd_operators(operators: nn.Sequential, qubit_labels: Sequence[Hashable] return operators -def separate_operators(operators: nn.Sequential, qubit_labels: Optional[Sequence[Hashable]] = None) -> Dict: +def separate_operators(operators: nn.Sequential, qubit_labels: Sequence[Hashable] | None = None) -> dict: """Separate the circuit into its disconnected components.""" nqubit = operators[0].nqubit operators = split_barriers(operators) @@ -203,8 +193,10 @@ def separate_operators(operators: nn.Sequential, qubit_labels: Optional[Sequence return label2sub_dict -def decompose_observables(observables: nn.ModuleList, qubit_labels: Sequence[Hashable]) -> Dict: +def decompose_observables(observables: nn.ModuleList | None, qubit_labels: Sequence[Hashable]) -> dict | None: """Decompose the observables with respect to qubit partition labels.""" + if observables is None: + return None qubit_map, label2qubits_dict = map_qubit(qubit_labels) label2obs_dict = {} for label, qubits in label2qubits_dict.items(): @@ -227,10 +219,8 @@ def decompose_observables(observables: nn.ModuleList, qubit_labels: Sequence[Has def partition_problem( - operators: nn.Sequential, - qubit_labels: Optional[Sequence[Hashable]] = None, - observables: Optional[nn.ModuleList] = None -) -> Tuple[Dict, Optional[Dict]]: + operators: nn.Sequential, qubit_labels: Sequence[Hashable] | None = None, observables: nn.ModuleList | None = None +) -> tuple[dict, dict | None]: """Separate the circuit and observables.""" if qubit_labels is None: qubit_labels = partition_labels(operators, lambda op: isinstance(op, DoubleGateQPD)) @@ -244,8 +234,5 @@ def partition_problem( operators_qpd.insert(i + 1, gate2) gate_label += 1 label2sub_dict = separate_operators(nn.Sequential(*operators_qpd), qubit_labels) - if observables is not None: - label2obs_dict = decompose_observables(observables, qubit_labels) - else: - label2obs_dict = None + label2obs_dict = decompose_observables(observables, qubit_labels) return label2sub_dict, label2obs_dict diff --git a/src/deepquantum/distributed.py b/src/deepquantum/distributed.py index 7a50c20c..22c51eaf 100644 --- a/src/deepquantum/distributed.py +++ b/src/deepquantum/distributed.py @@ -1,21 +1,18 @@ -""" -Distributed operations -""" +"""Distributed operations""" from collections import Counter -from typing import Dict, List, Union import torch import torch.distributed as dist -from .bitmath import log_base2, get_bit, flip_bit, flip_bits, all_bits_are_one, get_bit_mask -from .communication import comm_get_world_size, comm_exchange_arrays -from .qmath import evolve_state, block_sample, measure +from .bitmath import all_bits_are_one, flip_bit, flip_bits, get_bit, get_bit_mask, log_base2 +from .communication import comm_exchange_arrays, comm_get_world_size +from .qmath import block_sample, evolve_state, measure from .state import DistributedQubitState # The 0-th qubit is the rightmost in a ket for the `target` -def local_gate(state: torch.Tensor, targets: List[int], matrix: torch.Tensor) -> torch.Tensor: +def local_gate(state: torch.Tensor, targets: list[int], matrix: torch.Tensor) -> torch.Tensor: """Apply a gate to a state vector locally.""" nqubit = log_base2(len(state)) wires = [nqubit - target - 1 for target in targets] @@ -24,11 +21,7 @@ def local_gate(state: torch.Tensor, targets: List[int], matrix: torch.Tensor) -> def local_many_ctrl_one_targ_gate( - state: torch.Tensor, - controls: List[int], - target: int, - matrix: torch.Tensor, - derivative: bool = False + state: torch.Tensor, controls: list[int], target: int, matrix: torch.Tensor, derivative: bool = False ) -> torch.Tensor: """Apply a multi-control single-qubit gate to a state vector locally. @@ -37,7 +30,7 @@ def local_many_ctrl_one_targ_gate( indices = torch.arange(len(state), device=state.device) control_mask = torch.ones_like(indices, dtype=torch.bool) for control in controls: - control_mask &= (get_bit(indices, control) == 1) + control_mask &= get_bit(indices, control) == 1 mask = control_mask & (get_bit(indices, target) == 0) # Indices where controls are 1 AND target is 0 indices_0 = indices[mask] @@ -79,11 +72,7 @@ def dist_one_targ_gate(state: DistributedQubitState, target: int, matrix: torch. def dist_many_ctrl_one_targ_gate( - state: DistributedQubitState, - controls: List[int], - target: int, - matrix: torch.Tensor, - derivative: bool = False + state: DistributedQubitState, controls: list[int], target: int, matrix: torch.Tensor, derivative: bool = False ) -> DistributedQubitState: """Apply a multi-control single-qubit gate or its derivative to a distributed state vector. @@ -114,13 +103,9 @@ def dist_many_ctrl_one_targ_gate( def dist_ctrl_sub( - state: DistributedQubitState, - controls: List[int], - target: int, - matrix: torch.Tensor, - derivative: bool = False + state: DistributedQubitState, controls: list[int], target: int, matrix: torch.Tensor, derivative: bool = False ) -> DistributedQubitState: - """"A subroutine of `dist_many_ctrl_one_targ_gate`. + """A subroutine of `dist_many_ctrl_one_targ_gate`. See https://arxiv.org/abs/2311.01512 Alg.8 """ @@ -129,11 +114,11 @@ def dist_ctrl_sub( indices = torch.arange(state.num_amps_per_node, device=state.amps.device) control_mask = torch.ones_like(indices, dtype=torch.bool) for control in controls: - control_mask &= (get_bit(indices, control) == 1) + control_mask &= get_bit(indices, control) == 1 # Indices where controls are 1 indices = indices[control_mask] send = state.amps[indices].contiguous() - recv = state.buffer[:len(send)] + recv = state.buffer[: len(send)] comm_exchange_arrays(send, recv, pair_rank) if derivative: state.amps.zero_() @@ -165,16 +150,17 @@ def dist_swap_gate(state: DistributedQubitState, qb1: int, qb2: int): bit = 1 - get_bit(state.rank, qb2_rank) pair_rank = flip_bit(state.rank, qb2_rank) indices = torch.arange(state.num_amps_per_node, device=state.amps.device) - mask = (get_bit(indices, qb1) == bit) + mask = get_bit(indices, qb1) == bit indices = indices[mask] send = state.amps[indices].contiguous() - recv = state.buffer[:len(send)] + recv = state.buffer[: len(send)] comm_exchange_arrays(send, recv, pair_rank) state.amps[indices] = recv return state -def get_local_targets(targets: List[int], nqubit_local: int) -> List[int]: +def get_local_targets(targets: list[int], nqubit_local: int) -> list[int]: + """Map global target qubits to available local indices for distributed gates.""" mask = get_bit_mask(targets) min_non_targ = 0 while get_bit(mask, min_non_targ): @@ -192,9 +178,7 @@ def get_local_targets(targets: List[int], nqubit_local: int) -> List[int]: def dist_many_targ_gate( - state: DistributedQubitState, - targets: List[int], - matrix: torch.Tensor + state: DistributedQubitState, targets: list[int], matrix: torch.Tensor ) -> DistributedQubitState: """Apply a multi-qubit gate to a distributed state vector. @@ -222,9 +206,9 @@ def measure_dist( state: DistributedQubitState, shots: int = 1024, with_prob: bool = False, - wires: Union[int, List[int], None] = None, - block_size: int = 2 ** 24 -) -> Dict: + wires: int | list[int] | None = None, + block_size: int = 2**24, +) -> dict: """Measure a distributed state vector.""" if state.world_size == 1: return measure(state.amps, shots, with_prob, wires, False, block_size) @@ -240,7 +224,7 @@ def measure_dist( targets = [state.nqubit - wire - 1 for wire in wires] pm_shape = list(range(nqubit_local)) # Assume nqubit_global < nqubit_local - if num_bits <= nqubit_local: # All targets move to local qubits + if num_bits <= nqubit_local: # All targets move to local qubits if max(targets) >= nqubit_local: targets_new = get_local_targets(targets, nqubit_local) for i in range(num_bits): @@ -263,7 +247,7 @@ def measure_dist( results[k] = results[k], probs[index].item() return results return {} - else: # All targets are sorted, then move to global qubits + else: # All targets are sorted, then move to global qubits targets_sort = sorted(targets, reverse=True) wires_local = [] for i, target in enumerate(targets_sort): diff --git a/src/deepquantum/gate.py b/src/deepquantum/gate.py index 226ecf2c..c4f8dfd1 100644 --- a/src/deepquantum/gate.py +++ b/src/deepquantum/gate.py @@ -1,18 +1,15 @@ -""" -Quantum gates -""" +"""Quantum gates""" from copy import copy -from typing import Any, List, Optional, Tuple, Union -from typing import TYPE_CHECKING +from typing import Any, TYPE_CHECKING import torch from torch import nn from torch.autograd.functional import jacobian -from .distributed import dist_one_targ_gate, dist_many_ctrl_one_targ_gate, dist_swap_gate +from .distributed import dist_many_ctrl_one_targ_gate, dist_one_targ_gate, dist_swap_gate from .operation import Gate -from .qmath import multi_kron, is_unitary, inverse_permutation, evolve_state, svd +from .qmath import evolve_state, inverse_permutation, is_unitary, multi_kron, svd from .state import DistributedQubitState if TYPE_CHECKING: @@ -35,18 +32,26 @@ class SingleGate(Gate): and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + def __init__( self, - name: Optional[str] = None, + name: str | None = None, nqubit: int = 1, - wires: Union[int, List[int], None] = None, - controls: Union[int, List[int], None] = None, + wires: int | list[int] | None = None, + controls: int | list[int] | None = None, condition: bool = False, den_mat: bool = False, - tsr_mode: bool = False + tsr_mode: bool = False, ) -> None: - super().__init__(name=name, nqubit=nqubit, wires=wires, controls=controls, condition=condition, - den_mat=den_mat, tsr_mode=tsr_mode) + super().__init__( + name=name, + nqubit=nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=den_mat, + tsr_mode=tsr_mode, + ) assert len(self.wires) == 1 # MBQC self.nancilla = 2 @@ -97,30 +102,38 @@ class DoubleGate(Gate): and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + def __init__( self, - name: Optional[str] = None, + name: str | None = None, nqubit: int = 2, - wires: Optional[List[int]] = None, - controls: Union[int, List[int], None] = None, + wires: list[int] | None = None, + controls: int | list[int] | None = None, condition: bool = False, den_mat: bool = False, - tsr_mode: bool = False + tsr_mode: bool = False, ) -> None: if wires is None: wires = [0, 1] assert len(wires) == 2 - super().__init__(name=name, nqubit=nqubit, wires=wires, controls=controls, condition=condition, - den_mat=den_mat, tsr_mode=tsr_mode) + super().__init__( + name=name, + nqubit=nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=den_mat, + tsr_mode=tsr_mode, + ) def get_unitary(self) -> torch.Tensor: """Get the global unitary matrix.""" matrix = self.update_matrix() identity = torch.eye(2, dtype=matrix.dtype, device=matrix.device) zerozero = torch.tensor([[1, 0], [0, 0]], dtype=matrix.dtype, device=matrix.device) - zeroone = torch.tensor([[0, 1], [0, 0]], dtype=matrix.dtype, device=matrix.device) - onezero = torch.tensor([[0, 0], [1, 0]], dtype=matrix.dtype, device=matrix.device) - oneone = torch.tensor([[0, 0], [0, 1]], dtype=matrix.dtype, device=matrix.device) + zeroone = torch.tensor([[0, 1], [0, 0]], dtype=matrix.dtype, device=matrix.device) + onezero = torch.tensor([[0, 0], [1, 0]], dtype=matrix.dtype, device=matrix.device) + oneone = torch.tensor([[0, 0], [0, 1]], dtype=matrix.dtype, device=matrix.device) if self.controls == []: lst1 = [identity] * self.nqubit lst2 = [identity] * self.nqubit @@ -165,8 +178,14 @@ def get_unitary(self) -> torch.Tensor: lst6[self.wires[0]] = oneone lst6[self.wires[1]] = matrix[2:4, 2:4] - return multi_kron(lst1) - multi_kron(lst2) + \ - multi_kron(lst3) + multi_kron(lst4) + multi_kron(lst5) + multi_kron(lst6) + return ( + multi_kron(lst1) + - multi_kron(lst2) + + multi_kron(lst3) + + multi_kron(lst4) + + multi_kron(lst5) + + multi_kron(lst6) + ) class DoubleControlGate(DoubleGate): @@ -183,23 +202,25 @@ class DoubleControlGate(DoubleGate): and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + def __init__( self, - name: Optional[str] = None, + name: str | None = None, nqubit: int = 2, - wires: Optional[List[int]] = None, + wires: list[int] | None = None, den_mat: bool = False, - tsr_mode: bool = False + tsr_mode: bool = False, ) -> None: - super().__init__(name=name, nqubit=nqubit, wires=wires, controls=None, condition=False, - den_mat=den_mat, tsr_mode=tsr_mode) + super().__init__( + name=name, nqubit=nqubit, wires=wires, controls=None, condition=False, den_mat=den_mat, tsr_mode=tsr_mode + ) def get_unitary(self) -> torch.Tensor: """Get the global unitary matrix.""" matrix = self.update_matrix() identity = torch.eye(2, dtype=matrix.dtype, device=matrix.device) zerozero = torch.tensor([[1, 0], [0, 0]], dtype=matrix.dtype, device=matrix.device) - oneone = torch.tensor([[0, 0], [0, 1]], dtype=matrix.dtype, device=matrix.device) + oneone = torch.tensor([[0, 0], [0, 1]], dtype=matrix.dtype, device=matrix.device) lst1 = [identity] * self.nqubit lst2 = [identity] * self.nqubit @@ -226,49 +247,58 @@ class TripleGate(Gate): and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + def __init__( self, - name: Optional[str] = None, + name: str | None = None, nqubit: int = 3, - wires: Optional[List[int]] = None, - controls: Union[int, List[int], None] = None, + wires: list[int] | None = None, + controls: int | list[int] | None = None, condition: bool = False, den_mat: bool = False, - tsr_mode: bool = False + tsr_mode: bool = False, ) -> None: if wires is None: wires = [0, 1, 2] assert len(wires) == 3 - super().__init__(name=name, nqubit=nqubit, wires=wires, controls=controls, condition=condition, - den_mat=den_mat, tsr_mode=tsr_mode) + super().__init__( + name=name, + nqubit=nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=den_mat, + tsr_mode=tsr_mode, + ) class ArbitraryGate(Gate): r"""A base class for customized gates. - Args: - name (str or None, optional): The name of the gate. Default: ``None`` - nqubit (int, optional): The number of qubits that the quantum operation acts on. Default: 1 - wires (int, List[int] or None, optional): The indices of the qubits that the quantum operation acts on. - Default: ``None`` - minmax (List[int] or None, optional): The minimum and maximum indices of the qubits that the quantum - operation acts on. Only valid when ``wires`` is ``None``. Default: ``None`` - controls (int, List[int] or None, optional): The indices of the control qubits. Default: ``None`` - den_mat (bool, optional): Whether the quantum operation acts on density matrices or state vectors. - Default: ``False`` (which means state vectors) - tsr_mode (bool, optional): Whether the quantum operation is in tensor mode, which means the input - and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. - Default: ``False`` + Args: + name (str or None, optional): The name of the gate. Default: ``None`` + nqubit (int, optional): The number of qubits that the quantum operation acts on. Default: 1 + wires (int, List[int] or None, optional): The indices of the qubits that the quantum operation acts on. + Default: ``None`` + minmax (List[int] or None, optional): The minimum and maximum indices of the qubits that the quantum + operation acts on. Only valid when ``wires`` is ``None``. Default: ``None`` + controls (int, List[int] or None, optional): The indices of the control qubits. Default: ``None`` + den_mat (bool, optional): Whether the quantum operation acts on density matrices or state vectors. + Default: ``False`` (which means state vectors) + tsr_mode (bool, optional): Whether the quantum operation is in tensor mode, which means the input + and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. + Default: ``False`` """ + def __init__( self, - name: Optional[str] = None, + name: str | None = None, nqubit: int = 1, - wires: Union[int, List[int], None] = None, - minmax: Optional[List[int]] = None, - controls: Union[int, List[int], None] = None, + wires: int | list[int] | None = None, + minmax: list[int] | None = None, + controls: int | list[int] | None = None, den_mat: bool = False, - tsr_mode: bool = False + tsr_mode: bool = False, ) -> None: self.nqubit = nqubit if wires is None: @@ -276,8 +306,15 @@ def __init__( minmax = [0, nqubit - 1] self._check_minmax(minmax) wires = list(range(minmax[0], minmax[1] + 1)) - super().__init__(name=name, nqubit=nqubit, wires=wires, controls=controls, condition=False, - den_mat=den_mat, tsr_mode=tsr_mode) + super().__init__( + name=name, + nqubit=nqubit, + wires=wires, + controls=controls, + condition=False, + den_mat=den_mat, + tsr_mode=tsr_mode, + ) self.minmax = [min(self.wires), max(self.wires)] # whether the wires are consecutive integers self.local = True @@ -297,16 +334,13 @@ def get_unitary(self) -> torch.Tensor: return multi_kron(lst) else: matrix = self.update_matrix() - identity = torch.eye(2 ** self.nqubit, dtype=matrix.dtype, device=matrix.device) - identity = identity.reshape([2 ** self.nqubit] + [2] * self.nqubit) - return self.op_state_base(identity, matrix).reshape(2 ** self.nqubit, 2 ** self.nqubit).T + identity = torch.eye(2**self.nqubit, dtype=matrix.dtype, device=matrix.device) + identity = identity.reshape([2**self.nqubit] + [2] * self.nqubit) + return self.op_state_base(identity, matrix).reshape(2**self.nqubit, 2**self.nqubit).T def inverse(self) -> 'ArbitraryGate': """Get the inversed gate.""" - if isinstance(self.name, str): - name = self.name + '_dagger' - else: - name = self.name + name = self.name + '_dagger' if isinstance(self.name, str) else self.name gate = copy(self) gate.inv_mode = not self.inv_mode gate.name = name @@ -332,20 +366,28 @@ class ParametricSingleGate(SingleGate): requires_grad (bool, optional): Whether the parameters are ``nn.Parameter`` or ``buffer``. Default: ``False`` (which means ``buffer``) """ + def __init__( self, - name: Optional[str] = None, + name: str | None = None, inputs: Any = None, nqubit: int = 1, - wires: Union[int, List[int], None] = None, - controls: Union[int, List[int], None] = None, + wires: int | list[int] | None = None, + controls: int | list[int] | None = None, condition: bool = False, den_mat: bool = False, tsr_mode: bool = False, - requires_grad: bool = False + requires_grad: bool = False, ) -> None: - super().__init__(name=name, nqubit=nqubit, wires=wires, controls=controls, condition=condition, - den_mat=den_mat, tsr_mode=tsr_mode) + super().__init__( + name=name, + nqubit=nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=den_mat, + tsr_mode=tsr_mode, + ) self.npara = 1 self.requires_grad = requires_grad self.inv_mode = False @@ -363,10 +405,7 @@ def inputs_to_tensor(self, inputs: Any = None) -> torch.Tensor: def update_matrix(self) -> torch.Tensor: """Update the local unitary matrix.""" - if self.inv_mode: - theta = -self.theta - else: - theta = self.theta + theta = -self.theta if self.inv_mode else self.theta matrix = self.get_matrix(theta) self.matrix = matrix.detach() return matrix @@ -393,10 +432,7 @@ def inverse(self) -> 'ParametricSingleGate': return gate def extra_repr(self) -> str: - if self.inv_mode: - theta = -self.theta - else: - theta = self.theta + theta = -self.theta if self.inv_mode else self.theta s = f'wires={self.wires}, theta={theta.item()}' if self.controls == []: return s @@ -423,20 +459,28 @@ class ParametricDoubleGate(DoubleGate): requires_grad (bool, optional): Whether the parameters are ``nn.Parameter`` or ``buffer``. Default: ``False`` (which means ``buffer``) """ + def __init__( self, - name: Optional[str] = None, + name: str | None = None, inputs: Any = None, nqubit: int = 2, - wires: Optional[List[int]] = None, - controls: Union[int, List[int], None] = None, + wires: list[int] | None = None, + controls: int | list[int] | None = None, condition: bool = False, den_mat: bool = False, tsr_mode: bool = False, - requires_grad: bool = False + requires_grad: bool = False, ) -> None: - super().__init__(name=name, nqubit=nqubit, wires=wires, controls=controls, condition=condition, - den_mat=den_mat, tsr_mode=tsr_mode) + super().__init__( + name=name, + nqubit=nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=den_mat, + tsr_mode=tsr_mode, + ) self.npara = 1 self.requires_grad = requires_grad self.inv_mode = False @@ -454,10 +498,7 @@ def inputs_to_tensor(self, inputs: Any = None) -> torch.Tensor: def update_matrix(self) -> torch.Tensor: """Update the local unitary matrix.""" - if self.inv_mode: - theta = -self.theta - else: - theta = self.theta + theta = -self.theta if self.inv_mode else self.theta matrix = self.get_matrix(theta) self.matrix = matrix.detach() return matrix @@ -484,10 +525,7 @@ def inverse(self) -> 'ParametricDoubleGate': return gate def extra_repr(self) -> str: - if self.inv_mode: - theta = -self.theta - else: - theta = self.theta + theta = -self.theta if self.inv_mode else self.theta s = f'wires={self.wires}, theta={theta.item()}' if self.controls == []: return s @@ -526,30 +564,40 @@ class U3Gate(ParametricSingleGate): requires_grad (bool, optional): Whether the parameters are ``nn.Parameter`` or ``buffer``. Default: ``False`` (which means ``buffer``) """ + def __init__( self, inputs: Any = None, nqubit: int = 1, - wires: Union[int, List[int], None] = None, - controls: Union[int, List[int], None] = None, + wires: int | list[int] | None = None, + controls: int | list[int] | None = None, condition: bool = False, den_mat: bool = False, tsr_mode: bool = False, - requires_grad: bool = False + requires_grad: bool = False, ) -> None: - super().__init__(name='U3Gate', inputs=inputs, nqubit=nqubit, wires=wires, controls=controls, - condition=condition, den_mat=den_mat, tsr_mode=tsr_mode, requires_grad=requires_grad) + super().__init__( + name='U3Gate', + inputs=inputs, + nqubit=nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=den_mat, + tsr_mode=tsr_mode, + requires_grad=requires_grad, + ) self.npara = 3 - def inputs_to_tensor(self, inputs: Any = None) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + def inputs_to_tensor(self, inputs: Any = None) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: """Convert inputs to torch.Tensor.""" if inputs is None: theta = torch.rand(1)[0] * torch.pi - phi = torch.rand(1)[0] * 2 * torch.pi + phi = torch.rand(1)[0] * 2 * torch.pi lambd = torch.rand(1)[0] * 2 * torch.pi else: theta = inputs[0] - phi = inputs[1] + phi = inputs[1] lambd = inputs[2] if not isinstance(theta, (torch.Tensor, nn.Parameter)): theta = torch.tensor(theta, dtype=torch.float) @@ -564,8 +612,8 @@ def get_matrix(self, theta: Any, phi: Any, lambd: Any) -> torch.Tensor: theta, phi, lambd = self.inputs_to_tensor([theta, phi, lambd]) cos_t = torch.cos(theta / 2) sin_t = torch.sin(theta / 2) - e_il = torch.exp(1j * lambd) - e_ip = torch.exp(1j * phi) + e_il = torch.exp(1j * lambd) + e_ip = torch.exp(1j * phi) e_ipl = torch.exp(1j * (phi + lambd)) return torch.stack([cos_t, -e_il * sin_t, e_ip * sin_t, e_ipl * cos_t]).reshape(2, 2) @@ -573,11 +621,11 @@ def update_matrix(self) -> torch.Tensor: """Update the local unitary matrix.""" if self.inv_mode: theta = -self.theta - phi = -self.lambd + phi = -self.lambd lambd = -self.phi else: theta = self.theta - phi = self.phi + phi = self.phi lambd = self.lambd matrix = self.get_matrix(theta, phi, lambd) self.matrix = matrix.detach() @@ -600,7 +648,7 @@ def init_para(self, inputs: Any = None) -> None: theta, phi, lambd = self.inputs_to_tensor(inputs) if self.requires_grad: self.theta = nn.Parameter(theta) - self.phi = nn.Parameter(phi) + self.phi = nn.Parameter(phi) self.lambd = nn.Parameter(lambd) else: self.register_buffer('theta', theta) @@ -611,11 +659,11 @@ def init_para(self, inputs: Any = None) -> None: def extra_repr(self) -> str: if self.inv_mode: theta = -self.theta - phi = -self.lambd + phi = -self.lambd lambd = -self.phi else: theta = self.theta - phi = self.phi + phi = self.phi lambd = self.lambd s = f'wires={self.wires}, theta={theta.item()}, phi={phi.item()}, lambda={lambd.item()}' if self.controls == []: @@ -626,11 +674,11 @@ def extra_repr(self) -> str: def _qasm(self) -> str: if self.inv_mode: theta = -self.theta - phi = -self.lambd + phi = -self.lambd lambd = -self.phi else: theta = self.theta - phi = self.phi + phi = self.phi lambd = self.lambd if self.condition: return self._qasm_cond_measure() + f'u({theta.item()},{phi.item()},{lambd.item()}) q{self.wires};\n' @@ -670,19 +718,29 @@ class PhaseShift(ParametricSingleGate): requires_grad (bool, optional): Whether the parameter is ``nn.Parameter`` or ``buffer``. Default: ``False`` (which means ``buffer``) """ + def __init__( self, inputs: Any = None, nqubit: int = 1, - wires: Union[int, List[int], None] = None, - controls: Union[int, List[int], None] = None, + wires: int | list[int] | None = None, + controls: int | list[int] | None = None, condition: bool = False, den_mat: bool = False, tsr_mode: bool = False, - requires_grad: bool = False + requires_grad: bool = False, ) -> None: - super().__init__(name='PhaseShift', inputs=inputs, nqubit=nqubit, wires=wires, controls=controls, - condition=condition, den_mat=den_mat, tsr_mode=tsr_mode, requires_grad=requires_grad) + super().__init__( + name='PhaseShift', + inputs=inputs, + nqubit=nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=den_mat, + tsr_mode=tsr_mode, + requires_grad=requires_grad, + ) def inputs_to_tensor(self, inputs: Any = None) -> torch.Tensor: """Convert inputs to torch.Tensor.""" @@ -702,10 +760,7 @@ def get_matrix(self, theta: Any) -> torch.Tensor: return torch.block_diag(m1, e_it).reshape(2, 2) def _qasm(self) -> str: - if self.inv_mode: - theta = -self.theta - else: - theta = self.theta + theta = -self.theta if self.inv_mode else self.theta if self.condition: return self._qasm_cond_measure() + f'p({theta.item()}) q{self.wires};\n' if self.controls == []: @@ -738,16 +793,20 @@ class Identity(Gate): and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + def __init__( - self, - nqubit: int = 1, - wires: Union[int, List[int], None] = None, - den_mat: bool = False, - tsr_mode: bool = False + self, nqubit: int = 1, wires: int | list[int] | None = None, den_mat: bool = False, tsr_mode: bool = False ) -> None: - super().__init__(name='Identity', nqubit=nqubit, wires=wires, controls=None, condition=False, - den_mat=den_mat, tsr_mode=tsr_mode) - self.register_buffer('matrix', torch.eye(2 ** self.nqubit, dtype=torch.cfloat)) + super().__init__( + name='Identity', + nqubit=nqubit, + wires=wires, + controls=None, + condition=False, + den_mat=den_mat, + tsr_mode=tsr_mode, + ) + self.register_buffer('matrix', torch.eye(2**self.nqubit, dtype=torch.cfloat)) def get_unitary(self) -> torch.Tensor: """Get the global unitary matrix.""" @@ -782,17 +841,25 @@ class PauliX(SingleGate): and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + def __init__( self, nqubit: int = 1, - wires: Union[int, List[int], None] = None, - controls: Union[int, List[int], None] = None, + wires: int | list[int] | None = None, + controls: int | list[int] | None = None, condition: bool = False, den_mat: bool = False, - tsr_mode: bool = False + tsr_mode: bool = False, ) -> None: - super().__init__(name='PauliX', nqubit=nqubit, wires=wires, controls=controls, condition=condition, - den_mat=den_mat, tsr_mode=tsr_mode) + super().__init__( + name='PauliX', + nqubit=nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=den_mat, + tsr_mode=tsr_mode, + ) self.register_buffer('matrix', torch.tensor([[0, 1], [1, 0]], dtype=torch.cfloat)) def _qasm(self) -> str: @@ -807,9 +874,10 @@ def _qasm(self) -> str: else: return self._qasm_customized('x') - def pattern(self, nodes: Union[int, List[int]], ancilla: List[int]) -> nn.Sequential: + def pattern(self, nodes: int | list[int], ancilla: list[int]) -> nn.Sequential: """Get the MBQC pattern.""" - from .mbqc import Node, Entanglement, Measurement, Correction + from .mbqc import Correction, Entanglement, Measurement, Node + if isinstance(nodes, list): assert len(nodes) == len(self.wires) nodes = nodes[0] @@ -850,17 +918,25 @@ class PauliY(SingleGate): and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + def __init__( self, nqubit: int = 1, - wires: Union[int, List[int], None] = None, - controls: Union[int, List[int], None] = None, + wires: int | list[int] | None = None, + controls: int | list[int] | None = None, condition: bool = False, den_mat: bool = False, - tsr_mode: bool = False + tsr_mode: bool = False, ) -> None: - super().__init__(name='PauliY', nqubit=nqubit, wires=wires, controls=controls, condition=condition, - den_mat=den_mat, tsr_mode=tsr_mode) + super().__init__( + name='PauliY', + nqubit=nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=den_mat, + tsr_mode=tsr_mode, + ) self.register_buffer('matrix', torch.tensor([[0, -1j], [1j, 0]])) # MBQC self.nancilla = 4 @@ -875,9 +951,10 @@ def _qasm(self) -> str: else: return self._qasm_customized('y') - def pattern(self, nodes: Union[int, List[int]], ancilla: List[int]) -> nn.Sequential: + def pattern(self, nodes: int | list[int], ancilla: list[int]) -> nn.Sequential: """Get the MBQC pattern.""" - from .mbqc import Node, Entanglement, Measurement, Correction + from .mbqc import Correction, Entanglement, Measurement, Node + if isinstance(nodes, list): assert len(nodes) == len(self.wires) nodes = nodes[0] @@ -888,9 +965,9 @@ def pattern(self, nodes: Union[int, List[int]], ancilla: List[int]) -> nn.Sequen cmds.append(Entanglement(ancilla[0], ancilla[1])) cmds.append(Entanglement(ancilla[1], ancilla[2])) cmds.append(Entanglement(ancilla[2], ancilla[3])) - cmds.append(Measurement(nodes, angle=torch.pi/2)) + cmds.append(Measurement(nodes, angle=torch.pi / 2)) cmds.append(Measurement(ancilla[0], angle=torch.pi, s_domain=nodes)) - cmds.append(Measurement(ancilla[1], angle=-torch.pi/2, s_domain=nodes)) + cmds.append(Measurement(ancilla[1], angle=-torch.pi / 2, s_domain=nodes)) cmds.append(Measurement(ancilla[2])) cmds.append(Correction(ancilla[3], basis='x', domain=[ancilla[0], ancilla[2]])) cmds.append(Correction(ancilla[3], basis='z', domain=[ancilla[0], ancilla[1]])) @@ -922,17 +999,25 @@ class PauliZ(SingleGate): and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + def __init__( self, nqubit: int = 1, - wires: Union[int, List[int], None] = None, - controls: Union[int, List[int], None] = None, + wires: int | list[int] | None = None, + controls: int | list[int] | None = None, condition: bool = False, den_mat: bool = False, - tsr_mode: bool = False + tsr_mode: bool = False, ) -> None: - super().__init__(name='PauliZ', nqubit=nqubit, wires=wires, controls=controls, condition=condition, - den_mat=den_mat, tsr_mode=tsr_mode) + super().__init__( + name='PauliZ', + nqubit=nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=den_mat, + tsr_mode=tsr_mode, + ) self.register_buffer('matrix', torch.tensor([[1, 0], [0, -1]], dtype=torch.cfloat)) def _qasm(self) -> str: @@ -945,9 +1030,10 @@ def _qasm(self) -> str: else: return self._qasm_customized('z') - def pattern(self, nodes: Union[int, List[int]], ancilla: List[int]) -> nn.Sequential: + def pattern(self, nodes: int | list[int], ancilla: list[int]) -> nn.Sequential: """Get the MBQC pattern.""" - from .mbqc import Node, Entanglement, Measurement, Correction + from .mbqc import Correction, Entanglement, Measurement, Node + if isinstance(nodes, list): assert len(nodes) == len(self.wires) nodes = nodes[0] @@ -989,18 +1075,26 @@ class Hadamard(SingleGate): and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + def __init__( self, nqubit: int = 1, - wires: Union[int, List[int], None] = None, - controls: Union[int, List[int], None] = None, + wires: int | list[int] | None = None, + controls: int | list[int] | None = None, condition: bool = False, den_mat: bool = False, - tsr_mode: bool = False + tsr_mode: bool = False, ) -> None: - super().__init__(name='Hadamard', nqubit=nqubit, wires=wires, controls=controls, condition=condition, - den_mat=den_mat, tsr_mode=tsr_mode) - self.register_buffer('matrix', torch.tensor([[1, 1], [1, -1]], dtype=torch.cfloat) / 2 ** 0.5) + super().__init__( + name='Hadamard', + nqubit=nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=den_mat, + tsr_mode=tsr_mode, + ) + self.register_buffer('matrix', torch.tensor([[1, 1], [1, -1]], dtype=torch.cfloat) / 2**0.5) # MBQC self.nancilla = 1 @@ -1014,9 +1108,10 @@ def _qasm(self) -> str: else: return self._qasm_customized('h') - def pattern(self, nodes: Union[int, List[int]], ancilla: Union[int, List[int]]) -> nn.Sequential: + def pattern(self, nodes: int | list[int], ancilla: int | list[int]) -> nn.Sequential: """Get the MBQC pattern.""" - from .mbqc import Node, Entanglement, Measurement, Correction + from .mbqc import Correction, Entanglement, Measurement, Node + if isinstance(nodes, list): assert len(nodes) == len(self.wires) nodes = nodes[0] @@ -1056,23 +1151,37 @@ class SGate(SingleGate): and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + def __init__( self, nqubit: int = 1, - wires: Union[int, List[int], None] = None, - controls: Union[int, List[int], None] = None, + wires: int | list[int] | None = None, + controls: int | list[int] | None = None, condition: bool = False, den_mat: bool = False, - tsr_mode: bool = False + tsr_mode: bool = False, ) -> None: - super().__init__(name='SGate', nqubit=nqubit, wires=wires, controls=controls, condition=condition, - den_mat=den_mat, tsr_mode=tsr_mode) + super().__init__( + name='SGate', + nqubit=nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=den_mat, + tsr_mode=tsr_mode, + ) self.register_buffer('matrix', torch.tensor([[1, 0], [0, 1j]])) def inverse(self) -> 'SDaggerGate': """Get the inversed gate.""" - return SDaggerGate(nqubit=self.nqubit, wires=self.wires, controls=self.controls, - condition=self.condition, den_mat=self.den_mat, tsr_mode=self.tsr_mode) + return SDaggerGate( + nqubit=self.nqubit, + wires=self.wires, + controls=self.controls, + condition=self.condition, + den_mat=self.den_mat, + tsr_mode=self.tsr_mode, + ) def _qasm(self) -> str: if self.condition: @@ -1082,7 +1191,6 @@ def _qasm(self) -> str: elif len(self.controls) == 1: qasm_str1 = '' qasm_str2 = f'cs q{self.controls},q{self.wires};\n' - # pylint: disable=protected-access if 'cs' not in Gate._qasm_new_gate: qasm_str1 = 'gate cs q0,q1 { p(pi/4) q0; cx q0,q1; p(-pi/4) q1; cx q0,q1; p(pi/4) q1; }\n' Gate._qasm_new_gate.append('cs') @@ -1090,9 +1198,10 @@ def _qasm(self) -> str: else: return self._qasm_customized('s') - def pattern(self, nodes: Union[int, List[int]], ancilla: List[int]) -> nn.Sequential: + def pattern(self, nodes: int | list[int], ancilla: list[int]) -> nn.Sequential: """Get the MBQC pattern.""" - from .mbqc import Node, Entanglement, Measurement, Correction + from .mbqc import Correction, Entanglement, Measurement, Node + if isinstance(nodes, list): assert len(nodes) == len(self.wires) nodes = nodes[0] @@ -1101,7 +1210,7 @@ def pattern(self, nodes: Union[int, List[int]], ancilla: List[int]) -> nn.Sequen cmds.append(Node(ancilla)) cmds.append(Entanglement(nodes, ancilla[0])) cmds.append(Entanglement(ancilla[0], ancilla[1])) - cmds.append(Measurement(nodes, angle=-torch.pi/2)) + cmds.append(Measurement(nodes, angle=-torch.pi / 2)) cmds.append(Measurement(ancilla[0])) cmds.append(Correction(ancilla[1], basis='x', domain=ancilla[0])) cmds.append(Correction(ancilla[1], basis='z', domain=nodes)) @@ -1134,23 +1243,37 @@ class SDaggerGate(SingleGate): and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + def __init__( self, nqubit: int = 1, - wires: Union[int, List[int], None] = None, - controls: Union[int, List[int], None] = None, + wires: int | list[int] | None = None, + controls: int | list[int] | None = None, condition: bool = False, den_mat: bool = False, - tsr_mode: bool = False + tsr_mode: bool = False, ) -> None: - super().__init__(name='SDaggerGate', nqubit=nqubit, wires=wires, controls=controls, - condition=condition, den_mat=den_mat, tsr_mode=tsr_mode) + super().__init__( + name='SDaggerGate', + nqubit=nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=den_mat, + tsr_mode=tsr_mode, + ) self.register_buffer('matrix', torch.tensor([[1, 0], [0, -1j]])) def inverse(self) -> SGate: """Get the inversed gate.""" - return SGate(nqubit=self.nqubit, wires=self.wires, controls=self.controls, - condition=self.condition, den_mat=self.den_mat, tsr_mode=self.tsr_mode) + return SGate( + nqubit=self.nqubit, + wires=self.wires, + controls=self.controls, + condition=self.condition, + den_mat=self.den_mat, + tsr_mode=self.tsr_mode, + ) def _qasm(self) -> str: if self.condition: @@ -1160,7 +1283,6 @@ def _qasm(self) -> str: elif len(self.controls) == 1: qasm_str1 = '' qasm_str2 = f'csdg q{self.controls},q{self.wires};\n' - # pylint: disable=protected-access if 'csdg' not in Gate._qasm_new_gate: qasm_str1 = 'gate csdg q0,q1 { p(-pi/4) q0; cx q0,q1; p(pi/4) q1; cx q0,q1; p(-pi/4) q1; }\n' Gate._qasm_new_gate.append('csdg') @@ -1193,23 +1315,37 @@ class TGate(SingleGate): and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + def __init__( self, nqubit: int = 1, - wires: Union[int, List[int], None] = None, - controls: Union[int, List[int], None] = None, + wires: int | list[int] | None = None, + controls: int | list[int] | None = None, condition: bool = False, den_mat: bool = False, - tsr_mode: bool = False + tsr_mode: bool = False, ) -> None: - super().__init__(name='TGate', nqubit=nqubit, wires=wires, controls=controls, condition=condition, - den_mat=den_mat, tsr_mode=tsr_mode) - self.register_buffer('matrix', torch.tensor([[1, 0], [0, (1 + 1j) / 2 ** 0.5]])) + super().__init__( + name='TGate', + nqubit=nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=den_mat, + tsr_mode=tsr_mode, + ) + self.register_buffer('matrix', torch.tensor([[1, 0], [0, (1 + 1j) / 2**0.5]])) def inverse(self) -> 'TDaggerGate': """Get the inversed gate.""" - return TDaggerGate(nqubit=self.nqubit, wires=self.wires, controls=self.controls, - condition=self.condition, den_mat=self.den_mat, tsr_mode=self.tsr_mode) + return TDaggerGate( + nqubit=self.nqubit, + wires=self.wires, + controls=self.controls, + condition=self.condition, + den_mat=self.den_mat, + tsr_mode=self.tsr_mode, + ) def _qasm(self) -> str: if self.condition: @@ -1245,23 +1381,37 @@ class TDaggerGate(SingleGate): and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + def __init__( self, nqubit: int = 1, - wires: Union[int, List[int], None] = None, - controls: Union[int, List[int], None] = None, + wires: int | list[int] | None = None, + controls: int | list[int] | None = None, condition: bool = False, den_mat: bool = False, - tsr_mode: bool = False + tsr_mode: bool = False, ) -> None: - super().__init__(name='TDaggerGate', nqubit=nqubit, wires=wires, controls=controls, - condition=condition, den_mat=den_mat, tsr_mode=tsr_mode) - self.register_buffer('matrix', torch.tensor([[1, 0], [0, (1 - 1j) / 2 ** 0.5]])) + super().__init__( + name='TDaggerGate', + nqubit=nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=den_mat, + tsr_mode=tsr_mode, + ) + self.register_buffer('matrix', torch.tensor([[1, 0], [0, (1 - 1j) / 2**0.5]])) def inverse(self) -> TGate: """Get the inversed gate.""" - return TGate(nqubit=self.nqubit, wires=self.wires, controls=self.controls, - condition=self.condition, den_mat=self.den_mat, tsr_mode=self.tsr_mode) + return TGate( + nqubit=self.nqubit, + wires=self.wires, + controls=self.controls, + condition=self.condition, + den_mat=self.den_mat, + tsr_mode=self.tsr_mode, + ) def _qasm(self) -> str: if self.condition: @@ -1302,34 +1452,41 @@ class Rx(ParametricSingleGate): requires_grad (bool, optional): Whether the parameter is ``nn.Parameter`` or ``buffer``. Default: ``False`` (which means ``buffer``) """ + def __init__( self, inputs: Any = None, nqubit: int = 1, - wires: Union[int, List[int], None] = None, - controls: Union[int, List[int], None] = None, + wires: int | list[int] | None = None, + controls: int | list[int] | None = None, condition: bool = False, den_mat: bool = False, tsr_mode: bool = False, - requires_grad: bool = False + requires_grad: bool = False, ) -> None: - super().__init__(name='Rx', inputs=inputs, nqubit=nqubit, wires=wires, controls=controls, - condition=condition, den_mat=den_mat, tsr_mode=tsr_mode, requires_grad=requires_grad) + super().__init__( + name='Rx', + inputs=inputs, + nqubit=nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=den_mat, + tsr_mode=tsr_mode, + requires_grad=requires_grad, + ) # MBQC - self.idx_enc = [4] # index of the commands to encode + self.idx_enc = [4] # index of the commands to encode def get_matrix(self, theta: Any) -> torch.Tensor: """Get the local unitary matrix.""" theta = self.inputs_to_tensor(theta) - cos = torch.cos(theta / 2) + cos = torch.cos(theta / 2) isin = torch.sin(theta / 2) * 1j return torch.stack([cos, -isin, -isin, cos]).reshape(2, 2) def _qasm(self) -> str: - if self.inv_mode: - theta = -self.theta - else: - theta = self.theta + theta = -self.theta if self.inv_mode else self.theta if self.condition: return self._qasm_cond_measure() + f'rx({theta.item()}) q{self.wires};\n' if self.controls == []: @@ -1340,14 +1497,11 @@ def _qasm(self) -> str: return self._qasm_customized('rx') def pattern( - self, - nodes: Union[int, List[int]], - ancilla: List[int], - angle: Any, - requires_grad: bool = False + self, nodes: int | list[int], ancilla: list[int], angle: Any, requires_grad: bool = False ) -> nn.Sequential: """Get the MBQC pattern.""" - from .mbqc import Node, Entanglement, Measurement, Correction + from .mbqc import Correction, Entanglement, Measurement, Node + if isinstance(nodes, list): assert len(nodes) == len(self.wires) nodes = nodes[0] @@ -1394,22 +1548,32 @@ class Ry(ParametricSingleGate): requires_grad (bool, optional): Whether the parameter is ``nn.Parameter`` or ``buffer``. Default: ``False`` (which means ``buffer``) """ + def __init__( self, inputs: Any = None, nqubit: int = 1, - wires: Union[int, List[int], None] = None, - controls: Union[int, List[int], None] = None, + wires: int | list[int] | None = None, + controls: int | list[int] | None = None, condition: bool = False, den_mat: bool = False, tsr_mode: bool = False, - requires_grad: bool = False + requires_grad: bool = False, ) -> None: - super().__init__(name='Ry', inputs=inputs, nqubit=nqubit, wires=wires, controls=controls, - condition=condition, den_mat=den_mat, tsr_mode=tsr_mode, requires_grad=requires_grad) + super().__init__( + name='Ry', + inputs=inputs, + nqubit=nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=den_mat, + tsr_mode=tsr_mode, + requires_grad=requires_grad, + ) # MBQC self.nancilla = 4 - self.idx_enc = [6] # index of the commands to encode + self.idx_enc = [6] # index of the commands to encode def get_matrix(self, theta: Any) -> torch.Tensor: """Get the local unitary matrix.""" @@ -1419,10 +1583,7 @@ def get_matrix(self, theta: Any) -> torch.Tensor: return torch.stack([cos, -sin, sin, cos]).reshape(2, 2) + 0j def _qasm(self) -> str: - if self.inv_mode: - theta = -self.theta - else: - theta = self.theta + theta = -self.theta if self.inv_mode else self.theta if self.condition: return self._qasm_cond_measure() + f'ry({theta.item()}) q{self.wires};\n' if self.controls == []: @@ -1433,14 +1594,11 @@ def _qasm(self) -> str: return self._qasm_customized('ry') def pattern( - self, - nodes: Union[int, List[int]], - ancilla: List[int], - angle: Any, - requires_grad: bool = False + self, nodes: int | list[int], ancilla: list[int], angle: Any, requires_grad: bool = False ) -> nn.Sequential: """Get the MBQC pattern.""" - from .mbqc import Node, Entanglement, Measurement, Correction + from .mbqc import Correction, Entanglement, Measurement, Node + if isinstance(nodes, list): assert len(nodes) == len(self.wires) nodes = nodes[0] @@ -1489,21 +1647,31 @@ class Rz(ParametricSingleGate): requires_grad (bool, optional): Whether the parameter is ``nn.Parameter`` or ``buffer``. Default: ``False`` (which means ``buffer``) """ + def __init__( self, inputs: Any = None, nqubit: int = 1, - wires: Union[int, List[int], None] = None, - controls: Union[int, List[int], None] = None, + wires: int | list[int] | None = None, + controls: int | list[int] | None = None, condition: bool = False, den_mat: bool = False, tsr_mode: bool = False, - requires_grad: bool = False + requires_grad: bool = False, ) -> None: - super().__init__(name='Rz', inputs=inputs, nqubit=nqubit, wires=wires, controls=controls, - condition=condition, den_mat=den_mat, tsr_mode=tsr_mode, requires_grad=requires_grad) + super().__init__( + name='Rz', + inputs=inputs, + nqubit=nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=den_mat, + tsr_mode=tsr_mode, + requires_grad=requires_grad, + ) # MBQC - self.idx_enc = [3] # index of the commands to encode + self.idx_enc = [3] # index of the commands to encode def get_matrix(self, theta: Any) -> torch.Tensor: """Get the local unitary matrix.""" @@ -1513,10 +1681,7 @@ def get_matrix(self, theta: Any) -> torch.Tensor: return torch.stack([e_m_it, e_it]).reshape(-1).diag_embed().reshape(2, 2) def _qasm(self) -> str: - if self.inv_mode: - theta = -self.theta - else: - theta = self.theta + theta = -self.theta if self.inv_mode else self.theta if self.condition: return self._qasm_cond_measure() + f'rz({theta.item()}) q{self.wires};\n' if self.controls == []: @@ -1527,14 +1692,11 @@ def _qasm(self) -> str: return self._qasm_customized('rz') def pattern( - self, - nodes: Union[int, List[int]], - ancilla: List[int], - angle: Any, - requires_grad: bool = False + self, nodes: int | list[int], ancilla: list[int], angle: Any, requires_grad: bool = False ) -> nn.Sequential: """Get the MBQC pattern.""" - from .mbqc import Node, Entanglement, Measurement, Correction + from .mbqc import Correction, Entanglement, Measurement, Node + if isinstance(nodes, list): assert len(nodes) == len(self.wires) nodes = nodes[0] @@ -1604,21 +1766,31 @@ class ProjectionJ(ParametricSingleGate): requires_grad (bool, optional): Whether the parameter is ``nn.Parameter`` or ``buffer``. Default: ``False`` (which means ``buffer``) """ + def __init__( self, inputs: Any = None, nqubit: int = 1, - wires: Union[int, List[int], None] = None, + wires: int | list[int] | None = None, plane: str = 'xy', - controls: Union[int, List[int], None] = None, + controls: int | list[int] | None = None, condition: bool = False, den_mat: bool = False, tsr_mode: bool = False, - requires_grad: bool = False + requires_grad: bool = False, ) -> None: self.plane = plane.lower() - super().__init__(name='ProjectionJ', inputs=inputs, nqubit=nqubit, wires=wires, controls=controls, - condition=condition, den_mat=den_mat, tsr_mode=tsr_mode, requires_grad=requires_grad) + super().__init__( + name='ProjectionJ', + inputs=inputs, + nqubit=nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=den_mat, + tsr_mode=tsr_mode, + requires_grad=requires_grad, + ) def get_matrix(self, theta: Any) -> torch.Tensor: """Get the local unitary matrix.""" @@ -1626,11 +1798,11 @@ def get_matrix(self, theta: Any) -> torch.Tensor: if self.plane in ['xy', 'yx']: one = torch.ones_like(theta) e_m_theta = torch.exp(-1j * theta) - return torch.stack([one, e_m_theta, one, -e_m_theta]).reshape(2, 2) / 2 ** 0.5 + return torch.stack([one, e_m_theta, one, -e_m_theta]).reshape(2, 2) / 2**0.5 elif self.plane in ['yz', 'zy']: c_p_s = torch.cos(theta / 2) + torch.sin(theta / 2) c_m_s = torch.cos(theta / 2) - torch.sin(theta / 2) - return torch.stack([c_p_s, -1j * c_m_s, c_m_s, 1j * c_p_s]).reshape(2, 2) / 2 ** 0.5 + return torch.stack([c_p_s, -1j * c_m_s, c_m_s, 1j * c_p_s]).reshape(2, 2) / 2**0.5 elif self.plane in ['zx', 'xz']: cos = torch.cos(theta / 2) sin = torch.sin(theta / 2) @@ -1651,7 +1823,6 @@ def _qasm(self) -> str: qasm_lst2.append(f'q[{wire}],') qasm_str1 = ''.join(qasm_lst1)[:-1] + ';\n' qasm_str2 = ''.join(qasm_lst2)[:-1] + ';\n' - # pylint: disable=protected-access if name not in Gate._qasm_new_gate: Gate._qasm_new_gate.append(name) return qasm_str1 + self._qasm_cond_measure() + qasm_str2 @@ -1677,19 +1848,27 @@ class CombinedSingleGate(SingleGate): and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + def __init__( self, - gates: List[SingleGate], - name: Optional[str] = None, + gates: list[SingleGate], + name: str | None = None, nqubit: int = 1, - wires: Union[int, List[int], None] = None, - controls: Union[int, List[int], None] = None, + wires: int | list[int] | None = None, + controls: int | list[int] | None = None, condition: bool = False, den_mat: bool = False, - tsr_mode: bool = False + tsr_mode: bool = False, ) -> None: - super().__init__(name=name, nqubit=nqubit, wires=wires, controls=controls, condition=condition, - den_mat=den_mat, tsr_mode=tsr_mode) + super().__init__( + name=name, + nqubit=nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=den_mat, + tsr_mode=tsr_mode, + ) for gate in gates: gate.nqubit = self.nqubit gate.wires = self.wires @@ -1705,10 +1884,7 @@ def get_matrix(self) -> torch.Tensor: """Get the local unitary matrix.""" matrix = None for gate in self.gates: - if matrix is None: - matrix = gate.update_matrix() - else: - matrix = gate.update_matrix() @ matrix + matrix = gate.update_matrix() if matrix is None else gate.update_matrix() @ matrix return matrix def update_matrix(self) -> torch.Tensor: @@ -1725,7 +1901,7 @@ def get_derivative(self, inputs: Any) -> torch.Tensor: derivatives = [] count = 0 for gate in self.gates: - du_dx = gate.get_derivative(inputs[count:count+gate.npara]) + du_dx = gate.get_derivative(inputs[count : count + gate.npara]) if du_dx.ndim == 2: du_dx = du_dx.unsqueeze(0) derivatives.append(du_dx) @@ -1755,14 +1931,20 @@ def inverse(self) -> 'CombinedSingleGate': gates = nn.ModuleList() for gate in reversed(self.gates): gates.append(gate.inverse()) - return CombinedSingleGate(gates=gates, name=self.name, nqubit=self.nqubit, wires=self.wires, - controls=self.controls, condition=self.condition, den_mat=self.den_mat, - tsr_mode=self.tsr_mode) + return CombinedSingleGate( + gates=gates, + name=self.name, + nqubit=self.nqubit, + wires=self.wires, + controls=self.controls, + condition=self.condition, + den_mat=self.den_mat, + tsr_mode=self.tsr_mode, + ) def _qasm(self) -> str: lst = [] for gate in self.gates: - # pylint: disable=protected-access lst.append(gate._qasm()) return ''.join(lst) @@ -1792,27 +1974,22 @@ class CNOT(DoubleControlGate): and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + def __init__( - self, - nqubit: int = 2, - wires: Optional[List[int]] = None, - den_mat: bool = False, - tsr_mode: bool = False + self, nqubit: int = 2, wires: list[int] | None = None, den_mat: bool = False, tsr_mode: bool = False ) -> None: super().__init__(name='CNOT', nqubit=nqubit, wires=wires, den_mat=den_mat, tsr_mode=tsr_mode) - self.register_buffer('matrix', torch.tensor([[1, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, 0, 1], - [0, 0, 1, 0]]) + 0j) + self.register_buffer('matrix', torch.tensor([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]]) + 0j) # MBQC self.nancilla = 2 def _qasm(self) -> str: return f'cx q[{self.wires[0]}],q[{self.wires[1]}];\n' - def pattern(self, nodes: List[int], ancilla: List[int]) -> nn.Sequential: + def pattern(self, nodes: list[int], ancilla: list[int]) -> nn.Sequential: """Get the MBQC pattern.""" - from .mbqc import Node, Entanglement, Measurement, Correction + from .mbqc import Correction, Entanglement, Measurement, Node + assert len(nodes) == len(self.wires) assert len(ancilla) == self.nancilla control = nodes[0] @@ -1857,21 +2034,26 @@ class Swap(DoubleGate): and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + def __init__( self, nqubit: int = 2, - wires: Optional[List[int]] = None, - controls: Union[int, List[int], None] = None, + wires: list[int] | None = None, + controls: int | list[int] | None = None, condition: bool = False, den_mat: bool = False, - tsr_mode: bool = False + tsr_mode: bool = False, ) -> None: - super().__init__(name='Swap', nqubit=nqubit, wires=wires, controls=controls, condition=condition, - den_mat=den_mat, tsr_mode=tsr_mode) - self.register_buffer('matrix', torch.tensor([[1, 0, 0, 0], - [0, 0, 1, 0], - [0, 1, 0, 0], - [0, 0, 0, 1]]) + 0j) + super().__init__( + name='Swap', + nqubit=nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=den_mat, + tsr_mode=tsr_mode, + ) + self.register_buffer('matrix', torch.tensor([[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]]) + 0j) def op_dist_state(self, x: DistributedQubitState) -> DistributedQubitState: """Perform a forward pass of a gate for a distributed state vector.""" @@ -1917,26 +2099,30 @@ class ImaginarySwap(DoubleGate): and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + def __init__( self, nqubit: int = 2, - wires: Optional[List[int]] = None, - controls: Union[int, List[int], None] = None, + wires: list[int] | None = None, + controls: int | list[int] | None = None, condition: bool = False, den_mat: bool = False, - tsr_mode: bool = False + tsr_mode: bool = False, ) -> None: - super().__init__(name='ImaginarySwap', nqubit=nqubit, wires=wires, controls=controls, condition=condition, - den_mat=den_mat, tsr_mode=tsr_mode) - self.register_buffer('matrix', torch.tensor([[1, 0, 0, 0], - [0, 0, 1j, 0], - [0, 1j, 0, 0], - [0, 0, 0, 1]])) + super().__init__( + name='ImaginarySwap', + nqubit=nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=den_mat, + tsr_mode=tsr_mode, + ) + self.register_buffer('matrix', torch.tensor([[1, 0, 0, 0], [0, 0, 1j, 0], [0, 1j, 0, 0], [0, 0, 0, 1]])) def _qasm(self) -> str: qasm_str1 = '' qasm_str2 = f'iswap q[{self.wires[0]}],q[{self.wires[1]}];\n' - # pylint: disable=protected-access if 'iswap' not in Gate._qasm_new_gate: qasm_str1 = 'gate iswap q0,q1 { s q0; s q1; h q0; cx q0,q1; cx q1,q0; h q1; }\n' Gate._qasm_new_gate.append('iswap') @@ -1980,34 +2166,41 @@ class Rxx(ParametricDoubleGate): requires_grad (bool, optional): Whether the parameter is ``nn.Parameter`` or ``buffer``. Default: ``False`` (which means ``buffer``) """ + def __init__( self, inputs: Any = None, nqubit: int = 2, - wires: Optional[List[int]] = None, - controls: Union[int, List[int], None] = None, + wires: list[int] | None = None, + controls: int | list[int] | None = None, condition: bool = False, den_mat: bool = False, tsr_mode: bool = False, - requires_grad: bool = False + requires_grad: bool = False, ) -> None: - super().__init__(name='Rxx', inputs=inputs, nqubit=nqubit, wires=wires, controls=controls, - condition=condition, den_mat=den_mat, tsr_mode=tsr_mode, requires_grad=requires_grad) + super().__init__( + name='Rxx', + inputs=inputs, + nqubit=nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=den_mat, + tsr_mode=tsr_mode, + requires_grad=requires_grad, + ) def get_matrix(self, theta: Any) -> torch.Tensor: """Get the local unitary matrix.""" theta = self.inputs_to_tensor(theta) - cos = torch.cos(theta / 2) + cos = torch.cos(theta / 2) isin = torch.sin(theta / 2) * 1j m1 = torch.stack([cos, cos, cos, cos]).reshape(-1).diag_embed().reshape(4, 4) m2 = torch.stack([-isin, -isin, -isin, -isin]).reshape(-1).diag_embed().fliplr().reshape(4, 4) return m1 + m2 def _qasm(self) -> str: - if self.inv_mode: - theta = -self.theta - else: - theta = self.theta + theta = -self.theta if self.inv_mode else self.theta if self.condition: return self._qasm_cond_measure() + f'rxx({theta.item()}) q[{self.wires[0]}],q[{self.wires[1]}];\n' if self.controls == []: @@ -2048,40 +2241,50 @@ class Ryy(ParametricDoubleGate): requires_grad (bool, optional): Whether the parameter is ``nn.Parameter`` or ``buffer``. Default: ``False`` (which means ``buffer``) """ + def __init__( self, inputs: Any = None, nqubit: int = 2, - wires: Optional[List[int]] = None, - controls: Union[int, List[int], None] = None, + wires: list[int] | None = None, + controls: int | list[int] | None = None, condition: bool = False, den_mat: bool = False, tsr_mode: bool = False, - requires_grad: bool = False + requires_grad: bool = False, ) -> None: - super().__init__(name='Ryy', inputs=inputs, nqubit=nqubit, wires=wires, controls=controls, - condition=condition, den_mat=den_mat, tsr_mode=tsr_mode, requires_grad=requires_grad) + super().__init__( + name='Ryy', + inputs=inputs, + nqubit=nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=den_mat, + tsr_mode=tsr_mode, + requires_grad=requires_grad, + ) def get_matrix(self, theta: Any) -> torch.Tensor: """Get the local unitary matrix.""" theta = self.inputs_to_tensor(theta) - cos = torch.cos(theta / 2) + cos = torch.cos(theta / 2) isin = torch.sin(theta / 2) * 1j m1 = torch.stack([cos, cos, cos, cos]).reshape(-1).diag_embed().reshape(4, 4) m2 = torch.stack([isin, -isin, -isin, isin]).reshape(-1).diag_embed().fliplr().reshape(4, 4) return m1 + m2 def _qasm(self) -> str: - if self.inv_mode: - theta = -self.theta - else: - theta = self.theta + theta = -self.theta if self.inv_mode else self.theta qasm_str1 = '' qasm_str2 = f'ryy({theta.item()}) q[{self.wires[0]}],q[{self.wires[1]}];\n' - # pylint: disable=protected-access if 'ryy' not in Gate._qasm_new_gate: - # pylint: disable=line-too-long - qasm_str1 = 'gate ryy(param0) q0,q1 { rx(pi/2) q0; rx(pi/2) q1; cx q0,q1; rz(param0) q1; cx q0,q1; rx(-pi/2) q0; rx(-pi/2) q1; }\n' + qasm_str1 = ( + 'gate ryy(param0) q0,q1 { ' + 'rx(pi/2) q0; rx(pi/2) q1; cx q0,q1; ' + 'rz(param0) q1; cx q0,q1; ' + 'rx(-pi/2) q0; rx(-pi/2) q1; }\n' + ) Gate._qasm_new_gate.append('ryy') if self.condition: return qasm_str1 + self._qasm_cond_measure() + qasm_str2 @@ -2123,19 +2326,29 @@ class Rzz(ParametricDoubleGate): requires_grad (bool, optional): Whether the parameter is ``nn.Parameter`` or ``buffer``. Default: ``False`` (which means ``buffer``) """ + def __init__( self, inputs: Any = None, nqubit: int = 2, - wires: Optional[List[int]] = None, - controls: Union[int, List[int], None] = None, + wires: list[int] | None = None, + controls: int | list[int] | None = None, condition: bool = False, den_mat: bool = False, tsr_mode: bool = False, - requires_grad: bool = False + requires_grad: bool = False, ) -> None: - super().__init__(name='Rzz', inputs=inputs, nqubit=nqubit, wires=wires, controls=controls, - condition=condition, den_mat=den_mat, tsr_mode=tsr_mode, requires_grad=requires_grad) + super().__init__( + name='Rzz', + inputs=inputs, + nqubit=nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=den_mat, + tsr_mode=tsr_mode, + requires_grad=requires_grad, + ) def get_matrix(self, theta: Any) -> torch.Tensor: """Get the local unitary matrix.""" @@ -2145,10 +2358,7 @@ def get_matrix(self, theta: Any) -> torch.Tensor: return torch.stack([e_m_it, e_it, e_it, e_m_it]).reshape(-1).diag_embed().reshape(4, 4) def _qasm(self) -> str: - if self.inv_mode: - theta = -self.theta - else: - theta = self.theta + theta = -self.theta if self.inv_mode else self.theta if self.condition: return self._qasm_cond_measure() + f'rzz({theta.item()}) q[{self.wires[0]}],q[{self.wires[1]}];\n' if self.controls == []: @@ -2189,24 +2399,34 @@ class Rxy(ParametricDoubleGate): requires_grad (bool, optional): Whether the parameter is ``nn.Parameter`` or ``buffer``. Default: ``False`` (which means ``buffer``) """ + def __init__( self, inputs: Any = None, nqubit: int = 2, - wires: Optional[List[int]] = None, - controls: Union[int, List[int], None] = None, + wires: list[int] | None = None, + controls: int | list[int] | None = None, condition: bool = False, den_mat: bool = False, tsr_mode: bool = False, - requires_grad: bool = False + requires_grad: bool = False, ) -> None: - super().__init__(name='Rxy', inputs=inputs, nqubit=nqubit, wires=wires, controls=controls, - condition=condition, den_mat=den_mat, tsr_mode=tsr_mode, requires_grad=requires_grad) + super().__init__( + name='Rxy', + inputs=inputs, + nqubit=nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=den_mat, + tsr_mode=tsr_mode, + requires_grad=requires_grad, + ) def get_matrix(self, theta: Any) -> torch.Tensor: """Get the local unitary matrix.""" theta = self.inputs_to_tensor(theta) - cos = torch.cos(theta / 2) + cos = torch.cos(theta / 2) isin = torch.sin(theta / 2) * 1j m1 = torch.eye(1, dtype=theta.dtype, device=theta.device) m2 = torch.stack([cos, -isin, -isin, cos]).reshape(2, 2) @@ -2222,7 +2442,6 @@ def _qasm(self) -> str: qasm_lst2.append(f'q[{wire}],') qasm_str1 = ''.join(qasm_lst1)[:-1] + ';\n' qasm_str2 = ''.join(qasm_lst2)[:-1] + ';\n' - # pylint: disable=protected-access if name not in Gate._qasm_new_gate: Gate._qasm_new_gate.append(name) return qasm_str1 + self._qasm_cond_measure() + qasm_str2 @@ -2261,20 +2480,29 @@ class ReconfigurableBeamSplitter(ParametricDoubleGate): requires_grad (bool, optional): Whether the parameter is ``nn.Parameter`` or ``buffer``. Default: ``False`` (which means ``buffer``) """ + def __init__( self, inputs: Any = None, nqubit: int = 2, - wires: Optional[List[int]] = None, - controls: Union[int, List[int], None] = None, + wires: list[int] | None = None, + controls: int | list[int] | None = None, condition: bool = False, den_mat: bool = False, tsr_mode: bool = False, - requires_grad: bool = False + requires_grad: bool = False, ) -> None: - super().__init__(name='ReconfigurableBeamSplitter', inputs=inputs, nqubit=nqubit, wires=wires, - controls=controls, condition=condition, den_mat=den_mat, tsr_mode=tsr_mode, - requires_grad=requires_grad) + super().__init__( + name='ReconfigurableBeamSplitter', + inputs=inputs, + nqubit=nqubit, + wires=wires, + controls=controls, + condition=condition, + den_mat=den_mat, + tsr_mode=tsr_mode, + requires_grad=requires_grad, + ) def inputs_to_tensor(self, inputs: Any = None) -> torch.Tensor: """Convert inputs to torch.Tensor.""" @@ -2305,7 +2533,6 @@ def _qasm(self) -> str: qasm_lst2.append(f'q[{wire}],') qasm_str1 = ''.join(qasm_lst1)[:-1] + ';\n' qasm_str2 = ''.join(qasm_lst2)[:-1] + ';\n' - # pylint: disable=protected-access if name not in Gate._qasm_new_gate: Gate._qasm_new_gate.append(name) return qasm_str1 + self._qasm_cond_measure() + qasm_str2 @@ -2342,23 +2569,35 @@ class Toffoli(TripleGate): and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + def __init__( - self, - nqubit: int = 3, - wires: Optional[List[int]] = None, - den_mat: bool = False, - tsr_mode: bool = False + self, nqubit: int = 3, wires: list[int] | None = None, den_mat: bool = False, tsr_mode: bool = False ) -> None: - super().__init__(name='Toffoli', nqubit=nqubit, wires=wires, controls=None, condition=False, - den_mat=den_mat, tsr_mode=tsr_mode) - self.register_buffer('matrix', torch.tensor([[1, 0, 0, 0, 0, 0, 0, 0], - [0, 1, 0, 0, 0, 0, 0, 0], - [0, 0, 1, 0, 0, 0, 0, 0], - [0, 0, 0, 1, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 0, 0, 0], - [0, 0, 0, 0, 0, 1, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 1], - [0, 0, 0, 0, 0, 0, 1, 0]]) + 0j) + super().__init__( + name='Toffoli', + nqubit=nqubit, + wires=wires, + controls=None, + condition=False, + den_mat=den_mat, + tsr_mode=tsr_mode, + ) + self.register_buffer( + 'matrix', + torch.tensor( + [ + [1, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 0, 1, 0], + ] + ) + + 0j, + ) # MBQC self.nancilla = 18 @@ -2382,9 +2621,10 @@ def get_unitary(self) -> torch.Tensor: def _qasm(self) -> str: return f'ccx q[{self.wires[0]}],q[{self.wires[1]}],q[{self.wires[2]}];\n' - def pattern(self, nodes: List[int], ancilla: List[int]) -> nn.Sequential: + def pattern(self, nodes: list[int], ancilla: list[int]) -> nn.Sequential: """Get the MBQC pattern.""" - from .mbqc import Node, Entanglement, Measurement, Correction + from .mbqc import Correction, Entanglement, Measurement, Node + assert len(nodes) == len(self.wires) assert len(ancilla) == self.nancilla control1 = nodes[0] @@ -2423,31 +2663,52 @@ def pattern(self, nodes: List[int], ancilla: List[int]) -> nn.Sequential: cmds.append(Measurement(ancilla[2], angle=-torch.pi * 7 / 4, s_domain=[ancilla[1], target])) cmds.append(Measurement(ancilla[14], s_domain=control1)) cmds.append(Measurement(ancilla[3], s_domain=[ancilla[2], ancilla[0]])) - cmds.append(Measurement(ancilla[5], angle=-torch.pi / 4, - s_domain=[ancilla[3], ancilla[1], ancilla[14], target])) + cmds.append( + Measurement(ancilla[5], angle=-torch.pi / 4, s_domain=[ancilla[3], ancilla[1], ancilla[14], target]) + ) cmds.append(Measurement(control2, angle=-torch.pi / 4)) cmds.append(Measurement(ancilla[6], s_domain=[ancilla[5], ancilla[2], ancilla[0]])) cmds.append(Measurement(ancilla[9], s_domain=[control2, ancilla[5], ancilla[2]])) - cmds.append(Measurement(ancilla[7], angle=-torch.pi * 7 / 4, - s_domain=[ancilla[6], ancilla[3], ancilla[1], ancilla[14], target])) + cmds.append( + Measurement( + ancilla[7], angle=-torch.pi * 7 / 4, s_domain=[ancilla[6], ancilla[3], ancilla[1], ancilla[14], target] + ) + ) cmds.append(Measurement(ancilla[10], angle=-torch.pi * 7 / 4, s_domain=[ancilla[9], ancilla[14]])) cmds.append(Measurement(ancilla[4], angle=-torch.pi / 4, s_domain=ancilla[14])) cmds.append(Measurement(ancilla[8], s_domain=[ancilla[7], ancilla[5], ancilla[2], ancilla[0]])) cmds.append(Measurement(ancilla[11], s_domain=[ancilla[10], control2, ancilla[5], ancilla[2]])) - cmds.append(Measurement(ancilla[12], angle=-torch.pi / 4, - s_domain=[ancilla[8], ancilla[6], ancilla[3], ancilla[1], target])) - cmds.append(Measurement(ancilla[16], - s_domain=[ancilla[4], control1, ancilla[2], control2, ancilla[7], - ancilla[10], ancilla[2], control2, ancilla[5]])) + cmds.append( + Measurement( + ancilla[12], angle=-torch.pi / 4, s_domain=[ancilla[8], ancilla[6], ancilla[3], ancilla[1], target] + ) + ) + cmds.append( + Measurement( + ancilla[16], + s_domain=[ + ancilla[4], + control1, + ancilla[2], + control2, + ancilla[7], + ancilla[10], + ancilla[2], + control2, + ancilla[5], + ], + ) + ) cmds.append(Correction(ancilla[17], basis='x', domain=[ancilla[14], ancilla[16]])) cmds.append(Correction(ancilla[15], basis='x', domain=[ancilla[9], ancilla[11]])) - cmds.append(Correction(ancilla[13], basis='x', - domain=[ancilla[0], ancilla[2], ancilla[5], ancilla[7], ancilla[12]])) - cmds.append(Correction(ancilla[17], basis='z', - domain=[ancilla[4], ancilla[5], ancilla[7], ancilla[10], control1])) + cmds.append( + Correction(ancilla[13], basis='x', domain=[ancilla[0], ancilla[2], ancilla[5], ancilla[7], ancilla[12]]) + ) + cmds.append( + Correction(ancilla[17], basis='z', domain=[ancilla[4], ancilla[5], ancilla[7], ancilla[10], control1]) + ) cmds.append(Correction(ancilla[15], basis='z', domain=[control2, ancilla[2], ancilla[5], ancilla[10]])) - cmds.append(Correction(ancilla[13], basis='z', - domain=[ancilla[1], ancilla[3], ancilla[6], ancilla[8], target])) + cmds.append(Correction(ancilla[13], basis='z', domain=[ancilla[1], ancilla[3], ancilla[6], ancilla[8], target])) self.nodes = [ancilla[17], ancilla[15], ancilla[13]] return cmds @@ -2480,32 +2741,44 @@ class Fredkin(TripleGate): and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + def __init__( - self, - nqubit: int = 3, - wires: Optional[List[int]] = None, - den_mat: bool = False, - tsr_mode: bool = False + self, nqubit: int = 3, wires: list[int] | None = None, den_mat: bool = False, tsr_mode: bool = False ) -> None: - super().__init__(name='Fredkin', nqubit=nqubit, wires=wires, controls=None, condition=False, - den_mat=den_mat, tsr_mode=tsr_mode) - self.register_buffer('matrix', torch.tensor([[1, 0, 0, 0, 0, 0, 0, 0], - [0, 1, 0, 0, 0, 0, 0, 0], - [0, 0, 1, 0, 0, 0, 0, 0], - [0, 0, 0, 1, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 1, 0], - [0, 0, 0, 0, 0, 1, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 1]]) + 0j) + super().__init__( + name='Fredkin', + nqubit=nqubit, + wires=wires, + controls=None, + condition=False, + den_mat=den_mat, + tsr_mode=tsr_mode, + ) + self.register_buffer( + 'matrix', + torch.tensor( + [ + [1, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 1], + ] + ) + + 0j, + ) def get_unitary(self) -> torch.Tensor: """Get the global unitary matrix.""" matrix = self.update_matrix() identity = torch.eye(2, dtype=matrix.dtype, device=matrix.device) zerozero = torch.tensor([[1, 0], [0, 0]], dtype=matrix.dtype, device=matrix.device) - zeroone = torch.tensor([[0, 1], [0, 0]], dtype=matrix.dtype, device=matrix.device) - onezero = torch.tensor([[0, 0], [1, 0]], dtype=matrix.dtype, device=matrix.device) - oneone = torch.tensor([[0, 0], [0, 1]], dtype=matrix.dtype, device=matrix.device) + zeroone = torch.tensor([[0, 1], [0, 0]], dtype=matrix.dtype, device=matrix.device) + onezero = torch.tensor([[0, 0], [1, 0]], dtype=matrix.dtype, device=matrix.device) + oneone = torch.tensor([[0, 0], [0, 1]], dtype=matrix.dtype, device=matrix.device) lst1 = [identity] * self.nqubit lst2 = [identity] * self.nqubit lst3 = [identity] * self.nqubit @@ -2553,19 +2826,21 @@ class UAnyGate(ArbitraryGate): and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + def __init__( self, unitary: Any, nqubit: int = 1, - wires: Union[int, List[int], None] = None, - minmax: Optional[List[int]] = None, - controls: Union[int, List[int], None] = None, + wires: int | list[int] | None = None, + minmax: list[int] | None = None, + controls: int | list[int] | None = None, name: str = 'UAnyGate', den_mat: bool = False, - tsr_mode: bool = False + tsr_mode: bool = False, ) -> None: - super().__init__(name=name, nqubit=nqubit, wires=wires, minmax=minmax, controls=controls, - den_mat=den_mat, tsr_mode=tsr_mode) + super().__init__( + name=name, nqubit=nqubit, wires=wires, minmax=minmax, controls=controls, den_mat=den_mat, tsr_mode=tsr_mode + ) if not isinstance(unitary, torch.Tensor): unitary = torch.tensor(unitary, dtype=torch.cfloat).reshape(-1, 2 ** len(self.wires)) assert unitary.dtype in (torch.cfloat, torch.cdouble) @@ -2601,20 +2876,22 @@ class LatentGate(ArbitraryGate): requires_grad (bool, optional): Whether the parameters are ``nn.Parameter`` or ``buffer``. Default: ``False`` (which means ``buffer``) """ + def __init__( self, inputs: Any = None, nqubit: int = 1, - wires: Union[int, List[int], None] = None, - minmax: Optional[List[int]] = None, - controls: Union[int, List[int], None] = None, + wires: int | list[int] | None = None, + minmax: list[int] | None = None, + controls: int | list[int] | None = None, name: str = 'LatentGate', den_mat: bool = False, tsr_mode: bool = False, - requires_grad: bool = False + requires_grad: bool = False, ) -> None: - super().__init__(name=name, nqubit=nqubit, wires=wires, minmax=minmax, controls=controls, - den_mat=den_mat, tsr_mode=tsr_mode) + super().__init__( + name=name, nqubit=nqubit, wires=wires, minmax=minmax, controls=controls, den_mat=den_mat, tsr_mode=tsr_mode + ) self.requires_grad = requires_grad self.init_para(inputs) @@ -2634,10 +2911,7 @@ def get_matrix(self, inputs: Any) -> torch.Tensor: def update_matrix(self) -> torch.Tensor: """Update the local unitary matrix.""" - if self.inv_mode: - latent = self.latent.mH - else: - latent = self.latent + latent = self.latent.mH if self.inv_mode else self.latent matrix = self.get_matrix(latent) assert matrix.shape[-1] == matrix.shape[-2] == 2 ** len(self.wires) self.matrix = matrix.detach() @@ -2684,18 +2958,19 @@ class HamiltonianGate(ArbitraryGate): requires_grad (bool, optional): Whether the parameter is ``nn.Parameter`` or ``buffer``. Default: ``False`` (which means ``buffer``) """ + def __init__( self, hamiltonian: Any, t: Any = None, nqubit: int = 1, - wires: Union[int, List[int], None] = None, - minmax: Optional[List[int]] = None, - controls: Union[int, List[int], None] = None, + wires: int | list[int] | None = None, + minmax: list[int] | None = None, + controls: int | list[int] | None = None, name: str = 'HamiltonianGate', den_mat: bool = False, tsr_mode: bool = False, - requires_grad: bool = False + requires_grad: bool = False, ) -> None: self.nqubit = nqubit self.ham_lst = None @@ -2703,8 +2978,9 @@ def __init__( self.ham_lst = hamiltonian wires = None minmax = self.get_minmax(hamiltonian) - super().__init__(name=name, nqubit=nqubit, wires=wires, minmax=minmax, controls=controls, - den_mat=den_mat, tsr_mode=tsr_mode) + super().__init__( + name=name, nqubit=nqubit, wires=wires, minmax=minmax, controls=controls, den_mat=den_mat, tsr_mode=tsr_mode + ) self.npara = 1 self.requires_grad = requires_grad self.register_buffer('x', PauliX().matrix) @@ -2730,7 +3006,7 @@ def to(self, arg: Any) -> 'HamiltonianGate': super().to(arg) return self - def _convert_hamiltonian(self, hamiltonian: List) -> List[List]: + def _convert_hamiltonian(self, hamiltonian: list) -> list[list]: """Convert and check the list representation of the Hamiltonian.""" if len(hamiltonian) == 2 and isinstance(hamiltonian[1], str): hamiltonian = [hamiltonian] @@ -2739,7 +3015,7 @@ def _convert_hamiltonian(self, hamiltonian: List) -> List[List]: assert isinstance(pair[1], str), 'Invalid input type' return hamiltonian - def get_minmax(self, hamiltonian: List) -> List[int]: + def get_minmax(self, hamiltonian: list) -> list[int]: """Get ``minmax`` according to the Hamiltonian.""" hamiltonian = self._convert_hamiltonian(hamiltonian) minmax = [self.nqubit - 1, 0] @@ -2753,7 +3029,7 @@ def get_minmax(self, hamiltonian: List) -> List[int]: minmax[1] = i return minmax - def inputs_to_tensor(self, inputs: Optional[List] = None) -> Tuple[torch.Tensor, torch.Tensor]: + def inputs_to_tensor(self, inputs: list | None = None) -> tuple[torch.Tensor, torch.Tensor]: """Convert inputs to torch.Tensor.""" if inputs is None: t = torch.rand(1)[0] @@ -2772,14 +3048,14 @@ def inputs_to_tensor(self, inputs: Optional[List] = None) -> Tuple[torch.Tensor, coeff = pair[0] basis = pair[1][::2] wires = pair[1][1::2] - for wire, key in zip(wires, basis): + for wire, key in zip(wires, basis, strict=True): wire = int(wire) key = key.lower() lst[wire] = pauli_dict[key] if ham_tsr is None: - ham_tsr = multi_kron(lst[minmax[0]:minmax[1]+1]) * coeff + ham_tsr = multi_kron(lst[minmax[0] : minmax[1] + 1]) * coeff else: - ham_tsr += multi_kron(lst[minmax[0]:minmax[1]+1]) * coeff + ham_tsr += multi_kron(lst[minmax[0] : minmax[1] + 1]) * coeff elif not isinstance(ham, torch.Tensor): ham_tsr = torch.tensor(ham, dtype=self.x.dtype, device=self.x.device) else: @@ -2799,10 +3075,7 @@ def get_matrix(self, hamiltonian: Any, t: Any) -> torch.Tensor: def update_matrix(self) -> torch.Tensor: """Update the local unitary matrix.""" - if self.inv_mode: - t = -self.t - else: - t = self.t + t = -self.t if self.inv_mode else self.t matrix = self.get_matrix(self.ham_tsr, t) assert matrix.shape[-1] == matrix.shape[-2] == 2 ** len(self.wires) self.matrix = matrix.detach() @@ -2820,7 +3093,7 @@ def get_derivative(self, t: Any) -> torch.Tensor: du_dx = jacobian(self._real_wrapper, t) return du_dx[..., 0] + du_dx[..., 1] * 1j - def init_para(self, inputs: Optional[List] = None) -> None: + def init_para(self, inputs: list | None = None) -> None: """Initialize the parameters.""" ham, t = self.inputs_to_tensor(inputs) self.register_buffer('ham_tsr', ham) @@ -2844,12 +3117,9 @@ class Reset(Gate): and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + def __init__( - self, - nqubit: int = 1, - wires: Union[int, List[int], None] = None, - postselect: Optional[int] = 0, - tsr_mode: bool = False + self, nqubit: int = 1, wires: int | list[int] | None = None, postselect: int | None = 0, tsr_mode: bool = False ) -> None: if wires is None: wires = list(range(nqubit)) @@ -2867,20 +3137,20 @@ def op_state(self, x: torch.Tensor) -> torch.Tensor: x[:, 0] = 1 x = self.tensor_rep(x) else: - if self.postselect in (0, 1): # compatible with vmap + if self.postselect in (0, 1): # compatible with vmap for wire in self.wires: pm_shape = list(range(1, self.nqubit + 1)) pm_shape.remove(wire + 1) pm_shape = [wire + 1] + pm_shape + [0] - x = x.permute(pm_shape) # (2, ..., 2, batch) - probs = (x.abs() ** 2).sum(list(range(1, self.nqubit))) # (2, batch) - mask = 1 - torch.sign(probs[self.postselect]) # (batch) + x = x.permute(pm_shape) # (2, ..., 2, batch) + probs = (x.abs() ** 2).sum(list(range(1, self.nqubit))) # (2, batch) + mask = 1 - torch.sign(probs[self.postselect]) # (batch) norm = torch.sqrt(probs[self.postselect] + mask) state0 = ((1 - mask) * x[self.postselect] + mask * x[1 - self.postselect]) / norm state1 = torch.zeros_like(state0) x = torch.stack([state0, state1]) x = x.permute(inverse_permutation(pm_shape)) - elif self.postselect is None: # NOT compatible with vmap + elif self.postselect is None: # NOT compatible with vmap wires = sorted(self.wires) idx_sum = list(range(1, self.nqubit + 1)) for i in wires: @@ -2897,7 +3167,7 @@ def op_state(self, x: torch.Tensor) -> torch.Tensor: reset[sample, sample] = 0 reset[0, sample] = 1 mat = reset @ proj.diag_embed() + 0j - out.append(evolve_state(x[i:i+1], mat, self.nqubit, wires)) + out.append(evolve_state(x[i : i + 1], mat, self.nqubit, wires)) x = torch.cat(out) if not self.tsr_mode: x = self.vector_rep(x).squeeze(0) @@ -2919,7 +3189,8 @@ class Barrier(Gate): Default: ``None`` name (str, optional): The name of the gate. Default: ``'Barrier'`` """ - def __init__(self, nqubit: int = 1, wires: Union[int, List[int], None] = None, name: str = 'Barrier') -> None: + + def __init__(self, nqubit: int = 1, wires: int | list[int] | None = None, name: str = 'Barrier') -> None: if wires is None: wires = list(range(nqubit)) super().__init__(name=name, nqubit=nqubit, wires=wires) @@ -2939,7 +3210,7 @@ def _qasm(self) -> str: qasm_lst.append(f'q[{wire}],') return ''.join(qasm_lst)[:-1] + ';\n' - def pattern(self, nodes: List[int], ancilla: List[int]) -> nn.Sequential: + def pattern(self, nodes: list[int], ancilla: list[int]) -> nn.Sequential: """Get the MBQC pattern.""" assert len(ancilla) == self.nancilla self.nodes = nodes @@ -2953,7 +3224,8 @@ class WireCut(Barrier): nqubit (int, optional): The number of qubits that the quantum operation acts on. Default: 1 wires (int or List[int], optional): The indices of the qubits that the quantum operation acts on. Default: 0 """ - def __init__(self, nqubit: int = 1, wires: Union[int, List[int]] = 0) -> None: + + def __init__(self, nqubit: int = 1, wires: int | list[int] = 0) -> None: super().__init__(name='WireCut', nqubit=nqubit, wires=wires) @@ -2970,12 +3242,9 @@ class Move(DoubleGate): and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + def __init__( - self, - nqubit: int = 2, - wires: Optional[List[int]] = None, - postselect: Optional[int] = 0, - tsr_mode: bool = False + self, nqubit: int = 2, wires: list[int] | None = None, postselect: int | None = 0, tsr_mode: bool = False ) -> None: super().__init__(name='Move', nqubit=nqubit, wires=wires, tsr_mode=tsr_mode) reset = Reset(nqubit=nqubit, wires=self.wires[1], postselect=postselect, tsr_mode=True) @@ -2997,7 +3266,8 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: return self.vector_rep(x).squeeze(0) return x - def qpd(self, label: Optional[int] = None) -> 'MoveQPD': + def qpd(self, label: int | None = None) -> 'MoveQPD': """Get the quasiprobability-decomposition representation.""" from .qpd import MoveQPD + return MoveQPD(nqubit=self.nqubit, wires=self.wires, label=label, tsr_mode=self.tsr_mode) diff --git a/src/deepquantum/layer.py b/src/deepquantum/layer.py index aa013144..ad469d7c 100644 --- a/src/deepquantum/layer.py +++ b/src/deepquantum/layer.py @@ -1,14 +1,12 @@ -""" -Quantum layers -""" +"""Quantum layers""" from copy import deepcopy -from typing import List, Union, Optional, Any +from typing import Any import torch from torch import nn -from .gate import PauliX, PauliY, PauliZ, U3Gate, Hadamard, Rx, Ry, Rz, CNOT +from .gate import CNOT, Hadamard, PauliX, PauliY, PauliZ, Rx, Ry, Rz, U3Gate from .operation import Layer from .qmath import multi_kron @@ -27,13 +25,14 @@ class SingleLayer(Layer): and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + def __init__( self, - name: Optional[str] = None, + name: str | None = None, nqubit: int = 1, - wires: Union[int, List[int], List[List[int]], None] = None, + wires: int | list[int] | list[list[int]] | None = None, den_mat: bool = False, - tsr_mode: bool = False + tsr_mode: bool = False, ) -> None: if wires is None: wires = [[i] for i in range(nqubit)] @@ -67,14 +66,15 @@ class ParametricSingleLayer(SingleLayer): requires_grad (bool, optional): Whether the parameters are ``nn.Parameter`` or ``buffer``. Default: ``True`` (which means ``nn.Parameter``) """ + def __init__( self, - name: Optional[str] = None, + name: str | None = None, nqubit: int = 1, - wires: Union[int, List[int], List[List[int]], None] = None, + wires: int | list[int] | list[list[int]] | None = None, den_mat: bool = False, tsr_mode: bool = False, - requires_grad: bool = True + requires_grad: bool = True, ) -> None: super().__init__(name=name, nqubit=nqubit, wires=wires, den_mat=den_mat, tsr_mode=tsr_mode) self.requires_grad = requires_grad @@ -90,11 +90,7 @@ def inverse(self) -> 'ParametricSingleLayer': return layer def pattern( - self, - nodes: List[List[int]], - ancilla: List[List[int]], - angle: List[Any], - requires_grad: bool = False + self, nodes: list[list[int]], ancilla: list[list[int]], angle: list[Any], requires_grad: bool = False ) -> nn.Sequential: """Get the MBQC pattern.""" assert len(nodes) == len(ancilla) == len(self.gates) @@ -118,13 +114,14 @@ class DoubleLayer(Layer): and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + def __init__( self, - name: Optional[str] = None, + name: str | None = None, nqubit: int = 2, - wires: Optional[List[List[int]]] = None, + wires: list[list[int]] | None = None, den_mat: bool = False, - tsr_mode: bool = False + tsr_mode: bool = False, ) -> None: if wires is None: wires = [[i, i + 1] for i in range(0, nqubit - 1, 2)] @@ -148,13 +145,14 @@ class Observable(SingleLayer): and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + def __init__( self, nqubit: int = 1, - wires: Union[int, List[int], List[List[int]], None] = None, + wires: int | list[int] | list[list[int]] | None = None, basis: str = 'z', den_mat: bool = False, - tsr_mode: bool = False + tsr_mode: bool = False, ) -> None: super().__init__(name='Observable', nqubit=nqubit, wires=wires, den_mat=den_mat, tsr_mode=tsr_mode) basis = basis.lower() @@ -191,24 +189,24 @@ class U3Layer(ParametricSingleLayer): requires_grad (bool, optional): Whether the parameters are ``nn.Parameter`` or ``buffer``. Default: ``True`` (which means ``nn.Parameter``) """ + def __init__( self, nqubit: int = 1, - wires: Union[int, List[int], List[List[int]], None] = None, + wires: int | list[int] | list[list[int]] | None = None, inputs: Any = None, den_mat: bool = False, tsr_mode: bool = False, - requires_grad: bool = True + requires_grad: bool = True, ) -> None: - super().__init__(name='U3Layer', nqubit=nqubit, wires=wires, den_mat=den_mat, tsr_mode=tsr_mode, - requires_grad=requires_grad) + super().__init__( + name='U3Layer', nqubit=nqubit, wires=wires, den_mat=den_mat, tsr_mode=tsr_mode, requires_grad=requires_grad + ) for i, wire in enumerate(self.wires): - if inputs is None: - thetas = None - else: - thetas = inputs[3*i:3*i+3] - u3 = U3Gate(inputs=thetas, nqubit=nqubit, wires=wire, den_mat=den_mat, - tsr_mode=True, requires_grad=requires_grad) + thetas = None if inputs is None else inputs[3 * i : 3 * i + 3] + u3 = U3Gate( + inputs=thetas, nqubit=nqubit, wires=wire, den_mat=den_mat, tsr_mode=True, requires_grad=requires_grad + ) self.gates.append(u3) self.npara += u3.npara @@ -226,12 +224,13 @@ class XLayer(SingleLayer): and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + def __init__( self, nqubit: int = 1, - wires: Union[int, List[int], List[List[int]], None] = None, + wires: int | list[int] | list[list[int]] | None = None, den_mat: bool = False, - tsr_mode: bool = False + tsr_mode: bool = False, ) -> None: super().__init__(name='XLayer', nqubit=nqubit, wires=wires, den_mat=den_mat, tsr_mode=tsr_mode) for wire in self.wires: @@ -252,12 +251,13 @@ class YLayer(SingleLayer): and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + def __init__( self, nqubit: int = 1, - wires: Union[int, List[int], List[List[int]], None] = None, + wires: int | list[int] | list[list[int]] | None = None, den_mat: bool = False, - tsr_mode: bool = False + tsr_mode: bool = False, ) -> None: super().__init__(name='YLayer', nqubit=nqubit, wires=wires, den_mat=den_mat, tsr_mode=tsr_mode) for wire in self.wires: @@ -278,12 +278,13 @@ class ZLayer(SingleLayer): and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + def __init__( self, nqubit: int = 1, - wires: Union[int, List[int], List[List[int]], None] = None, + wires: int | list[int] | list[list[int]] | None = None, den_mat: bool = False, - tsr_mode: bool = False + tsr_mode: bool = False, ) -> None: super().__init__(name='ZLayer', nqubit=nqubit, wires=wires, den_mat=den_mat, tsr_mode=tsr_mode) for wire in self.wires: @@ -304,12 +305,13 @@ class HLayer(SingleLayer): and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + def __init__( self, nqubit: int = 1, - wires: Union[int, List[int], List[List[int]], None] = None, + wires: int | list[int] | list[list[int]] | None = None, den_mat: bool = False, - tsr_mode: bool = False + tsr_mode: bool = False, ) -> None: super().__init__(name='HLayer', nqubit=nqubit, wires=wires, den_mat=den_mat, tsr_mode=tsr_mode) for wire in self.wires: @@ -333,24 +335,24 @@ class RxLayer(ParametricSingleLayer): requires_grad (bool, optional): Whether the parameters are ``nn.Parameter`` or ``buffer``. Default: ``True`` (which means ``nn.Parameter``) """ + def __init__( self, nqubit: int = 1, - wires: Union[int, List[int], List[List[int]], None] = None, + wires: int | list[int] | list[list[int]] | None = None, inputs: Any = None, den_mat: bool = False, tsr_mode: bool = False, - requires_grad: bool = True + requires_grad: bool = True, ) -> None: - super().__init__(name='RxLayer', nqubit=nqubit, wires=wires, den_mat=den_mat, tsr_mode=tsr_mode, - requires_grad=requires_grad) + super().__init__( + name='RxLayer', nqubit=nqubit, wires=wires, den_mat=den_mat, tsr_mode=tsr_mode, requires_grad=requires_grad + ) for i, wire in enumerate(self.wires): - if inputs is None: - theta = None - else: - theta = inputs[i] - rx = Rx(inputs=theta, nqubit=nqubit, wires=wire, den_mat=den_mat, - tsr_mode=True, requires_grad=requires_grad) + theta = None if inputs is None else inputs[i] + rx = Rx( + inputs=theta, nqubit=nqubit, wires=wire, den_mat=den_mat, tsr_mode=True, requires_grad=requires_grad + ) self.gates.append(rx) self.npara += rx.npara @@ -371,24 +373,24 @@ class RyLayer(ParametricSingleLayer): requires_grad (bool, optional): Whether the parameters are ``nn.Parameter`` or ``buffer``. Default: ``True`` (which means ``nn.Parameter``) """ + def __init__( self, nqubit: int = 1, - wires: Union[int, List[int], List[List[int]], None] = None, + wires: int | list[int] | list[list[int]] | None = None, inputs: Any = None, den_mat: bool = False, tsr_mode: bool = False, - requires_grad: bool = True + requires_grad: bool = True, ) -> None: - super().__init__(name='RyLayer', nqubit=nqubit, wires=wires, den_mat=den_mat, tsr_mode=tsr_mode, - requires_grad=requires_grad) + super().__init__( + name='RyLayer', nqubit=nqubit, wires=wires, den_mat=den_mat, tsr_mode=tsr_mode, requires_grad=requires_grad + ) for i, wire in enumerate(self.wires): - if inputs is None: - theta = None - else: - theta = inputs[i] - ry = Ry(inputs=theta, nqubit=nqubit, wires=wire, den_mat=den_mat, - tsr_mode=True, requires_grad=requires_grad) + theta = None if inputs is None else inputs[i] + ry = Ry( + inputs=theta, nqubit=nqubit, wires=wire, den_mat=den_mat, tsr_mode=True, requires_grad=requires_grad + ) self.gates.append(ry) self.npara += ry.npara @@ -409,24 +411,24 @@ class RzLayer(ParametricSingleLayer): requires_grad (bool, optional): Whether the parameters are ``nn.Parameter`` or ``buffer``. Default: ``True`` (which means ``nn.Parameter``) """ + def __init__( self, nqubit: int = 1, - wires: Union[int, List[int], List[List[int]], None] = None, + wires: int | list[int] | list[list[int]] | None = None, inputs: Any = None, den_mat: bool = False, tsr_mode: bool = False, - requires_grad: bool = True + requires_grad: bool = True, ) -> None: - super().__init__(name='RzLayer', nqubit=nqubit, wires=wires, den_mat=den_mat, tsr_mode=tsr_mode, - requires_grad=requires_grad) + super().__init__( + name='RzLayer', nqubit=nqubit, wires=wires, den_mat=den_mat, tsr_mode=tsr_mode, requires_grad=requires_grad + ) for i, wire in enumerate(self.wires): - if inputs is None: - theta = None - else: - theta = inputs[i] - rz = Rz(inputs=theta, nqubit=nqubit, wires=wire, den_mat=den_mat, - tsr_mode=True, requires_grad=requires_grad) + theta = None if inputs is None else inputs[i] + rz = Rz( + inputs=theta, nqubit=nqubit, wires=wire, den_mat=den_mat, tsr_mode=True, requires_grad=requires_grad + ) self.gates.append(rz) self.npara += rz.npara @@ -434,24 +436,25 @@ def __init__( class CnotLayer(DoubleLayer): r"""A layer of CNOT gates. - Args: - nqubit (int, optional): The number of qubits that the quantum operation acts on. Default: 2 - wires (List[List[int]] or None, optional): The indices of the qubits that the quantum operation - acts on. Default: ``None`` - name (str, optional): The name of the layer. Default: ``'CnotLayer'`` - den_mat (bool, optional): Whether the quantum operation acts on density matrices or state vectors. - Default: ``False`` (which means state vectors) - tsr_mode (bool, optional): Whether the quantum operation is in tensor mode, which means the input - and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. - Default: ``False`` + Args: + nqubit (int, optional): The number of qubits that the quantum operation acts on. Default: 2 + wires (List[List[int]] or None, optional): The indices of the qubits that the quantum operation + acts on. Default: ``None`` + name (str, optional): The name of the layer. Default: ``'CnotLayer'`` + den_mat (bool, optional): Whether the quantum operation acts on density matrices or state vectors. + Default: ``False`` (which means state vectors) + tsr_mode (bool, optional): Whether the quantum operation is in tensor mode, which means the input + and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. + Default: ``False`` """ + def __init__( self, nqubit: int = 2, - wires: Optional[List[List[int]]] = None, + wires: list[list[int]] | None = None, name: str = 'CnotLayer', den_mat: bool = False, - tsr_mode: bool = False + tsr_mode: bool = False, ) -> None: super().__init__(name=name, nqubit=nqubit, wires=wires, den_mat=den_mat, tsr_mode=tsr_mode) for wire in self.wires: @@ -463,8 +466,7 @@ def inverse(self) -> 'CnotLayer': wires = [] for wire in reversed(self.wires): wires.append(wire) - return CnotLayer(nqubit=self.nqubit, wires=wires, name=self.name, - den_mat=self.den_mat, tsr_mode=self.tsr_mode) + return CnotLayer(nqubit=self.nqubit, wires=wires, name=self.name, den_mat=self.den_mat, tsr_mode=self.tsr_mode) class CnotRing(CnotLayer): @@ -484,17 +486,18 @@ class CnotRing(CnotLayer): and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + def __init__( self, nqubit: int = 2, - minmax: Optional[List[int]] = None, + minmax: list[int] | None = None, step: int = 1, reverse: bool = False, den_mat: bool = False, - tsr_mode: bool = False + tsr_mode: bool = False, ) -> None: if minmax is None: - minmax = [0, nqubit-1] + minmax = [0, nqubit - 1] self.nqubit = nqubit self._check_minmax(minmax) assert minmax[0] < minmax[1] @@ -502,8 +505,8 @@ def __init__( self.step = step self.reverse = reverse nwires = minmax[1] - minmax[0] + 1 - if reverse: # from minmax[1] to minmax[0] - wires = [[minmax[0] + i, minmax[0] + (i-step) % nwires] for i in range(minmax[1] - minmax[0], -1, -1)] + if reverse: # from minmax[1] to minmax[0] + wires = [[minmax[0] + i, minmax[0] + (i - step) % nwires] for i in range(minmax[1] - minmax[0], -1, -1)] else: - wires = [[minmax[0] + i, minmax[0] + (i+step) % nwires] for i in range(minmax[1] - minmax[0] + 1)] + wires = [[minmax[0] + i, minmax[0] + (i + step) % nwires] for i in range(minmax[1] - minmax[0] + 1)] super().__init__(nqubit=nqubit, wires=wires, name='CnotRing', den_mat=den_mat, tsr_mode=tsr_mode) diff --git a/src/deepquantum/mbqc/__init__.py b/src/deepquantum/mbqc/__init__.py index 866570a0..978ef8f4 100644 --- a/src/deepquantum/mbqc/__init__.py +++ b/src/deepquantum/mbqc/__init__.py @@ -1,12 +1,6 @@ -""" -MBQC Module -""" +"""MBQC Module""" -from . import command -from . import operation -from . import pattern -from . import state - -from .command import Node, Entanglement, Measurement, Correction +from . import command, operation, pattern, state +from .command import Correction, Entanglement, Measurement, Node from .pattern import Pattern -from .state import SubGraphState, GraphState +from .state import GraphState, SubGraphState diff --git a/src/deepquantum/mbqc/command.py b/src/deepquantum/mbqc/command.py index c07b23a3..800dffb6 100644 --- a/src/deepquantum/mbqc/command.py +++ b/src/deepquantum/mbqc/command.py @@ -1,8 +1,7 @@ -""" -MBQC commands -""" +"""MBQC commands""" -from typing import Any, Iterable, List, Union +from collections.abc import Iterable +from typing import Any import torch from torch import nn @@ -18,7 +17,8 @@ class Node(Command): Args: nodes (int or List[int]): The indices of the nodes to prepare. """ - def __init__(self, nodes: Union[int, List[int]]) -> None: + + def __init__(self, nodes: int | list[int]) -> None: super().__init__(name='Node', nodes=nodes) def forward(self, x: GraphState) -> GraphState: @@ -38,6 +38,7 @@ class Entanglement(Command): node1 (int): The first node index. node2 (int): The second node index. """ + def __init__(self, node1: int, node2: int) -> None: super().__init__(name='Entanglement', nodes=[node1, node2]) @@ -79,14 +80,15 @@ class Measurement(Command): requires_grad (bool, optional): Whether the parameter is ``nn.Parameter`` or ``buffer``. Default: ``False`` (which means ``buffer``) """ + def __init__( self, - nodes: Union[int, List[int]], - angle: Any = 0., + nodes: int | list[int], + angle: Any = 0.0, plane: str = 'xy', - s_domain: Union[int, Iterable[int], None] = None, - t_domain: Union[int, Iterable[int], None] = None, - requires_grad: bool = False + s_domain: int | Iterable[int] | None = None, + t_domain: int | Iterable[int] | None = None, + requires_grad: bool = False, ) -> None: super().__init__(name='Measurement', nodes=nodes) self.plane = plane.lower() @@ -130,10 +132,7 @@ def forward(self, x: GraphState) -> GraphState: if init_state.ndim == 2: init_state = init_state.unsqueeze(0) wire = sgs.node2wire_dict[self.nodes[0]] - if self.angle.ndim == 0: - angle = self.angle.unsqueeze(0) - else: - angle = self.angle + angle = self.angle.unsqueeze(0) if self.angle.ndim == 0 else self.angle if len(self.s_domain) != 0: qs = sum(map(lambda s: torch.tensor(sgs.measure_dict[s], device=angle.device), self.s_domain)) qs = qs.reshape(-1, 1) @@ -145,15 +144,15 @@ def forward(self, x: GraphState) -> GraphState: else: qt = torch.zeros(init_state.shape[0], device=angle.device).reshape(-1, 1) if self.plane in ['xy', 'yx']: - alpha = (-1)**qs * angle + torch.pi * qt + alpha = (-1) ** qs * angle + torch.pi * qt # M^{XY,α} X^s Z^t = M^{XY,(-1)^s·α+tπ} elif self.plane in ['zx', 'xz']: - alpha = (-1)**(qs + qt) * angle + torch.pi * qs + alpha = (-1) ** (qs + qt) * angle + torch.pi * qs # M^{XZ,α} X^s Z^t = M^{XZ,(-1)^t((-1)^s·α+sπ)} # = M^{XZ,(-1)^{s+t}·α+(-1)^t·sπ} # = M^{XZ,(-1)^{s+t}·α+sπ} (since (-1)^t·π ≡ π (mod 2π)) elif self.plane in ['yz', 'zy']: - alpha = (-1)**qt * angle + torch.pi * (qs + qt) + alpha = (-1) ** qt * angle + torch.pi * (qs + qt) # positive Y axis as 0 angle # M^{YZ,α} X^s Z^t = M^{YZ,(-1)^t·α+(s+t)π)} cir = QubitCircuit(nqubit=nqubit) @@ -163,11 +162,11 @@ def forward(self, x: GraphState) -> GraphState: state = [] if isinstance(rst, list): for i, d in enumerate(rst): - (k, _), = d.items() + ((k, _),) = d.items() state.append(cir._slice_state_vector(state=final_state[i], wires=wire, bits=k)) sgs.measure_dict[self.nodes[0]].append(int(k)) else: - (k, _), = rst.items() + ((k, _),) = rst.items() state.append(cir._slice_state_vector(state=final_state[0], wires=wire, bits=k)) sgs.measure_dict[self.nodes[0]].append(int(k)) state = torch.stack(state) @@ -199,12 +198,8 @@ class Correction(Command): domain (Union[int, Iterable[int], None], optional): The indices of the nodes that contribute to signal domain s. Default: ``None`` """ - def __init__( - self, - nodes: Union[int, List[int]], - basis: str = 'x', - domain: Union[int, Iterable[int], None] = None - ) -> None: + + def __init__(self, nodes: int | list[int], basis: str = 'x', domain: int | Iterable[int] | None = None) -> None: super().__init__(name='Correction', nodes=nodes) self.basis = basis.lower() if domain is None: @@ -236,9 +231,9 @@ def forward(self, x: GraphState) -> GraphState: theta = torch.pi * qs.to(init_state.real.dtype) cir = QubitCircuit(nqubit=nqubit) if self.basis == 'x': - cir.rx(wires=wire, encode=True) # global phase + cir.rx(wires=wire, encode=True) # global phase elif self.basis == 'z': - cir.rz(wires=wire, encode=True) # global phase + cir.rz(wires=wire, encode=True) # global phase else: raise ValueError(f'Invalid basis {self.basis}') state = cir(data=theta.reshape(-1, 1).squeeze(0), state=init_state.squeeze(0)) diff --git a/src/deepquantum/mbqc/operation.py b/src/deepquantum/mbqc/operation.py index 9d79f052..3a47eab1 100644 --- a/src/deepquantum/mbqc/operation.py +++ b/src/deepquantum/mbqc/operation.py @@ -1,8 +1,4 @@ -""" -Base classes -""" - -from typing import List, Optional, Union +"""Base classes for MBQC operations""" from torch import nn @@ -17,17 +13,14 @@ class Operation(nn.Module): nodes (int, List[int] or None, optional): The indices of the nodes that the quantum operation acts on. Default: ``None`` """ - def __init__( - self, - name: Optional[str] = None, - nodes: Union[int, List[int], None] = None - ) -> None: + + def __init__(self, name: str | None = None, nodes: int | list[int] | None = None) -> None: super().__init__() self.name = name self.nodes = nodes self.npara = 0 - def _convert_indices(self, indices: Union[int, List[int]]) -> List[int]: + def _convert_indices(self, indices: int | list[int]) -> list[int]: """Convert and check the indices of the modes.""" if isinstance(indices, int): indices = [indices] @@ -44,11 +37,8 @@ class Command(Operation): name (str): The name of the command. nodes (int or List[int]): The indices of the nodes that the command acts on. """ - def __init__( - self, - name: str, - nodes: Union[int, List[int]] - ) -> None: + + def __init__(self, name: str, nodes: int | list[int]) -> None: nodes = self._convert_indices(nodes) super().__init__(name=name, nodes=nodes) diff --git a/src/deepquantum/mbqc/pattern.py b/src/deepquantum/mbqc/pattern.py index da572785..ba9b7898 100644 --- a/src/deepquantum/mbqc/pattern.py +++ b/src/deepquantum/mbqc/pattern.py @@ -1,19 +1,18 @@ -""" -Measurement pattern -""" +"""Measurement pattern""" +from collections.abc import Iterable from copy import copy, deepcopy -from typing import Any, Dict, Iterable, List, Optional, Union +from typing import Any import matplotlib.pyplot as plt import numpy as np import torch -from networkx import MultiDiGraph, draw_networkx_nodes, draw_networkx_edges, draw_networkx_labels, multipartite_layout +from networkx import MultiDiGraph, draw_networkx_edges, draw_networkx_labels, draw_networkx_nodes, multipartite_layout from torch import nn -from .command import Node, Entanglement, Measurement, Correction +from .command import Correction, Entanglement, Measurement, Node from .operation import Operation -from .state import SubGraphState, GraphState +from .state import GraphState, SubGraphState class Pattern(Operation): @@ -37,14 +36,15 @@ class Pattern(Operation): Ref: V. Danos, E. Kashefi and P. Panangaden. J. ACM 54.2 8 (2007) """ + def __init__( self, - nodes_state: Union[int, List[int], None] = None, + nodes_state: int | list[int] | None = None, state: Any = 'plus', - edges: Optional[List] = None, - nodes: Union[int, List[int], None] = None, - name: Optional[str] = None, - reupload: bool = False + edges: list | None = None, + nodes: int | list[int] | None = None, + name: str | None = None, + reupload: bool = False, ) -> None: super().__init__(name=name, nodes=None) self.reupload = reupload @@ -63,7 +63,7 @@ def to(self, arg: Any) -> 'Pattern': op.to(arg) return self - def forward(self, data: Optional[torch.Tensor] = None, state: Optional[GraphState] = None) -> GraphState: + def forward(self, data: torch.Tensor | None = None, state: GraphState | None = None) -> GraphState: """Perform a forward pass of the MBQC pattern and return the final graph state. Args: @@ -80,13 +80,12 @@ def forward(self, data: Optional[torch.Tensor] = None, state: Optional[GraphStat self.encode(data) self.state = self.commands(self.state) self.state.set_nodes_out_seq(self.nodes_out_seq) - if data is not None: - if data.ndim == 2: - # for plotting the last data - self.encode(data[-1]) + if data is not None and data.ndim == 2: + # for plotting the last data + self.encode(data[-1]) return self.state - def encode(self, data: Optional[torch.Tensor]) -> None: + def encode(self, data: torch.Tensor | None) -> None: """Encode the input data into the measurement angles as parameters. This method iterates over the ``encoders`` of the MBQC pattern and initializes their parameters @@ -116,12 +115,13 @@ def encode(self, data: Optional[torch.Tensor]) -> None: op.init_para(data[count:count_up]) count = count_up - def add_graph(self, - nodes_state: Union[int, List[int], None] = None, + def add_graph( + self, + nodes_state: int | list[int] | None = None, state: Any = 'plus', - edges: Optional[List] = None, - nodes: Union[int, List[int], None] = None, - index: Optional[int] = None + edges: list | None = None, + nodes: int | list[int] | None = None, + index: int | None = None, ) -> None: """Add a subgraph state to the graph state. @@ -146,15 +146,11 @@ def graph(self) -> SubGraphState: else: return self.state.graph - def set_nodes_out_seq(self, nodes: Optional[List[int]] = None) -> None: + def set_nodes_out_seq(self, nodes: list[int] | None = None) -> None: """Set the output sequence of the nodes.""" self.nodes_out_seq = nodes - def add( - self, - op: Operation, - encode: bool = False - ) -> None: + def add(self, op: Operation, encode: bool = False) -> None: """A method that adds an operation to the MBQC pattern. Args: @@ -171,7 +167,7 @@ def add( else: self.npara += op.npara - def n(self, nodes: Union[int, List[int]]) -> None: + def n(self, nodes: int | list[int]) -> None: """Add a node command.""" n = Node(nodes=nodes) self.add(n) @@ -184,26 +180,27 @@ def e(self, node1: int, node2: int) -> None: def m( self, node: int, - angle: float = 0., + angle: float = 0.0, plane: str = 'xy', - t_domain: Union[int, Iterable[int], None] = None, - s_domain: Union[int, Iterable[int], None] = None, - encode: bool = False + t_domain: int | Iterable[int] | None = None, + s_domain: int | Iterable[int] | None = None, + encode: bool = False, ) -> None: """Add a measurement command.""" requires_grad = not encode if angle is not None: requires_grad = False - m = Measurement(nodes=node, angle=angle, plane=plane, t_domain=t_domain, s_domain=s_domain, - requires_grad=requires_grad) + m = Measurement( + nodes=node, angle=angle, plane=plane, t_domain=t_domain, s_domain=s_domain, requires_grad=requires_grad + ) self.add(m, encode=encode) - def x(self, node: int, domain: Union[int, Iterable[int], None] = None) -> None: + def x(self, node: int, domain: int | Iterable[int] | None = None) -> None: """Add an X-correction command.""" x = Correction(nodes=node, basis='x', domain=domain) self.add(x) - def z(self, node: int, domain: Union[int, Iterable[int], None] = None) -> None: + def z(self, node: int, domain: int | Iterable[int] | None = None) -> None: """Add a Z-correction command.""" z = Correction(nodes=node, basis='z', domain=domain) self.add(z) @@ -233,15 +230,18 @@ def draw(self): pos = multipartite_layout(g, subset_key='layer') draw_networkx_nodes(g, pos, nodelist=nodes_init, node_color='#1f78b4', node_shape='s') draw_networkx_nodes(g, pos, nodelist=nodes_measured, node_color='#1f78b4') - draw_networkx_nodes(g, pos, nodelist=list(set(g.nodes()) - set(nodes_measured)), - node_color='#d7dde0', node_shape='o') + draw_networkx_nodes( + g, pos, nodelist=list(set(g.nodes()) - set(nodes_measured)), node_color='#d7dde0', node_shape='o' + ) draw_networkx_edges(g, pos, g.edges(), arrows=False) - draw_networkx_edges(g, pos, edges_t_domain, arrows=True, style=':', - edge_color='#4cd925', connectionstyle='arc3,rad=-0.2') - draw_networkx_edges(g, pos, edges_s_domain, arrows=True, style=':', - edge_color='#db1d2c', connectionstyle='arc3,rad=0.2') + draw_networkx_edges( + g, pos, edges_t_domain, arrows=True, style=':', edge_color='#4cd925', connectionstyle='arc3,rad=-0.2' + ) + draw_networkx_edges( + g, pos, edges_s_domain, arrows=True, style=':', edge_color='#db1d2c', connectionstyle='arc3,rad=0.2' + ) draw_networkx_labels(g, pos) - plt.plot([], [], color='k',label='graph edge') + plt.plot([], [], color='k', label='graph edge') plt.plot([], [], ':', color='#4cd925', label='zflow') plt.plot([], [], ':', color='#db1d2c', label='xflow') plt.plot([], [], 's', color='#1f78b4', label='input nodes') @@ -297,7 +297,7 @@ def standardize(self) -> None: z_dict = {} # Tracks Z corrections by node x_dict = {} # Tracks X corrections by node - def add_correction_domain(domain_dict: Dict, node, domain) -> None: + def add_correction_domain(domain_dict: dict, node, domain) -> None: """Helper function to update correction domains with XOR operation""" if previous_domain := domain_dict.get(node): previous_domain ^= domain @@ -311,7 +311,7 @@ def add_correction_domain(domain_dict: Dict, node, domain) -> None: elif isinstance(op, Entanglement): for side in (0, 1): # Propagate X corrections through entanglement (generates Z corrections) - if s_domain := x_dict.get(op.nodes[side], None): + if s_domain := x_dict.get(op.nodes[side]): add_correction_domain(z_dict, op.nodes[1 - side], s_domain) e_list.append(op) elif isinstance(op, Measurement): @@ -330,14 +330,14 @@ def add_correction_domain(domain_dict: Dict, node, domain) -> None: # Reconstruct command sequence in standard order self.commands = nn.Sequential( - *n_list, - *e_list, - *m_list, - *(Correction(nodes=node, basis='z', domain=domain) for node, domain in z_dict.items()), - *(Correction(nodes=node, basis='x', domain=domain) for node, domain in x_dict.items()) + *n_list, + *e_list, + *m_list, + *(Correction(nodes=node, basis='z', domain=domain) for node, domain in z_dict.items()), + *(Correction(nodes=node, basis='x', domain=domain) for node, domain in x_dict.items()), ) - def shift_signals(self) -> Dict: + def shift_signals(self) -> dict: """Perform signal shifting procedure. This allows one to dispose of dependencies induced by the Z-action, @@ -381,7 +381,7 @@ def expand_domain(domain: set[int]) -> None: signal_dict[op.nodes[0]] = s_domain t_domain ^= s_domain s_domain = set() - elif op.plane in ['yz', 'zy']: + elif op.plane in ['yz', 'zy']: # noqa: SIM102 # positive Y axis as 0 angle # M^{YZ,α} X^s Z^t = M^{YZ,(-1)^t·α+(s+t)π)} # = S^s M^{YZ,(-1)^t·α+tπ} diff --git a/src/deepquantum/mbqc/state.py b/src/deepquantum/mbqc/state.py index cc7e07f4..a486a53e 100644 --- a/src/deepquantum/mbqc/state.py +++ b/src/deepquantum/mbqc/state.py @@ -1,9 +1,7 @@ -""" -Quantum states -""" +"""Quantum states""" from collections import defaultdict -from typing import Any, Dict, List, Optional, Union +from typing import Any import networkx as nx import numpy as np @@ -11,7 +9,7 @@ from torch import nn, vmap from ..circuit import QubitCircuit -from ..qmath import multi_kron, inverse_permutation +from ..qmath import inverse_permutation, multi_kron from ..state import QubitState @@ -26,18 +24,19 @@ class SubGraphState(nn.Module): edges (List or None, optional): Additional edges connecting the nodes in the subgraph state. Default: ``None`` nodes (int, List[int] or None, optional): Additional nodes to include in the subgraph state. Default: ``None`` """ + def __init__( self, - nodes_state: Union[int, List[int], None] = None, + nodes_state: int | list[int] | None = None, state: Any = 'plus', - edges: Optional[List] = None, - nodes: Union[int, List[int], None] = None # primarily, for the single-node case + edges: list | None = None, + nodes: int | list[int] | None = None, # primarily, for the single-node case ) -> None: super().__init__() self.nodes_out_seq = None self.set_graph(nodes_state, edges, nodes) self.set_state(state) - self.measure_dict = defaultdict(list) # record the measurement results: {node: batched_bit} + self.measure_dict = defaultdict(list) # record the measurement results: {node: batched_bit} def to(self, arg: Any) -> 'SubGraphState': """Set dtype or device of the ``SubGraphState``.""" @@ -67,10 +66,10 @@ def full_state(self) -> torch.Tensor: for i in self.nodes_state: nodes_bg.remove(i) nodes = self.nodes_state + nodes_bg - wires = [0] + list(map(lambda node: self.node2wire_dict[node] + 1, nodes)) # [0] for batch - plus = torch.tensor([[1], [1]], dtype=self.state.dtype, device=self.state.device) / 2 ** 0.5 + wires = [0] + list(map(lambda node: self.node2wire_dict[node] + 1, nodes)) # [0] for batch + plus = torch.tensor([[1], [1]], dtype=self.state.dtype, device=self.state.device) / 2**0.5 init_state = multi_kron([self.state] + [plus] * len(nodes_bg)).reshape([-1] + [2] * nqubit) - init_state = init_state.permute(inverse_permutation(wires)).reshape([-1, 2 ** nqubit]) + init_state = init_state.permute(inverse_permutation(wires)).reshape([-1, 2**nqubit]) cir = QubitCircuit(nqubit=nqubit, init_state=init_state) edges = list(filter(lambda edge: edge[2]['cz'], self.edges(data=True))) for edge in edges: @@ -80,9 +79,9 @@ def full_state(self) -> torch.Tensor: def set_graph( self, - nodes_state: Union[int, List[int], None] = None, - edges: Optional[List] = None, - nodes: Union[int, List[int], None] = None + nodes_state: int | list[int] | None = None, + edges: list | None = None, + nodes: int | list[int] | None = None, ) -> None: """Set the graph structure for the subgraph state.""" if nodes_state is None: @@ -97,7 +96,7 @@ def set_graph( nodes = [nodes] graph = nx.Graph() if len(nodes_state) > 1: - nx.add_cycle(graph, nodes_state, cz=False) # 'cz' is the label for entanglement + nx.add_cycle(graph, nodes_state, cz=False) # 'cz' is the label for entanglement else: graph.add_nodes_from(nodes_state) graph.add_edges_from(edges, cz=True) @@ -111,9 +110,9 @@ def set_state(self, state: Any = 'plus') -> None: nqubit = len(self.nodes_state) if isinstance(state, str): if state == 'plus': - state = torch.tensor([1, 1]) / 2 ** 0.5 + 0j + state = torch.tensor([1, 1]) / 2**0.5 + 0j elif state == 'minus': - state = torch.tensor([1, -1]) / 2 ** 0.5 + 0j + state = torch.tensor([1, -1]) / 2**0.5 + 0j elif state == 'zero': state = torch.tensor([1, 0]) + 0j elif state == 'one': @@ -127,7 +126,7 @@ def set_state(self, state: Any = 'plus') -> None: else: self.register_buffer('state', torch.tensor(1, dtype=state.dtype, device=state.device)) - def set_nodes_out_seq(self, nodes: Optional[List[int]] = None) -> None: + def set_nodes_out_seq(self, nodes: list[int] | None = None) -> None: """Set the output sequence of the nodes.""" if nodes is not None: assert len(nodes) == len(self.nodes) @@ -135,14 +134,14 @@ def set_nodes_out_seq(self, nodes: Optional[List[int]] = None) -> None: self.nodes_out_seq = nodes self.update_node2wire_dict() - def add_nodes(self, nodes: Union[int, List[int]]) -> None: + def add_nodes(self, nodes: int | list[int]) -> None: """Add nodes to the subgraph state.""" if isinstance(nodes, int): nodes = [nodes] self.graph.add_nodes_from(nodes) self.update_node2wire_dict() - def add_edges(self, edges: List) -> None: + def add_edges(self, edges: list) -> None: """Add edges to the subgraph state.""" self.graph.add_edges_from(edges, cz=True) self.update_node2wire_dict() @@ -184,7 +183,7 @@ def compose(self, other: 'SubGraphState', relabel: bool = True) -> 'SubGraphStat sgs.measure_dict.update(other.measure_dict) return sgs - def update_node2wire_dict(self) -> Dict: + def update_node2wire_dict(self) -> dict: """Update the mapping from nodes to wire indices. Returns: @@ -192,7 +191,7 @@ def update_node2wire_dict(self) -> Dict: """ if self.nodes_out_seq is None: wires = inverse_permutation(np.argsort(self.nodes).tolist()) - self.node2wire_dict = {node: wire for node, wire in zip(self.nodes, wires)} + self.node2wire_dict = {node: wire for node, wire in zip(self.nodes, wires, strict=True)} else: self.node2wire_dict = {node: i for i, node in enumerate(self.nodes_out_seq)} return self.node2wire_dict @@ -218,12 +217,13 @@ class GraphState(nn.Module): nodes (int, List[int] or None, optional): Additional nodes to include in the initial graph state. Default: ``None`` """ + def __init__( self, - nodes_state: Union[int, List[int], None] = None, + nodes_state: int | list[int] | None = None, state: Any = 'plus', - edges: Optional[List] = None, - nodes: Union[int, List[int], None] = None + edges: list | None = None, + nodes: int | list[int] | None = None, ) -> None: super().__init__() sgs = SubGraphState(nodes_state, state, edges, nodes) @@ -238,12 +238,12 @@ def to(self, arg: Any) -> 'GraphState': def add_subgraph( self, - nodes_state: Union[int, List[int], None] = None, + nodes_state: int | list[int] | None = None, state: Any = 'plus', - edges: Optional[List] = None, - nodes: Union[int, List[int], None] = None, - measure_dict: Optional[Dict] = None, - index: Optional[int] = None + edges: list | None = None, + nodes: int | list[int] | None = None, + measure_dict: dict | None = None, + index: int | None = None, ) -> None: """Add a subgraph state to the graph state. @@ -276,10 +276,7 @@ def graph(self) -> SubGraphState: """The combined graph state of all subgraph states.""" graph = None for subgraph in self.subgraphs: - if graph is None: - graph = subgraph - else: - graph = graph.compose(subgraph, relabel=True) + graph = subgraph if graph is None else graph.compose(subgraph, relabel=True) graph.set_nodes_out_seq(self.nodes_out_seq) return graph @@ -289,10 +286,10 @@ def full_state(self) -> torch.Tensor: return self.graph.full_state @property - def measure_dict(self) -> Dict: + def measure_dict(self) -> dict: """A dictionary containing all measurement results for the graph state.""" return self.graph.measure_dict - def set_nodes_out_seq(self, nodes: Optional[List[int]] = None) -> None: + def set_nodes_out_seq(self, nodes: list[int] | None = None) -> None: """Set the output sequence of the nodes.""" self.nodes_out_seq = nodes diff --git a/src/deepquantum/operation.py b/src/deepquantum/operation.py index bcfe4383..3d216af5 100644 --- a/src/deepquantum/operation.py +++ b/src/deepquantum/operation.py @@ -1,17 +1,15 @@ -""" -Base classes -""" +"""Base classes for quantum operations""" from copy import copy -from typing import Any, List, Optional, Tuple, Union +from typing import Any import numpy as np import torch from torch import nn, vmap from .distributed import dist_many_targ_gate -from .qmath import inverse_permutation, state_to_tensors, evolve_state, evolve_den_mat -from .state import MatrixProductState, DistributedQubitState +from .qmath import evolve_den_mat, evolve_state, inverse_permutation, state_to_tensors +from .state import DistributedQubitState, MatrixProductState class Operation(nn.Module): @@ -27,13 +25,14 @@ class Operation(nn.Module): tsr_mode (bool, optional): Whether the quantum operation is in tensor mode, which means the input and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + def __init__( self, - name: Optional[str] = None, + name: str | None = None, nqubit: int = 1, - wires: Union[int, List[int], None] = None, + wires: int | list[int] | None = None, den_mat: bool = False, - tsr_mode: bool = False + tsr_mode: bool = False, ) -> None: super().__init__() self.name = name @@ -46,22 +45,22 @@ def __init__( def tensor_rep(self, x: torch.Tensor) -> torch.Tensor: """Get the tensor representation of the state.""" if self.den_mat: - assert x.shape[-1] == x.shape[-2] == 2 ** self.nqubit + assert x.shape[-1] == x.shape[-2] == 2**self.nqubit return x.reshape([-1] + [2] * 2 * self.nqubit) else: if x.ndim == 1: - assert x.shape[-1] == 2 ** self.nqubit + assert x.shape[-1] == 2**self.nqubit else: - assert x.shape[-1] == 2 ** self.nqubit or x.shape[-2] == 2 ** self.nqubit + assert x.shape[-1] == 2**self.nqubit or x.shape[-2] == 2**self.nqubit return x.reshape([-1] + [2] * self.nqubit) def vector_rep(self, x: torch.Tensor) -> torch.Tensor: """Get the vector representation of the state.""" - return x.reshape(-1, 2 ** self.nqubit, 1) + return x.reshape(-1, 2**self.nqubit, 1) def matrix_rep(self, x: torch.Tensor) -> torch.Tensor: """Get the density matrix representation of the state.""" - return x.reshape(-1, 2 ** self.nqubit, 2 ** self.nqubit) + return x.reshape(-1, 2**self.nqubit, 2**self.nqubit) def get_unitary(self) -> torch.Tensor: """Get the global unitary matrix.""" @@ -75,7 +74,7 @@ def set_nqubit(self, nqubit: int) -> None: """Set the number of qubits of the ``Operation``.""" self.nqubit = nqubit - def set_wires(self, wires: Union[int, List[int]]) -> None: + def set_wires(self, wires: int | list[int]) -> None: """Set the wires of the ``Operation``.""" self.wires = self._convert_indices(wires) @@ -89,7 +88,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: else: return self.vector_rep(x) - def _convert_indices(self, indices: Union[int, List[int]]) -> List[int]: + def _convert_indices(self, indices: int | list[int]) -> list[int]: """Convert and check the indices of the qubits.""" if isinstance(indices, int): indices = [indices] @@ -100,7 +99,7 @@ def _convert_indices(self, indices: Union[int, List[int]]) -> List[int]: assert len(set(indices)) == len(indices), 'Invalid input' return indices - def _check_minmax(self, minmax: List[int]) -> None: + def _check_minmax(self, minmax: list[int]) -> None: """Check the minimum and maximum indices of the qubits.""" assert isinstance(minmax, list) assert len(minmax) == 2 @@ -123,18 +122,19 @@ class Gate(Operation): tsr_mode (bool, optional): Whether the quantum operation is in tensor mode, which means the input and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + # include default names in QASM _qasm_new_gate = ['c3x', 'c4x'] def __init__( self, - name: Optional[str] = None, + name: str | None = None, nqubit: int = 1, - wires: Union[int, List[int], None] = None, - controls: Union[int, List[int], None] = None, + wires: int | list[int] | None = None, + controls: int | list[int] | None = None, condition: bool = False, den_mat: bool = False, - tsr_mode: bool = False + tsr_mode: bool = False, ) -> None: self.nqubit = nqubit if wires is None: @@ -170,7 +170,7 @@ def to(self, arg: Any) -> 'Gate': super().to(arg) return self - def set_controls(self, controls: Union[int, List[int]]) -> None: + def set_controls(self, controls: int | list[int]) -> None: """Set the control wires of the ``Operation``.""" self.controls = self._convert_indices(controls) @@ -193,10 +193,7 @@ def get_derivative(self, inputs: Any) -> torch.Tensor: def op_state(self, x: torch.Tensor) -> torch.Tensor: """Perform a forward pass for state vectors.""" matrix = self.update_matrix() - if self.controls == []: - x = self.op_state_base(x=x, matrix=matrix) - else: - x = self.op_state_control(x=x, matrix=matrix) + x = self.op_state_base(x, matrix) if self.controls == [] else self.op_state_control(x, matrix) if not self.tsr_mode: x = self.vector_rep(x).squeeze(0) return x @@ -217,7 +214,7 @@ def op_state_control(self, x: torch.Tensor, matrix: torch.Tensor) -> torch.Tenso for i in controls: pm_shape.remove(i) pm_shape = wires + pm_shape + controls - x = x.permute(pm_shape).reshape(2 ** nt, -1, 2 ** nc) + x = x.permute(pm_shape).reshape(2**nt, -1, 2**nc) x = torch.cat([x[:, :, :-1], (matrix @ x[:, :, -1]).unsqueeze(-1)], dim=-1) x = x.reshape([2] * nt + [-1] + [2] * (self.nqubit - nt - nc) + [2] * nc) x = x.permute(inverse_permutation(pm_shape)) @@ -226,10 +223,7 @@ def op_state_control(self, x: torch.Tensor, matrix: torch.Tensor) -> torch.Tenso def op_den_mat(self, x: torch.Tensor) -> torch.Tensor: """Perform a forward pass for density matrices.""" matrix = self.update_matrix() - if self.controls == []: - x = self.op_den_mat_base(x=x, matrix=matrix) - else: - x = self.op_den_mat_control(x=x, matrix=matrix) + x = self.op_den_mat_base(x, matrix) if self.controls == [] else self.op_den_mat_control(x, matrix) if not self.tsr_mode: x = self.matrix_rep(x).squeeze(0) return x @@ -251,7 +245,7 @@ def op_den_mat_control(self, x: torch.Tensor, matrix: torch.Tensor) -> torch.Ten for i in controls: pm_shape.remove(i) pm_shape = wires + pm_shape + controls - x = x.permute(pm_shape).reshape(2 ** nt, -1, 2 ** nc) + x = x.permute(pm_shape).reshape(2**nt, -1, 2**nc) x = torch.cat([x[:, :, :-1], (matrix @ x[:, :, -1]).unsqueeze(-1)], dim=-1) x = x.reshape([2] * nt + [-1] + [2] * (2 * self.nqubit - nt - nc) + [2] * nc) x = x.permute(inverse_permutation(pm_shape)) @@ -264,7 +258,7 @@ def op_den_mat_control(self, x: torch.Tensor, matrix: torch.Tensor) -> torch.Ten for i in controls: pm_shape.remove(i) pm_shape = wires + pm_shape + controls - x = x.permute(pm_shape).reshape(2 ** nt, -1, 2 ** nc) + x = x.permute(pm_shape).reshape(2**nt, -1, 2**nc) x = torch.cat([x[:, :, :-1], (matrix.conj() @ x[:, :, -1]).unsqueeze(-1)], dim=-1) x = x.reshape([2] * nt + [-1] + [2] * (2 * self.nqubit - nt - nc) + [2] * nc) x = x.permute(inverse_permutation(pm_shape)) @@ -280,9 +274,8 @@ def op_dist_state(self, x: DistributedQubitState) -> DistributedQubitState: return dist_many_targ_gate(x, targets, unitary) def forward( - self, - x: Union[torch.Tensor, MatrixProductState, DistributedQubitState] - ) -> Union[torch.Tensor, MatrixProductState, DistributedQubitState]: + self, x: torch.Tensor | MatrixProductState | DistributedQubitState + ) -> torch.Tensor | MatrixProductState | DistributedQubitState: """Perform a forward pass.""" if isinstance(x, MatrixProductState): return self.op_mps(x) @@ -301,7 +294,7 @@ def inverse(self) -> 'Gate': """Get the inversed gate.""" return self - def qpd(self, label: Optional[int] = None) -> 'Gate': + def qpd(self, label: int | None = None) -> 'Gate': """Get the quasiprobability-decomposition representation.""" return self @@ -325,11 +318,8 @@ def _qasm_cond_measure(self) -> str: def _qasm_customized(self, name: str) -> str: """Get QASM for multi-controlled gates.""" - name = name.lower() - if len(self.controls) > 2: - name = f'c{len(self.controls)}' + name - else: - name = 'c' * len(self.controls) + name + prefix = f'c{len(self.controls)}' if len(self.controls) > 2 else 'c' * len(self.controls) + name = prefix + name.lower() qasm_lst1 = [f'opaque {name} '] qasm_lst2 = [f'{name} '] for i, wire in enumerate(self.controls + self.wires): @@ -346,7 +336,7 @@ def _qasm_customized(self, name: str) -> str: def _qasm(self) -> str: return self._qasm_customized(self.name) - def get_mpo(self) -> Tuple[List[torch.Tensor], int]: + def get_mpo(self) -> tuple[list[torch.Tensor], int]: r"""Convert gate to MPO form with identities at empty sites. Note: @@ -376,8 +366,8 @@ def get_mpo(self) -> Tuple[List[torch.Tensor], int]: # use shallow copy to share parameters gate_copy = copy(self) gate_copy.nqubit = nindex - gate_copy.wires = index_local[:len(gate_copy.wires)] - gate_copy.controls = index_local[len(gate_copy.wires):] + gate_copy.wires = index_local[: len(gate_copy.wires)] + gate_copy.controls = index_local[len(gate_copy.wires) :] u = gate_copy.get_unitary() # transform gate from (out1, out2, ..., in1, in2 ...) to (out1, in1, out2, in2, ...) order = list(np.arange(2 * nindex).reshape((2, nindex)).T.flatten()) @@ -386,7 +376,7 @@ def get_mpo(self) -> Tuple[List[torch.Tensor], int]: # each tensor is in shape of (i, a, b, j) tensors = [] previous_i = None - for i, main_tensor in zip(index_sort, main_tensors): + for i, main_tensor in zip(index_sort, main_tensors, strict=True): # insert identities in the middle if previous_i is not None: for _ in range(previous_i + 1, i): @@ -427,20 +417,21 @@ class Layer(Operation): Args: name (str, optional): The name of the layer. Default: ``None`` nqubit (int, optional): The number of qubits that the quantum operation acts on. Default: 1 - wires (int, List[int], List[List[int]] or None, optional): The indices of the qubits that the quantum operation acts on. - Default: ``None`` + wires (int | list[int] | list[list[int]] | None, optional): The indices of the qubits that + the quantum operation acts on. Default: ``None`` den_mat (bool, optional): Whether the quantum operation acts on density matrices or state vectors. Default: ``False`` (which means state vectors) tsr_mode (bool, optional): Whether the quantum operation is in tensor mode, which means the input and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + def __init__( self, - name: Optional[str] = None, + name: str | None = None, nqubit: int = 1, - wires: Union[int, List[int], List[List[int]], None] = None, + wires: int | list[int] | list[list[int]] | None = None, den_mat: bool = False, - tsr_mode: bool = False + tsr_mode: bool = False, ) -> None: super().__init__(name=name, nqubit=nqubit, wires=None, den_mat=den_mat, tsr_mode=tsr_mode) if wires is None: @@ -460,10 +451,7 @@ def get_unitary(self) -> torch.Tensor: """Get the global unitary matrix.""" u = None for gate in self.gates: - if u is None: - u = gate.get_unitary() - else: - u = gate.get_unitary() @ u + u = gate.get_unitary() if u is None else gate.get_unitary() @ u return u def init_para(self, inputs: Any = None) -> None: @@ -473,7 +461,7 @@ def init_para(self, inputs: Any = None) -> None: if inputs is None: gate.init_para() else: - gate.init_para(inputs[count:count+gate.npara]) + gate.init_para(inputs[count : count + gate.npara]) count += gate.npara def update_npara(self) -> None: @@ -488,16 +476,15 @@ def set_nqubit(self, nqubit: int) -> None: for gate in self.gates: gate.nqubit = nqubit - def set_wires(self, wires: Union[int, List[int], List[List[int]]]) -> None: + def set_wires(self, wires: int | list[int] | list[list[int]]) -> None: """Set the wires of the ``Layer``.""" self.wires = self._convert_indices(wires) for i, gate in enumerate(self.gates): gate.wires = self.wires[i] def forward( - self, - x: Union[torch.Tensor, MatrixProductState, DistributedQubitState] - ) -> Union[torch.Tensor, MatrixProductState, DistributedQubitState]: + self, x: torch.Tensor | MatrixProductState | DistributedQubitState + ) -> torch.Tensor | MatrixProductState | DistributedQubitState: """Perform a forward pass.""" if isinstance(x, (MatrixProductState, DistributedQubitState)): return self.gates(x) @@ -515,7 +502,7 @@ def inverse(self) -> 'Layer': """Get the inversed layer.""" return self - def _convert_indices(self, indices: Union[int, List]) -> List[List[int]]: + def _convert_indices(self, indices: int | list) -> list[list[int]]: if isinstance(indices, int): indices = [[indices]] assert isinstance(indices, list), 'Invalid input type' @@ -531,11 +518,10 @@ def _convert_indices(self, indices: Union[int, List]) -> List[List[int]]: def _qasm(self) -> str: lst = [] for gate in self.gates: - # pylint: disable=protected-access lst.append(gate._qasm()) return ''.join(lst) - def pattern(self, nodes: List[List[int]], ancilla: List[List[int]]) -> nn.Sequential: + def pattern(self, nodes: list[list[int]], ancilla: list[list[int]]) -> nn.Sequential: """Get the MBQC pattern.""" assert len(nodes) == len(ancilla) == len(self.gates) cmds = nn.Sequential() @@ -559,17 +545,18 @@ class Channel(Operation): requires_grad (bool, optional): Whether the parameter is ``nn.Parameter`` or ``buffer``. Default: ``False`` (which means ``buffer``) """ + # include default names in QASM _qasm_new_gate = [] def __init__( self, inputs: Any = None, - name: Optional[str] = None, + name: str | None = None, nqubit: int = 1, - wires: Union[int, List[int], None] = None, + wires: int | list[int] | None = None, tsr_mode: bool = False, - requires_grad: bool = False + requires_grad: bool = False, ) -> None: self.nqubit = nqubit if wires is None: @@ -673,16 +660,17 @@ class GateQPD(Gate): tsr_mode (bool, optional): Whether the quantum operation is in tensor mode, which means the input and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + def __init__( self, - bases: List[Tuple[nn.Sequential, ...]], - coeffs: List[float], - label: Optional[int] = None, - name: Optional[str] = None, + bases: list[tuple[nn.Sequential, ...]], + coeffs: list[float], + label: int | None = None, + name: str | None = None, nqubit: int = 1, - wires: Union[int, List[int], None] = None, + wires: int | list[int] | None = None, den_mat: bool = False, - tsr_mode: bool = False + tsr_mode: bool = False, ) -> None: self.nqubit = nqubit if wires is None: @@ -710,7 +698,7 @@ def set_nqubit(self, nqubit: int) -> None: for op in ops: op.nqubit = nqubit - def set_wires(self, wires: Union[int, List[int]]) -> None: + def set_wires(self, wires: int | list[int]) -> None: """Set the wires of the ``GateQPD``.""" self.wires = self._convert_indices(wires) for basis in self.bases: @@ -718,7 +706,7 @@ def set_wires(self, wires: Union[int, List[int]]) -> None: for op in ops: op.set_wires(self.wires[i]) - def forward(self, x: torch.Tensor, idx: Optional[int] = None) -> torch.Tensor: + def forward(self, x: torch.Tensor, idx: int | None = None) -> torch.Tensor: """Perform a forward pass. Args: @@ -732,10 +720,7 @@ def forward(self, x: torch.Tensor, idx: Optional[int] = None) -> torch.Tensor: for ops in self.bases[self.idx]: x = ops(x) if not self.tsr_mode: - if self.den_mat: - x = self.matrix_rep(x).squeeze(0) - else: - x = self.vector_rep(x).squeeze(0) + x = self.matrix_rep(x).squeeze(0) if self.den_mat else self.vector_rep(x).squeeze(0) return x @@ -747,7 +732,8 @@ class MeasureQPD(Operation): wires (int, List[int] or None, optional): The indices of the qubits that the quantum operation acts on. Default: ``None`` """ - def __init__(self, nqubit: int = 1, wires: Union[int, List[int], None] = None) -> None: + + def __init__(self, nqubit: int = 1, wires: int | list[int] | None = None) -> None: self.nqubit = nqubit if wires is None: wires = [0] diff --git a/src/deepquantum/optimizer.py b/src/deepquantum/optimizer.py index 2049e339..5c12bbff 100644 --- a/src/deepquantum/optimizer.py +++ b/src/deepquantum/optimizer.py @@ -1,9 +1,6 @@ -""" -Optimizer: various on-chip optimization algorthims -""" +"""Optimizer: various on-chip optimization algorthims""" import copy -from typing import Dict, List, Union import numpy as np import torch @@ -11,7 +8,7 @@ from torch import nn -class Optimizer(object): +class Optimizer: """A base class for optimizers. Args: @@ -21,7 +18,8 @@ class Optimizer(object): the target function. The keys of it should be consistent with inputs of ``target_func``. random_state (int): The random seed for this optimization process. """ - def __init__(self, target_func, param_init, random_state = 0): + + def __init__(self, target_func, param_init, random_state=0): self.target_func = target_func if isinstance(param_init, (list, np.ndarray)): self.param_dict = {} @@ -35,9 +33,11 @@ def __init__(self, target_func, param_init, random_state = 0): elif isinstance(param_init, dict): self.param_dict = copy.deepcopy(param_init) self.random_state = random_state + def __str__(self) -> str: return 'Optimizer' + class OptimizerBayesian(Optimizer): r"""Optimizer based on Bayesian optimization. @@ -55,47 +55,38 @@ class OptimizerBayesian(Optimizer): so in this program the ``pbound`` (a parameter determining the search region in Bayesian-Optimization package) is fixed from 0 to :math:`2\pi`. """ - def __init__(self, target_func, param_init, random_state = 0): + + def __init__(self, target_func, param_init, random_state=0): super().__init__(target_func, param_init, random_state) - def func_to_maximize(**param_dict: Dict) -> float: + + def func_to_maximize(**param_dict: dict) -> float: return -self.target_func(**param_dict) self.pbounds = self.gen_pbounds() # BO 内置用法是最大化目标 - self.optimizer = BayesianOptimization( - f = func_to_maximize, - pbounds = self.pbounds, - random_state = self.random_state - ) - self.util = UtilityFunction( - kind='ucb', - kappa=2.576, - xi=0.0, - kappa_decay=1, - kappa_decay_delay=0) + self.optimizer = BayesianOptimization(f=func_to_maximize, pbounds=self.pbounds, random_state=self.random_state) + self.util = UtilityFunction(kind='ucb', kappa=2.576, xi=0.0, kappa_decay=1, kappa_decay_delay=0) self.best_param_dict = copy.deepcopy(self.param_dict) self.best_target = -np.inf self.iter = 0 - def gen_pbounds(self) -> Dict: + def gen_pbounds(self) -> dict: pbounds = {} - for key in self.param_dict.keys(): - pbounds[key] = (0,np.pi*2) + for key in self.param_dict: + pbounds[key] = (0, np.pi * 2) return pbounds def param_suggest(self) -> np.ndarray: self.util.update_params() x_probe = self.optimizer.suggest(self.util) - # pylint: disable=protected-access - x = self.optimizer._space._as_array(x_probe) # a list + x = self.optimizer._space._as_array(x_probe) # a list param_array = np.asarray(x).reshape(-1) return param_array def param_register(self, param_array: np.ndarray, target: float) -> None: for i in range(len(param_array)): x = param_array[i] - param_dict = dict(zip(self.param_dict.keys(), x)) - # pylint: disable=protected-access + param_dict = dict(zip(self.param_dict.keys(), x, strict=True)) if self.optimizer._space._constraint is None: self.optimizer._space.register(x, target[i]) else: @@ -107,7 +98,7 @@ def param_register(self, param_array: np.ndarray, target: float) -> None: self.best_target = target[i] self.iter += 1 - def run(self, nstep: int, if_print: bool = False) -> List: + def run(self, nstep: int, if_print: bool = False) -> list: for step in range(nstep): p1 = self.param_suggest() f1 = -self.target_func(p1) @@ -131,26 +122,21 @@ class OptimizerSPSA(Optimizer): the target function. The keys of it should be consistent with inputs of ``target_func``. random_state (int): The random seed for this optimization process. """ - def __init__(self, target_func, param_init, random_state = 0): + + def __init__(self, target_func, param_init, random_state=0): super().__init__(target_func, param_init, random_state) self.random_state_ori = np.random.get_state() np.random.seed(self.random_state) - self.hyperparam = { - 'a': 1e-1, - 'c': 1e-2, - 'A': 200, - 'nepoch': 2000, - 'alpha': 0.602, - 'gamma': 0.101 - } + self.hyperparam = {'a': 1e-1, 'c': 1e-2, 'A': 200, 'nepoch': 2000, 'alpha': 0.602, 'gamma': 0.101} self.iter = 0 self.nparam = len(param_init) self.best_param_dict = copy.deepcopy(self.param_dict) self.best_target = np.inf - def set_hyperparam(self, hyperparam: Dict) -> None: - """Set hyperparameters whose keys include ``'a'``, ``'c'``, ``'A'``, ``'nepoch'``, - ``'alpha'``, ``'gamma'``. + def set_hyperparam(self, hyperparam: dict) -> None: + """Set hyperparameters. + + The keys include ``'a'``, ``'c'``, ``'A'``, ``'nepoch'``, ``'alpha'``, ``'gamma'``. """ self.hyperparam = hyperparam @@ -163,7 +149,7 @@ def param_suggest(self) -> np.ndarray: param_array[1] = tmp_param + delta return param_array - def param_register(self, param_array: Union[np.ndarray, List], target: Union[np.ndarray, List]) -> None: + def param_register(self, param_array: np.ndarray | list, target: np.ndarray | list) -> None: assert len(param_array) == 2 assert len(target) == 2 param_lr = self.hyperparam['a'] / (1 + self.iter + self.hyperparam['A']) ** self.hyperparam['alpha'] @@ -174,21 +160,21 @@ def param_register(self, param_array: Union[np.ndarray, List], target: Union[np. delta = param2 - param1 grad = (target2 - target1) / delta param_new = 0.5 * (param1 + param2) - param_lr * grad - self.param_dict = dict(zip(self.param_dict.keys(), param_new)) + self.param_dict = dict(zip(self.param_dict.keys(), param_new, strict=True)) self.iter += 1 if target1 < self.best_target: - self.best_param_dict = dict(zip(self.param_dict.keys(), param1)) + self.best_param_dict = dict(zip(self.param_dict.keys(), param1, strict=True)) self.best_target = target1 if target2 < self.best_target: - self.best_param_dict = dict(zip(self.param_dict.keys(), param2)) + self.best_param_dict = dict(zip(self.param_dict.keys(), param2, strict=True)) self.best_target = target2 def ori_random_state(self) -> None: np.random.set_state(self.random_state_ori) - def run(self, nstep: int, if_print: bool = False) -> List: + def run(self, nstep: int, if_print: bool = False) -> list: for step in range(nstep): p1, p2 = self.param_suggest() f1 = self.target_func(p1) @@ -196,7 +182,7 @@ def run(self, nstep: int, if_print: bool = False) -> List: if isinstance(f1, torch.Tensor): f1 = f1.item() f2 = f2.item() - self.param_register([p1,p2], [f1,f2]) + self.param_register([p1, p2], [f1, f2]) if if_print: print(step, '|', f1, f2) return list(self.best_param_dict.values()) @@ -217,7 +203,8 @@ class OptimizerFourier(Optimizer): (namely, gradient descent process). random_state (int): The random seed for this optimization process. """ - def __init__(self, target_func, param_init, order = 5, lr = 0.1, random_state = 0): + + def __init__(self, target_func, param_init, order=5, lr=0.1, random_state=0): super().__init__(target_func, param_init, random_state) self.iter = 0 self.r = order @@ -226,53 +213,55 @@ def __init__(self, target_func, param_init, order = 5, lr = 0.1, random_state = self.best_target = np.inf self.lr = lr self.a = self.gen_a() - self.u = np.zeros((2*order+1)*self.nparam) + self.u = np.zeros((2 * order + 1) * self.nparam) self.iter = 0 def gen_a(self) -> np.ndarray: - a = np.zeros((2*self.r+1, 2*self.r+1)) - mu = np.arange(2*self.r+1) + a = np.zeros((2 * self.r + 1, 2 * self.r + 1)) + mu = np.arange(2 * self.r + 1) x_mu = 2 * np.pi * (mu - self.r) / (2 * self.r + 1) - a[:,0] = 1 - a[:,1:self.r+1] = np.cos(x_mu.reshape(-1,1)@np.arange(1,self.r+1).reshape(1,-1)) - a[:,self.r+1:2*self.r+2] = np.sin(x_mu.reshape(-1,1)@np.arange(1,self.r+1).reshape(1,-1)) + a[:, 0] = 1 + a[:, 1 : self.r + 1] = np.cos(x_mu.reshape(-1, 1) @ np.arange(1, self.r + 1).reshape(1, -1)) + a[:, self.r + 1 : 2 * self.r + 2] = np.sin(x_mu.reshape(-1, 1) @ np.arange(1, self.r + 1).reshape(1, -1)) return a def param_suggest(self) -> np.ndarray: tmp_param = np.asarray(list(self.param_dict.values()), dtype=float).reshape(1, -1) - mu = np.arange(2*self.r+1) + mu = np.arange(2 * self.r + 1) varied_param = 2 * np.pi * (mu - self.r) / (2 * self.r + 1) - param_array = np.repeat(tmp_param, self.nparam*(2*self.r+1), axis=0) + param_array = np.repeat(tmp_param, self.nparam * (2 * self.r + 1), axis=0) for param_id in range(self.nparam): - param_array[param_id*(2*self.r+1):(param_id+1)*(2*self.r+1), param_id] = varied_param - return param_array + param_array[param_id * (2 * self.r + 1) : (param_id + 1) * (2 * self.r + 1), param_id] = varied_param + return param_array def param_register(self, param_array: np.ndarray, target: np.ndarray): - assert len(param_array)==(2*self.r+1)*self.nparam - assert len(target)==(2*self.r+1)*self.nparam + assert len(param_array) == (2 * self.r + 1) * self.nparam + assert len(target) == (2 * self.r + 1) * self.nparam # 求解线性方程组得出组合系数 param = np.asarray(list(self.param_dict.values())) for param_id in range(self.nparam): - idx1 = param_id*(2*self.r+1) - idx2 = (1+param_id)*(2*self.r+1) - self.u[idx1:idx2] = np.linalg.solve(self.a,target[idx1:idx2]) + idx1 = param_id * (2 * self.r + 1) + idx2 = (1 + param_id) * (2 * self.r + 1) + self.u[idx1:idx2] = np.linalg.solve(self.a, target[idx1:idx2]) # 根据组合系数计算当前位置处的偏导数 grad = np.zeros(self.nparam) for param_id in range(self.nparam): theta = param[param_id] - idx = 1+param_id*(2*self.r+1) - grad[param_id] = -(np.arange(1,self.r+1)*np.sin(theta*np.arange(1,self.r+1)))@\ - self.u[idx:self.r+idx]+(np.arange(1,self.r+1)*\ - np.cos(theta*np.arange(1,self.r+1)))@self.u[self.r+idx:self.r*2+idx] + idx = 1 + param_id * (2 * self.r + 1) + grad[param_id] = ( + -(np.arange(1, self.r + 1) * np.sin(theta * np.arange(1, self.r + 1))) @ self.u[idx : self.r + idx] + + (np.arange(1, self.r + 1) * np.cos(theta * np.arange(1, self.r + 1))) + @ self.u[self.r + idx : self.r * 2 + idx] + ) param_new = param - self.lr * grad - self.param_dict = dict(zip(self.param_dict.keys(), param_new)) + self.param_dict = dict(zip(self.param_dict.keys(), param_new, strict=True)) if target.min() < self.best_target: self.best_target = target.min() - self.best_param_dict = dict(zip(self.param_dict.keys(), param_array[target.argmin()])) + self.best_param_dict = dict(zip(self.param_dict.keys(), param_array[target.argmin()], strict=True)) self.iter += 1 - def run(self, nstep: int, if_print: bool = False) -> List: + def run(self, nstep: int, if_print: bool = False) -> list: for step in range(nstep): param_array = self.param_suggest() target = np.zeros(len(param_array)) diff --git a/src/deepquantum/photonic/__init__.py b/src/deepquantum/photonic/__init__.py index b2365ba5..69f95193 100644 --- a/src/deepquantum/photonic/__init__.py +++ b/src/deepquantum/photonic/__init__.py @@ -1,38 +1,68 @@ -""" -Photonic Module -""" +"""Photonic Module""" -from . import ansatz -from . import channel -from . import circuit -from . import decompose -from . import distributed -from . import draw -from . import gate -from . import hafnian_ -from . import mapper -from . import measurement -from . import operation -from . import qmath -from . import state -from . import tdm -from . import torontonian_ -from . import utils - -from .ansatz import Clements, GaussianBosonSampling, GBS_Graph +from . import ( + ansatz, + channel, + circuit, + decompose, + distributed, + draw, + gate, + hafnian_, + mapper, + measurement, + operation, + qmath, + state, + tdm, + torontonian_, + utils, +) +from .ansatz import Clements, GaussianBosonSampling, GraphGBS from .channel import PhotonLoss -from .circuit import QumodeCircuit, DistributedQumodeCircuit +from .circuit import DistributedQumodeCircuit, QumodeCircuit from .decompose import UnitaryDecomposer from .draw import DrawClements -from .gate import PhaseShift, BeamSplitter, MZI, BeamSplitterTheta, BeamSplitterPhi, BeamSplitterSingle, UAnyGate -from .gate import Squeezing, Squeezing2, Displacement, DisplacementPosition, DisplacementMomentum -from .gate import QuadraticPhase, ControlledX, ControlledZ, CubicPhase, Kerr, CrossKerr, DelayBS, DelayMZI, Barrier +from .gate import ( + Barrier, + BeamSplitter, + BeamSplitterPhi, + BeamSplitterSingle, + BeamSplitterTheta, + ControlledX, + ControlledZ, + CrossKerr, + CubicPhase, + DelayBS, + DelayMZI, + Displacement, + DisplacementMomentum, + DisplacementPosition, + Kerr, + MZI, + PhaseShift, + QuadraticPhase, + Squeezing, + Squeezing2, + UAnyGate, +) from .hafnian_ import hafnian from .mapper import UnitaryMapper -from .measurement import Generaldyne, Homodyne, GeneralBosonic, PhotonNumberResolvingBosonic -from .qmath import permanent, takagi, sqrtm_herm, schur_anti_symm_even, williamson -from .qmath import xxpp_to_xpxp, xpxp_to_xxpp, quadrature_to_ladder, ladder_to_quadrature, fock_to_wigner, cv_to_wigner -from .state import FockState, GaussianState, BosonicState, CatState, GKPState, FockStateBosonic, DistributedFockState +from .measurement import GeneralBosonic, Generaldyne, Homodyne, PhotonNumberResolvingBosonic +from .qmath import ( + cv_to_wigner, + fock_to_wigner, + ladder_to_quadrature, + permanent, + quadrature_to_ladder, + schur_anti_symm_even, + sqrtm_herm, + takagi, + williamson, + xpxp_to_xxpp, + xxpp_to_xpxp, +) +from .state import BosonicState, CatState, DistributedFockState, FockState, FockStateBosonic, GKPState, GaussianState from .tdm import QumodeCircuitTDM from .torontonian_ import torontonian from .utils import set_hbar, set_kappa, set_perm_chunksize diff --git a/src/deepquantum/photonic/ansatz.py b/src/deepquantum/photonic/ansatz.py index 94f161fe..23fd59ed 100644 --- a/src/deepquantum/photonic/ansatz.py +++ b/src/deepquantum/photonic/ansatz.py @@ -1,10 +1,8 @@ -""" -Ansatze: various photonic quantum circuits -""" - -from typing import Any, Dict, List, Optional +"""Ansatze: various photonic quantum circuits""" import copy +from typing import Any + import networkx as nx import numpy as np import torch @@ -18,19 +16,28 @@ class Clements(QumodeCircuit): """Clements circuit.""" + def __init__( self, nmode: int, init_state: Any, - cutoff: Optional[int] = None, + cutoff: int | None = None, basis: bool = True, phi_first: bool = True, noise: bool = False, mu: float = 0, - sigma: float = 0.1 + sigma: float = 0.1, ) -> None: - super().__init__(nmode=nmode, init_state=init_state, cutoff=cutoff, basis=basis, name='Clements', - noise=noise, mu=mu, sigma=sigma) + super().__init__( + nmode=nmode, + init_state=init_state, + cutoff=cutoff, + basis=basis, + name='Clements', + noise=noise, + mu=mu, + sigma=sigma, + ) self.phi_first = phi_first wires1 = self.wires[1::2] wires2 = self.wires[2::2] @@ -48,10 +55,10 @@ def __init__( for wire in self.wires: self.ps(wire, encode=True) - def dict2data(self, angle_dict: Dict, dtype = torch.float) -> torch.Tensor: + def dict2data(self, angle_dict: dict, dtype=torch.float) -> torch.Tensor: """Convert the dictionary of angles to the input data for the circuit.""" angle_dict = angle_dict.copy() - for key in angle_dict.keys(): + for key in angle_dict: angle = angle_dict[key] if not isinstance(angle, torch.Tensor): angle = torch.tensor(angle) @@ -69,11 +76,11 @@ def dict2data(self, angle_dict: Dict, dtype = torch.float) -> torch.Tensor: for j in range(len(wires1)): wire = wires1[j] - 1 if self.phi_first: - phi = angle_dict[(wire, columns[wire])] + phi = angle_dict[(wire, columns[wire])] theta = angle_dict[(wire, columns[wire] + 1)] else: theta = angle_dict[(wire, columns[wire])] - phi = angle_dict[(wire, columns[wire] + 1)] + phi = angle_dict[(wire, columns[wire] + 1)] data.append(theta) data.append(phi) columns[wire] += 2 @@ -81,11 +88,11 @@ def dict2data(self, angle_dict: Dict, dtype = torch.float) -> torch.Tensor: for j in range(len(wires2)): wire = wires2[j] - 1 if self.phi_first: - phi = angle_dict[(wire, columns[wire])] + phi = angle_dict[(wire, columns[wire])] theta = angle_dict[(wire, columns[wire] + 1)] else: theta = angle_dict[(wire, columns[wire])] - phi = angle_dict[(wire, columns[wire] + 1)] + phi = angle_dict[(wire, columns[wire] + 1)] data.append(theta) data.append(phi) columns[wire] += 2 @@ -98,18 +105,19 @@ def dict2data(self, angle_dict: Dict, dtype = torch.float) -> torch.Tensor: class GaussianBosonSampling(QumodeCircuit): """Gaussian Boson Sampling circuit.""" + def __init__( self, nmode: int, squeezing: Any, unitary: Any, - cutoff: Optional[int] = None, + cutoff: int | None = None, backend: str = 'gaussian', basis: bool = True, detector: str = 'pnrd', noise: bool = False, mu: float = 0, - sigma: float = 0.1 + sigma: float = 0.1, ) -> None: if not isinstance(squeezing, torch.Tensor): squeezing = torch.tensor(squeezing).reshape(-1) @@ -120,24 +128,35 @@ def __init__( assert is_unitary(unitary) if cutoff is None: cutoff = 3 - super().__init__(nmode=nmode, init_state='vac', cutoff=cutoff, backend=backend, basis=basis, - detector=detector, name='GBS', noise=noise, mu=mu, sigma=sigma) + super().__init__( + nmode=nmode, + init_state='vac', + cutoff=cutoff, + backend=backend, + basis=basis, + detector=detector, + name='GBS', + noise=noise, + mu=mu, + sigma=sigma, + ) for i in range(self.nmode): self.s(i, squeezing[i]) self.clements(unitary) -class GBS_Graph(GaussianBosonSampling): +class GraphGBS(GaussianBosonSampling): """Simulate Gaussian Boson Sampling for graph problems.""" + def __init__( self, adj_mat: Any, - cutoff: Optional[int] = None, - mean_photon_num: Optional[int] = None, + cutoff: int | None = None, + mean_photon_num: int | None = None, detector: str = 'pnrd', noise: bool = False, mu: float = 0, - sigma: float = 0.1 + sigma: float = 0.1, ) -> None: if not isinstance(adj_mat, torch.Tensor): adj_mat = torch.tensor(adj_mat) @@ -150,9 +169,19 @@ def __init__( c = self.norm_factor_c(mean_photon_num, lambd)[0] lambda_c = lambd * c squeezing = np.arctanh(lambda_c) - super().__init__(nmode=nmode, squeezing=squeezing, unitary=unitary, cutoff=cutoff, backend='gaussian', - basis=False, detector=detector, noise=noise, mu=mu, sigma=sigma) - self.name = 'GBS_Graph' + super().__init__( + nmode=nmode, + squeezing=squeezing, + unitary=unitary, + cutoff=cutoff, + backend='gaussian', + basis=False, + detector=detector, + noise=noise, + mu=mu, + sigma=sigma, + ) + self.name = 'GraphGBS' self.to(adj_mat.dtype) @staticmethod @@ -173,24 +202,21 @@ def f(c, lambd, n_num): return sol_re @staticmethod - def postselect(samples: Dict, nodes_list: List) -> List: + def postselect(samples: dict, nodes_list: list) -> list: """Postselect the results with the fixed node subgraph.""" dic_list = [{} for _ in range(len(nodes_list))] - for key in samples.keys(): - if isinstance(key, FockState): - temp = sum(key.state.tolist()) - else: - temp = sum(key) + for key in samples: + temp = sum(key.state.tolist()) if isinstance(key, FockState) else sum(key) if temp in nodes_list: temp_idx = nodes_list.index(temp) dic_list[temp_idx][key] = samples[key] return dic_list @staticmethod - def graph_density(graph: nx.Graph, samples: Dict) -> Dict: + def graph_density(graph: nx.Graph, samples: dict) -> dict: """Get all subgraph densities.""" samples_ = copy.deepcopy(samples) - for key in samples_.keys(): + for key in samples_: temp_prob = copy.deepcopy(samples_[key]) if isinstance(key, FockState): idx = torch.nonzero(key.state).squeeze() diff --git a/src/deepquantum/photonic/channel.py b/src/deepquantum/photonic/channel.py index 22989394..d6976812 100644 --- a/src/deepquantum/photonic/channel.py +++ b/src/deepquantum/photonic/channel.py @@ -1,12 +1,11 @@ -""" -Photonic quantum channels -""" +"""Photonic quantum channels""" -from typing import Any, List, Optional, Tuple, Union +from typing import Any import torch import deepquantum.photonic as dqp + from .gate import BeamSplitterSingle from .operation import Channel @@ -30,18 +29,27 @@ class PhotonLoss(Channel): requires_grad (bool, optional): Whether the parameter is ``nn.Parameter`` or ``buffer``. Default: ``False`` (which means ``buffer``) """ + def __init__( self, inputs: Any = None, nmode: int = 1, - wires: Union[int, List[int], None] = None, - cutoff: Optional[int] = None, - requires_grad: bool = False + wires: int | list[int] | None = None, + cutoff: int | None = None, + requires_grad: bool = False, ) -> None: super().__init__(name='PhotonLoss', nmode=nmode, wires=wires, cutoff=cutoff) self.requires_grad = requires_grad - self.gate = BeamSplitterSingle(inputs=inputs, nmode=self.nmode + 1, wires=self.wires + [self.nmode], - cutoff=cutoff, den_mat=True, convention='h', requires_grad=requires_grad, noise=False) + self.gate = BeamSplitterSingle( + inputs=inputs, + nmode=self.nmode + 1, + wires=self.wires + [self.nmode], + cutoff=cutoff, + den_mat=True, + convention='h', + requires_grad=requires_grad, + noise=False, + ) self.npara = 1 @property @@ -69,7 +77,7 @@ def init_para(self, inputs: Any = None) -> None: """Initialize the parameters.""" self.gate.init_para(inputs) - def update_transform_xy(self) -> Tuple[torch.Tensor, torch.Tensor]: + def update_transform_xy(self) -> tuple[torch.Tensor, torch.Tensor]: """Update the local transformation matrices X and Y acting on Gaussian states. See https://arxiv.org/pdf/quant-ph/0503237 Eq.(4.19), Eq.(4.20) @@ -77,7 +85,7 @@ def update_transform_xy(self) -> Tuple[torch.Tensor, torch.Tensor]: g_t_sqrt = self.theta.new_ones(2).diag() * torch.cos(self.theta / 2) g_t = self.theta.new_ones(2).diag() * torch.cos(self.theta / 2) ** 2 identity = self.theta.new_ones(2).diag() - sigma_h = self.theta.new_ones(2).diag() * dqp.hbar / (4 * dqp.kappa ** 2) + sigma_h = self.theta.new_ones(2).diag() * dqp.hbar / (4 * dqp.kappa**2) matrix_x = g_t_sqrt matrix_y = (identity - g_t) @ sigma_h self.matrix_x = matrix_x.detach() diff --git a/src/deepquantum/photonic/circuit.py b/src/deepquantum/photonic/circuit.py index 3ef6f8c5..0d684faa 100644 --- a/src/deepquantum/photonic/circuit.py +++ b/src/deepquantum/photonic/circuit.py @@ -1,13 +1,11 @@ -""" -Photonic quantum circuit -""" +"""Photonic quantum circuit""" import itertools import random import warnings -from collections import defaultdict, Counter +from collections import Counter, defaultdict from copy import copy, deepcopy -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any import numpy as np import torch @@ -21,18 +19,58 @@ from .decompose import UnitaryDecomposer from .distributed import measure_dist from .draw import DrawCircuit -from .gate import PhaseShift, BeamSplitter, MZI, BeamSplitterTheta, BeamSplitterPhi, BeamSplitterSingle, UAnyGate -from .gate import Squeezing, Squeezing2, Displacement, DisplacementPosition, DisplacementMomentum -from .gate import QuadraticPhase, ControlledX, ControlledZ, CubicPhase, Kerr, CrossKerr, DelayBS, DelayMZI, Barrier +from .gate import ( + Barrier, + BeamSplitter, + BeamSplitterPhi, + BeamSplitterSingle, + BeamSplitterTheta, + ControlledX, + ControlledZ, + CrossKerr, + CubicPhase, + DelayBS, + DelayMZI, + Displacement, + DisplacementMomentum, + DisplacementPosition, + Kerr, + MZI, + PhaseShift, + QuadraticPhase, + Squeezing, + Squeezing2, + UAnyGate, +) from .hafnian_ import hafnian -from .measurement import Homodyne, Generaldyne -from .operation import Operation, Gate, Channel, Delay -from .qmath import fock_combinations, permanent, product_factorial, sort_dict_fock_basis, sub_matrix -from .qmath import measure_fock_tensor, sample_homodyne_fock, sample_reject_bosonic -from .qmath import photon_number_mean_var_cv, photon_number_mean_var_fock, quadrature_mean_fock -from .qmath import quadrature_to_ladder, shift_func, align_shape, williamson -from .state import FockState, GaussianState, BosonicState, CatState, GKPState, DistributedFockState -from .state import combine_bosonic_states +from .measurement import Generaldyne, Homodyne +from .operation import Channel, Delay, Gate, Operation +from .qmath import ( + align_shape, + fock_combinations, + measure_fock_tensor, + permanent, + photon_number_mean_var_cv, + photon_number_mean_var_fock, + product_factorial, + quadrature_mean_fock, + quadrature_to_ladder, + sample_homodyne_fock, + sample_reject_bosonic, + shift_func, + sort_dict_fock_basis, + sub_matrix, + williamson, +) +from .state import ( + BosonicState, + CatState, + DistributedFockState, + FockState, + GKPState, + GaussianState, + combine_bosonic_states, +) from .torontonian_ import torontonian @@ -65,24 +103,33 @@ class QumodeCircuit(Operation): mu (float, optional): The mean of Gaussian noise. Default: 0 sigma (float, optional): The standard deviation of Gaussian noise. Default: 0.1 """ + def __init__( self, nmode: int, init_state: Any, - cutoff: Optional[int] = None, + cutoff: int | None = None, backend: str = 'fock', basis: bool = True, den_mat: bool = False, detector: str = 'pnrd', - name: Optional[str] = None, + name: str | None = None, mps: bool = False, - chi: Optional[int] = None, + chi: int | None = None, noise: bool = False, mu: float = 0, - sigma: float = 0.1 + sigma: float = 0.1, ) -> None: - super().__init__(name=name, nmode=nmode, wires=list(range(nmode)), cutoff=cutoff, den_mat=den_mat, - noise=noise, mu=mu, sigma=sigma) + super().__init__( + name=name, + nmode=nmode, + wires=list(range(nmode)), + cutoff=cutoff, + den_mat=den_mat, + noise=noise, + mu=mu, + sigma=sigma, + ) self.backend = backend.lower() self.basis = basis self.detector = detector.lower() @@ -101,18 +148,18 @@ def __init__( self._lossy = False self._nloss = 0 # Fock basis state - self._is_batch_expanded = False # whether to expand batched states (add a mode) to align photon numbers - self._init_state_forward = None # prepared initial state in forward() (init_state + nloss + batch expansion) - self._out_fock_basis = None # the output Fock basis states - self._reset_fock_basis = True # whether to recompute the output Fock basis states in forward() - self._init_state_sample = None # for _sample_mcmc_fock() + self._is_batch_expanded = False # whether to expand batched states (add a mode) to align photon numbers + self._init_state_forward = None # prepared initial state in forward() (init_state + nloss + batch expansion) + self._out_fock_basis = None # the output Fock basis states + self._reset_fock_basis = True # whether to recompute the output Fock basis states in forward() + self._init_state_sample = None # for _sample_mcmc_fock() # Bosonic - self._bosonic_states = None # list of initial Bosonic states + self._bosonic_states = None # list of initial Bosonic states # TDM self._with_delay = False self._nmode_tdm = self.nmode - self._ntau_dict = defaultdict(list) # {wire: [tau1, tau2, ...]} - self._unroll_dict = None # {wire_space: [wires_delay_n, ..., wires_delay_1, wire_space_concurrent]} + self._ntau_dict = defaultdict(list) # {wire: [tau1, tau2, ...]} + self._unroll_dict = None # {wire_space: [wires_delay_n, ..., wires_delay_1, wire_space_concurrent]} self._operators_tdm = None self._measurements_tdm = None @@ -121,15 +168,16 @@ def set_init_state(self, init_state: Any) -> None: if isinstance(init_state, (FockState, GaussianState, BosonicState, MatrixProductState)): if isinstance(init_state, MatrixProductState): assert self.nmode == init_state.nsite - assert self.backend == 'fock' and not self.basis, \ + assert self.backend == 'fock' and not self.basis, ( 'Only support MPS for Fock backend with Fock state tensor.' + ) self.mps = True self.chi = init_state.chi - self.cutoff = init_state.qudit # if self.cutoff is changed, the operators should be reset + self.cutoff = init_state.qudit # if self.cutoff is changed, the operators should be reset else: assert self.nmode == init_state.nmode self.mps = False - self.cutoff = init_state.cutoff # if self.cutoff is changed, the operators should be reset + self.cutoff = init_state.cutoff # if self.cutoff is changed, the operators should be reset if isinstance(init_state, FockState): self.backend = 'fock' self.basis = init_state.basis @@ -140,15 +188,18 @@ def set_init_state(self, init_state: Any) -> None: self.init_state = init_state else: if self.mps: - assert self.backend == 'fock' and not self.basis, \ + assert self.backend == 'fock' and not self.basis, ( 'Only support MPS for Fock backend with Fock state tensor.' + ) assert self.cutoff is not None, 'Please set the cutoff.' - self.init_state = MatrixProductState(nsite=self.nmode, state=init_state, chi=self.chi, - qudit=self.cutoff, normalize=False) + self.init_state = MatrixProductState( + nsite=self.nmode, state=init_state, chi=self.chi, qudit=self.cutoff, normalize=False + ) else: if self.backend == 'fock': - self.init_state = FockState(state=init_state, nmode=self.nmode, cutoff=self.cutoff, - basis=self.basis, den_mat=self.den_mat) + self.init_state = FockState( + state=init_state, nmode=self.nmode, cutoff=self.cutoff, basis=self.basis, den_mat=self.den_mat + ) elif self.backend == 'gaussian': self.init_state = GaussianState(state=init_state, nmode=self.nmode, cutoff=self.cutoff) elif self.backend == 'bosonic': @@ -169,9 +220,21 @@ def __add__(self, rhs: 'QumodeCircuit') -> 'QumodeCircuit': The initial state is the same as the first ``QumodeCircuit``. """ assert self.nmode == rhs.nmode - cir = QumodeCircuit(nmode=self.nmode, init_state=self.init_state, cutoff=self.cutoff, backend=self.backend, - basis=self.basis, den_mat=self.den_mat, detector=self.detector, name=self.name, - mps=self.mps, chi=self.chi, noise=self.noise, mu=self.mu, sigma=self.sigma) + cir = QumodeCircuit( + nmode=self.nmode, + init_state=self.init_state, + cutoff=self.cutoff, + backend=self.backend, + basis=self.basis, + den_mat=self.den_mat, + detector=self.detector, + name=self.name, + mps=self.mps, + chi=self.chi, + noise=self.noise, + mu=self.mu, + sigma=self.sigma, + ) cir.operators = self.operators + rhs.operators cir.encoders = self.encoders + rhs.encoders cir.measurements = rhs.measurements @@ -209,16 +272,15 @@ def to(self, arg: Any) -> 'QumodeCircuit': bs.to(arg) return self - # pylint: disable=arguments-renamed def forward( self, - data: Optional[torch.Tensor] = None, + data: torch.Tensor | None = None, state: Any = None, - is_prob: Optional[bool] = None, - detector: Optional[str] = None, + is_prob: bool | None = None, + detector: str | None = None, sort: bool = True, - stepwise: bool = False - ) -> Union[torch.Tensor, Dict, List[torch.Tensor]]: + stepwise: bool = False, + ) -> torch.Tensor | dict | list[torch.Tensor]: """Perform a forward pass of the photonic quantum circuit and return the final-state-related result. Args: @@ -244,12 +306,8 @@ def forward( return self._forward_cv(data, state, is_prob, detector, stepwise) def _forward_fock( - self, - data: Optional[torch.Tensor] = None, - state: Any = None, - is_prob: Optional[bool] = None, - sort: bool = True - ) -> Union[torch.Tensor, Dict, List[torch.Tensor]]: + self, data: torch.Tensor | None = None, state: Any = None, is_prob: bool | None = None, sort: bool = True + ) -> torch.Tensor | dict | list[torch.Tensor]: """Perform a forward pass based on the Fock backend. Args: @@ -277,7 +335,7 @@ def _forward_fock( state = FockState(state, self.nmode, self.cutoff, self.basis, self.den_mat).state # preprocessing of batched initial states if self.basis: - self._is_batch_expanded = False # reset + self._is_batch_expanded = False # reset state = self._prepare_init_state(state, reset_fock_basis=self._reset_fock_basis) self._init_state_forward = state if self.ndata == 0: @@ -323,11 +381,8 @@ def _forward_fock( return self.state def _forward_helper_basis( - self, - data: Optional[torch.Tensor] = None, - state: Optional[torch.Tensor] = None, - is_prob: Optional[bool] = None - ) -> Union[torch.Tensor, Dict]: + self, data: torch.Tensor | None = None, state: torch.Tensor | None = None, is_prob: bool | None = None + ) -> torch.Tensor | dict: """Perform a forward pass for one sample if the input is a Fock basis state.""" self.encode(data) unitary = self.get_unitary() @@ -349,25 +404,27 @@ def _forward_helper_basis( for i in range(len(final_states)): final_state = FockState(state=final_states[i], nmode=self.nmode, cutoff=self.cutoff, basis=self.basis) if not is_prob: - assert final_state not in out_dict, \ + assert final_state not in out_dict, ( 'Amplitudes of reduced states can not be added, please set "is_prob" to be True.' + ) out_dict[final_state] += rst[i] return dict(out_dict) def _forward_helper_tensor( self, - data: Optional[torch.Tensor] = None, - state: Union[torch.Tensor, List[torch.Tensor], None] = None, - is_prob: Optional[bool] = None - ) -> Union[torch.Tensor, List[torch.Tensor]]: + data: torch.Tensor | None = None, + state: torch.Tensor | list[torch.Tensor] | None = None, + is_prob: bool | None = None, + ) -> torch.Tensor | list[torch.Tensor]: """Perform a forward pass for one sample if the input is a Fock state tensor.""" self.encode(data) if state is None: state = self.init_state if self.mps: if not isinstance(state, MatrixProductState): - state = MatrixProductState(nsite=self.nmode, state=state, chi=self.chi, qudit=self.cutoff, - normalize=self.init_state.normalize) + state = MatrixProductState( + nsite=self.nmode, state=state, chi=self.chi, qudit=self.cutoff, normalize=self.init_state.normalize + ) return self.operators(state).tensors else: if isinstance(state, FockState): @@ -375,7 +432,7 @@ def _forward_helper_tensor( x = self.operators(self.tensor_rep(state)).squeeze(0) if is_prob: if self.den_mat: - x = x.reshape(-1, self.cutoff ** self.nmode, self.cutoff ** self.nmode).diagonal(dim1=-2, dim2=-1) + x = x.reshape(-1, self.cutoff**self.nmode, self.cutoff**self.nmode).diagonal(dim1=-2, dim2=-1) x = abs(x).reshape([-1] + [self.cutoff] * self.nmode).squeeze(0) else: x = abs(x) ** 2 @@ -383,12 +440,12 @@ def _forward_helper_tensor( def _forward_cv( self, - data: Optional[torch.Tensor] = None, + data: torch.Tensor | None = None, state: Any = None, - is_prob: Optional[bool] = None, - detector: Optional[str] = None, - stepwise: bool = False - ) -> Union[List[torch.Tensor], Dict]: + is_prob: bool | None = None, + detector: str | None = None, + stepwise: bool = False, + ) -> list[torch.Tensor] | dict: """Perform a forward pass based on the Gaussian (Bosonic) backend. Args: @@ -411,18 +468,15 @@ def _forward_cv( state = self.init_state elif not isinstance(state, (GaussianState, BosonicState)): nmode = self.nmode - if self._nmode_tdm is not None and isinstance(state, list): - if isinstance(state[0], torch.Tensor) and state[0].shape[-1] // 2 == self._nmode_tdm: - nmode = self._nmode_tdm + is_list_of_tensors = isinstance(state, list) and state and isinstance(state[0], torch.Tensor) + if is_list_of_tensors and state[0].shape[-1] == 2 * self._nmode_tdm: + nmode = self._nmode_tdm if self.backend == 'gaussian': state = GaussianState(state=state, nmode=nmode, cutoff=self.cutoff) elif self.backend == 'bosonic': state = BosonicState(state=state, nmode=nmode, cutoff=self.cutoff) cov, mean = state.cov, state.mean - if self.backend == 'bosonic': - weight = state.weight - else: - weight = None + weight = state.weight if self.backend == 'bosonic' else None if self._with_delay: self._prepare_unroll_dict() cov, mean = self._unroll_init_state([cov, mean]) @@ -441,7 +495,7 @@ def _forward_cv( cov, mean = vmap(self._forward_helper_gaussian, in_dims=(0, 0, None))(data, [cov, mean], stepwise) self.encode(data[-1]) if is_prob: - self.state = [cov, mean] # for checking purity + self.state = [cov, mean] # for checking purity self.state = self._forward_cv_prob(cov, mean, weight, detector) else: if self._with_delay: @@ -453,27 +507,16 @@ def _forward_cv( return self.state def _forward_helper_gaussian( - self, - data: Optional[torch.Tensor] = None, - state: Optional[List[torch.Tensor]] = None, - stepwise: bool = False - ) -> List[torch.Tensor]: + self, data: torch.Tensor | None = None, state: list[torch.Tensor] | None = None, stepwise: bool = False + ) -> list[torch.Tensor]: """Perform a forward pass for one sample if the input is a Gaussian state.""" if self._lossy: stepwise = True self.encode(data) - if self._with_delay: - operators = self._operators_tdm - else: - operators = self.operators - if state is None: - cov = self.init_state.cov - mean = self.init_state.mean - else: - cov, mean = state - if self.backend == 'bosonic': - if cov.ndim == 3: - cov = cov.unsqueeze(0) + operators = self._operators_tdm if self._with_delay else self.operators + cov, mean = (self.init_state.cov, self.init_state.mean) if state is None else state + if self.backend == 'bosonic' and cov.ndim == 3: + cov = cov.unsqueeze(0) if stepwise: cov, mean = operators([cov, mean]) else: @@ -483,12 +526,8 @@ def _forward_helper_gaussian( return [cov.squeeze(0), mean.squeeze(0)] def _forward_cv_prob( - self, - cov: torch.Tensor, - mean: torch.Tensor, - weight: Optional[torch.Tensor] = None, - detector: Optional[str] = None - ) -> Dict: + self, cov: torch.Tensor, mean: torch.Tensor, weight: torch.Tensor | None = None, detector: str | None = None + ) -> dict: """Get the probabilities of all possible final states for Gaussian (Bosonic) backend by different detectors. Args: @@ -516,7 +555,7 @@ def _forward_cv_prob( self.detector = detector basis = self._get_odd_even_fock_basis(detector=detector) if detector == 'pnrd': - idx_loop = torch.all(mean==0, dim=1) + idx_loop = torch.all(mean == 0, dim=1) idx_loop = idx_loop.squeeze(1) cov_0 = cov[idx_loop] mean_0 = mean[idx_loop] @@ -532,10 +571,10 @@ def _forward_cv_prob( loop = True probs_1 = batch_forward(cov_1, mean_1, basis, detector, purity, loop) probs.append(probs_1) - probs = torch.cat(probs) # reorder the result here + probs = torch.cat(probs) # reorder the result here if len(cov_0) * len(cov_1) > 0: - idx0 = torch.where(~idx_loop==0)[0] - idx1 = torch.where(~idx_loop==1)[0] + idx0 = torch.where(~idx_loop == 0)[0] + idx1 = torch.where(~idx_loop == 1)[0] probs = probs[torch.argsort(torch.cat([idx0, idx1]))] elif detector == 'threshold': final_states = torch.cat(basis) @@ -546,7 +585,7 @@ def _forward_cv_prob( # if weight is not None: # probs = probs.reshape(weight.shape[0], weight.shape[1], -1) # (batch, ncomb, nfock) # probs = (probs * weight.unsqueeze(-1)).sum(1).real - return dict(zip(keys, probs.mT)) + return dict(zip(keys, probs.mT, strict=True)) def _forward_gaussian_prob_helper(self, cov, mean, basis, detector, purity, loop): prob_lst = [] @@ -584,16 +623,18 @@ def set_fock_basis(self, state: Any = None, reset_in_forward: bool = False) -> N assert self.basis assert self.init_state.state.ndim == 1, 'Manual setting for batched initial states is not allowed.' if state is None: - if not reset_in_forward: # avoid double calculation of the output Fock basis states + if not reset_in_forward: # avoid double calculation of the output Fock basis states self._prepare_init_state(self.init_state.state, reset_fock_basis=True) else: state = FockState(state).state if state.ndim == 1: state = state.unsqueeze(0) - assert torch.all(state.sum(dim=-1) == state[0].sum(dim=-1)), \ - "The number of photons must be the same and equal to initial states." - assert state.shape[-1] == self.nmode + self._nloss, \ + assert torch.all(state.sum(dim=-1) == state[0].sum(dim=-1)), ( + 'The number of photons must be the same and equal to initial states.' + ) + assert state.shape[-1] == self.nmode + self._nloss, ( 'Please fill in the right number of modes (including all ancilla modes in lossy case.)' + ) self._out_fock_basis = state self._reset_fock_basis = reset_in_forward @@ -607,22 +648,19 @@ def _get_all_fock_basis(self, init_state: torch.Tensor) -> torch.Tensor: """Calculate all possible Fock basis states according to the initial state.""" nphoton = torch.max(torch.sum(init_state, dim=-1)) nmode = len(init_state) - if self._with_delay: - nancilla = nmode - self._nmode_tdm - else: - nancilla = nmode - self.nmode - states = torch.tensor(fock_combinations(nmode, nphoton, self.cutoff, nancilla=nancilla), - dtype=torch.long, device=init_state.device) + nancilla = nmode - self._nmode_tdm if self._with_delay else nmode - self.nmode + states = torch.tensor( + fock_combinations(nmode, nphoton, self.cutoff, nancilla=nancilla), + dtype=torch.long, + device=init_state.device, + ) return states - def _get_odd_even_fock_basis(self, detector: Optional[str] = None) -> Union[Tuple[List, List], List]: + def _get_odd_even_fock_basis(self, detector: str | None = None) -> tuple[list, list] | list: """Split the Fock basis into the odd and even photon number parts.""" if detector is None: detector = self.detector - if self._with_delay: - nmode = self._nmode_tdm - else: - nmode = self.nmode + nmode = self._nmode_tdm if self._with_delay else self.nmode if detector == 'pnrd': max_photon = nmode * (self.cutoff - 1) odd_lst = [] @@ -639,7 +677,7 @@ def _get_odd_even_fock_basis(self, detector: Optional[str] = None) -> Union[Tupl final_states = torch.tensor(list(itertools.product(range(2), repeat=nmode))) keys = torch.sum(final_states, dim=1) dic_temp = defaultdict(list) - for state, s in zip(final_states, keys): + for state, s in zip(final_states, keys, strict=True): dic_temp[s.item()].append(state) state_lst = [torch.stack(i) for i in list(dic_temp.values())] return state_lst @@ -664,7 +702,7 @@ def _prepare_init_state(self, state: torch.Tensor, reset_fock_basis: bool = Fals self._out_fock_basis = self._get_all_fock_basis(state[0]) return state - def _prepare_unroll_dict(self) -> Dict[int, List]: + def _prepare_unroll_dict(self) -> dict[int, list]: """Create a dictionary that maps spatial modes to concurrent modes.""" if self._unroll_dict is None: self._unroll_dict = defaultdict(list) @@ -672,13 +710,13 @@ def _prepare_unroll_dict(self) -> Dict[int, List]: start = 0 for i in range(self.nmode): for ntau in reversed(self._ntau_dict[i]): - self._unroll_dict[i].append(wires[start:start+ntau]) # modes in delay line + self._unroll_dict[i].append(wires[start : start + ntau]) # modes in delay line start += ntau - self._unroll_dict[i].append(wires[start]) # spatial mode + self._unroll_dict[i].append(wires[start]) # spatial mode start += 1 return self._unroll_dict - def _unroll_init_state(self, state: List[torch.Tensor]) -> List[torch.Tensor]: + def _unroll_init_state(self, state: list[torch.Tensor]) -> list[torch.Tensor]: """Unroll the initial state from spatial modes to concurrent modes.""" idx = torch.tensor([value[-1] for value in self._unroll_dict.values()]) idx = torch.cat([idx, idx + self._nmode_tdm]) @@ -699,7 +737,7 @@ def _unroll_circuit(self) -> None: nmode = self._nmode_tdm if self._operators_tdm is None: self._operators_tdm = nn.Sequential() - ndelay = np.array([0] * self.nmode) # counter of delay loops for each mode + ndelay = np.array([0] * self.nmode) # counter of delay loops for each mode for op in self.operators: if isinstance(op, Delay): wire = op.wires[0] @@ -738,35 +776,39 @@ def global_circuit(self, nstep: int, use_deepcopy: bool = False) -> 'QumodeCircu """ self._prepare_unroll_dict() nmode = self._nmode_tdm + (nstep - 1) * self.nmode - cir = QumodeCircuit(nmode, init_state='vac', cutoff=self.cutoff, backend=self.backend, basis=self.basis, - den_mat=self.den_mat, detector=self.detector, name=self.name, mps=self.mps, chi=self.chi, - noise=self.noise, mu=self.mu, sigma=self.sigma) + cir = QumodeCircuit( + nmode, + init_state='vac', + cutoff=self.cutoff, + backend=self.backend, + basis=self.basis, + den_mat=self.den_mat, + detector=self.detector, + name=self.name, + mps=self.mps, + chi=self.chi, + noise=self.noise, + mu=self.mu, + sigma=self.sigma, + ) for i in range(nstep): - ndelay = np.array([0] * self.nmode) # counter of delay loops for each mode + ndelay = np.array([0] * self.nmode) # counter of delay loops for each mode for op in self.operators: encode = op in self.encoders + is_deep = use_deepcopy or encode if isinstance(op, Delay): wire = op.wires[0] ndelay[wire] += 1 idx_delay = -ndelay[wire] - 1 wire1 = self._unroll_dict[wire][idx_delay][i % op.ntau] - if i == 0: - wire2 = self._unroll_dict[wire][-1] - else: - wire2 = self._nmode_tdm + self.nmode * (i - 1) + wire - if use_deepcopy or encode: - op_tdm = deepcopy(op.gates[0]) - else: - op_tdm = copy(op.gates[0]) + wire2 = self._unroll_dict[wire][-1] if i == 0 else self._nmode_tdm + self.nmode * (i - 1) + wire + op_tdm = deepcopy(op.gates[0]) if is_deep else copy(op.gates[0]) op_tdm.nmode = nmode op_tdm.wires = [wire1, wire2] cir.add(op_tdm, encode=encode) if len(op.gates) > 1: for gate in op.gates[1:]: - if use_deepcopy or encode: - op_gate = deepcopy(gate) - else: - op_gate = copy(gate) + op_gate = deepcopy(gate) if is_deep else copy(gate) op_gate.nmode = nmode op_gate.wires = [wire1] if isinstance(gate, PhotonLoss): @@ -774,10 +816,7 @@ def global_circuit(self, nstep: int, use_deepcopy: bool = False) -> 'QumodeCircu cir._nloss += 1 cir.add(op_gate, encode=encode) else: - if use_deepcopy or encode: - op_tdm = deepcopy(op) - else: - op_tdm = copy(op) + op_tdm = deepcopy(op) if is_deep else copy(op) op_tdm.nmode = nmode if i == 0: op_tdm.wires = [self._unroll_dict[wire][-1] for wire in op.wires] @@ -798,7 +837,7 @@ def global_circuit(self, nstep: int, use_deepcopy: bool = False) -> 'QumodeCircu cir.barrier() return cir - def _shift_state(self, state: List[torch.Tensor], nstep: int = 1, reverse: bool = False) -> List[torch.Tensor]: + def _shift_state(self, state: list[torch.Tensor], nstep: int = 1, reverse: bool = False) -> list[torch.Tensor]: """Shift the state according to ``nstep``, which is equivalent to shifting the TDM circuit.""" cov, mean = state idx_shift = [] @@ -817,7 +856,7 @@ def _shift_state(self, state: List[torch.Tensor], nstep: int = 1, reverse: bool mean = mean[..., idx_shift, :] return [cov, mean] - def encode(self, data: Optional[torch.Tensor]) -> None: + def encode(self, data: torch.Tensor | None) -> None: """Encode the input data into the photonic quantum circuit parameters. This method iterates over the ``encoders`` of the circuit and initializes their parameters @@ -838,10 +877,7 @@ def encode(self, data: Optional[torch.Tensor]) -> None: def get_unitary(self) -> torch.Tensor: """Get the unitary matrix of the photonic quantum circuit.""" u = None - if self._with_delay: - operators = self._operators_tdm - else: - operators = self.operators + operators = self._operators_tdm if self._with_delay else self.operators nloss = 0 for op in operators: if isinstance(op, Barrier): @@ -886,10 +922,7 @@ def get_symplectic(self) -> torch.Tensor: for op in operators: if isinstance(op, Barrier): continue - if s is None: - s = op.get_symplectic() - else: - s = op.get_symplectic() @ s + s = op.get_symplectic() if s is None else op.get_symplectic() @ s if s is None: return torch.eye(2 * nmode, dtype=torch.float) return s @@ -927,10 +960,7 @@ def _get_permanent_norms(self, init_state: torch.Tensor, final_state: torch.Tens return torch.sqrt(product_factorial(init_state) * product_factorial(final_state)) def get_amplitude( - self, - final_state: Any, - init_state: Any = None, - unitary: Optional[torch.Tensor] = None + self, final_state: Any, init_state: Any = None, unitary: torch.Tensor | None = None ) -> torch.Tensor: """Get the transfer amplitude between the final state and the initial state. @@ -978,12 +1008,7 @@ def _get_amplitude_fock_vmap(self, sub_mat: torch.Tensor, per_norm: torch.Tensor amp = per / per_norm return amp.reshape(-1) - def get_prob( - self, - final_state: Any, - refer_state: Any = None, - unitary: Optional[torch.Tensor] = None - ) -> torch.Tensor: + def get_prob(self, final_state: Any, refer_state: Any = None, unitary: torch.Tensor | None = None) -> torch.Tensor: """Get the probability of the final state related to the reference state. Args: @@ -1012,31 +1037,29 @@ def get_prob( nphoton_final = torch.sum(final_state, dim=-1) max_photon = torch.sum(refer_state, dim=-1).max().item() nmode_expand = refer_state.shape[-1] - nmode - expand_state = torch.tensor(fock_combinations(nmode_expand, max_photon - nphoton_final), - dtype=torch.long, device=final_state.device) + expand_state = torch.tensor( + fock_combinations(nmode_expand, max_photon - nphoton_final), + dtype=torch.long, + device=final_state.device, + ) final_state = final_state.reshape(-1, nmode).expand(expand_state.shape[0], -1) final_states = torch.cat([final_state, expand_state], dim=-1) if refer_state.ndim == 1: rst = self._measure_fock_unitary_helper(refer_state, unitary, wires, final_states) else: - rst = vmap(self._measure_fock_unitary_helper, - in_dims=(0, None, None, None))(refer_state, unitary, wires, final_states) + rst = vmap(self._measure_fock_unitary_helper, in_dims=(0, None, None, None))( + refer_state, unitary, wires, final_states + ) rst = list(rst.values())[0] return rst elif self.backend == 'gaussian': - if self._with_delay: - nmode = self._nmode_tdm - else: - nmode = self.nmode + nmode = self._nmode_tdm if self._with_delay else self.nmode if refer_state is None: refer_state = GaussianState(self.state, nmode=nmode, cutoff=self.cutoff) return self._get_prob_gaussian(final_state, refer_state) def _get_prob_fock( - self, - final_state: Any, - init_state: Any = None, - unitary: Optional[torch.Tensor] = None + self, final_state: Any, init_state: Any = None, unitary: torch.Tensor | None = None ) -> torch.Tensor: """Get the transfer probability between the final state and the initial state for the Fock backend. @@ -1045,10 +1068,10 @@ def _get_prob_fock( init_state (Any, optional): The initial Fock basis state. Default: ``None`` unitary (torch.Tensor or None, optional): The unitary matrix. Default: ``None`` """ - if init_state is None: # when mcmc + if init_state is None: # when mcmc nmode = self.nmode + self._nloss + self._is_batch_expanded init_state = FockState(state=self._init_state_sample, nmode=nmode, cutoff=self.cutoff, basis=self.basis) - if unitary is None: # when mcmc + if unitary is None: # when mcmc unitary = self._unitary amplitude = self.get_amplitude(final_state, init_state, unitary) prob = torch.abs(amplitude) ** 2 @@ -1060,11 +1083,7 @@ def _get_prob_fock_vmap(self, sub_mat: torch.Tensor, per_norm: torch.Tensor) -> prob = torch.abs(amplitude) ** 2 return prob - def _get_prob_gaussian( - self, - final_state: Any, - state: Any = None - ) -> torch.Tensor: + def _get_prob_gaussian(self, final_state: Any, state: Any = None) -> torch.Tensor: """Get the batched probabilities of the final state for Gaussian backend.""" if not isinstance(final_state, torch.Tensor): final_state = torch.tensor(final_state, dtype=torch.long) @@ -1094,12 +1113,12 @@ def _get_probs_gaussian_helper( cov: torch.Tensor, mean: torch.Tensor, detector: str = 'pnrd', - purity: Optional[bool] = None, - loop: Optional[bool] = None + purity: bool | None = None, + loop: bool | None = None, ) -> torch.Tensor: """Get the probabilities of the final states for Gaussian backend.""" if loop is None: - loop = ~torch.all(mean==0) + loop = ~torch.all(mean == 0) if final_states.ndim == 1: final_states = final_states.unsqueeze(0) assert final_states.ndim == 2 @@ -1133,13 +1152,13 @@ def _get_prob_gaussian_base( p_vac: torch.Tensor, detector: str = 'pnrd', purity: bool = True, - loop: bool = False + loop: bool = False, ) -> torch.Tensor: """Get the probability of the final state for Gaussian backend.""" gamma = gamma.squeeze() nmode = len(final_state) with warnings.catch_warnings(): - warnings.filterwarnings('ignore') # local warning + warnings.filterwarnings('ignore') # local warning gamma_n1 = torch.repeat_interleave(gamma[:nmode], final_state) gamma_n2 = torch.repeat_interleave(gamma[nmode:], final_state) sub_gamma = torch.cat([gamma_n1, gamma_n2]) @@ -1155,10 +1174,7 @@ def _get_prob_gaussian_base( sub_mat = sub_gamma else: sub_mat[torch.arange(len(sub_gamma)), torch.arange(len(sub_gamma))] = sub_gamma - if purity: - haf = abs(hafnian(sub_mat, loop=loop)) ** 2 - else: - haf = hafnian(sub_mat, loop=loop) + haf = abs(hafnian(sub_mat, loop=loop)) ** 2 if purity else hafnian(sub_mat, loop=loop) prob = p_vac * haf / product_factorial(final_state).to(device=haf.device, dtype=haf.dtype) elif detector == 'threshold': final_state_double = torch.cat([final_state, final_state]) @@ -1166,7 +1182,7 @@ def _get_prob_gaussian_base( prob = p_vac * torontonian(sub_mat, sub_gamma) return abs(prob.real).squeeze() - def _get_prob_mps(self, final_state: Any, wires: Union[int, List[int], None] = None) -> torch.Tensor: + def _get_prob_mps(self, final_state: Any, wires: int | list[int] | None = None) -> torch.Tensor: """Get the probability of the given bit string for MPS. Args: @@ -1176,10 +1192,7 @@ def _get_prob_mps(self, final_state: Any, wires: Union[int, List[int], None] = N """ if isinstance(final_state, FockState): final_state = final_state.state.tolist() - if wires is None: - wires = list(range(self.nmode)) - else: - wires = self._convert_indices(wires) + wires = list(range(self.nmode)) if wires is None else self._convert_indices(wires) assert len(final_state) == len(wires) state = copy(self.state) if self.state[0].ndim == 3: @@ -1192,10 +1205,10 @@ def measure( self, shots: int = 1024, with_prob: bool = False, - wires: Union[int, List[int], None] = None, - detector: Optional[str] = None, - mcmc: bool = False - ) -> Union[Dict, List[Dict], None]: + wires: int | list[int] | None = None, + detector: str | None = None, + mcmc: bool = False, + ) -> dict | list[dict] | None: """Measure the final state. Args: @@ -1219,16 +1232,13 @@ def measure( if self.backend == 'fock': results = self._measure_fock(shots, with_prob, wires, mcmc) elif self.backend == 'gaussian': - if detector is None: - detector = self.detector - else: - detector = detector.lower() + detector = self.detector if detector is None else detector.lower() results = self._measure_gaussian(shots, with_prob, wires, detector, mcmc) if len(results) == 1: results = results[0] return results - def _prob_dict_to_measure_result(self, prob_dict: Dict, shots: int, with_prob: bool) -> Dict: + def _prob_dict_to_measure_result(self, prob_dict: dict, shots: int, with_prob: bool) -> dict: """Get the measurement result from the dictionary of probabilities.""" keys = list(prob_dict.keys()) probs = torch.cat(list(prob_dict.values())) @@ -1238,7 +1248,7 @@ def _prob_dict_to_measure_result(self, prob_dict: Dict, shots: int, with_prob: b results = {key: (value, prob_dict[key]) for key, value in results.items()} return results - def _measure_fock(self, shots: int, with_prob: bool, wires: List[int], mcmc: bool) -> List[Dict]: + def _measure_fock(self, shots: int, with_prob: bool, wires: list[int], mcmc: bool) -> list[dict]: """Measure the final state for Fock backend.""" if isinstance(self.state, torch.Tensor): if self.basis: @@ -1246,16 +1256,16 @@ def _measure_fock(self, shots: int, with_prob: bool, wires: List[int], mcmc: boo else: assert not mcmc, "Final states have been calculated, we don't need mcmc!" return self._measure_fock_tensor(shots, with_prob, wires) - elif isinstance(self.state, Dict): + elif isinstance(self.state, dict): assert not mcmc, "Final states have been calculated, we don't need mcmc!" return self._measure_dict(shots, with_prob, wires) - elif isinstance(self.state, List): + elif isinstance(self.state, list): assert not mcmc, "Final states have been calculated, we don't need mcmc!" return self._measure_mps(shots, with_prob, wires) else: - assert False, 'Check your forward function or input!' + raise ValueError('Check your forward function or input!') - def _measure_fock_unitary(self, shots: int, with_prob: bool, wires: List[int], mcmc: bool) -> List[Dict]: + def _measure_fock_unitary(self, shots: int, with_prob: bool, wires: list[int], mcmc: bool) -> list[dict]: """Measure the final state according to the unitary matrix for Fock backend.""" if self.state.ndim == 2: self.state = self.state.unsqueeze(0) @@ -1272,26 +1282,25 @@ def _measure_fock_unitary(self, shots: int, with_prob: bool, wires: List[int], m if mcmc: for i in range(batch): if batch_init == 1: - samples_i = self._sample_mcmc_fock(shots=shots, init_state=init_state[0], unitary=unitary[i], - num_chain=5) + samples_i = self._sample_mcmc_fock( + shots=shots, init_state=init_state[0], unitary=unitary[i], num_chain=5 + ) else: - samples_i = self._sample_mcmc_fock(shots=shots, init_state=init_state[i], unitary=unitary[i], - num_chain=5) + samples_i = self._sample_mcmc_fock( + shots=shots, init_state=init_state[i], unitary=unitary[i], num_chain=5 + ) results = defaultdict(list) if with_prob: for k in samples_i: prob = self._get_prob_fock(k) samples_i[k] = samples_i[k], prob - for key in samples_i.keys(): + for key in samples_i: state_b = [key[wire] for wire in wires] state_b = FockState(state=state_b) results[state_b].append(samples_i[key]) if with_prob: results = { - key: ( - sum(count for count, _ in value), - sum(prob for _, prob in value) - ) + key: (sum(count for count, _ in value), sum(prob for _, prob in value)) for key, value in results.items() } else: @@ -1299,11 +1308,13 @@ def _measure_fock_unitary(self, shots: int, with_prob: bool, wires: List[int], m all_results.append(results) else: if batch_init == 1: - prob_dict_batch = vmap(self._measure_fock_unitary_helper, - in_dims=(None, 0, None))(init_state[0], unitary, wires) + prob_dict_batch = vmap(self._measure_fock_unitary_helper, in_dims=(None, 0, None))( + init_state[0], unitary, wires + ) else: - prob_dict_batch = vmap(self._measure_fock_unitary_helper, - in_dims=(0, 0, None))(init_state, unitary, wires) + prob_dict_batch = vmap(self._measure_fock_unitary_helper, in_dims=(0, 0, None))( + init_state, unitary, wires + ) for i in range(batch): prob_dict = {key: value[i] for key, value in prob_dict_batch.items()} results = self._prob_dict_to_measure_result(prob_dict, shots, with_prob) @@ -1314,9 +1325,9 @@ def _measure_fock_unitary_helper( self, init_state: torch.Tensor, unitary: torch.Tensor, - wires: Union[int, List[int], None] = None, - final_states: Optional[torch.Tensor] = None - ) -> Dict: + wires: int | list[int] | None = None, + final_states: torch.Tensor | None = None, + ) -> dict: """VMAP helper for measuring the final state according to the unitary matrix for Fock backend. Returns: @@ -1332,26 +1343,24 @@ def _measure_fock_unitary_helper( for i in range(len(final_states)): final_state = FockState(state=final_states[i]) state_dict[final_state] = rst[i] - for key in state_dict.keys(): + for key in state_dict: state_b = key.state[wires] state_b = FockState(state=state_b) prob_dict[state_b].append(state_dict[key]) prob_dict = {key: sum(value) for key, value in prob_dict.items()} return prob_dict - def _measure_dict(self, shots: int, with_prob: bool, wires: List[int]) -> List[Dict]: + def _measure_dict(self, shots: int, with_prob: bool, wires: list[int]) -> list[dict]: """Measure the final state according to the dictionary of amplitudes or probabilities.""" if self._with_delay: wires = [self._unroll_dict[wire][-1] for wire in wires] all_results = [] batch = len(self.state[list(self.state.keys())[0]]) - if self.backend == 'fock' and any(value.dtype.is_complex for value in self.state.values()): - is_prob = False - else: - is_prob = True + is_complex = any(v.dtype.is_complex for v in self.state.values()) + is_prob = not (self.backend == 'fock' and is_complex) for i in range(batch): prob_dict = defaultdict(list) - for key in self.state.keys(): + for key in self.state: if wires == self.wires: state_b = key else: @@ -1366,12 +1375,12 @@ def _measure_dict(self, shots: int, with_prob: bool, wires: List[int]) -> List[D all_results.append(results) return all_results - def _measure_fock_tensor(self, shots: int, with_prob: bool, wires: List[int]) -> List[Dict]: + def _measure_fock_tensor(self, shots: int, with_prob: bool, wires: list[int]) -> list[dict]: """Measure the final state according to Fock state tensor for Fock backend.""" all_results = [] if self.state.is_complex(): if self.den_mat: - state_tensor = self.state.reshape(-1, self.cutoff ** self.nmode, self.cutoff ** self.nmode) + state_tensor = self.state.reshape(-1, self.cutoff**self.nmode, self.cutoff**self.nmode) state_tensor = self.tensor_rep(abs(state_tensor.diagonal(dim1=-2, dim2=-1))) else: state_tensor = self.tensor_rep(abs(self.state) ** 2) @@ -1396,7 +1405,7 @@ def _measure_fock_tensor(self, shots: int, with_prob: bool, wires: List[int]) -> all_results.append(results) return all_results - def _measure_mps(self, shots: int, with_prob: bool, wires: List[int]) -> List[Dict]: + def _measure_mps(self, shots: int, with_prob: bool, wires: list[int]) -> list[dict]: """Measure the final state according to MPS.""" all_results = [] samples = [] @@ -1406,7 +1415,7 @@ def _measure_mps(self, shots: int, with_prob: bool, wires: List[int]) -> List[Di samples_j = [tuple(sample[j].tolist()) for sample in samples] samples_j = dict(Counter(samples_j)) keys = list(map(FockState, samples_j.keys())) - results = dict(zip(keys, samples_j.values())) + results = dict(zip(keys, samples_j.values(), strict=True)) if with_prob: for k in results: prob = self._get_prob_mps(k, wires)[j] @@ -1419,38 +1428,25 @@ def _sample_mcmc_fock(self, shots: int, init_state: torch.Tensor, unitary: torch self._init_state_sample = init_state self._unitary = unitary self._out_fock_basis = self._get_all_fock_basis(init_state) - merged_samples = sample_sc_mcmc(prob_func=self._get_prob_fock, - proposal_sampler=self._proposal_sampler, - shots=shots, - num_chain=num_chain) + merged_samples = sample_sc_mcmc( + prob_func=self._get_prob_fock, proposal_sampler=self._proposal_sampler, shots=shots, num_chain=num_chain + ) return merged_samples - def _measure_gaussian( - self, - shots: int, - with_prob: bool, - wires: List[int], - detector: str, - mcmc: bool - ) -> List[Dict]: + def _measure_gaussian(self, shots: int, with_prob: bool, wires: list[int], detector: str, mcmc: bool) -> list[dict]: """Measure the final state for Gaussian backend.""" - if isinstance(self.state, List): + if isinstance(self.state, list): return self._measure_gaussian_state(shots, with_prob, wires, detector, mcmc) - elif isinstance(self.state, Dict): + elif isinstance(self.state, dict): assert not mcmc, "Final states have been calculated, we don't need mcmc!" print('Automatically using the default detector!') return self._measure_dict(shots, with_prob, wires) else: - assert False, 'Check your forward function or input!' + raise ValueError('Check your forward function or input!') def _measure_gaussian_state( - self, - shots: int, - with_prob: bool, - wires: List[int], - detector: str, - mcmc: bool - ) -> List[Dict]: + self, shots: int, with_prob: bool, wires: list[int], detector: str, mcmc: bool + ) -> list[dict]: """Measure the final state according to Gaussian state for Gaussian backend. See https://arxiv.org/pdf/2108.01622 @@ -1463,22 +1459,23 @@ def _measure_gaussian_state( if mcmc: print('Using MCMC method to sample the final states!') for i in range(batch): - samples_i = self._sample_mcmc_gaussian(shots=shots, cov=cov[i], mean=mean[i], - detector=detector, num_chain=5) + samples_i = self._sample_mcmc_gaussian( + shots=shots, cov=cov[i], mean=mean[i], detector=detector, num_chain=5 + ) all_samples.append(samples_i) - else: # chain-rule method with small number of shots + else: # chain-rule method with small number of shots print('Using chain-rule method to sample the final states!') samples = [] for _ in range(shots): sample = self._generate_chain_sample(wires) samples.append(sample) - samples = torch.stack(samples).permute(1, 0, 2) # (batch, shots, wires) + samples = torch.stack(samples).permute(1, 0, 2) # (batch, shots, wires) for i in range(batch): sample_lst = samples[i].tolist() sample_tup = [tuple(s) for s in sample_lst] samples_i = defaultdict(int, Counter(sample_tup)) all_samples.append(samples_i) - for i, samples_i in enumerate(all_samples): # post-process samples + for i, samples_i in enumerate(all_samples): # post-process samples results = defaultdict(list) if with_prob: for k in samples_i: @@ -1489,19 +1486,13 @@ def _measure_gaussian_state( idx = torch.cat([wires_, wires_ + self.nmode]) prob = self._get_prob_gaussian(k, [cov[i][idx[:, None], idx], mean[i][idx, :]]) samples_i[k] = samples_i[k], prob - for key in samples_i.keys(): - if mcmc: - state_b = [key[wire] for wire in wires] - else: - state_b = list(key) + for key in samples_i: + state_b = [key[wire] for wire in wires] if mcmc else list(key) state_b = FockState(state=state_b) results[state_b].append(samples_i[key]) if with_prob: results = { - key: ( - sum(count for count, _ in value), - sum(prob for _, prob in value) - ) + key: (sum(count for count, _ in value), sum(prob for _, prob in value)) for key, value in results.items() } else: @@ -1517,10 +1508,12 @@ def _sample_mcmc_gaussian(self, shots: int, cov: torch.Tensor, mean: torch.Tenso if detector == 'threshold' and not torch.allclose(mean, torch.zeros_like(mean)): # For the displaced state, aggregate PNRD detector samples to derive threshold detector results self.detector = 'pnrd' - merged_samples_pnrd = sample_sc_mcmc(prob_func=self._get_prob_gaussian, - proposal_sampler=self._proposal_sampler, - shots=shots, - num_chain=num_chain) + merged_samples_pnrd = sample_sc_mcmc( + prob_func=self._get_prob_gaussian, + proposal_sampler=self._proposal_sampler, + shots=shots, + num_chain=num_chain, + ) merged_samples = defaultdict(int) for key in list(merged_samples_pnrd.keys()): key_threshold = (torch.tensor(key) != 0).int() @@ -1528,10 +1521,12 @@ def _sample_mcmc_gaussian(self, shots: int, cov: torch.Tensor, mean: torch.Tenso merged_samples[key_threshold] += merged_samples_pnrd[key] self.detector = 'threshold' else: - merged_samples = sample_sc_mcmc(prob_func=self._get_prob_gaussian, - proposal_sampler=self._proposal_sampler, - shots=shots, - num_chain=num_chain) + merged_samples = sample_sc_mcmc( + prob_func=self._get_prob_gaussian, + proposal_sampler=self._proposal_sampler, + shots=shots, + num_chain=num_chain, + ) return merged_samples def _proposal_sampler(self): @@ -1545,17 +1540,14 @@ def _proposal_sampler(self): def _generate_rand_sample(self, detector: str = 'pnrd'): """Generate a random sample according to uniform proposal distribution.""" - if self._with_delay: - nmode = self._nmode_tdm - else: - nmode = self.nmode + nmode = self._nmode_tdm if self._with_delay else self.nmode if detector == 'threshold': sample = torch.randint(0, 2, [nmode]) elif detector == 'pnrd': sample = torch.randint(0, self.cutoff, [nmode]) return sample - def _generate_chain_sample(self, wires: Union[int, List[int], None] = None) -> torch.Tensor: + def _generate_chain_sample(self, wires: int | list[int] | None = None) -> torch.Tensor: """Generate batched random samples via chain rule. Args: @@ -1582,16 +1574,17 @@ def _generate_chain_sample(self, wires: Union[int, List[int], None] = None) -> t index = sample_single_wire.reshape(-1, 1, 1, 1).expand(-1, mps[i].shape[-3], -1, mps[i].shape[-1]) mps[i] = torch.gather(mps[i], dim=2, index=index) sample = torch.stack(sample, dim=-1).squeeze(1) - elif self.backend == 'gaussian': # chain rule for GBS + elif self.backend == 'gaussian': # chain rule for GBS sample = self._generate_chain_sample_gaussian(wires) return sample - def _generate_chain_sample_gaussian(self, wires: List[int]) -> torch.Tensor: + def _generate_chain_sample_gaussian(self, wires: list[int]) -> torch.Tensor: """Generate batched random samples via chain rule for Gaussian backend. See https://research-information.bris.ac.uk/en/studentTheses/classical-simulations-of-gaussian-boson-sampling Chapter 5 """ + def _sample_wire(sample, cov_sub, mean_sub, cutoff, detector): """Sample for a wire""" states = [torch.tensor(sample + [i], device=cov_sub.device) for i in range(cutoff)] @@ -1611,29 +1604,29 @@ def _sample_pure(cov, mean, wires, nmode, cutoff, detector): sample.append(sample_wire) return torch.cat(sample) - def _sample_mixed(cov, mean, wires, nmode, cutoff, detector, eps = 5e-5): + def _sample_mixed(cov, mean, wires, nmode, cutoff, detector, eps=5e-5): """Sample for a mixed state""" wires = torch.tensor(wires, device=cov.device) _, s = williamson(cov) cov_t = s @ s.mT * dqp.hbar / (4 * dqp.kappa**2) - cov_w = cov - cov_t # cov_mix = cov_t + cov_w + cov_w = cov - cov_t # cov_mix = cov_t + cov_w cov_w += cov.new_ones(cov_w.shape[-1]).diag_embed() * eps - mean0 = MultivariateNormal(mean.squeeze(-1), cov_w).sample([1])[0] # may be numerically unstable + mean0 = MultivariateNormal(mean.squeeze(-1), cov_w).sample([1])[0] # may be numerically unstable sample = [] mean_m = None for i in range(1, len(wires) + 1): wires_i = wires[i:].tolist() - cov_m = cov.new_ones(2 * len(wires_i)).diag_embed() * dqp.hbar / (4 * dqp.kappa**2) # See Eq.(5.18) + cov_m = cov.new_ones(2 * len(wires_i)).diag_embed() * dqp.hbar / (4 * dqp.kappa**2) # See Eq.(5.18) heterodyne = Generaldyne(cov_m=cov_m, nmode=nmode, wires=wires_i) # collapse the state state = [cov_t.unsqueeze(0), mean0.reshape(1, -1, 1)] if i < len(wires): cov_out, mean_out = heterodyne(state, mean_m) - mean_m = heterodyne.samples[0] # with batch + mean_m = heterodyne.samples[0] # with batch mask = torch.ones_like(mean_m, dtype=bool) idx_discard = torch.tensor([0, len(mean_m) // 2], device=mask.device) mask[idx_discard] = False - mean_m = mean_m[mask] # discard the first mode + mean_m = mean_m[mask] # discard the first mode else: cov_out, mean_out = state idx = torch.cat([wires[:i], wires[:i] + nmode]) @@ -1656,10 +1649,7 @@ def _sample_mixed(cov, mean, wires, nmode, cutoff, detector, eps = 5e-5): sample = torch.stack(sample) return sample - def photon_number_mean_var( - self, - wires: Union[int, List[int], None] = None - ) -> Optional[Tuple[torch.Tensor, torch.Tensor]]: + def photon_number_mean_var(self, wires: int | list[int] | None = None) -> tuple[torch.Tensor, torch.Tensor] | None: """Get the expectation value and variance of the photon number operator. Args: @@ -1693,7 +1683,7 @@ def photon_number_mean_var( weights = None elif self.backend == 'bosonic': covs = covs.reshape(*shape_cov[:2], nwire, 2, 2).transpose(1, 2) - covs = covs.reshape(-1, shape_cov[-3], 2, 2) # (batch*nwire, ncomb, 2, 2) + covs = covs.reshape(-1, shape_cov[-3], 2, 2) # (batch*nwire, ncomb, 2, 2) means = means.reshape(*shape_mean[:2], nwire, 2, 1).transpose(1, 2) means = means.reshape(-1, shape_mean[-3], 2, 1) if weight.shape[0] == 1: @@ -1710,11 +1700,7 @@ def photon_number_mean_var( var = var.reshape(batch, nwire).squeeze() return exp, var - def quadrature_mean( - self, - wires: Union[int, List[int], None] = None, - phi: Optional[Any] = None - ) -> torch.Tensor: + def quadrature_mean(self, wires: int | list[int] | None = None, phi: Any | None = None) -> torch.Tensor: r"""Get the expectation value of the quadratuere operator :math:`\hat{X}\cos\phi + \hat{P}\sin\phi`. If ``self.measurements`` is empty, this method directly computes the quadrature expectation values @@ -1754,29 +1740,28 @@ def quadrature_mean( state = state.reshape([-1] + [self.cutoff] * 2 * self.nmode) for i in range(len(wires)): if not torch.isclose(phi[i], phi.new_tensor(0.0)): - r = PhaseShift(inputs=-phi[i], nmode=self.nmode, wires=wires[i], - cutoff=self.cutoff, den_mat=self.den_mat) + r = PhaseShift( + inputs=-phi[i], nmode=self.nmode, wires=wires[i], cutoff=self.cutoff, den_mat=self.den_mat + ) state = r(state) mean = quadrature_mean_fock(state, self.nmode, self.cutoff, wires, self.den_mat) return mean elif self.backend in ('gaussian', 'bosonic'): wires = torch.tensor(wires) - idx = torch.cat([wires, wires + self.nmode]) # xxpp order + idx = torch.cat([wires, wires + self.nmode]) # xxpp order means = self.state[1][..., idx, :] phi = phi.reshape(-1, 1) - mean = means[..., :len(wires), :] * torch.cos(phi) + means[..., len(wires):, :] * torch.sin(phi) + mean = means[..., : len(wires), :] * torch.cos(phi) + means[..., len(wires) :, :] * torch.sin(phi) if self.backend == 'bosonic': weight = self.state[2].unsqueeze(-1).unsqueeze(-2) mean = torch.sum(weight * mean, dim=1) return mean def _get_local_covs_means( - self, - cov: torch.Tensor, - mean: torch.Tensor, - wires: List[int] - ) -> Tuple[torch.Tensor, torch.Tensor]: + self, cov: torch.Tensor, mean: torch.Tensor, wires: list[int] + ) -> tuple[torch.Tensor, torch.Tensor]: """Get the local covariance matrices and mean vectors of a Gaussian state according to the wires to measure.""" + def extract_blocks(mat: torch.Tensor, idx: torch.Tensor) -> torch.Tensor: """Extract specified blocks from the input tensor. @@ -1789,33 +1774,26 @@ def extract_blocks(mat: torch.Tensor, idx: torch.Tensor) -> torch.Tensor: torch.Tensor: Output tensor of shape (batch, nblock, block_size, -1) containing all extracted blocks. """ nblock, block_size = idx.shape - if mat.shape[-2] == mat.shape[-1]: # cov - rows = idx[:, :, None].expand(-1, -1, block_size) # (nblock, block_size, block_size) - cols = idx[:, None, :].expand(-1, block_size, -1) # (nblock, block_size, block_size) + if mat.shape[-2] == mat.shape[-1]: # cov + rows = idx[:, :, None].expand(-1, -1, block_size) # (nblock, block_size, block_size) + cols = idx[:, None, :].expand(-1, block_size, -1) # (nblock, block_size, block_size) all_rows = rows.reshape(-1) all_cols = cols.reshape(-1) out = mat[:, all_rows, all_cols] - elif mat.shape[-1] == 1: # mean + elif mat.shape[-1] == 1: # mean out = mat[:, idx, :] return out.reshape(mat.shape[0], nblock, block_size, -1) indices = [] - if self._with_delay: - nmode = self._nmode_tdm - else: - nmode = self.nmode + nmode = self._nmode_tdm if self._with_delay else self.nmode for wire in wires: indices.append([wire] + [wire + nmode]) indices = torch.tensor(indices, device=cov.device) - covs = extract_blocks(cov, indices).reshape(-1, 2, 2) # batch * nwire + covs = extract_blocks(cov, indices).reshape(-1, 2, 2) # batch * nwire means = extract_blocks(mean, indices).reshape(-1, 2, 1) return covs, means - def measure_homodyne( - self, - shots: int = 10, - wires: Union[int, List[int], None] = None - ) -> Optional[torch.Tensor]: + def measure_homodyne(self, shots: int = 10, wires: int | list[int] | None = None) -> torch.Tensor | None: """Get the homodyne measurement results. If ``self.measurements`` is specified via ``self.homodyne``, return the results of @@ -1835,10 +1813,7 @@ def measure_homodyne( return assert isinstance(self.state, (list, torch.Tensor)), 'NOT valid when "is_prob" is True' if len(self.measurements) > 0: - if self._with_delay: - measurements = self._measurements_tdm - else: - measurements = self.measurements + measurements = self._measurements_tdm if self._with_delay else self.measurements samples = [] if self.backend == 'fock': assert not self.basis @@ -1849,18 +1824,15 @@ def measure_homodyne( else: batch = self.state[0].shape[0] self.state_measured = [] - if self.backend == 'bosonic': - state = align_shape(*self.state) - else: - state = self.state - for s in state: # [cov, mean, weight] + state = align_shape(*self.state) if self.backend == 'bosonic' else self.state + for s in state: # [cov, mean, weight] shape = s.shape self.state_measured.append(torch.stack([s] * shots).reshape(-1, *shape[1:])) for op_m in measurements: self.state_measured = op_m(self.state_measured) nwire = len(op_m.wires) samples.append(op_m.samples[:, :nwire].reshape(shots, batch, nwire).permute(1, 0, 2)) - return torch.cat(samples, dim=-1).squeeze() # (batch, shots, nwire) + return torch.cat(samples, dim=-1).squeeze() # (batch, shots, nwire) else: if wires is None: wires = self.wires @@ -1898,7 +1870,7 @@ def max_depth(self) -> int: """Get the max number of gates on the wires.""" return max(self.depth) - def draw(self, filename: Optional[str] = None, unroll: bool = False): + def draw(self, filename: str | None = None, unroll: bool = False): """Visualize the photonic quantum circuit. Args: @@ -1924,13 +1896,7 @@ def draw(self, filename: Optional[str] = None, unroll: bool = False): print('Too many modes in the circuit, please set filename to save the figure.') return self.draw_circuit.draw_ - def cat( - self, - wires: int, - r: Any = None, - theta: Any = None, - p: int = 1 - ) -> None: + def cat(self, wires: int, r: Any = None, theta: Any = None, p: int = 1) -> None: """Prepare a cat state. ``r`` and ``theta`` are the displacement magnitude and angle respectively. @@ -1942,12 +1908,7 @@ def cat( self._bosonic_states[wires] = cat def gkp( - self, - wires: int, - theta: Any = None, - phi: Any = None, - amp_cutoff: float = 0.1, - epsilon: float = 0.05 + self, wires: int, theta: Any = None, phi: Any = None, amp_cutoff: float = 0.1, epsilon: float = 0.05 ) -> None: """Prepare a GKP state. @@ -1960,12 +1921,7 @@ def gkp( gkp = GKPState(theta=theta, phi=phi, amp_cutoff=amp_cutoff, epsilon=epsilon, cutoff=self.cutoff) self._bosonic_states[wires] = gkp - def add( - self, - op: Operation, - encode: bool = False, - wires: Union[int, List[int], None] = None - ) -> None: + def add(self, op: Operation, encode: bool = False, wires: int | list[int] | None = None) -> None: """A method that adds an operation to the photonic quantum circuit. The operation can be a gate or another photonic quantum circuit. The method also updates the @@ -1993,7 +1949,7 @@ def add( if isinstance(op, QumodeCircuit): assert self.nmode == op.nmode self.operators += op.operators - self.encoders += op.encoders + self.encoders += op.encoders self.measurements = op.measurements self.wires_homodyne = op.wires_homodyne self.npara += op.npara @@ -2027,12 +1983,7 @@ def add( self.wires_homodyne.append(op.wires[0]) def ps( - self, - wires: int, - inputs: Any = None, - encode: bool = False, - mu: Optional[float] = None, - sigma: Optional[float] = None + self, wires: int, inputs: Any = None, encode: bool = False, mu: float | None = None, sigma: float | None = None ) -> None: """Add a phase shifter.""" requires_grad = not encode @@ -2042,17 +1993,26 @@ def ps( mu = self.mu if sigma is None: sigma = self.sigma - ps = PhaseShift(inputs=inputs, nmode=self.nmode, wires=wires, cutoff=self.cutoff, den_mat=self.den_mat, - requires_grad=requires_grad, noise=self.noise, mu=mu, sigma=sigma) + ps = PhaseShift( + inputs=inputs, + nmode=self.nmode, + wires=wires, + cutoff=self.cutoff, + den_mat=self.den_mat, + requires_grad=requires_grad, + noise=self.noise, + mu=mu, + sigma=sigma, + ) self.add(ps, encode=encode) def bs( self, - wires: List[int], + wires: list[int], inputs: Any = None, encode: bool = False, - mu: Optional[float] = None, - sigma: Optional[float] = None + mu: float | None = None, + sigma: float | None = None, ) -> None: """Add a beam splitter.""" requires_grad = not encode @@ -2062,18 +2022,27 @@ def bs( mu = self.mu if sigma is None: sigma = self.sigma - bs = BeamSplitter(inputs=inputs, nmode=self.nmode, wires=wires, cutoff=self.cutoff, den_mat=self.den_mat, - requires_grad=requires_grad, noise=self.noise, mu=mu, sigma=sigma) + bs = BeamSplitter( + inputs=inputs, + nmode=self.nmode, + wires=wires, + cutoff=self.cutoff, + den_mat=self.den_mat, + requires_grad=requires_grad, + noise=self.noise, + mu=mu, + sigma=sigma, + ) self.add(bs, encode=encode) def mzi( self, - wires: List[int], + wires: list[int], inputs: Any = None, phi_first: bool = True, encode: bool = False, - mu: Optional[float] = None, - sigma: Optional[float] = None + mu: float | None = None, + sigma: float | None = None, ) -> None: """Add a Mach-Zehnder interferometer.""" requires_grad = not encode @@ -2083,17 +2052,27 @@ def mzi( mu = self.mu if sigma is None: sigma = self.sigma - mzi = MZI(inputs=inputs, nmode=self.nmode, wires=wires, cutoff=self.cutoff, den_mat=self.den_mat, - phi_first=phi_first, requires_grad=requires_grad, noise=self.noise, mu=mu, sigma=sigma) + mzi = MZI( + inputs=inputs, + nmode=self.nmode, + wires=wires, + cutoff=self.cutoff, + den_mat=self.den_mat, + phi_first=phi_first, + requires_grad=requires_grad, + noise=self.noise, + mu=mu, + sigma=sigma, + ) self.add(mzi, encode=encode) def bs_theta( self, - wires: List[int], + wires: list[int], inputs: Any = None, encode: bool = False, - mu: Optional[float] = None, - sigma: Optional[float] = None + mu: float | None = None, + sigma: float | None = None, ) -> None: r"""Add a beam splitter with fixed :math:`\phi` at :math:`\pi/2`.""" requires_grad = not encode @@ -2103,17 +2082,26 @@ def bs_theta( mu = self.mu if sigma is None: sigma = self.sigma - bs = BeamSplitterTheta(inputs=inputs, nmode=self.nmode, wires=wires, cutoff=self.cutoff, den_mat=self.den_mat, - requires_grad=requires_grad, noise=self.noise, mu=mu, sigma=sigma) + bs = BeamSplitterTheta( + inputs=inputs, + nmode=self.nmode, + wires=wires, + cutoff=self.cutoff, + den_mat=self.den_mat, + requires_grad=requires_grad, + noise=self.noise, + mu=mu, + sigma=sigma, + ) self.add(bs, encode=encode) def bs_phi( self, - wires: List[int], + wires: list[int], inputs: Any = None, encode: bool = False, - mu: Optional[float] = None, - sigma: Optional[float] = None + mu: float | None = None, + sigma: float | None = None, ) -> None: r"""Add a beam splitter with fixed :math:`\theta` at :math:`\pi/4`.""" requires_grad = not encode @@ -2123,17 +2111,26 @@ def bs_phi( mu = self.mu if sigma is None: sigma = self.sigma - bs = BeamSplitterPhi(inputs=inputs, nmode=self.nmode, wires=wires, cutoff=self.cutoff, den_mat=self.den_mat, - requires_grad=requires_grad, noise=self.noise, mu=mu, sigma=sigma) + bs = BeamSplitterPhi( + inputs=inputs, + nmode=self.nmode, + wires=wires, + cutoff=self.cutoff, + den_mat=self.den_mat, + requires_grad=requires_grad, + noise=self.noise, + mu=mu, + sigma=sigma, + ) self.add(bs, encode=encode) def bs_rx( self, - wires: List[int], + wires: list[int], inputs: Any = None, encode: bool = False, - mu: Optional[float] = None, - sigma: Optional[float] = None + mu: float | None = None, + sigma: float | None = None, ) -> None: """Add an Rx-type beam splitter.""" requires_grad = not encode @@ -2143,17 +2140,27 @@ def bs_rx( mu = self.mu if sigma is None: sigma = self.sigma - bs = BeamSplitterSingle(inputs=inputs, nmode=self.nmode, wires=wires, cutoff=self.cutoff, den_mat=self.den_mat, - convention='rx', requires_grad=requires_grad, noise=self.noise, mu=mu, sigma=sigma) + bs = BeamSplitterSingle( + inputs=inputs, + nmode=self.nmode, + wires=wires, + cutoff=self.cutoff, + den_mat=self.den_mat, + convention='rx', + requires_grad=requires_grad, + noise=self.noise, + mu=mu, + sigma=sigma, + ) self.add(bs, encode=encode) def bs_ry( self, - wires: List[int], + wires: list[int], inputs: Any = None, encode: bool = False, - mu: Optional[float] = None, - sigma: Optional[float] = None + mu: float | None = None, + sigma: float | None = None, ) -> None: """Add an Ry-type beam splitter.""" requires_grad = not encode @@ -2163,17 +2170,27 @@ def bs_ry( mu = self.mu if sigma is None: sigma = self.sigma - bs = BeamSplitterSingle(inputs=inputs, nmode=self.nmode, wires=wires, cutoff=self.cutoff, den_mat=self.den_mat, - convention='ry', requires_grad=requires_grad, noise=self.noise, mu=mu, sigma=sigma) + bs = BeamSplitterSingle( + inputs=inputs, + nmode=self.nmode, + wires=wires, + cutoff=self.cutoff, + den_mat=self.den_mat, + convention='ry', + requires_grad=requires_grad, + noise=self.noise, + mu=mu, + sigma=sigma, + ) self.add(bs, encode=encode) def bs_h( self, - wires: List[int], + wires: list[int], inputs: Any = None, encode: bool = False, - mu: Optional[float] = None, - sigma: Optional[float] = None + mu: float | None = None, + sigma: float | None = None, ) -> None: """Add an H-type beam splitter.""" requires_grad = not encode @@ -2183,51 +2200,84 @@ def bs_h( mu = self.mu if sigma is None: sigma = self.sigma - bs = BeamSplitterSingle(inputs=inputs, nmode=self.nmode, wires=wires, cutoff=self.cutoff, den_mat=self.den_mat, - convention='h', requires_grad=requires_grad, noise=self.noise, mu=mu, sigma=sigma) + bs = BeamSplitterSingle( + inputs=inputs, + nmode=self.nmode, + wires=wires, + cutoff=self.cutoff, + den_mat=self.den_mat, + convention='h', + requires_grad=requires_grad, + noise=self.noise, + mu=mu, + sigma=sigma, + ) self.add(bs, encode=encode) - def dc(self, wires: List[int], mu: Optional[float] = None, sigma: Optional[float] = None) -> None: + def dc(self, wires: list[int], mu: float | None = None, sigma: float | None = None) -> None: """Add a directional coupler.""" theta = torch.pi / 2 if mu is None: mu = self.mu if sigma is None: sigma = self.sigma - bs = BeamSplitterSingle(inputs=theta, nmode=self.nmode, wires=wires, cutoff=self.cutoff, den_mat=self.den_mat, - convention='rx', requires_grad=False, noise=self.noise, mu=mu, sigma=sigma) + bs = BeamSplitterSingle( + inputs=theta, + nmode=self.nmode, + wires=wires, + cutoff=self.cutoff, + den_mat=self.den_mat, + convention='rx', + requires_grad=False, + noise=self.noise, + mu=mu, + sigma=sigma, + ) self.add(bs) - def h(self, wires: List[int], mu: Optional[float] = None, sigma: Optional[float] = None) -> None: + def h(self, wires: list[int], mu: float | None = None, sigma: float | None = None) -> None: """Add a photonic Hadamard gate.""" theta = torch.pi / 2 if mu is None: mu = self.mu if sigma is None: sigma = self.sigma - bs = BeamSplitterSingle(inputs=theta, nmode=self.nmode, wires=wires, cutoff=self.cutoff, den_mat=self.den_mat, - convention='h', requires_grad=False, noise=self.noise, mu=mu, sigma=sigma) + bs = BeamSplitterSingle( + inputs=theta, + nmode=self.nmode, + wires=wires, + cutoff=self.cutoff, + den_mat=self.den_mat, + convention='h', + requires_grad=False, + noise=self.noise, + mu=mu, + sigma=sigma, + ) self.add(bs) def any( - self, - unitary: Any, - wires: Union[int, List[int], None] = None, - minmax: Optional[List[int]] = None, - name: str = 'uany' + self, unitary: Any, wires: int | list[int] | None = None, minmax: list[int] | None = None, name: str = 'uany' ) -> None: """Add an arbitrary unitary gate.""" - uany = UAnyGate(unitary=unitary, nmode=self.nmode, wires=wires, minmax=minmax, cutoff=self.cutoff, - den_mat=self.den_mat, name=name) + uany = UAnyGate( + unitary=unitary, + nmode=self.nmode, + wires=wires, + minmax=minmax, + cutoff=self.cutoff, + den_mat=self.den_mat, + name=name, + ) self.add(uany) def clements( self, unitary: Any, - wires: Union[int, List[int], None] = None, - minmax: Optional[List[int]] = None, - mu: Optional[float] = None, - sigma: Optional[float] = None + wires: int | list[int] | None = None, + minmax: list[int] | None = None, + mu: float | None = None, + sigma: float | None = None, ) -> None: """Add the Clements architecture of the unitary matrix. @@ -2253,7 +2303,7 @@ def clements( assert len(phase_angle) == len(wires), 'Please check wires' wires1 = wires[1::2] wires2 = wires[2::2] - shift = wires[0] # clements decomposition starts from 0 + shift = wires[0] # clements decomposition starts from 0 for i in range(len(wires)): if i % 2 == 0: idx = i // 2 @@ -2266,7 +2316,7 @@ def clements( phi, theta = dic_mzi[(wires2[j] - 1 - shift, wires2[j] - shift)][idx] self.mzi(wires=[wires2[j] - 1, wires2[j]], inputs=[theta, phi], mu=mu, sigma=sigma) for wire in wires: - self.ps(wires=wire, inputs=phase_angle[wire-shift], mu=mu, sigma=sigma) + self.ps(wires=wire, inputs=phase_angle[wire - shift], mu=mu, sigma=sigma) def s( self, @@ -2274,8 +2324,8 @@ def s( r: Any = None, theta: Any = None, encode: bool = False, - mu: Optional[float] = None, - sigma: Optional[float] = None + mu: float | None = None, + sigma: float | None = None, ) -> None: """Add a squeezing gate.""" requires_grad = not encode @@ -2293,18 +2343,27 @@ def s( mu = self.mu if sigma is None: sigma = self.sigma - s = Squeezing(inputs=inputs, nmode=self.nmode, wires=wires, cutoff=self.cutoff, den_mat=self.den_mat, - requires_grad=requires_grad, noise=self.noise, mu=mu, sigma=sigma) + s = Squeezing( + inputs=inputs, + nmode=self.nmode, + wires=wires, + cutoff=self.cutoff, + den_mat=self.den_mat, + requires_grad=requires_grad, + noise=self.noise, + mu=mu, + sigma=sigma, + ) self.add(s, encode=encode) def s2( self, - wires: List[int], + wires: list[int], r: Any = None, theta: Any = None, encode: bool = False, - mu: Optional[float] = None, - sigma: Optional[float] = None + mu: float | None = None, + sigma: float | None = None, ) -> None: """Add a two-mode squeezing gate.""" requires_grad = not encode @@ -2322,8 +2381,17 @@ def s2( mu = self.mu if sigma is None: sigma = self.sigma - s2 = Squeezing2(inputs=inputs, nmode=self.nmode, wires=wires, cutoff=self.cutoff, den_mat=self.den_mat, - requires_grad=requires_grad, noise=self.noise, mu=mu, sigma=sigma) + s2 = Squeezing2( + inputs=inputs, + nmode=self.nmode, + wires=wires, + cutoff=self.cutoff, + den_mat=self.den_mat, + requires_grad=requires_grad, + noise=self.noise, + mu=mu, + sigma=sigma, + ) self.add(s2, encode=encode) def d( @@ -2332,8 +2400,8 @@ def d( r: Any = None, theta: Any = None, encode: bool = False, - mu: Optional[float] = None, - sigma: Optional[float] = None + mu: float | None = None, + sigma: float | None = None, ) -> None: """Add a displacement gate.""" requires_grad = not encode @@ -2351,17 +2419,21 @@ def d( mu = self.mu if sigma is None: sigma = self.sigma - d = Displacement(inputs=inputs, nmode=self.nmode, wires=wires, cutoff=self.cutoff, den_mat=self.den_mat, - requires_grad=requires_grad, noise=self.noise, mu=mu, sigma=sigma) + d = Displacement( + inputs=inputs, + nmode=self.nmode, + wires=wires, + cutoff=self.cutoff, + den_mat=self.den_mat, + requires_grad=requires_grad, + noise=self.noise, + mu=mu, + sigma=sigma, + ) self.add(d, encode=encode) def x( - self, - wires: int, - inputs: Any = None, - encode: bool = False, - mu: Optional[float] = None, - sigma: Optional[float] = None + self, wires: int, inputs: Any = None, encode: bool = False, mu: float | None = None, sigma: float | None = None ) -> None: """Add a position displacement gate.""" requires_grad = not encode @@ -2371,18 +2443,21 @@ def x( mu = self.mu if sigma is None: sigma = self.sigma - dx = DisplacementPosition(inputs=inputs, nmode=self.nmode, wires=wires, cutoff=self.cutoff, - den_mat=self.den_mat, requires_grad=requires_grad, noise=self.noise, - mu=mu, sigma=sigma) + dx = DisplacementPosition( + inputs=inputs, + nmode=self.nmode, + wires=wires, + cutoff=self.cutoff, + den_mat=self.den_mat, + requires_grad=requires_grad, + noise=self.noise, + mu=mu, + sigma=sigma, + ) self.add(dx, encode=encode) def z( - self, - wires: int, - inputs: Any = None, - encode: bool = False, - mu: Optional[float] = None, - sigma: Optional[float] = None + self, wires: int, inputs: Any = None, encode: bool = False, mu: float | None = None, sigma: float | None = None ) -> None: """Add a momentum displacement gate.""" requires_grad = not encode @@ -2392,9 +2467,17 @@ def z( mu = self.mu if sigma is None: sigma = self.sigma - dp = DisplacementMomentum(inputs=inputs, nmode=self.nmode, wires=wires, cutoff=self.cutoff, - den_mat=self.den_mat, requires_grad=requires_grad, noise=self.noise, - mu=mu, sigma=sigma) + dp = DisplacementMomentum( + inputs=inputs, + nmode=self.nmode, + wires=wires, + cutoff=self.cutoff, + den_mat=self.den_mat, + requires_grad=requires_grad, + noise=self.noise, + mu=mu, + sigma=sigma, + ) self.add(dp, encode=encode) def r( @@ -2403,8 +2486,8 @@ def r( inputs: Any = None, encode: bool = False, inv_mode: bool = False, - mu: Optional[float] = None, - sigma: Optional[float] = None + mu: float | None = None, + sigma: float | None = None, ) -> None: """Add a rotation gate.""" requires_grad = not encode @@ -2414,28 +2497,42 @@ def r( mu = self.mu if sigma is None: sigma = self.sigma - r = PhaseShift(inputs=inputs, nmode=self.nmode, wires=wires, cutoff=self.cutoff, den_mat=self.den_mat, - requires_grad=requires_grad, noise=self.noise, mu=mu, sigma=sigma, inv_mode=inv_mode) + r = PhaseShift( + inputs=inputs, + nmode=self.nmode, + wires=wires, + cutoff=self.cutoff, + den_mat=self.den_mat, + requires_grad=requires_grad, + noise=self.noise, + mu=mu, + sigma=sigma, + inv_mode=inv_mode, + ) self.add(r, encode=encode) - def f(self, wires: int, mu: Optional[float] = None, sigma: Optional[float] = None) -> None: + def f(self, wires: int, mu: float | None = None, sigma: float | None = None) -> None: """Add a Fourier gate.""" theta = torch.pi / 2 if mu is None: mu = self.mu if sigma is None: sigma = self.sigma - f = PhaseShift(inputs=theta, nmode=self.nmode, wires=wires, cutoff=self.cutoff, den_mat=self.den_mat, - requires_grad=False, noise=self.noise, mu=mu, sigma=sigma) + f = PhaseShift( + inputs=theta, + nmode=self.nmode, + wires=wires, + cutoff=self.cutoff, + den_mat=self.den_mat, + requires_grad=False, + noise=self.noise, + mu=mu, + sigma=sigma, + ) self.add(f) def qp( - self, - wires: int, - inputs: Any = None, - encode: bool = False, - mu: Optional[float] = None, - sigma: Optional[float] = None + self, wires: int, inputs: Any = None, encode: bool = False, mu: float | None = None, sigma: float | None = None ) -> None: """Add a quadratic phase gate.""" requires_grad = not encode @@ -2445,17 +2542,26 @@ def qp( mu = self.mu if sigma is None: sigma = self.sigma - qp = QuadraticPhase(inputs=inputs, nmode=self.nmode, wires=wires, cutoff=self.cutoff, den_mat=self.den_mat, - requires_grad=requires_grad, noise=self.noise, mu=mu, sigma=sigma) + qp = QuadraticPhase( + inputs=inputs, + nmode=self.nmode, + wires=wires, + cutoff=self.cutoff, + den_mat=self.den_mat, + requires_grad=requires_grad, + noise=self.noise, + mu=mu, + sigma=sigma, + ) self.add(qp, encode=encode) def cx( self, - wires: List[int], + wires: list[int], inputs: Any = None, encode: bool = False, - mu: Optional[float] = None, - sigma: Optional[float] = None + mu: float | None = None, + sigma: float | None = None, ) -> None: """Add a controlled-X gate.""" requires_grad = not encode @@ -2465,17 +2571,26 @@ def cx( mu = self.mu if sigma is None: sigma = self.sigma - cx = ControlledX(inputs=inputs, nmode=self.nmode, wires=wires, cutoff=self.cutoff, den_mat=self.den_mat, - requires_grad=requires_grad, noise=self.noise, mu=mu, sigma=sigma) + cx = ControlledX( + inputs=inputs, + nmode=self.nmode, + wires=wires, + cutoff=self.cutoff, + den_mat=self.den_mat, + requires_grad=requires_grad, + noise=self.noise, + mu=mu, + sigma=sigma, + ) self.add(cx, encode=encode) def cz( self, - wires: List[int], + wires: list[int], inputs: Any = None, encode: bool = False, - mu: Optional[float] = None, - sigma: Optional[float] = None + mu: float | None = None, + sigma: float | None = None, ) -> None: """Add a controlled-Z gate.""" requires_grad = not encode @@ -2485,17 +2600,21 @@ def cz( mu = self.mu if sigma is None: sigma = self.sigma - cz = ControlledZ(inputs=inputs, nmode=self.nmode, wires=wires, cutoff=self.cutoff, den_mat=self.den_mat, - requires_grad=requires_grad, noise=self.noise, mu=mu, sigma=sigma) + cz = ControlledZ( + inputs=inputs, + nmode=self.nmode, + wires=wires, + cutoff=self.cutoff, + den_mat=self.den_mat, + requires_grad=requires_grad, + noise=self.noise, + mu=mu, + sigma=sigma, + ) self.add(cz, encode=encode) def cp( - self, - wires: int, - inputs: Any = None, - encode: bool = False, - mu: Optional[float] = None, - sigma: Optional[float] = None + self, wires: int, inputs: Any = None, encode: bool = False, mu: float | None = None, sigma: float | None = None ) -> None: """Add a cubic phase gate.""" requires_grad = not encode @@ -2505,17 +2624,21 @@ def cp( mu = self.mu if sigma is None: sigma = self.sigma - cp = CubicPhase(inputs=inputs, nmode=self.nmode, wires=wires, cutoff=self.cutoff, den_mat=self.den_mat, - requires_grad=requires_grad, noise=self.noise, mu=mu, sigma=sigma) + cp = CubicPhase( + inputs=inputs, + nmode=self.nmode, + wires=wires, + cutoff=self.cutoff, + den_mat=self.den_mat, + requires_grad=requires_grad, + noise=self.noise, + mu=mu, + sigma=sigma, + ) self.add(cp, encode=encode) def k( - self, - wires: int, - inputs: Any = None, - encode: bool = False, - mu: Optional[float] = None, - sigma: Optional[float] = None + self, wires: int, inputs: Any = None, encode: bool = False, mu: float | None = None, sigma: float | None = None ) -> None: """Add a Kerr gate.""" requires_grad = not encode @@ -2525,17 +2648,26 @@ def k( mu = self.mu if sigma is None: sigma = self.sigma - k = Kerr(inputs=inputs, nmode=self.nmode, wires=wires, cutoff=self.cutoff, den_mat=self.den_mat, - requires_grad=requires_grad, noise=self.noise, mu=mu, sigma=sigma) + k = Kerr( + inputs=inputs, + nmode=self.nmode, + wires=wires, + cutoff=self.cutoff, + den_mat=self.den_mat, + requires_grad=requires_grad, + noise=self.noise, + mu=mu, + sigma=sigma, + ) self.add(k, encode=encode) def ck( self, - wires: List[int], + wires: list[int], inputs: Any = None, encode: bool = False, - mu: Optional[float] = None, - sigma: Optional[float] = None + mu: float | None = None, + sigma: float | None = None, ) -> None: """Add a cross-Kerr gate.""" requires_grad = not encode @@ -2545,8 +2677,17 @@ def ck( mu = self.mu if sigma is None: sigma = self.sigma - ck = CrossKerr(inputs=inputs, nmode=self.nmode, wires=wires, cutoff=self.cutoff, den_mat=self.den_mat, - requires_grad=requires_grad, noise=self.noise, mu=mu, sigma=sigma) + ck = CrossKerr( + inputs=inputs, + nmode=self.nmode, + wires=wires, + cutoff=self.cutoff, + den_mat=self.den_mat, + requires_grad=requires_grad, + noise=self.noise, + mu=mu, + sigma=sigma, + ) self.add(ck, encode=encode) def delay( @@ -2556,9 +2697,9 @@ def delay( inputs: Any = None, convention: str = 'bs', encode: bool = False, - loop_gates: Optional[List] = None, - mu: Optional[float] = None, - sigma: Optional[float] = None + loop_gates: list | None = None, + mu: float | None = None, + sigma: float | None = None, ) -> None: """Add a delay loop.""" requires_grad = not encode @@ -2569,72 +2710,100 @@ def delay( if sigma is None: sigma = self.sigma if convention == 'bs': - delay = DelayBS(inputs=inputs, ntau=ntau, nmode=self.nmode, wires=wires, cutoff=self.cutoff, - den_mat=self.den_mat, requires_grad=requires_grad, loop_gates=loop_gates, - noise=self.noise, mu=mu, sigma=sigma) + delay = DelayBS( + inputs=inputs, + ntau=ntau, + nmode=self.nmode, + wires=wires, + cutoff=self.cutoff, + den_mat=self.den_mat, + requires_grad=requires_grad, + loop_gates=loop_gates, + noise=self.noise, + mu=mu, + sigma=sigma, + ) elif convention == 'mzi': - delay = DelayMZI(inputs=inputs, ntau=ntau, nmode=self.nmode, wires=wires, cutoff=self.cutoff, - den_mat=self.den_mat, requires_grad=requires_grad, loop_gates=loop_gates, - noise=self.noise, mu=mu, sigma=sigma) + delay = DelayMZI( + inputs=inputs, + ntau=ntau, + nmode=self.nmode, + wires=wires, + cutoff=self.cutoff, + den_mat=self.den_mat, + requires_grad=requires_grad, + loop_gates=loop_gates, + noise=self.noise, + mu=mu, + sigma=sigma, + ) self.add(delay, encode=encode) def homodyne( - self, - wires: int, - phi: Any = None, - eps: float = 2e-4, - mu: Optional[float] = None, - sigma: Optional[float] = None + self, wires: int, phi: Any = None, eps: float = 2e-4, mu: float | None = None, sigma: float | None = None ) -> None: """Add a homodyne measurement.""" if mu is None: mu = self.mu if sigma is None: sigma = self.sigma - homodyne = Homodyne(phi=phi, nmode=self.nmode, wires=wires, cutoff=self.cutoff, den_mat=self.den_mat, - eps=eps, requires_grad=False, noise=self.noise, mu=mu, sigma=sigma) + homodyne = Homodyne( + phi=phi, + nmode=self.nmode, + wires=wires, + cutoff=self.cutoff, + den_mat=self.den_mat, + eps=eps, + requires_grad=False, + noise=self.noise, + mu=mu, + sigma=sigma, + ) self.add(homodyne) - def homodyne_x( - self, - wires: int, - eps: float = 2e-4, - mu: Optional[float] = None, - sigma: Optional[float] = None - ) -> None: + def homodyne_x(self, wires: int, eps: float = 2e-4, mu: float | None = None, sigma: float | None = None) -> None: """Add a homodyne measurement for quadrature x.""" - phi = 0. + phi = 0.0 if mu is None: mu = self.mu if sigma is None: sigma = self.sigma - homodyne = Homodyne(phi=phi, nmode=self.nmode, wires=wires, cutoff=self.cutoff, den_mat=self.den_mat, - eps=eps, requires_grad=False, noise=self.noise, mu=mu, sigma=sigma) + homodyne = Homodyne( + phi=phi, + nmode=self.nmode, + wires=wires, + cutoff=self.cutoff, + den_mat=self.den_mat, + eps=eps, + requires_grad=False, + noise=self.noise, + mu=mu, + sigma=sigma, + ) self.add(homodyne) - def homodyne_p( - self, - wires: int, - eps: float = 2e-4, - mu: Optional[float] = None, - sigma: Optional[float] = None - ) -> None: + def homodyne_p(self, wires: int, eps: float = 2e-4, mu: float | None = None, sigma: float | None = None) -> None: """Add a homodyne measurement for quadrature p.""" phi = np.pi / 2 if mu is None: mu = self.mu if sigma is None: sigma = self.sigma - homodyne = Homodyne(phi=phi, nmode=self.nmode, wires=wires, cutoff=self.cutoff, den_mat=self.den_mat, - eps=eps, requires_grad=False, noise=self.noise, mu=mu, sigma=sigma) + homodyne = Homodyne( + phi=phi, + nmode=self.nmode, + wires=wires, + cutoff=self.cutoff, + den_mat=self.den_mat, + eps=eps, + requires_grad=False, + noise=self.noise, + mu=mu, + sigma=sigma, + ) self.add(homodyne) - def loss( - self, - wires: int, - inputs: Any = None, - encode: bool = False - ) -> None: + def loss(self, wires: int, inputs: Any = None, encode: bool = False) -> None: """Add a photon loss channel. The `inputs` corresponds to `theta` of the loss channel. @@ -2646,16 +2815,10 @@ def loss( requires_grad = not encode if inputs is not None: requires_grad = False - loss = PhotonLoss(inputs=inputs, nmode=self.nmode, wires=wires, cutoff=self.cutoff, - requires_grad=requires_grad) + loss = PhotonLoss(inputs=inputs, nmode=self.nmode, wires=wires, cutoff=self.cutoff, requires_grad=requires_grad) self.add(loss, encode=encode) - def loss_t( - self, - wires: int, - inputs: Any = None, - encode: bool = False - ) -> None: + def loss_t(self, wires: int, inputs: Any = None, encode: bool = False) -> None: """Add a photon loss channel. The `inputs` corresponds to the transmittance of the loss channel. @@ -2669,17 +2832,11 @@ def loss_t( requires_grad = False if not isinstance(inputs, torch.Tensor): inputs = torch.tensor(inputs, dtype=torch.float) - theta = torch.arccos(inputs ** 0.5) * 2 - loss = PhotonLoss(inputs=theta, nmode=self.nmode, wires=wires, cutoff=self.cutoff, - requires_grad=requires_grad) + theta = torch.arccos(inputs**0.5) * 2 + loss = PhotonLoss(inputs=theta, nmode=self.nmode, wires=wires, cutoff=self.cutoff, requires_grad=requires_grad) self.add(loss, encode=encode) - def loss_db( - self, - wires: int, - inputs: Any = None, - encode: bool = False - ) -> None: + def loss_db(self, wires: int, inputs: Any = None, encode: bool = False) -> None: """Add a photon loss channel. The `inputs` corresponds to the probability of loss with the unit of dB and is positive. @@ -2694,16 +2851,16 @@ def loss_db( if not isinstance(inputs, torch.Tensor): inputs = torch.tensor(inputs, dtype=torch.float) t = 10 ** (-inputs / 10) - theta = torch.arccos(t ** 0.5) * 2 - loss = PhotonLoss(inputs=theta, nmode=self.nmode, wires=wires, cutoff=self.cutoff, - requires_grad=requires_grad) + theta = torch.arccos(t**0.5) * 2 + loss = PhotonLoss(inputs=theta, nmode=self.nmode, wires=wires, cutoff=self.cutoff, requires_grad=requires_grad) self.add(loss, encode=encode) - def barrier(self, wires: Union[int, List[int], None] = None) -> None: + def barrier(self, wires: int | list[int] | None = None) -> None: """Add a barrier.""" br = Barrier(nmode=self.nmode, wires=wires, cutoff=self.cutoff) self.add(br) + class DistributedQumodeCircuit(QumodeCircuit): """Photonic quantum circuit for a distributed Fock state. @@ -2715,9 +2872,23 @@ class DistributedQumodeCircuit(QumodeCircuit): cutoff (int or None, optional): The Fock space truncation. Default: ``None`` name (str or None, optional): The name of the circuit. Default: ``None`` """ - def __init__(self, nmode: int, init_state: Any, cutoff: Optional[int] = None, name: Optional[str] = None) -> None: - super().__init__(nmode, init_state, cutoff, backend='fock', basis=False, den_mat=False, - detector='pnrd', name=name, mps=False, chi=None, noise=False, mu=0, sigma=0.1) + + def __init__(self, nmode: int, init_state: Any, cutoff: int | None = None, name: str | None = None) -> None: + super().__init__( + nmode, + init_state, + cutoff, + backend='fock', + basis=False, + den_mat=False, + detector='pnrd', + name=name, + mps=False, + chi=None, + noise=False, + mu=0, + sigma=0.1, + ) def set_init_state(self, init_state: Any = None) -> None: """Set the initial state of the circuit.""" @@ -2727,12 +2898,9 @@ def set_init_state(self, init_state: Any = None) -> None: self.init_state = DistributedFockState(init_state, self.nmode, self.cutoff) self.cutoff = self.init_state.cutoff - # pylint: disable=arguments-renamed @torch.no_grad() def forward( - self, - data: Optional[torch.Tensor] = None, - state: Optional[DistributedFockState] = None + self, data: torch.Tensor | None = None, state: DistributedFockState | None = None ) -> DistributedFockState: """Perform a forward pass of the photonic quantum circuit and return the final state. @@ -2754,12 +2922,8 @@ def forward( return self.state def measure( - self, - shots: int = 1024, - with_prob: bool = False, - wires: Union[int, List[int], None] = None, - block_size: int = 2 ** 24 - ) -> Union[Dict, None]: + self, shots: int = 1024, with_prob: bool = False, wires: int | list[int] | None = None, block_size: int = 2**24 + ) -> dict | None: """Measure the final state. Args: diff --git a/src/deepquantum/photonic/decompose.py b/src/deepquantum/photonic/decompose.py index fcf774e7..40d478c9 100644 --- a/src/deepquantum/photonic/decompose.py +++ b/src/deepquantum/photonic/decompose.py @@ -1,15 +1,12 @@ -""" -Decompose the unitary matrix -""" +"""Decompose the unitary matrix""" from collections import defaultdict -from typing import Dict, Tuple, Union import numpy as np import torch -class UnitaryDecomposer(): +class UnitaryDecomposer: """This class is to decompose a unitary matrix into the Clements/Reck architecture. Args: @@ -23,7 +20,8 @@ class UnitaryDecomposer(): The last char denotes the position of a column of phase shifters, i.e., ``'l'`` for left and ``'r'`` for right. Default: ``'cssr'`` """ - def __init__(self, unitary: Union[np.ndarray, torch.Tensor], method: str = 'cssr') -> None: + + def __init__(self, unitary: np.ndarray | torch.Tensor, method: str = 'cssr') -> None: if isinstance(unitary, np.ndarray): self.unitary = unitary.copy() elif isinstance(unitary, torch.Tensor): @@ -37,213 +35,217 @@ def __init__(self, unitary: Union[np.ndarray, torch.Tensor], method: str = 'cssr self.unitary[np.abs(self.unitary) < 1e-32] = 1e-32 self.method = method - def decomp(self) -> Tuple[Dict, Dict, Dict]: - """Decompose the unitary matrix.The third dictionary is the representation - of the positions and the angles of all phase shifters. + def decomp(self) -> tuple[dict, dict, dict]: + """Decompose the unitary matrix. + + The third dictionary is the representation of the positions and the angles of all phase shifters. """ + def period_cut(input_angle: float, period: float = np.pi * 2) -> float: - return input_angle - np.floor(input_angle/period)*period + return input_angle - np.floor(input_angle / period) * period - def decomp_rr(unitary: np.ndarray, method: str) -> Dict: + def decomp_rr(unitary: np.ndarray, method: str) -> dict: n_dim = len(unitary) info = {} info['N'] = n_dim info['method'] = method - info['MZI_list'] = [] # jj,ii,phi,theta + info['MZI_list'] = [] # jj,ii,phi,theta if 'dd' in method: - period_theta = 2*np.pi - period_phi = 4*np.pi + period_theta = 2 * np.pi + period_phi = 4 * np.pi elif 'ds' in method: - period_theta = 4*np.pi - period_phi = 4*np.pi + period_theta = 4 * np.pi + period_phi = 4 * np.pi else: - period_theta = 2*np.pi - period_phi = 2*np.pi + period_theta = 2 * np.pi + period_phi = 2 * np.pi for i in range(n_dim): - ii = n_dim - 1 - i # 基准列 ii + ii = n_dim - 1 - i # 基准列 ii for jj in range(ii)[::-1]: # print(ii,jj) # 要用 uniatry[:,ii] 把 unitary[:,jj]的第ii号元素变成0 # if uniatry[ii,jj] == 0: # continue - ratio = unitary[ii,ii] / (unitary[ii,jj]+1e-32) + ratio = unitary[ii, ii] / (unitary[ii, jj] + 1e-32) theta = 2 * np.arctan(np.abs(ratio)) - phi = - np.angle(-ratio) - multiple = get_matrix_inverse_r([jj,ii,phi,theta], n_dim, method) + phi = -np.angle(-ratio) + multiple = get_matrix_inverse_r([jj, ii, phi, theta], n_dim, method) unitary = unitary @ multiple phi = period_cut(phi, period_phi) theta = period_cut(theta, period_theta) - info['MZI_list'].append([jj,ii,phi,theta]) + info['MZI_list'].append([jj, ii, phi, theta]) diagonal = np.diag(unitary) info['phase_angle'] = np.angle(diagonal) - mask = np.logical_or(info['phase_angle']>=2*np.pi, info['phase_angle']<0) - info['phase_angle'][mask] -= np.floor(info['phase_angle'][mask]/np.pi/2)*np.pi*2 + mask = np.logical_or(info['phase_angle'] >= 2 * np.pi, info['phase_angle'] < 0) + info['phase_angle'][mask] -= np.floor(info['phase_angle'][mask] / np.pi / 2) * np.pi * 2 return info, unitary - def decomp_cr(unitary: np.ndarray, method: str) -> Dict: + def decomp_cr(unitary: np.ndarray, method: str) -> dict: n_dim = len(unitary) info = {} - info['N']= n_dim + info['N'] = n_dim info['method'] = method - info['MZI_list'] = [] # jj,ii,phi,theta + info['MZI_list'] = [] # jj,ii,phi,theta info['right'] = [] info['left'] = [] if 'dd' in method: - period_theta = 2*np.pi - period_phi = 4*np.pi + period_theta = 2 * np.pi + period_phi = 4 * np.pi elif 'ds' in method: - period_theta = 4*np.pi - period_phi = 4*np.pi + period_theta = 4 * np.pi + period_phi = 4 * np.pi else: - period_theta = 2*np.pi - period_phi = 2*np.pi - for i in range(n_dim-1): # 从下往上第i个反对角线 - if i % 2: # 左乘, 利用TU消元; - for j in range(i+1): # 反对角线的元素计数 + period_theta = 2 * np.pi + period_phi = 2 * np.pi + for i in range(n_dim - 1): # 从下往上第i个反对角线 + if i % 2: # 左乘, 利用TU消元; + for j in range(i + 1): # 反对角线的元素计数 # 消元顺序:从左上到右下 - jj = j # 当前待消元元素列号 - ii = n_dim - 1 - i + j # 当前待消元元素行号 + jj = j # 当前待消元元素列号 + ii = n_dim - 1 - i + j # 当前待消元元素行号 # print(ii,jj) # if unitary[ii,jj] == 0: # continue - ratio = unitary[ii-1,jj]/(unitary[ii,jj]+1e-32) - theta = 2 * np.arctan( np.abs(ratio) ) - phi = - np.angle(ratio) - multiple = get_matrix_constr_r([ii-1,ii,phi,theta], n_dim, method) + ratio = unitary[ii - 1, jj] / (unitary[ii, jj] + 1e-32) + theta = 2 * np.arctan(np.abs(ratio)) + phi = -np.angle(ratio) + multiple = get_matrix_constr_r([ii - 1, ii, phi, theta], n_dim, method) unitary = multiple @ unitary - info['left'].append([ii-1,ii,phi,theta]) - else: # 利用UT^{-1}消元,即利用 unitary[ii,jj+1] 消去 unitary[ii,jj] - for j in range(i+1)[::-1]: # 反对角线的元素计数 + info['left'].append([ii - 1, ii, phi, theta]) + else: # 利用UT^{-1}消元,即利用 unitary[ii,jj+1] 消去 unitary[ii,jj] + for j in range(i + 1)[::-1]: # 反对角线的元素计数 # 消元顺序:从右下到左上 - jj = j # 当前待消元元素列号 - ii = n_dim - 1 - i + j # 当前待消元元素行号 + jj = j # 当前待消元元素列号 + ii = n_dim - 1 - i + j # 当前待消元元素行号 # print(ii,jj) # if unitary[ii,jj] == 0: # continue - ratio = unitary[ii,jj+1]/(unitary[ii,jj]+1e-32) - theta = 2 * np.arctan( np.abs( ratio ) ) - phi = - np.angle(- ratio) - multiple = get_matrix_inverse_r([jj,jj+1,phi,theta], n_dim, method) + ratio = unitary[ii, jj + 1] / (unitary[ii, jj] + 1e-32) + theta = 2 * np.arctan(np.abs(ratio)) + phi = -np.angle(-ratio) + multiple = get_matrix_inverse_r([jj, jj + 1, phi, theta], n_dim, method) unitary = unitary @ multiple - info['right'].append([jj,jj+1,phi,theta]) + info['right'].append([jj, jj + 1, phi, theta]) phase_angle = np.angle(np.diag(unitary)) - info['phase_angle_ori'] = phase_angle.copy() # unitary=LLLDRRR,本行保存D + info['phase_angle_ori'] = phase_angle.copy() # unitary=LLLDRRR,本行保存D for idx in range(len(info['right'])): - info['right'][idx][2] = period_cut(info['right'][idx][2],period_phi) - info['right'][idx][3] = period_cut(info['right'][idx][3],period_theta) + info['right'][idx][2] = period_cut(info['right'][idx][2], period_phi) + info['right'][idx][3] = period_cut(info['right'][idx][3], period_theta) info['MZI_list'].append(info['right'][idx]) left_list = info['left'][::-1] for idx in range(len(left_list)): - jj,ii,phi,theta = left_list[idx] - phi_, theta_, phase_angle[jj], phase_angle[ii] = \ - clements_diagonal_transform(phi, theta, phase_angle[jj], phase_angle[ii],method) - phi_ = period_cut(phi_,period_phi) - theta_ = period_cut(theta_,period_theta) - info['MZI_list'].append([jj,ii,phi_,theta_]) - info['phase_angle'] = phase_angle.copy() # unitary=D'L'L'L'RRR,本行保存新的D - mask = np.logical_or(info['phase_angle']>=2*np.pi, info['phase_angle']<0) - info['phase_angle'][mask] -= np.floor(info['phase_angle'][mask]/np.pi/2)*np.pi*2 + jj, ii, phi, theta = left_list[idx] + phi_, theta_, phase_angle[jj], phase_angle[ii] = clements_diagonal_transform( + phi, theta, phase_angle[jj], phase_angle[ii], method + ) + phi_ = period_cut(phi_, period_phi) + theta_ = period_cut(theta_, period_theta) + info['MZI_list'].append([jj, ii, phi_, theta_]) + info['phase_angle'] = phase_angle.copy() # unitary=D'L'L'L'RRR,本行保存新的D + mask = np.logical_or(info['phase_angle'] >= 2 * np.pi, info['phase_angle'] < 0) + info['phase_angle'][mask] -= np.floor(info['phase_angle'][mask] / np.pi / 2) * np.pi * 2 return info, unitary - def decomp_rl(unitary: np.ndarray, method: str) -> Dict: + def decomp_rl(unitary: np.ndarray, method: str) -> dict: n_dim = len(unitary) info = {} info['N'] = n_dim info['method'] = method - info['MZI_list'] = [] # jj,ii,phi,theta + info['MZI_list'] = [] # jj,ii,phi,theta if 'dd' in method: - period_theta = 2*np.pi - period_phi = 4*np.pi + period_theta = 2 * np.pi + period_phi = 4 * np.pi elif 'ds' in method: - period_theta = 4*np.pi - period_phi = 4*np.pi + period_theta = 4 * np.pi + period_phi = 4 * np.pi else: - period_theta = 2*np.pi - period_phi = 2*np.pi + period_theta = 2 * np.pi + period_phi = 2 * np.pi for i in range(n_dim): - ii = n_dim - 1 - i # 基准行 ii + ii = n_dim - 1 - i # 基准行 ii for jj in range(ii)[::-1]: # print(ii,jj) # 要用 unitary[ii] 把 unitary[jj]的第ii号元素变成0 # if unitary[jj,ii] == 0: # continue - ratio = unitary[ii,ii] / (unitary[jj,ii]+1e-32) - theta = 2 * np.arctan( np.abs( ratio ) ) - phi = - np.angle(- ratio) - multiple = get_matrix_inverse_l([jj,ii,phi,theta], n_dim, method) + ratio = unitary[ii, ii] / (unitary[jj, ii] + 1e-32) + theta = 2 * np.arctan(np.abs(ratio)) + phi = -np.angle(-ratio) + multiple = get_matrix_inverse_l([jj, ii, phi, theta], n_dim, method) unitary = multiple @ unitary - phi = period_cut(phi,period_phi) - theta = period_cut(theta,period_theta) - info['MZI_list'].append([jj,ii,phi,theta]) + phi = period_cut(phi, period_phi) + theta = period_cut(theta, period_theta) + info['MZI_list'].append([jj, ii, phi, theta]) diagonal = np.diag(unitary) info['phase_angle'] = np.angle(diagonal) - mask = np.logical_or(info['phase_angle']>=2*np.pi, info['phase_angle']<0) - info['phase_angle'][mask] -= np.floor(info['phase_angle'][mask]/np.pi/2)*np.pi*2 + mask = np.logical_or(info['phase_angle'] >= 2 * np.pi, info['phase_angle'] < 0) + info['phase_angle'][mask] -= np.floor(info['phase_angle'][mask] / np.pi / 2) * np.pi * 2 return info, unitary - def decomp_cl(unitary: np.ndarray, method: str) -> Dict: + def decomp_cl(unitary: np.ndarray, method: str) -> dict: n_dim = len(unitary) info = {} info['N'] = n_dim info['method'] = method - info['MZI_list'] = [] # jj,ii,phi,theta + info['MZI_list'] = [] # jj,ii,phi,theta info['right'] = [] info['left'] = [] if 'dd' in method: - period_theta = 2*np.pi - period_phi = 4*np.pi + period_theta = 2 * np.pi + period_phi = 4 * np.pi elif 'ds' in method: - period_theta = 4*np.pi - period_phi = 4*np.pi + period_theta = 4 * np.pi + period_phi = 4 * np.pi else: - period_theta = 2*np.pi - period_phi = 2*np.pi - for i in range(n_dim-1): # 从下往上第i个反对角线 - if i % 2: # 左乘, 利用T^{-1}U消元; - for j in range(i+1): # 反对角线的元素计数 + period_theta = 2 * np.pi + period_phi = 2 * np.pi + for i in range(n_dim - 1): # 从下往上第i个反对角线 + if i % 2: # 左乘, 利用T^{-1}U消元; + for j in range(i + 1): # 反对角线的元素计数 # 消元顺序:从左上到右下 - jj = j # 当前待消元元素列号 - ii = n_dim - 1 - i + j # 当前待消元元素行号 + jj = j # 当前待消元元素列号 + ii = n_dim - 1 - i + j # 当前待消元元素行号 # print(ii,jj) # if unitary[ii,jj] == 0: # continue - ratio = unitary[ii-1,jj]/(unitary[ii,jj]+1e-32) - theta = 2 * np.arctan( np.abs(ratio) ) + ratio = unitary[ii - 1, jj] / (unitary[ii, jj] + 1e-32) + theta = 2 * np.arctan(np.abs(ratio)) phi = np.angle(ratio) - multiple = get_matrix_inverse_l([ii-1,ii,phi,theta], n_dim, method) + multiple = get_matrix_inverse_l([ii - 1, ii, phi, theta], n_dim, method) unitary = multiple @ unitary - info['left'].append([ii-1,ii,phi,theta]) - else: # 利用UT消元,即利用 unitary[ii,jj+1] 消去 unitary[ii,jj] - for j in range(i+1)[::-1]: # 反对角线的元素计数 + info['left'].append([ii - 1, ii, phi, theta]) + else: # 利用UT消元,即利用 unitary[ii,jj+1] 消去 unitary[ii,jj] + for j in range(i + 1)[::-1]: # 反对角线的元素计数 # 消元顺序:从右下到左上 - jj = j # 当前待消元元素列号 - ii = n_dim - 1 - i + j # 当前待消元元素行号 + jj = j # 当前待消元元素列号 + ii = n_dim - 1 - i + j # 当前待消元元素行号 # print(ii,jj) # if unitary[ii,jj] == 0: # continue - ratio = unitary[ii,jj+1]/(unitary[ii,jj]+1e-32) - theta = 2 * np.arctan( np.abs( ratio ) ) - phi = np.angle(- ratio) - multiple = get_matrix_constr_l([jj,jj+1,phi,theta], n_dim, method) + ratio = unitary[ii, jj + 1] / (unitary[ii, jj] + 1e-32) + theta = 2 * np.arctan(np.abs(ratio)) + phi = np.angle(-ratio) + multiple = get_matrix_constr_l([jj, jj + 1, phi, theta], n_dim, method) unitary = unitary @ multiple - info['right'].append([jj,jj+1,phi,theta]) + info['right'].append([jj, jj + 1, phi, theta]) phase_angle = np.angle(np.diag(unitary)) - info['phase_angle_ori'] = phase_angle.copy() # U=LLLDRRR,本行保存D + info['phase_angle_ori'] = phase_angle.copy() # U=LLLDRRR,本行保存D for idx in range(len(info['left'])): - info['left'][idx][2] = period_cut(info['left'][idx][2],period_phi) - info['left'][idx][3] = period_cut(info['left'][idx][3],period_theta) + info['left'][idx][2] = period_cut(info['left'][idx][2], period_phi) + info['left'][idx][3] = period_cut(info['left'][idx][3], period_theta) info['MZI_list'].append(info['left'][idx]) left_list = info['right'][::-1] for idx in range(len(left_list)): - jj,ii,phi,theta = left_list[idx] - phi_, theta_, phase_angle[jj], phase_angle[ii] = \ - clements_diagonal_transform(phi, theta, phase_angle[jj], phase_angle[ii], method) - phi_ = period_cut(phi_,period_phi) - theta_ = period_cut(theta_,period_theta) - info['MZI_list'].append([jj,ii,phi_,theta_]) - info['phase_angle'] = phase_angle.copy() # unitary =D'L'L'L'RRR,本行保存新的D - mask = np.logical_or(info['phase_angle']>=2*np.pi, info['phase_angle']<0) - info['phase_angle'][mask] -= np.floor(info['phase_angle'][mask]/np.pi/2)*np.pi*2 + jj, ii, phi, theta = left_list[idx] + phi_, theta_, phase_angle[jj], phase_angle[ii] = clements_diagonal_transform( + phi, theta, phase_angle[jj], phase_angle[ii], method + ) + phi_ = period_cut(phi_, period_phi) + theta_ = period_cut(theta_, period_theta) + info['MZI_list'].append([jj, ii, phi_, theta_]) + info['phase_angle'] = phase_angle.copy() # unitary =D'L'L'L'RRR,本行保存新的D + mask = np.logical_or(info['phase_angle'] >= 2 * np.pi, info['phase_angle'] < 0) + info['phase_angle'][mask] -= np.floor(info['phase_angle'][mask] / np.pi / 2) * np.pi * 2 return info, unitary def calc_factor_inverse(method, phi, theta): @@ -251,126 +253,138 @@ def calc_factor_inverse(method, phi, theta): if 'sd' in method: return -1j elif 'ss' in method: - return -1j*np.exp(-1j*theta/2) + return -1j * np.exp(-1j * theta / 2) elif 'dd' in method: - return -1j*np.exp(-1j*(theta-phi)/2) + return -1j * np.exp(-1j * (theta - phi) / 2) elif 'ds' in method: - return -1j*np.exp(1j*phi/2) + return -1j * np.exp(1j * phi / 2) def calc_factor_constr(method, phi, theta): # 计算MZI矩阵T的系数(相当于全局相位) - return calc_factor_inverse(method,phi,theta).conjugate() + return calc_factor_inverse(method, phi, theta).conjugate() def get_matrix_constr_l(info, n_dim, method): - jj,ii,phi,theta = info - factor = calc_factor_constr(method,phi,theta) + jj, ii, phi, theta = info + factor = calc_factor_constr(method, phi, theta) multiple = np.eye(n_dim, dtype=complex) - multiple[jj,jj] = factor*np.exp(1j*phi)*np.sin(theta/2) - multiple[jj,ii] = factor*np.exp(1j*phi)*np.cos(theta/2) - multiple[ii,jj] = factor*np.cos(theta/2) - multiple[ii,ii] = factor*-np.sin(theta/2) + multiple[jj, jj] = factor * np.exp(1j * phi) * np.sin(theta / 2) + multiple[jj, ii] = factor * np.exp(1j * phi) * np.cos(theta / 2) + multiple[ii, jj] = factor * np.cos(theta / 2) + multiple[ii, ii] = factor * -np.sin(theta / 2) return multiple def get_matrix_inverse_l(info, n_dim, method): - jj,ii,phi,theta = info - factor = calc_factor_inverse(method,phi,theta) + jj, ii, phi, theta = info + factor = calc_factor_inverse(method, phi, theta) multiple = np.eye(n_dim, dtype=complex) - multiple[jj,jj] = factor*np.exp(-1j*phi)*np.sin(theta/2) - multiple[jj,ii] = factor*np.cos(theta/2) - multiple[ii,jj] = factor*np.exp(-1j*phi)*np.cos(theta/2) - multiple[ii,ii] = factor*-np.sin(theta/2) + multiple[jj, jj] = factor * np.exp(-1j * phi) * np.sin(theta / 2) + multiple[jj, ii] = factor * np.cos(theta / 2) + multiple[ii, jj] = factor * np.exp(-1j * phi) * np.cos(theta / 2) + multiple[ii, ii] = factor * -np.sin(theta / 2) return multiple def get_matrix_constr_r(info, n_dim, method): - jj,ii,phi,theta = info - factor = calc_factor_constr(method,phi,theta) + jj, ii, phi, theta = info + factor = calc_factor_constr(method, phi, theta) multiple = np.eye(n_dim, dtype=complex) - multiple[jj,jj] = factor*np.exp(1j*phi)*np.sin(theta/2) - multiple[jj,ii] = factor*np.cos(theta/2) - multiple[ii,jj] = factor*np.exp(1j*phi)*np.cos(theta/2) - multiple[ii,ii] = factor*-np.sin(theta/2) + multiple[jj, jj] = factor * np.exp(1j * phi) * np.sin(theta / 2) + multiple[jj, ii] = factor * np.cos(theta / 2) + multiple[ii, jj] = factor * np.exp(1j * phi) * np.cos(theta / 2) + multiple[ii, ii] = factor * -np.sin(theta / 2) return multiple def get_matrix_inverse_r(info, n_dim, method): - jj,ii,phi,theta = info - factor = calc_factor_inverse(method,phi,theta) + jj, ii, phi, theta = info + factor = calc_factor_inverse(method, phi, theta) multiple = np.eye(n_dim, dtype=complex) - multiple[jj,jj] = factor*np.exp(-1j*phi)*np.sin(theta/2) - multiple[jj,ii] = factor*np.exp(-1j*phi)*np.cos(theta/2) - multiple[ii,jj] = factor*np.cos(theta/2) - multiple[ii,ii] = factor*-np.sin(theta/2) + multiple[jj, jj] = factor * np.exp(-1j * phi) * np.sin(theta / 2) + multiple[jj, ii] = factor * np.exp(-1j * phi) * np.cos(theta / 2) + multiple[ii, jj] = factor * np.cos(theta / 2) + multiple[ii, ii] = factor * -np.sin(theta / 2) return multiple def clements_diagonal_transform(phi, theta, a1, a2, method): if 'sd' in method: theta_ = theta phi_ = a1 - a2 - b1 = a2 - phi + np.pi - b2 = a2 + np.pi + b1 = a2 - phi + np.pi + b2 = a2 + np.pi return phi_, theta_, b1, b2 elif 'ss' in method: theta_ = theta phi_ = a1 - a2 - b1 = a2 - phi + np.pi - theta - b2 = a2 + np.pi - theta + b1 = a2 - phi + np.pi - theta + b2 = a2 + np.pi - theta return phi_, theta_, b1, b2 elif 'dd' in method: theta_ = theta phi_ = a1 - a2 - b1 = a2 - phi + np.pi - theta + (phi+phi_)/2 - b2 = a2 + np.pi - theta + (phi+phi_)/2 + b1 = a2 - phi + np.pi - theta + (phi + phi_) / 2 + b2 = a2 + np.pi - theta + (phi + phi_) / 2 return phi_, theta_, b1, b2 elif 'ds' in method: theta_ = theta phi_ = a1 - a2 - b1 = a2 - phi + np.pi + (phi+phi_)/2 - b2 = a2 + np.pi + (phi+phi_)/2 + b1 = a2 - phi + np.pi + (phi + phi_) / 2 + b2 = a2 + np.pi + (phi + phi_) / 2 return phi_, theta_, b1, b2 method = self.method - if method not in ['rssr','rsdr', 'rdsr', 'rddr', 'rssl', 'rsdl', 'rdsl', 'rddl',\ - 'cssr', 'csdr', 'cdsr', 'cddr', 'cssl', 'csdl', 'cdsl', 'cddl']: + if method not in [ + 'rssr', + 'rsdr', + 'rdsr', + 'rddr', + 'rssl', + 'rsdl', + 'rdsl', + 'rddl', + 'cssr', + 'csdr', + 'cdsr', + 'cddr', + 'cssl', + 'csdl', + 'cdsl', + 'cddl', + ]: raise LookupError('请检查分解方式!') - elif method[0]+method[-1] == 'cr': + elif method[0] + method[-1] == 'cr': temp_0 = decomp_cr(self.unitary, method)[0] - elif method[0]+method[-1] == 'cl': + elif method[0] + method[-1] == 'cl': temp_0 = decomp_cl(self.unitary, method)[0] - elif method[0]+method[-1] == 'rr': + elif method[0] + method[-1] == 'rr': temp_0 = decomp_rr(self.unitary, method)[0] - elif method[0]+method[-1] == 'rl': + elif method[0] + method[-1] == 'rl': temp_0 = decomp_rl(self.unitary, method)[0] temp_1 = self.sort_mzi(temp_0) temp_2 = self.ps_pos(temp_1, temp_0['phase_angle']) return temp_0, temp_1, temp_2 def sort_mzi(self, mzi_info): - """ - Sort mzi parameters in the same array for plotting. - """ - dic_mzi = defaultdict(list) #当key不存在时对应的value是[] + """Sort mzi parameters in the same array for plotting.""" + dic_mzi = defaultdict(list) # 当key不存在时对应的value是[] mzi_list = mzi_info['MZI_list'] for i in mzi_list: dic_mzi[tuple(i[0:2])].append(i[2:]) return dic_mzi def ps_pos(self, dic_mzi, phase_angle): - """ - Label the position of each phaseshifter for ``'cssr'`` case. - """ + """Label the position of each phaseshifter for ``'cssr'`` case.""" if self.method == 'cssr': dic_pos = {} nmode = self.unitary.shape[0] - dic_ =dic_mzi + dic_ = dic_mzi for mode in range(nmode): - pair = (mode, mode+1) + pair = (mode, mode + 1) value = dic_[pair] value = np.array(value).flatten() for k in range(len(value)): dic_pos[(mode, k)] = np.round((value[k]), 4) - if mode == nmode -1: + if mode == nmode - 1: dic_pos[(mode, 0)] = np.round((phase_angle[mode]), 4) else: - dic_pos[(mode, k+1)] = np.round((phase_angle[mode]), 4) + dic_pos[(mode, k + 1)] = np.round((phase_angle[mode]), 4) return dic_pos else: return None diff --git a/src/deepquantum/photonic/distributed.py b/src/deepquantum/photonic/distributed.py index 69ae843c..d94ed384 100644 --- a/src/deepquantum/photonic/distributed.py +++ b/src/deepquantum/photonic/distributed.py @@ -1,24 +1,21 @@ -""" -Distributed operations -""" +"""Distributed operations""" from collections import Counter -from typing import Dict, List, Union import torch import torch.distributed as dist from ..communication import comm_exchange_arrays from ..distributed import get_local_targets -from ..qmath import list_to_decimal, decimal_to_list, inverse_permutation, evolve_state, block_sample -from .state import FockState, DistributedFockState +from ..qmath import block_sample, decimal_to_list, evolve_state, inverse_permutation, list_to_decimal +from .state import DistributedFockState, FockState # The 0-th mode is the rightmost for the `target` def get_pair_rank(rank: int, target_rank: int, new_digit: int, cutoff: int, ndigit: int) -> int: """Get the pair rank for communication.""" digits = decimal_to_list(rank, cutoff, ndigit) - digits[-(target_rank+1)] = new_digit + digits[-(target_rank + 1)] = new_digit return list_to_decimal(digits, cutoff) @@ -28,10 +25,10 @@ def get_digit(decimal: int, target: int, cutoff: int) -> int: if target >= len(digits): return 0 else: - return digits[-(target+1)] + return digits[-(target + 1)] -def local_gate(state: torch.Tensor, targets: List[int], matrix: torch.Tensor) -> torch.Tensor: +def local_gate(state: torch.Tensor, targets: list[int], matrix: torch.Tensor) -> torch.Tensor: """Apply a gate to a Fock state tensor locally.""" shape = state.shape nmode = len(shape) @@ -82,11 +79,7 @@ def dist_swap_gate(state: DistributedFockState, target1: int, target2: int): return state -def dist_gate( - state: DistributedFockState, - targets: List[int], - matrix: torch.Tensor -) -> DistributedFockState: +def dist_gate(state: DistributedFockState, targets: list[int], matrix: torch.Tensor) -> DistributedFockState: """Apply a gate to a distributed Fock state tensor.""" nt = len(targets) assert nt <= state.nmode_local @@ -108,9 +101,9 @@ def measure_dist( state: DistributedFockState, shots: int = 1024, with_prob: bool = False, - wires: Union[int, List[int], None] = None, - block_size: int = 2 ** 24 -) -> Dict: + wires: int | list[int] | None = None, + block_size: int = 2**24, +) -> dict: """Measure a distributed Fock state tensor.""" if isinstance(wires, int): wires = [wires] @@ -119,7 +112,7 @@ def measure_dist( targets = [state.nmode - wire - 1 for wire in wires] pm_shape = list(range(state.nmode_local)) # Assume nmode_global < nmode_local - if nwires <= state.nmode_local: # All targets move to local modes + if nwires <= state.nmode_local: # All targets move to local modes if max(targets) >= state.nmode_local: targets_new = get_local_targets(targets, state.nmode_local) for i in range(nwires): @@ -143,7 +136,7 @@ def measure_dist( results[k] = results[k], probs[index] return results return {} - else: # All targets are sorted, then move to global modes + else: # All targets are sorted, then move to global modes targets_sort = sorted(targets, reverse=True) wires_local = [] for i, target in enumerate(targets_sort): @@ -165,7 +158,7 @@ def measure_dist( blocks = torch.multinomial(probs_rank, shots, replacement=True) dist.broadcast(blocks, src=0) block_dict = Counter(blocks.cpu().numpy()) - key_offset = state.rank * state.cutoff**(nwires - state.nmode_global) + key_offset = state.rank * state.cutoff ** (nwires - state.nmode_global) if state.rank in block_dict: samples = Counter(block_sample(probs, block_dict[state.rank], block_size)) results = {FockState(decimal_to_list(k + key_offset, state.cutoff, nwires)): v for k, v in samples.items()} diff --git a/src/deepquantum/photonic/draw.py b/src/deepquantum/photonic/draw.py index 1817a3b9..77bec00b 100644 --- a/src/deepquantum/photonic/draw.py +++ b/src/deepquantum/photonic/draw.py @@ -1,9 +1,6 @@ -""" -Draw quantum circuit. -""" +"""Draw photonic quantum circuit""" from collections import defaultdict -from typing import Dict, Optional import matplotlib.pyplot as plt import numpy as np @@ -12,26 +9,42 @@ from torch import nn from .channel import PhotonLoss -from .gate import PhaseShift, BeamSplitter, MZI, BeamSplitterSingle, UAnyGate, Squeezing, Squeezing2, Displacement -from .gate import QuadraticPhase, ControlledX, ControlledZ, CubicPhase, Kerr, CrossKerr, Barrier +from .gate import ( + Barrier, + BeamSplitter, + BeamSplitterSingle, + ControlledX, + ControlledZ, + CrossKerr, + CubicPhase, + Displacement, + Kerr, + MZI, + PhaseShift, + QuadraticPhase, + Squeezing, + Squeezing2, + UAnyGate, +) from .measurement import Homodyne from .operation import Delay - -info_dic = {'PS': ['teal', 0], - 'S': ['royalblue', 3], - 'S2': ['royalblue', 0], - 'D': ['green', 3], - 'U': ['cadetblue', 0], - 'QP': ['peru', 0], - 'CP': ['peru', 0], - 'K': ['pink', 3], - 'CX': ['gold', 0], - 'CZ': ['gold', 0], - 'CK': ['pink', 0]} - - -class DrawCircuit(): +info_dic = { + 'PS': ['teal', 0], + 'S': ['royalblue', 3], + 'S2': ['royalblue', 0], + 'D': ['green', 3], + 'U': ['cadetblue', 0], + 'QP': ['peru', 0], + 'CP': ['peru', 0], + 'K': ['pink', 3], + 'CX': ['gold', 0], + 'CZ': ['gold', 0], + 'CK': ['pink', 0], +} + + +class DrawCircuit: """Draw the photonic quantum circuit. Args: @@ -40,19 +53,16 @@ class DrawCircuit(): circuit_operators (nn.Sequential): The operators of the circuit. measurements (nn.ModuleList): The measurements of the circuit. """ + def __init__( - self, - circuit_name: str, - circuit_nmode: int, - circuit_operators: nn.Sequential, - measurements: nn.ModuleList + self, circuit_name: str, circuit_nmode: int, circuit_operators: nn.Sequential, measurements: nn.ModuleList ) -> None: if circuit_name is None: circuit_name = 'circuit' nmode = circuit_nmode name = circuit_name + '.svg' self.draw_ = svgwrite.Drawing(name, profile='full') - self.draw_['height'] = f'{10.5/11 * nmode}cm' + self.draw_['height'] = f'{10.5 / 11 * nmode}cm' self.nmode = nmode self.name = name self.ops = circuit_operators @@ -60,10 +70,10 @@ def __init__( def draw(self, depth=None, ops=None, measurements=None): """Draw circuit.""" - order_dic = defaultdict(list) # 当key不存在时对应的value是[] + order_dic = defaultdict(list) # 当key不存在时对应的value是[] nmode = self.nmode if depth is None: - depth = [0] * nmode # record the depth of each mode + depth = [0] * nmode # record the depth of each mode if ops is None: ops = self.ops if measurements is None: @@ -71,10 +81,7 @@ def draw(self, depth=None, ops=None, measurements=None): for op in ops: if isinstance(op, BeamSplitter): if isinstance(op, MZI): - if op.phi_first: - name = 'MZI-PT' - else: - name = 'MZI-TP' + name = 'MZI-PT' if op.phi_first else 'MZI-TP' elif isinstance(op, BeamSplitterSingle): name = 'BS-' + op.convention.upper() else: @@ -82,7 +89,7 @@ def draw(self, depth=None, ops=None, measurements=None): theta = op.theta.item() try: phi = op.phi.item() - except: + except Exception: phi = None order = max(depth[op.wires[0]], depth[op.wires[1]]) self.draw_bs(name, order, op.wires, theta, phi) @@ -90,7 +97,7 @@ def draw(self, depth=None, ops=None, measurements=None): for i in op.wires: depth[i] = depth[i] + 1 bs_depth = [depth[op.wires[0]], depth[op.wires[1]]][:] - depth[op.wires[0]] = max(bs_depth) ## BS 经过后相同线路深度 + depth[op.wires[0]] = max(bs_depth) ## BS 经过后相同线路深度 depth[op.wires[1]] = max(bs_depth) elif isinstance(op, PhaseShift): name_ = 'PS' @@ -99,30 +106,27 @@ def draw(self, depth=None, ops=None, measurements=None): self.draw_ps(order, op.wires, theta, name_) order_dic[order] = order_dic[order] + op.wires for i in op.wires: - depth[i] = depth[i]+1 + depth[i] = depth[i] + 1 elif isinstance(op, (UAnyGate, Squeezing2)): - order = max(depth[min(op.wires) : max(op.wires)+1]) + order = max(depth[min(op.wires) : max(op.wires) + 1]) if isinstance(op, UAnyGate): name_ = 'U' self.draw_any(order, op.wires, name_) else: name_ = 'S2' - para_dic = {'r':op.r.item(), 'θ': op.theta.item()} + para_dic = {'r': op.r.item(), 'θ': op.theta.item()} self.draw_sq(order, op.wires, para_dic, name_) order_dic[order] = order_dic[order] + op.wires for i in op.wires: depth[i] = order + 1 elif isinstance(op, (Squeezing, Displacement)): - para_dic = {'r':op.r.item(), 'θ': op.theta.item()} + para_dic = {'r': op.r.item(), 'θ': op.theta.item()} order = depth[op.wires[0]] - if isinstance(op, Squeezing): - name_ = 'S' - else: - name_ = 'D' + name_ = 'S' if isinstance(op, Squeezing) else 'D' self.draw_sq(order, op.wires, para_dic, name=name_) order_dic[order] = order_dic[order] + op.wires for i in op.wires: - depth[i] = depth[i]+1 + depth[i] = depth[i] + 1 elif isinstance(op, Delay): name_ = '' order = depth[op.wires[0]] @@ -130,7 +134,7 @@ def draw(self, depth=None, ops=None, measurements=None): self.draw_delay(order, op.wires, inputs=inputs) order_dic[order] = order_dic[order] + op.wires for i in op.wires: - depth[i] = depth[i]+1 + depth[i] = depth[i] + 1 elif isinstance(op, PhotonLoss): name_ = 'loss' order = depth[op.wires[0]] @@ -138,7 +142,7 @@ def draw(self, depth=None, ops=None, measurements=None): self.draw_loss(order, op.wires, name_, t) order_dic[order] = order_dic[order] + op.wires for i in op.wires: - depth[i] = depth[i]+1 + depth[i] = depth[i] + 1 elif isinstance(op, Barrier): wires = op.wires order = int(max(np.array(depth)[wires])) @@ -149,24 +153,24 @@ def draw(self, depth=None, ops=None, measurements=None): if isinstance(op, (QuadraticPhase, CubicPhase, Kerr)): order = depth[op.wires[0]] if isinstance(op, QuadraticPhase): - para_dic = {'s':op.s.item()} + para_dic = {'s': op.s.item()} name_ = 'QP' elif isinstance(op, CubicPhase): - para_dic = {'γ':op.gamma.item()} + para_dic = {'γ': op.gamma.item()} name_ = 'CP' elif isinstance(op, Kerr): - para_dic = {'κ':op.kappa.item()} + para_dic = {'κ': op.kappa.item()} name_ = 'K' elif isinstance(op, (ControlledX, ControlledZ, CrossKerr)): - order = max(depth[min(op.wires) : max(op.wires)+1]) + order = max(depth[min(op.wires) : max(op.wires) + 1]) if isinstance(op, ControlledX): - para_dic = {'s':op.s.item()} + para_dic = {'s': op.s.item()} name_ = 'CX' elif isinstance(op, ControlledZ): - para_dic = {'s':op.s.item()} + para_dic = {'s': op.s.item()} name_ = 'CZ' elif isinstance(op, CrossKerr): - para_dic = {'κ':op.kappa.item()} + para_dic = {'κ': op.kappa.item()} name_ = 'CK' self.draw_sq(order, op.wires, para_dic, name=name_) order_dic[order] = order_dic[order] + op.wires @@ -182,181 +186,253 @@ def draw(self, depth=None, ops=None, measurements=None): order = depth[i] self.draw_homodyne(order, i, phi.item(), name_) order_dic[order] = order_dic[order] + [i] - depth[i] = depth[i]+1 + depth[i] = depth[i] + 1 for key, value in order_dic.items(): op_line = value ## here lines represent for no operation line_wires = [i for i in range(nmode) if i not in op_line] if len(line_wires) > 0: self.draw_lines(key, line_wires) - self.draw_mode_num() ## mode draw numbers + self.draw_mode_num() ## mode draw numbers self.order_dic = order_dic self.depth = depth wid = 3 * (90 * (max(self.depth)) + 40) / 100 self.draw_['width'] = f'{wid}cm' def save(self, filename): - """ - Save the circuit as svg. - """ + """Save the circuit as svg.""" self.draw_.saveas(filename) def draw_mode_num(self): - nmode = self.nmode + nmode = self.nmode for i in range(nmode): - self.draw_.add(self.draw_.text(str(i), insert=(25, i*30+30), font_size=12)) + self.draw_.add(self.draw_.text(str(i), insert=(25, i * 30 + 30), font_size=12)) - def draw_bs(self, name, order, wires, theta, phi = None): - """ - Draw beamsplitter. - """ + def draw_bs(self, name, order, wires, theta, phi=None): + """Draw beamsplitter.""" x = 90 * order + 40 wires = sorted(wires) y_up = wires[0] y_down = wires[1] y_delta = abs(y_down - y_up) shift = -10 - self.draw_.add(self.draw_.polyline(points=[(x, y_up*30+30), (x+30+shift, y_up*30+30), # need shift - (x+60+shift, y_up*30+30+30*y_delta), (x+90, y_up*30+30+30*y_delta)], - fill='none', stroke='black', stroke_width=2)) - self.draw_.add(self.draw_.polyline(points=[(x, y_up*30+30+30*y_delta), (x+30+shift, y_up*30+30+30*y_delta), - (x+60+shift, y_up*30+30), (x+90, y_up*30+30)], - fill='none', stroke='black', stroke_width=2)) - self.draw_.add(self.draw_.text(name, insert=(x+40-(len(name)-2)*3+shift, y_up*30+25), font_size=9)) - self.draw_.add(self.draw_.text('θ='+ str(np.round(theta,3)), - insert=(x+55+shift, y_up*30+30+20-6), - font_size=7)) + self.draw_.add( + self.draw_.polyline( + points=[ + (x, y_up * 30 + 30), + (x + 30 + shift, y_up * 30 + 30), # need shift + (x + 60 + shift, y_up * 30 + 30 + 30 * y_delta), + (x + 90, y_up * 30 + 30 + 30 * y_delta), + ], + fill='none', + stroke='black', + stroke_width=2, + ) + ) + self.draw_.add( + self.draw_.polyline( + points=[ + (x, y_up * 30 + 30 + 30 * y_delta), + (x + 30 + shift, y_up * 30 + 30 + 30 * y_delta), + (x + 60 + shift, y_up * 30 + 30), + (x + 90, y_up * 30 + 30), + ], + fill='none', + stroke='black', + stroke_width=2, + ) + ) + self.draw_.add( + self.draw_.text(name, insert=(x + 40 - (len(name) - 2) * 3 + shift, y_up * 30 + 25), font_size=9) + ) + self.draw_.add( + self.draw_.text( + 'θ=' + str(np.round(theta, 3)), insert=(x + 55 + shift, y_up * 30 + 30 + 20 - 6), font_size=7 + ) + ) if phi is not None: - self.draw_.add(self.draw_.text('ϕ='+ str(np.round(phi,3)), - insert=(x+55+shift, y_up*30+30+26-6), - font_size=7)) + self.draw_.add( + self.draw_.text( + 'ϕ=' + str(np.round(phi, 3)), insert=(x + 55 + shift, y_up * 30 + 30 + 26 - 6), font_size=7 + ) + ) def draw_ps(self, order, wires, theta=0, name=None): - """ - Draw phaseshift (rotation) gate. - """ + """Draw phaseshift (rotation) gate.""" fill_c = info_dic[name][0] shift = info_dic[name][1] x = 90 * order + 40 y_up = wires[0] # y_down = wires[1] - self.draw_.add(self.draw_.polyline(points=[(x, y_up*30+30), (x+90, y_up*30+30)], - fill='none', stroke='black', stroke_width=2)) - self.draw_.add(self.draw_.rect(insert=(x+42.5, y_up*30+25), size=(6,12), rx=0, ry=0, - fill=fill_c, stroke='black', stroke_width=1.5)) - - self.draw_.add(self.draw_.text(name, insert=(x+40+shift, y_up*30+20), font_size=9)) - self.draw_.add(self.draw_.text('θ='+str(np.round(theta,3)), insert=(x+55, y_up*30+20), font_size=7)) + self.draw_.add( + self.draw_.polyline( + points=[(x, y_up * 30 + 30), (x + 90, y_up * 30 + 30)], fill='none', stroke='black', stroke_width=2 + ) + ) + self.draw_.add( + self.draw_.rect( + insert=(x + 42.5, y_up * 30 + 25), + size=(6, 12), + rx=0, + ry=0, + fill=fill_c, + stroke='black', + stroke_width=1.5, + ) + ) + + self.draw_.add(self.draw_.text(name, insert=(x + 40 + shift, y_up * 30 + 20), font_size=9)) + self.draw_.add(self.draw_.text('θ=' + str(np.round(theta, 3)), insert=(x + 55, y_up * 30 + 20), font_size=7)) def draw_homodyne(self, order, wire, phi, name=None): - """ - Draw homodyne measurement. - """ + """Draw homodyne measurement.""" fill_c = 'black' shift = 5 x = 90 * order + 40 y_up = wire - self.draw_.add(self.draw_.polyline(points=[(x, y_up*30+30), (x+90, y_up*30+30)], - fill='none', stroke='black', stroke_width=2)) - self.draw_.add(self.draw_.rect(insert=(x+42.5, y_up*30+25), size=(14,14), rx=0, ry=0, - fill=fill_c, stroke='black', stroke_width=1.5)) - self.draw_.add(self.draw_.text(name, insert=(x+40+shift, y_up*30+20), font_size=9)) + self.draw_.add( + self.draw_.polyline( + points=[(x, y_up * 30 + 30), (x + 90, y_up * 30 + 30)], fill='none', stroke='black', stroke_width=2 + ) + ) + self.draw_.add( + self.draw_.rect( + insert=(x + 42.5, y_up * 30 + 25), + size=(14, 14), + rx=0, + ry=0, + fill=fill_c, + stroke='black', + stroke_width=1.5, + ) + ) + self.draw_.add(self.draw_.text(name, insert=(x + 40 + shift, y_up * 30 + 20), font_size=9)) arc_radius = 6 - arc_center_x = x + 42.5 + 14/2 - arc_center_y = y_up*30+25 + 14/2 + arc_center_x = x + 42.5 + 14 / 2 + arc_center_y = y_up * 30 + 25 + 14 / 2 start_x = arc_center_x - arc_radius - start_y = arc_center_y +3 + start_y = arc_center_y + 3 end_x = arc_center_x + arc_radius - end_y = arc_center_y +3 - arc_path = f"M {start_x} {start_y} A {arc_radius} {arc_radius} 0 0 1 {end_x} {end_y}" + end_y = arc_center_y + 3 + arc_path = f'M {start_x} {start_y} A {arc_radius} {arc_radius} 0 0 1 {end_x} {end_y}' self.draw_.add(self.draw_.path(d=arc_path, stroke='white', fill='none', stroke_width=1.5)) line_start_x = arc_center_x - line_start_y = arc_center_y+3 + line_start_y = arc_center_y + 3 line_end_x = arc_center_x line_end_y = arc_center_y - arc_radius - line_path = f"M {line_start_x} {line_start_y} L {line_end_x} {line_end_y}" + line_path = f'M {line_start_x} {line_start_y} L {line_end_x} {line_end_y}' rotation = 45 - self.draw_.add(self.draw_.path(d=line_path, stroke='white', fill='none', stroke_width=1.5, - transform = f"rotate({rotation} {arc_center_x} {arc_center_y})")) - self.draw_.add(self.draw_.text('ϕ='+str(np.round(phi,3)), insert=(x+55, y_up*30+20), font_size=7)) + self.draw_.add( + self.draw_.path( + d=line_path, + stroke='white', + fill='none', + stroke_width=1.5, + transform=f'rotate({rotation} {arc_center_x} {arc_center_y})', + ) + ) + self.draw_.add(self.draw_.text('ϕ=' + str(np.round(phi, 3)), insert=(x + 55, y_up * 30 + 20), font_size=7)) def draw_sq(self, order, wires, para_dic, name=None): - """ - Draw squeezing gate, displacement gate. - """ + """Draw squeezing gate, displacement gate.""" x = 90 * order + 40 wires = sorted(wires) y_up = wires[0] for i in range(len(wires)): wire_i = wires[i] - self.draw_.add(self.draw_.polyline(points=[(x, wire_i*30+30), (x+90, wire_i*30+30)], - fill='none', stroke='black', stroke_width=2)) + self.draw_.add( + self.draw_.polyline( + points=[(x, wire_i * 30 + 30), (x + 90, wire_i * 30 + 30)], + fill='none', + stroke='black', + stroke_width=2, + ) + ) fill_c = info_dic[name][0] # squeezing gate or displacement gate - shift= info_dic[name][1] + shift = info_dic[name][1] - if len(wires)==1: + if len(wires) == 1: height = 12 - if len(wires)==2: - height = 12*3+3 - - self.draw_.add(self.draw_.rect(insert=(x+42.5, y_up*30+25), size=(10, height), rx=0, ry=0, - fill=fill_c, stroke='black', stroke_width=1.5)) - self.draw_.add(self.draw_.text(name, insert=(x+40+shift, y_up*30+20), font_size=9)) - - k = 0 - for key in para_dic.keys(): - self.draw_.add(self.draw_.text(key + '=' + str(np.round(para_dic[key],3)), - insert=(x+55, y_up*30+18+6*k), font_size=7)) - k += 1 + if len(wires) == 2: + height = 12 * 3 + 3 + + self.draw_.add( + self.draw_.rect( + insert=(x + 42.5, y_up * 30 + 25), + size=(10, height), + rx=0, + ry=0, + fill=fill_c, + stroke='black', + stroke_width=1.5, + ) + ) + self.draw_.add(self.draw_.text(name, insert=(x + 40 + shift, y_up * 30 + 20), font_size=9)) + + for k, key in enumerate(para_dic): + self.draw_.add( + self.draw_.text( + key + '=' + str(np.round(para_dic[key], 3)), insert=(x + 55, y_up * 30 + 18 + 6 * k), font_size=7 + ) + ) def draw_delay(self, order, wires, inputs=None): - """ - Draw delay loop. - """ + """Draw delay loop.""" x = 90 * order + 40 y_up = wires[0] for i in range(len(wires)): wire_i = wires[i] - self.draw_.add(self.draw_.polyline(points=[(x, wire_i*30+30), (x+90, wire_i*30+30)], - fill='none', stroke='black', stroke_width=2)) - self.draw_.add(self.draw_.circle(center=(x+46, y_up*30+25-4), r=9, stroke='black', fill='white', stroke_width=1.2)) - self.draw_.add(self.draw_.text('N='+str(inputs[0]), insert=(x+40, y_up*30+18), font_size=5)) - self.draw_.add(self.draw_.text('θ='+str(np.round(inputs[1],2)), insert=(x+58, y_up*30+18), font_size=6)) - self.draw_.add(self.draw_.text('ϕ='+str(np.round(inputs[2],2)), insert=(x+58, y_up*30+24), font_size=6)) + self.draw_.add( + self.draw_.polyline( + points=[(x, wire_i * 30 + 30), (x + 90, wire_i * 30 + 30)], + fill='none', + stroke='black', + stroke_width=2, + ) + ) + self.draw_.add( + self.draw_.circle(center=(x + 46, y_up * 30 + 25 - 4), r=9, stroke='black', fill='white', stroke_width=1.2) + ) + self.draw_.add(self.draw_.text('N=' + str(inputs[0]), insert=(x + 40, y_up * 30 + 18), font_size=5)) + self.draw_.add( + self.draw_.text('θ=' + str(np.round(inputs[1], 2)), insert=(x + 58, y_up * 30 + 18), font_size=6) + ) + self.draw_.add( + self.draw_.text('ϕ=' + str(np.round(inputs[2], 2)), insert=(x + 58, y_up * 30 + 24), font_size=6) + ) def draw_loss(self, order, wires, name, t): - """ - Draw loss gate. - """ + """Draw loss gate.""" x = 90 * order + 40 y_up = wires[0] - self.draw_.add(self.draw_.polyline(points=[(x, y_up*30+30), (x+90, y_up*30+30)], - fill='none', stroke='black', stroke_width=2)) - - start = (x+18, y_up*30+23) - end = (x+38, y_up*30+23) + self.draw_.add( + self.draw_.polyline( + points=[(x, y_up * 30 + 30), (x + 90, y_up * 30 + 30)], fill='none', stroke='black', stroke_width=2 + ) + ) + + start = (x + 18, y_up * 30 + 23) + end = (x + 38, y_up * 30 + 23) num_waves = 4 - wave_amplitude = [1.5]*3 + [3]*2 + [1.5]*3 + wave_amplitude = [1.5] * 3 + [3] * 2 + [1.5] * 3 wave_length = (end[0] - start[0]) / num_waves - path_d = f"M {start[0]},{start[1]} " + path_d = f'M {start[0]},{start[1]} ' for i in range(num_waves * 2): x = start[0] + i * wave_length / 2 - y = start[1] + (-1)**i * wave_amplitude[i] - path_d += f"L {x},{y} " - path_d += f"L {end[0]},{end[1]}" - path_d += f"L {end[0]+12},{end[1]}" - path = self.draw_.path(d=path_d, fill="none", stroke="gray", stroke_width=2) - arrow_marker = self.draw_.marker(insert=(3.5, 1.8), size=(10, 5), orient="auto") - arrow_marker.add(self.draw_.path(d="M 0 0 L 5 1.5 L 0 4 Z", fill="gray")) + y = start[1] + (-1) ** i * wave_amplitude[i] + path_d += f'L {x},{y} ' + path_d += f'L {end[0]},{end[1]}' + path_d += f'L {end[0] + 12},{end[1]}' + path = self.draw_.path(d=path_d, fill='none', stroke='gray', stroke_width=2) + arrow_marker = self.draw_.marker(insert=(3.5, 1.8), size=(10, 5), orient='auto') + arrow_marker.add(self.draw_.path(d='M 0 0 L 5 1.5 L 0 4 Z', fill='gray')) self.draw_.defs.add(arrow_marker) path.set_markers((None, None, arrow_marker)) - path.rotate(angle=-45, center=(x+10, y_up*30+18)) + path.rotate(angle=-45, center=(x + 10, y_up * 30 + 18)) self.draw_.add(path) - self.draw_.add(self.draw_.text('T='+ str(np.round(t, 3)), insert=(x-14, y_up*30+25), font_size=7)) + self.draw_.add(self.draw_.text('T=' + str(np.round(t, 3)), insert=(x - 14, y_up * 30 + 25), font_size=7)) def draw_any(self, order, wires, name, para_dict=None): - """ - Draw arbitrary unitary gate. - """ + """Draw arbitrary unitary gate.""" fill_c = info_dic[name][0] # shift= info_dic[name][1] x = 90 * order + 40 @@ -365,39 +441,64 @@ def draw_any(self, order, wires, name, para_dict=None): h = (int(len(wires)) - 1) * 30 + 20 width = 50 for k in wires: - self.draw_.add(self.draw_.polyline(points=[(x, k*30+30),(x+20, k*30+30)], - fill='none', stroke='black', stroke_width=2)) - - self.draw_.add(self.draw_.polyline(points=[(x+70, k*30+30),(x+90, k*30+30)], - fill='none', stroke='black', stroke_width=2)) - - self.draw_.add(self.draw_.rect(insert=(x+20, y_up*30+20), size=(width, h), rx=0, ry=0, - fill=fill_c, stroke='black', stroke_width=2)) - self.draw_.add(self.draw_.text(name, insert=((x+2*(10+width)/3), y_up*30+15+h/2), font_size=10)) + self.draw_.add( + self.draw_.polyline( + points=[(x, k * 30 + 30), (x + 20, k * 30 + 30)], fill='none', stroke='black', stroke_width=2 + ) + ) + + self.draw_.add( + self.draw_.polyline( + points=[(x + 70, k * 30 + 30), (x + 90, k * 30 + 30)], fill='none', stroke='black', stroke_width=2 + ) + ) + + self.draw_.add( + self.draw_.rect( + insert=(x + 20, y_up * 30 + 20), + size=(width, h), + rx=0, + ry=0, + fill=fill_c, + stroke='black', + stroke_width=2, + ) + ) + self.draw_.add(self.draw_.text(name, insert=((x + 2 * (10 + width) / 3), y_up * 30 + 15 + h / 2), font_size=10)) if para_dict is not None: for i, key in enumerate(para_dict): - self.draw_.add(self.draw_.text(key + '=' + str(np.round(para_dict[key],3)), - insert=((x+2*(10+width)/3-2), y_up*30+15+h/2+8*(i+1)), font_size=7)) + self.draw_.add( + self.draw_.text( + key + '=' + str(np.round(para_dict[key], 3)), + insert=((x + 2 * (10 + width) / 3 - 2), y_up * 30 + 15 + h / 2 + 8 * (i + 1)), + font_size=7, + ) + ) def draw_lines(self, order, wires): - """ - Act nothing. - """ + """Act nothing.""" x = 90 * order + 40 for k in wires: - self.draw_.add(self.draw_.polyline(points=[(x, k*30+30),(x+90, k*30+30)], - fill='none', stroke='black', stroke_width=2)) + self.draw_.add( + self.draw_.polyline( + points=[(x, k * 30 + 30), (x + 90, k * 30 + 30)], fill='none', stroke='black', stroke_width=2 + ) + ) + def barrier(self, order, wires, cl='black'): x = 90 * order + 40 y_min = 15 y_max = self.nmode * 30 + 25 - y_up = wires[0] * (y_max-y_min)/self.nmode + y_min - y_down = (1 + wires[-1]) * (y_max-y_min)/self.nmode + y_min - self.draw_.add(self.draw_.polyline(points=[(x, y_up),(x, y_down)], - fill='none', stroke_dasharray='5,5', stroke=cl, stroke_width=2)) + y_up = wires[0] * (y_max - y_min) / self.nmode + y_min + y_down = (1 + wires[-1]) * (y_max - y_min) / self.nmode + y_min + self.draw_.add( + self.draw_.polyline( + points=[(x, y_up), (x, y_down)], fill='none', stroke_dasharray='5,5', stroke=cl, stroke_width=2 + ) + ) -class DrawClements(): +class DrawClements: """Draw the n-mode Clements architecture. Args: @@ -408,40 +509,32 @@ class DrawClements(): method (str, optional): The way for Clements decomposition, ``'cssr'`` or ``'cssl'``. Default: ``'cssr'`` """ - def __init__( - self, - nmode: int, - mzi_info: Dict, - cl: str = 'dodgerblue', - fs: int = 30, - method: str = 'cssr' - ) -> None: + + def __init__(self, nmode: int, mzi_info: dict, cl: str = 'dodgerblue', fs: int = 30, method: str = 'cssr') -> None: self.nmode = nmode self.method = method self.mzi_info = mzi_info self.color = cl - self.fontsize =fs + self.fontsize = fs self.wid = 0.1 self.height = 0.08 self.axis_off = 'off' - self.phase_angle = self.mzi_info['phase_angle'] # for phase shifter - self.dic_mzi = self.sort_mzi() # for mzi parameters in the same array + self.phase_angle = self.mzi_info['phase_angle'] # for phase shifter + self.dic_mzi = self.sort_mzi() # for mzi parameters in the same array self.ps_position = self.ps_pos() def plotting_clements(self): - """Plot clements structure with ``cssr`` or ``cssl`` type.""" + """Plot Clements structure with ``'cssr'`` or ``'cssl'`` type.""" if self.method == 'cssr': - assert(self.nmode%2 == 0), 'plotting only valid for even modes' + assert self.nmode % 2 == 0, 'plotting only valid for even modes' self.plotting_clements_1() if self.method == 'cssl': self.plotting_clements_2() def plotting_clements_1(self): - """ - Plot ``cssr`` with left to right order. - """ + """Plot ``'cssr'`` with left to right order.""" fig, ax = plt.subplots(1, 1) - fig.set_size_inches(8*3,5*3) + fig.set_size_inches(8 * 3, 5 * 3) # plt.rcParams['figure.figsize'] = (8*3,5.0*3) coords1 = [] coords2 = [] @@ -452,78 +545,86 @@ def plotting_clements_1(self): wid = self.wid height = self.height for i in range(nmode): - plt.annotate('',xy=(-0.1,1-0.25*i), - xytext=(-0.5,1-0.25*i), - arrowprops={'arrowstyle': '-|>', 'lw':5}, - va = 'center',) - plt.text(-0.8, 1-0.25*i, f'{i}', fontsize = fs ) - plt.plot([0, 1.2], [1-0.25*i,1-0.25*i], color = cl) - plt.text( 3.2*(nmode/2-1)+2.2+2.1, 1-0.25*i+0.05, f'{phase_angle[i]:.3f}', fontsize=fs-8 ) # phase angle + plt.annotate( + '', + xy=(-0.1, 1 - 0.25 * i), + xytext=(-0.5, 1 - 0.25 * i), + arrowprops={'arrowstyle': '-|>', 'lw': 5}, + va='center', + ) + plt.text(-0.8, 1 - 0.25 * i, f'{i}', fontsize=fs) + plt.plot([0, 1.2], [1 - 0.25 * i, 1 - 0.25 * i], color=cl) + plt.text( + 3.2 * (nmode / 2 - 1) + 2.2 + 2.1, 1 - 0.25 * i + 0.05, f'{phase_angle[i]:.3f}', fontsize=fs - 8 + ) # phase angle ax.add_patch( - patches.Rectangle( - (3.2*(nmode/2-1)+2.2+2.1, 1-0.25*i-0.05), - wid, - height, - edgecolor = 'green', - facecolor = 'green', - fill=True, - zorder=3) ) ## for PS - if nmode%2==1: - plt.plot([2.2+3.2*(int((nmode+1)/2)-1), - 3.2*int((nmode+1)/2-1)+2.2+2.2], - [1-0.25*i,1-0.25*i], - color = cl) - if nmode%2==0: # for even mode - for i in range(int(nmode/2)): - plt.plot([2.2+3.2*i, 3.2*i+2.2+2.2], [1,1], color = cl) - plt.plot([2.2+3.2*i, 3.2*i+2.2+2.2], [1-0.25*(nmode-1), 1-0.25*(nmode-1) ], color = cl) + patches.Rectangle( + (3.2 * (nmode / 2 - 1) + 2.2 + 2.1, 1 - 0.25 * i - 0.05), + wid, + height, + edgecolor='green', + facecolor='green', + fill=True, + zorder=3, + ) + ) ## for PS + if nmode % 2 == 1: + plt.plot( + [2.2 + 3.2 * (int((nmode + 1) / 2) - 1), 3.2 * int((nmode + 1) / 2 - 1) + 2.2 + 2.2], + [1 - 0.25 * i, 1 - 0.25 * i], + color=cl, + ) + if nmode % 2 == 0: # for even mode + for i in range(int(nmode / 2)): + plt.plot([2.2 + 3.2 * i, 3.2 * i + 2.2 + 2.2], [1, 1], color=cl) + plt.plot( + [2.2 + 3.2 * i, 3.2 * i + 2.2 + 2.2], [1 - 0.25 * (nmode - 1), 1 - 0.25 * (nmode - 1)], color=cl + ) for j in range(nmode): - plt.plot([1.5+3.2*i, 3.2*i+1.9], [1-0.25*j,1-0.25*j], color = cl) - coords1.append( [1.5+3.2*i, 3.2*i+1.9, 1-0.25*j,1-0.25*j]) - if 0= nmode-1: - plt.plot([1.2+3.2*i, 3.2*i+2.2], [1-0.25*j,1-0.25*j], color = cl) - if i< int((nmode+1)/2)-1 and 0= nmode - 1: + plt.plot([1.2 + 3.2 * i, 3.2 * i + 2.2], [1 - 0.25 * j, 1 - 0.25 * j], color=cl) + if i < int((nmode + 1) / 2) - 1 and 0 < j < nmode: # remove the last column + plt.plot([3.1 + 3.2 * i, 3.2 * i + 3.5], [1 - 0.25 * j, 1 - 0.25 * j], color=cl) + coords2.append([3.1 + 3.2 * i, 3.2 * i + 3.5, 1 - 0.25 * j, 1 - 0.25 * j]) + plt.plot([2.2 + 3.2 * i, 3.2 * i + 2.8], [1 - 0.25 * j, 1 - 0.25 * j], color=cl) + plt.plot([3.8 + 3.2 * i, 3.2 * i + 4.4], [1 - 0.25 * j, 1 - 0.25 * j], color=cl) # connecting lines i, i+1 - for i in range(len(coords1)): - if i%2==0: - self.connect1(coords1[i], ax, a=-0.5-0.4, c=0.7-0.7, cl=self.color) - if i%2==1: + for i in range(len(coords1)): + if i % 2 == 0: + self.connect1(coords1[i], ax, a=-0.5 - 0.4, c=0.7 - 0.7, cl=self.color) + if i % 2 == 1: self.connect2(coords1[i], cl=self.color) - for i in range(len(coords2)): - if i%2==0: - self.connect1(coords2[i], ax, a=-0.5-0.4, c=0.7-0.7, cl=self.color) - if i%2==1: + for i in range(len(coords2)): + if i % 2 == 0: + self.connect1(coords2[i], ax, a=-0.5 - 0.4, c=0.7 - 0.7, cl=self.color) + if i % 2 == 1: self.connect2(coords2[i], cl=self.color) # plotting paras - self.plot_paras_1(self.dic_mzi, fs=self.fontsize-8) + self.plot_paras_1(self.dic_mzi, fs=self.fontsize - 8) plt.axis(self.axis_off) # if self.axis_off: # plt.axis('off') plt.show() def plotting_clements_2(self): - """ - Plot ``cssl`` with right to left order. - """ + """Plot ``cssl`` with right to left order.""" fig, ax = plt.subplots(1, 1) - fig.set_size_inches(8*3,5*3) + fig.set_size_inches(8 * 3, 5 * 3) # plt.rcParams['figure.figsize'] = (8*3,5.0*3) coords1 = [] coords2 = [] @@ -534,186 +635,168 @@ def plotting_clements_2(self): wid = self.wid height = self.height for i in range(nmode): - plt.annotate('',xy=(-0.1,1-0.25*i), - xytext=(-0.5,1-0.25*i), - arrowprops={'arrowstyle': '-|>', 'lw':5}, - va='center',) - plt.text(-0.8, 1-0.25*i, f'{i}', fontsize = fs ) - plt.plot([0, 1.2], [1-0.25*i,1-0.25*i], color = cl) - plt.text( 0.4,1-0.25*i+0.05, f'{phase_angle[i]:.3f}', fontsize=fs-8 ) # phase angle + plt.annotate( + '', + xy=(-0.1, 1 - 0.25 * i), + xytext=(-0.5, 1 - 0.25 * i), + arrowprops={'arrowstyle': '-|>', 'lw': 5}, + va='center', + ) + plt.text(-0.8, 1 - 0.25 * i, f'{i}', fontsize=fs) + plt.plot([0, 1.2], [1 - 0.25 * i, 1 - 0.25 * i], color=cl) + plt.text(0.4, 1 - 0.25 * i + 0.05, f'{phase_angle[i]:.3f}', fontsize=fs - 8) # phase angle ax.add_patch( - patches.Rectangle( - (0.5,1-0.25*i-0.05), - wid, - height, - edgecolor = cl, - facecolor = cl, - fill=True - ) ) - if nmode%2==1: - plt.plot([2.2+3.2*(int((nmode+1)/2)-1), - 3.2*int((nmode+1)/2-1)+2.2+2.2], - [1-0.25*i,1-0.25*i], - color = cl) - if nmode%2==0: # for even mode - for i in range(int(nmode/2)): - plt.plot([2.2+3.2*i, 3.2*i+2.2+2.2], [1,1], color = cl) - plt.plot([2.2+3.2*i, 3.2*i+2.2+2.2], [1-0.25*(nmode-1), 1-0.25*(nmode-1) ], color = cl) + patches.Rectangle((0.5, 1 - 0.25 * i - 0.05), wid, height, edgecolor=cl, facecolor=cl, fill=True) + ) + if nmode % 2 == 1: + plt.plot( + [2.2 + 3.2 * (int((nmode + 1) / 2) - 1), 3.2 * int((nmode + 1) / 2 - 1) + 2.2 + 2.2], + [1 - 0.25 * i, 1 - 0.25 * i], + color=cl, + ) + if nmode % 2 == 0: # for even mode + for i in range(int(nmode / 2)): + plt.plot([2.2 + 3.2 * i, 3.2 * i + 2.2 + 2.2], [1, 1], color=cl) + plt.plot( + [2.2 + 3.2 * i, 3.2 * i + 2.2 + 2.2], [1 - 0.25 * (nmode - 1), 1 - 0.25 * (nmode - 1)], color=cl + ) for j in range(nmode): - plt.plot([1.5+3.2*i, 3.2*i+1.9], [1-0.25*j,1-0.25*j], color = cl) - coords1.append([1.5+3.2*i, 3.2*i+1.9, 1-0.25*j,1-0.25*j]) - if 0= nmode-1: - plt.plot([1.2+3.2*i, 3.2*i+2.2], [1-0.25*j,1-0.25*j], color = cl) - if i< int((nmode+1)/2)-1 and 0= nmode - 1: + plt.plot([1.2 + 3.2 * i, 3.2 * i + 2.2], [1 - 0.25 * j, 1 - 0.25 * j], color=cl) + if i < int((nmode + 1) / 2) - 1 and 0 < j < nmode: # remove the last column + plt.plot([3.1 + 3.2 * i, 3.2 * i + 3.5], [1 - 0.25 * j, 1 - 0.25 * j], color=cl) + coords2.append([3.1 + 3.2 * i, 3.2 * i + 3.5, 1 - 0.25 * j, 1 - 0.25 * j]) + plt.plot([2.2 + 3.2 * i, 3.2 * i + 2.8], [1 - 0.25 * j, 1 - 0.25 * j], color=cl) + plt.plot([3.8 + 3.2 * i, 3.2 * i + 4.4], [1 - 0.25 * j, 1 - 0.25 * j], color=cl) # connecting lines i, i+1 - for i in range(len(coords1)): - if i%2==0: + for i in range(len(coords1)): + if i % 2 == 0: self.connect1(coordinate=coords1[i], ax=ax, cl=self.color) - if i%2==1: + if i % 2 == 1: self.connect2(coordinate=coords1[i], ax=ax, cl=self.color) - for i in range(len(coords2)): - if i%2==0: + for i in range(len(coords2)): + if i % 2 == 0: self.connect1(coordinate=coords2[i], ax=ax, cl=self.color) - if i%2==1: + if i % 2 == 1: self.connect2(coordinate=coords2[i], ax=ax, cl='black') # plotting paras - self.plot_paras(self.dic_mzi, self.nmode, fs=self.fontsize-8) + self.plot_paras(self.dic_mzi, self.nmode, fs=self.fontsize - 8) plt.axis(self.axis_off) # if self.axis_off: # plt.axis('off') plt.show() def sort_mzi(self): - """ - Sort mzi parameters in the same array for plotting. - """ - dic_mzi = defaultdict( list) #当key不存在时对应的value是[] + """Sort mzi parameters in the same array for plotting.""" + dic_mzi = defaultdict(list) # 当key不存在时对应的value是[] mzi_list = self.mzi_info['MZI_list'] for i in mzi_list: dic_mzi[tuple(i[0:2])].append(i[2:]) return dic_mzi def ps_pos(self): - """ - Label the position of each phaseshifter for ``cssr`` case. - """ + """Label the position of each phaseshifter for ``'cssr'`` case.""" if self.method == 'cssr': - dic_pos = { } + dic_pos = {} nmode = self.nmode phase_angle = self.phase_angle - dic_ =self.dic_mzi + dic_ = self.dic_mzi for mode in range(nmode): - pair = (mode, mode+1) + pair = (mode, mode + 1) value = dic_[pair] value = np.array(value).flatten() for k in range(len(value)): dic_pos[(mode, k)] = np.round(value[k], 4) - if mode == nmode -1: + if mode == nmode - 1: dic_pos[(mode, 0)] = np.round(phase_angle[mode], 4) else: - dic_pos[(mode, k+1)] = np.round(phase_angle[mode], 4) + dic_pos[(mode, k + 1)] = np.round(phase_angle[mode], 4) return dic_pos else: return None @staticmethod def connect1(coordinate, ax, cl, wid=0.1, height=0.08, a=-0.05, b=-0.05, c=0.7, d=-0.05): - """ - Connect odd column. - """ + """Connect odd column.""" x0, x1, y0, y1 = coordinate - # print(x0,x1,y0,y1) - plt.plot([x0, x0-0.3],[y0, y0-0.25], color = cl) - plt.plot([x1, x1+0.3],[y1, y1-0.25], color = cl) - ax.add_patch(patches.Rectangle( - ((x0+x1)/2 + a, y0 + b), - wid, - height, - edgecolor = cl, - facecolor = cl, - fill=True - ) ) - ax.add_patch(patches.Rectangle( - ((x0+x1)/2 + c, y0 + d), - wid, - height, - edgecolor = cl, - facecolor = cl, - fill=True - ) ) + # print(x0,x1,y0,y1) + plt.plot([x0, x0 - 0.3], [y0, y0 - 0.25], color=cl) + plt.plot([x1, x1 + 0.3], [y1, y1 - 0.25], color=cl) + ax.add_patch(patches.Rectangle(((x0 + x1) / 2 + a, y0 + b), wid, height, edgecolor=cl, facecolor=cl, fill=True)) + ax.add_patch(patches.Rectangle(((x0 + x1) / 2 + c, y0 + d), wid, height, edgecolor=cl, facecolor=cl, fill=True)) @staticmethod def connect2(coordinate, cl): - """ - Connect even column. - """ + """Connect even column.""" x0, x1, y0, y1 = coordinate - plt.plot([x0, x0-0.3],[y0, y0+0.25], color = cl) - plt.plot([x1, x1+0.3],[y1, y1+0.25], color = cl) + plt.plot([x0, x0 - 0.3], [y0, y0 + 0.25], color=cl) + plt.plot([x1, x1 + 0.3], [y1, y1 + 0.25], color=cl) @staticmethod def plot_paras(sort_mzi_dic, nmode, fs=20): - """ - Plot mzi parameters for ``cssl`` case. - """ - for i in sort_mzi_dic.keys(): - if i[0]%2 == 0: # 0, 2, 4, 6.. + """Plot mzi parameters for ``'cssl'`` case.""" + for i in sort_mzi_dic: + if i[0] % 2 == 0: # 0, 2, 4, 6.. temp_values = sort_mzi_dic[i] len_ = len(temp_values) for j in range(len_): - plt.text(8.6-3.2*j+3.2*((nmode-6)//2+nmode%2), - 1-0.25*i[0]+0.05, - f'{temp_values[j][0]:.3f}', - fontsize=fs) - plt.text(7.8-3.2*j+3.2*((nmode-6)//2+nmode%2), - 1-0.25*i[0]+0.05, - f'{temp_values[j][1]:.3f}', - fontsize=fs) - if i[0]%2 ==1: # 1, 3.. + plt.text( + 8.6 - 3.2 * j + 3.2 * ((nmode - 6) // 2 + nmode % 2), + 1 - 0.25 * i[0] + 0.05, + f'{temp_values[j][0]:.3f}', + fontsize=fs, + ) + plt.text( + 7.8 - 3.2 * j + 3.2 * ((nmode - 6) // 2 + nmode % 2), + 1 - 0.25 * i[0] + 0.05, + f'{temp_values[j][1]:.3f}', + fontsize=fs, + ) + if i[0] % 2 == 1: # 1, 3.. temp_values = sort_mzi_dic[i] len_ = len(temp_values) for j in range(len_): - plt.text(8.6-3.2*j+1.6+3.2*((nmode-6)//2), - 1-0.25*i[0]+0.05, - f'{temp_values[j][0]:.3f}', - fontsize=fs) - plt.text(7.8-3.2*j+1.6+3.2*((nmode-6)//2), - 1-0.25*i[0]+0.05, - f'{temp_values[j][1]:.3f}', - fontsize=fs) + plt.text( + 8.6 - 3.2 * j + 1.6 + 3.2 * ((nmode - 6) // 2), + 1 - 0.25 * i[0] + 0.05, + f'{temp_values[j][0]:.3f}', + fontsize=fs, + ) + plt.text( + 7.8 - 3.2 * j + 1.6 + 3.2 * ((nmode - 6) // 2), + 1 - 0.25 * i[0] + 0.05, + f'{temp_values[j][1]:.3f}', + fontsize=fs, + ) @staticmethod def plot_paras_1(sort_mzi_dic, fs=20): - """ - Plot mzi parameters for ``cssr`` case. - """ - for i in sort_mzi_dic.keys(): - if i[0]%2 == 0: # 0, 2, 4, 6.. + """Plot mzi parameters for ``'cssr'`` case.""" + for i in sort_mzi_dic: + if i[0] % 2 == 0: # 0, 2, 4, 6.. temp_values = sort_mzi_dic[i] len_ = len(temp_values) for j in range(len_): - plt.text(3.2*j+0.6, 1-0.25*i[0]+0.05, f'{temp_values[j][0]:.3f}', fontsize=fs) - plt.text(3.2*j+0.6+0.9, 1-0.25*i[0]+0.05, f'{temp_values[j][1]:.3f}', fontsize=fs) - if i[0]%2 ==1: # 1, 3.. + plt.text(3.2 * j + 0.6, 1 - 0.25 * i[0] + 0.05, f'{temp_values[j][0]:.3f}', fontsize=fs) + plt.text(3.2 * j + 0.6 + 0.9, 1 - 0.25 * i[0] + 0.05, f'{temp_values[j][1]:.3f}', fontsize=fs) + if i[0] % 2 == 1: # 1, 3.. temp_values = sort_mzi_dic[i] len_ = len(temp_values) for j in range(len_): - plt.text(3.2*j+0.6+1.6, 1-0.25*i[0]+0.05, f'{temp_values[j][0]:.3f}', fontsize=fs) - plt.text(3.2*j+0.6+2.4, 1-0.25*i[0]+0.05, f'{temp_values[j][1]:.3f}', fontsize=fs) + plt.text(3.2 * j + 0.6 + 1.6, 1 - 0.25 * i[0] + 0.05, f'{temp_values[j][0]:.3f}', fontsize=fs) + plt.text(3.2 * j + 0.6 + 2.4, 1 - 0.25 * i[0] + 0.05, f'{temp_values[j][1]:.3f}', fontsize=fs) diff --git a/src/deepquantum/photonic/gate.py b/src/deepquantum/photonic/gate.py index f1a19c7f..4923c508 100644 --- a/src/deepquantum/photonic/gate.py +++ b/src/deepquantum/photonic/gate.py @@ -1,17 +1,16 @@ -""" -Photonic quantum gates -""" +"""Photonic quantum gates""" import copy import itertools -from typing import Any, List, Optional, Tuple, Union +from typing import Any import torch from torch import nn import deepquantum.photonic as dqp + from ..qmath import is_unitary -from .operation import Gate, Delay +from .operation import Delay, Gate from .qmath import ladder_ops @@ -32,21 +31,23 @@ class SingleGate(Gate): mu (float, optional): The mean of Gaussian noise. Default: 0 sigma (float, optional): The standard deviation of Gaussian noise. Default: 0.1 """ + def __init__( self, - name: Optional[str] = None, + name: str | None = None, inputs: Any = None, nmode: int = 1, - wires: Union[int, List[int], None] = None, - cutoff: Optional[int] = None, + wires: int | list[int] | None = None, + cutoff: int | None = None, den_mat: bool = False, requires_grad: bool = False, noise: bool = False, mu: float = 0, - sigma: float = 0.1 + sigma: float = 0.1, ) -> None: - super().__init__(name=name, nmode=nmode, wires=wires, cutoff=cutoff, - den_mat=den_mat, noise=noise, mu=mu, sigma=sigma) + super().__init__( + name=name, nmode=nmode, wires=wires, cutoff=cutoff, den_mat=den_mat, noise=noise, mu=mu, sigma=sigma + ) assert len(self.wires) == 1, f'{self.name} must act on one mode' self.requires_grad = requires_grad self.init_para(inputs) @@ -69,23 +70,25 @@ class DoubleGate(Gate): mu (float, optional): The mean of Gaussian noise. Default: 0 sigma (float, optional): The standard deviation of Gaussian noise. Default: 0.1 """ + def __init__( self, - name: Optional[str] = None, + name: str | None = None, inputs: Any = None, nmode: int = 2, - wires: Union[int, List[int], None] = None, - cutoff: Optional[int] = None, + wires: int | list[int] | None = None, + cutoff: int | None = None, den_mat: bool = False, requires_grad: bool = False, noise: bool = False, mu: float = 0, - sigma: float = 0.1 + sigma: float = 0.1, ) -> None: if wires is None: wires = [0, 1] - super().__init__(name=name, nmode=nmode, wires=wires, cutoff=cutoff, - den_mat=den_mat, noise=noise, mu=mu, sigma=sigma) + super().__init__( + name=name, nmode=nmode, wires=wires, cutoff=cutoff, den_mat=den_mat, noise=noise, mu=mu, sigma=sigma + ) assert len(self.wires) == 2, f'{self.name} must act on two modes' self.requires_grad = requires_grad self.init_para(inputs) @@ -133,22 +136,33 @@ class PhaseShift(SingleGate): sigma (float, optional): The standard deviation of Gaussian noise. Default: 0.1 inv_mode (bool, optional): Whether the rotation in the phase space is clockwise. Default: ``False`` """ + def __init__( self, inputs: Any = None, nmode: int = 1, - wires: Union[int, List[int], None] = None, - cutoff: Optional[int] = None, + wires: int | list[int] | None = None, + cutoff: int | None = None, den_mat: bool = False, requires_grad: bool = False, noise: bool = False, mu: float = 0, sigma: float = 0.1, - inv_mode: bool = False + inv_mode: bool = False, ) -> None: self.inv_mode = inv_mode - super().__init__(name='PhaseShift', inputs=inputs, nmode=nmode, wires=wires, cutoff=cutoff, - den_mat=den_mat, requires_grad=requires_grad, noise=noise, mu=mu, sigma=sigma) + super().__init__( + name='PhaseShift', + inputs=inputs, + nmode=nmode, + wires=wires, + cutoff=cutoff, + den_mat=den_mat, + requires_grad=requires_grad, + noise=noise, + mu=mu, + sigma=sigma, + ) self.npara = 1 def inputs_to_tensor(self, inputs: Any = None) -> torch.Tensor: @@ -160,7 +174,7 @@ def inputs_to_tensor(self, inputs: Any = None) -> torch.Tensor: elif not isinstance(inputs, (torch.Tensor, nn.Parameter)): inputs = torch.tensor(inputs, dtype=torch.float) if self.noise: - inputs = inputs + torch.normal(self.mu, self.sigma, size=(1, )).squeeze() + inputs = inputs + torch.normal(self.mu, self.sigma, size=(1,)).squeeze() return inputs def get_matrix(self, theta: Any) -> torch.Tensor: @@ -181,7 +195,7 @@ def get_matrix_state(self, matrix: torch.Tensor) -> torch.Tensor: """Get the local transformation matrix acting on Fock state tensors.""" return torch.stack([matrix[0, 0] ** n for n in range(self.cutoff)]).reshape(-1).diag_embed() - def get_transform_xp(self, theta: Any) -> Tuple[torch.Tensor, torch.Tensor]: + def get_transform_xp(self, theta: Any) -> tuple[torch.Tensor, torch.Tensor]: """Get the local affine symplectic transformation acting on quadrature operators in ``xxpp`` order.""" theta = self.inputs_to_tensor(theta) if self.inv_mode: @@ -192,7 +206,7 @@ def get_transform_xp(self, theta: Any) -> Tuple[torch.Tensor, torch.Tensor]: vector_xp = torch.zeros(2, 1, dtype=theta.dtype, device=theta.device) return matrix_xp, vector_xp - def update_transform_xp(self) -> Tuple[torch.Tensor, torch.Tensor]: + def update_transform_xp(self) -> tuple[torch.Tensor, torch.Tensor]: """Update the local affine symplectic transformation acting on quadrature operators in ``xxpp`` order.""" matrix_xp, vector_xp = self.get_transform_xp(self.theta) self.matrix_xp = matrix_xp.detach() @@ -213,10 +227,7 @@ def init_para(self, inputs: Any = None) -> None: self.update_transform_xp() def extra_repr(self) -> str: - if self.inv_mode: - theta = -self.theta - else: - theta = self.theta + theta = -self.theta if self.inv_mode else self.theta return f'wires={self.wires}, theta={theta.item()}' @@ -271,30 +282,41 @@ class BeamSplitter(DoubleGate): mu (float, optional): The mean of Gaussian noise. Default: 0 sigma (float, optional): The standard deviation of Gaussian noise. Default: 0.1 """ + def __init__( self, inputs: Any = None, nmode: int = 2, - wires: Optional[List[int]] = None, - cutoff: Optional[int] = None, + wires: list[int] | None = None, + cutoff: int | None = None, den_mat: bool = False, requires_grad: bool = False, noise: bool = False, mu: float = 0, - sigma: float = 0.1 + sigma: float = 0.1, ) -> None: - super().__init__(name='BeamSplitter', inputs=inputs, nmode=nmode, wires=wires, cutoff=cutoff, - den_mat=den_mat, requires_grad=requires_grad, noise=noise, mu=mu, sigma=sigma) + super().__init__( + name='BeamSplitter', + inputs=inputs, + nmode=nmode, + wires=wires, + cutoff=cutoff, + den_mat=den_mat, + requires_grad=requires_grad, + noise=noise, + mu=mu, + sigma=sigma, + ) self.npara = 2 - def inputs_to_tensor(self, inputs: Any = None) -> Tuple[torch.Tensor, torch.Tensor]: + def inputs_to_tensor(self, inputs: Any = None) -> tuple[torch.Tensor, torch.Tensor]: """Convert inputs to torch.Tensor.""" if inputs is None: theta = torch.rand(1)[0] * 2 * torch.pi - phi = torch.rand(1)[0] * 2 * torch.pi + phi = torch.rand(1)[0] * 2 * torch.pi else: theta = inputs[0] - phi = inputs[1] + phi = inputs[1] if not isinstance(theta, (torch.Tensor, nn.Parameter)): theta = torch.tensor(theta, dtype=torch.float) if not isinstance(phi, (torch.Tensor, nn.Parameter)): @@ -303,10 +325,10 @@ def inputs_to_tensor(self, inputs: Any = None) -> Tuple[torch.Tensor, torch.Tens theta, phi = self._add_noise(theta, phi) return theta, phi - def _add_noise(self, theta: torch.Tensor, phi: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: + def _add_noise(self, theta: torch.Tensor, phi: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: """Add Gaussian noise to the parameters.""" - theta = theta + torch.normal(self.mu, self.sigma, size=(1, )).squeeze() - phi = phi + torch.normal(self.mu, self.sigma, size=(1, )).squeeze() + theta = theta + torch.normal(self.mu, self.sigma, size=(1,)).squeeze() + phi = phi + torch.normal(self.mu, self.sigma, size=(1,)).squeeze() return theta, phi def get_matrix(self, theta: Any, phi: Any) -> torch.Tensor: @@ -354,18 +376,19 @@ def get_matrix_state(self, matrix: torch.Tensor) -> torch.Tensor: ) return tran_mat - def get_transform_xp(self, theta: Any, phi: Any) -> Tuple[torch.Tensor, torch.Tensor]: + def get_transform_xp(self, theta: Any, phi: Any) -> tuple[torch.Tensor, torch.Tensor]: """Get the local affine symplectic transformation acting on quadrature operators in ``xxpp`` order.""" theta, phi = self.inputs_to_tensor([theta, phi]) matrix = self.get_matrix(theta, phi) # correspond to: U a^+ U^+ = u^T @ a^+ and U^+ a U = u @ a # correspond to: U a U^+ = (u^*)^T @ a and U^+ a^+ U = u^* @ a^+ - matrix_xp = torch.cat([torch.cat([matrix.real, -matrix.imag], dim=-1), - torch.cat([matrix.imag, matrix.real], dim=-1)], dim=-2).reshape(4, 4) + matrix_xp = torch.cat( + [torch.cat([matrix.real, -matrix.imag], dim=-1), torch.cat([matrix.imag, matrix.real], dim=-1)], dim=-2 + ).reshape(4, 4) vector_xp = torch.zeros(4, 1, dtype=theta.dtype, device=theta.device) return matrix_xp, vector_xp - def update_transform_xp(self) -> Tuple[torch.Tensor, torch.Tensor]: + def update_transform_xp(self) -> tuple[torch.Tensor, torch.Tensor]: """Update the local affine symplectic transformation acting on quadrature operators in ``xxpp`` order.""" matrix_xp, vector_xp = self.get_transform_xp(self.theta, self.phi) self.matrix_xp = matrix_xp.detach() @@ -455,22 +478,32 @@ class MZI(BeamSplitter): mu (float, optional): The mean of Gaussian noise. Default: 0 sigma (float, optional): The standard deviation of Gaussian noise. Default: 0.1 """ + def __init__( self, inputs: Any = None, nmode: int = 2, - wires: Optional[List[int]] = None, - cutoff: Optional[int] = None, + wires: list[int] | None = None, + cutoff: int | None = None, den_mat: bool = False, phi_first: bool = True, requires_grad: bool = False, noise: bool = False, mu: float = 0, - sigma: float = 0.1 + sigma: float = 0.1, ) -> None: self.phi_first = phi_first - super().__init__(inputs=inputs, nmode=nmode, wires=wires, cutoff=cutoff, den_mat=den_mat, - requires_grad=requires_grad, noise=noise, mu=mu, sigma=sigma) + super().__init__( + inputs=inputs, + nmode=nmode, + wires=wires, + cutoff=cutoff, + den_mat=den_mat, + requires_grad=requires_grad, + noise=noise, + mu=mu, + sigma=sigma, + ) self.name = 'MZI' def get_matrix(self, theta: Any, phi: Any) -> torch.Tensor: @@ -536,25 +569,35 @@ class BeamSplitterTheta(BeamSplitter): mu (float, optional): The mean of Gaussian noise. Default: 0 sigma (float, optional): The standard deviation of Gaussian noise. Default: 0.1 """ + def __init__( self, inputs: Any = None, nmode: int = 2, - wires: Optional[List[int]] = None, - cutoff: Optional[int] = None, + wires: list[int] | None = None, + cutoff: int | None = None, den_mat: bool = False, requires_grad: bool = False, noise: bool = False, mu: float = 0, - sigma: float = 0.1 + sigma: float = 0.1, ) -> None: - super().__init__(inputs=inputs, nmode=nmode, wires=wires, cutoff=cutoff, den_mat=den_mat, - requires_grad=requires_grad, noise=noise, mu=mu, sigma=sigma) + super().__init__( + inputs=inputs, + nmode=nmode, + wires=wires, + cutoff=cutoff, + den_mat=den_mat, + requires_grad=requires_grad, + noise=noise, + mu=mu, + sigma=sigma, + ) self.npara = 1 - def _add_noise(self, theta: torch.Tensor, phi: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: + def _add_noise(self, theta: torch.Tensor, phi: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: """Add Gaussian noise to the parameters.""" - theta = theta + torch.normal(self.mu, self.sigma, size=(1, )).squeeze() + theta = theta + torch.normal(self.mu, self.sigma, size=(1,)).squeeze() return theta, phi def init_para(self, inputs: Any = None) -> None: @@ -624,25 +667,35 @@ class BeamSplitterPhi(BeamSplitter): mu (float, optional): The mean of Gaussian noise. Default: 0 sigma (float, optional): The standard deviation of Gaussian noise. Default: 0.1 """ + def __init__( self, inputs: Any = None, nmode: int = 2, - wires: Optional[List[int]] = None, - cutoff: Optional[int] = None, + wires: list[int] | None = None, + cutoff: int | None = None, den_mat: bool = False, requires_grad: bool = False, noise: bool = False, mu: float = 0, - sigma: float = 0.1 + sigma: float = 0.1, ) -> None: - super().__init__(inputs=inputs, nmode=nmode, wires=wires, cutoff=cutoff, den_mat=den_mat, - requires_grad=requires_grad, noise=noise, mu=mu, sigma=sigma) + super().__init__( + inputs=inputs, + nmode=nmode, + wires=wires, + cutoff=cutoff, + den_mat=den_mat, + requires_grad=requires_grad, + noise=noise, + mu=mu, + sigma=sigma, + ) self.npara = 1 - def _add_noise(self, theta: torch.Tensor, phi: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: + def _add_noise(self, theta: torch.Tensor, phi: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: """Add Gaussian noise to the parameters.""" - phi = phi + torch.normal(self.mu, self.sigma, size=(1, )).squeeze() + phi = phi + torch.normal(self.mu, self.sigma, size=(1,)).squeeze() return theta, phi def init_para(self, inputs: Any = None) -> None: @@ -736,22 +789,32 @@ class BeamSplitterSingle(BeamSplitter): mu (float, optional): The mean of Gaussian noise. Default: 0 sigma (float, optional): The standard deviation of Gaussian noise. Default: 0.1 """ + def __init__( self, inputs: Any = None, nmode: int = 2, - wires: Optional[List[int]] = None, - cutoff: Optional[int] = None, + wires: list[int] | None = None, + cutoff: int | None = None, den_mat: bool = False, convention: str = 'rx', requires_grad: bool = False, noise: bool = False, mu: float = 0, - sigma: float = 0.1 + sigma: float = 0.1, ) -> None: self.convention = convention - super().__init__(inputs=inputs, nmode=nmode, wires=wires, cutoff=cutoff, den_mat=den_mat, - requires_grad=requires_grad, noise=noise, mu=mu, sigma=sigma) + super().__init__( + inputs=inputs, + nmode=nmode, + wires=wires, + cutoff=cutoff, + den_mat=den_mat, + requires_grad=requires_grad, + noise=noise, + mu=mu, + sigma=sigma, + ) self.npara = 1 def inputs_to_tensor(self, inputs: Any = None) -> torch.Tensor: @@ -763,7 +826,7 @@ def inputs_to_tensor(self, inputs: Any = None) -> torch.Tensor: elif not isinstance(inputs, (torch.Tensor, nn.Parameter)): inputs = torch.tensor(inputs, dtype=torch.float) if self.noise: - inputs = inputs + torch.normal(self.mu, self.sigma, size=(1, )).squeeze() + inputs = inputs + torch.normal(self.mu, self.sigma, size=(1,)).squeeze() return inputs def get_matrix(self, theta: Any) -> torch.Tensor: @@ -785,18 +848,19 @@ def update_matrix(self) -> torch.Tensor: self.matrix = matrix.detach() return matrix - def get_transform_xp(self, theta: Any) -> Tuple[torch.Tensor, torch.Tensor]: + def get_transform_xp(self, theta: Any) -> tuple[torch.Tensor, torch.Tensor]: """Get the local affine symplectic transformation acting on quadrature operators in ``xxpp`` order.""" theta = self.inputs_to_tensor(theta) matrix = self.get_matrix(theta) # correspond to: U a^+ U^+ = u^T @ a^+ and U^+ a U = u @ a # correspond to: U a U^+ = (u^*)^T @ a and U^+ a^+ U = u^* @ a^+ - matrix_xp = torch.cat([torch.cat([matrix.real, -matrix.imag], dim=-1), - torch.cat([matrix.imag, matrix.real], dim=-1)], dim=-2).reshape(4, 4) + matrix_xp = torch.cat( + [torch.cat([matrix.real, -matrix.imag], dim=-1), torch.cat([matrix.imag, matrix.real], dim=-1)], dim=-2 + ).reshape(4, 4) vector_xp = torch.zeros(4, 1, dtype=theta.dtype, device=theta.device) return matrix_xp, vector_xp - def update_transform_xp(self) -> Tuple[torch.Tensor, torch.Tensor]: + def update_transform_xp(self) -> tuple[torch.Tensor, torch.Tensor]: """Update the local affine symplectic transformation acting on quadrature operators in ``xxpp`` order.""" matrix_xp, vector_xp = self.get_transform_xp(self.theta) self.matrix_xp = matrix_xp.detach() @@ -834,15 +898,16 @@ class UAnyGate(Gate): den_mat (bool, optional): Whether to use density matrix representation. Default: ``False`` name (str, optional): The name of the gate. Default: ``'UAnyGate'`` """ + def __init__( self, unitary: Any, nmode: int = 1, - wires: Optional[List[int]] = None, - minmax: Optional[List[int]] = None, - cutoff: Optional[int] = None, + wires: list[int] | None = None, + minmax: list[int] | None = None, + cutoff: int | None = None, den_mat: bool = False, - name: str = 'UAnyGate' + name: str = 'UAnyGate', ) -> None: self.nmode = nmode if wires is None: @@ -887,7 +952,7 @@ def get_matrix_state(self, matrix: torch.Tensor) -> torch.Tensor: col_num = rank - nt - 1 matrix_j = matrix[:, col_num] # all combinations of the first `rank-1` modes - combs = itertools.product(range(self.cutoff), repeat=rank-1) + combs = itertools.product(range(self.cutoff), repeat=rank - 1) for modes in combs: mode_out = modes[:nt] mode_in_part = modes[nt:] @@ -914,17 +979,18 @@ def update_matrix_state(self) -> torch.Tensor: tran_mat = self.matrix_state return tran_mat - def get_transform_xp(self, matrix: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: + def get_transform_xp(self, matrix: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: """Get the local affine symplectic transformation acting on quadrature operators in ``xxpp`` order.""" # correspond to: U a^+ U^+ = u^T @ a^+ and U^+ a U = u @ a # correspond to: U a U^+ = (u^*)^T @ a and U^+ a^+ U = u^* @ a^+ n = len(self.wires) - matrix_xp = torch.cat([torch.cat([matrix.real, -matrix.imag], dim=-1), - torch.cat([matrix.imag, matrix.real], dim=-1)], dim=-2).reshape(2 * n, 2 * n) + matrix_xp = torch.cat( + [torch.cat([matrix.real, -matrix.imag], dim=-1), torch.cat([matrix.imag, matrix.real], dim=-1)], dim=-2 + ).reshape(2 * n, 2 * n) vector_xp = torch.zeros(2 * n, 1, dtype=matrix.real.dtype, device=matrix.real.device) return matrix_xp, vector_xp - def update_transform_xp(self) -> Tuple[torch.Tensor, torch.Tensor]: + def update_transform_xp(self) -> tuple[torch.Tensor, torch.Tensor]: """Update the local affine symplectic transformation acting on quadrature operators in ``xxpp`` order.""" matrix_xp, vector_xp = self.get_transform_xp(self.matrix) self.matrix_xp = matrix_xp.detach() @@ -967,23 +1033,34 @@ class Squeezing(SingleGate): mu (float, optional): The mean of Gaussian noise. Default: 0 sigma (float, optional): The standard deviation of Gaussian noise. Default: 0.1 """ + def __init__( self, inputs: Any = None, nmode: int = 1, - wires: Union[int, List[int], None] = None, - cutoff: Optional[int] = None, + wires: int | list[int] | None = None, + cutoff: int | None = None, den_mat: bool = False, requires_grad: bool = False, noise: bool = False, mu: float = 0, - sigma: float = 0.1 + sigma: float = 0.1, ) -> None: - super().__init__(name='Squeezing', inputs=inputs, nmode=nmode, wires=wires, cutoff=cutoff, den_mat=den_mat, - requires_grad=requires_grad, noise=noise, mu=mu, sigma=sigma) + super().__init__( + name='Squeezing', + inputs=inputs, + nmode=nmode, + wires=wires, + cutoff=cutoff, + den_mat=den_mat, + requires_grad=requires_grad, + noise=noise, + mu=mu, + sigma=sigma, + ) self.npara = 2 - def inputs_to_tensor(self, inputs: Any = None) -> Tuple[torch.Tensor, torch.Tensor]: + def inputs_to_tensor(self, inputs: Any = None) -> tuple[torch.Tensor, torch.Tensor]: """Convert inputs to torch.Tensor.""" if inputs is None: r = torch.rand(1)[0] @@ -996,8 +1073,8 @@ def inputs_to_tensor(self, inputs: Any = None) -> Tuple[torch.Tensor, torch.Tens if not isinstance(theta, (torch.Tensor, nn.Parameter)): theta = torch.tensor(theta, dtype=torch.float) if self.noise: - r = r + torch.normal(self.mu, self.sigma, size=(1, )).squeeze() - theta = theta + torch.normal(self.mu, self.sigma, size=(1, )).squeeze() + r = r + torch.normal(self.mu, self.sigma, size=(1,)).squeeze() + theta = theta + torch.normal(self.mu, self.sigma, size=(1,)).squeeze() return r, theta def get_matrix(self, r: Any, theta: Any) -> torch.Tensor: @@ -1045,7 +1122,7 @@ def update_matrix_state(self) -> torch.Tensor: """Update the local transformation matrix acting on Fock state tensors.""" return self.get_matrix_state(self.r, self.theta) - def get_transform_xp(self, r: Any, theta: Any) -> Tuple[torch.Tensor, torch.Tensor]: + def get_transform_xp(self, r: Any, theta: Any) -> tuple[torch.Tensor, torch.Tensor]: """Get the local affine symplectic transformation acting on quadrature operators in ``xxpp`` order.""" r, theta = self.inputs_to_tensor([r, theta]) ch = torch.cosh(r) @@ -1056,7 +1133,7 @@ def get_transform_xp(self, r: Any, theta: Any) -> Tuple[torch.Tensor, torch.Tens vector_xp = torch.zeros(2, 1, dtype=r.dtype, device=r.device) return matrix_xp, vector_xp - def update_transform_xp(self) -> Tuple[torch.Tensor, torch.Tensor]: + def update_transform_xp(self) -> tuple[torch.Tensor, torch.Tensor]: """Update the local affine symplectic transformation acting on quadrature operators in ``xxpp`` order.""" matrix_xp, vector_xp = self.get_transform_xp(self.r, self.theta) self.matrix_xp = matrix_xp.detach() @@ -1123,23 +1200,34 @@ class Squeezing2(DoubleGate): mu (float, optional): The mean of Gaussian noise. Default: 0 sigma (float, optional): The standard deviation of Gaussian noise. Default: 0.1 """ + def __init__( self, inputs: Any = None, nmode: int = 2, - wires: Optional[List[int]] = None, - cutoff: Optional[int] = None, + wires: list[int] | None = None, + cutoff: int | None = None, den_mat: bool = False, requires_grad: bool = False, noise: bool = False, mu: float = 0, - sigma: float = 0.1 + sigma: float = 0.1, ) -> None: - super().__init__(name='Squeezing2', inputs=inputs, nmode=nmode, wires=wires, cutoff=cutoff, den_mat=den_mat, - requires_grad=requires_grad, noise=noise, mu=mu, sigma=sigma) + super().__init__( + name='Squeezing2', + inputs=inputs, + nmode=nmode, + wires=wires, + cutoff=cutoff, + den_mat=den_mat, + requires_grad=requires_grad, + noise=noise, + mu=mu, + sigma=sigma, + ) self.npara = 2 - def inputs_to_tensor(self, inputs: Any = None) -> Tuple[torch.Tensor, torch.Tensor]: + def inputs_to_tensor(self, inputs: Any = None) -> tuple[torch.Tensor, torch.Tensor]: """Convert inputs to torch.Tensor.""" if inputs is None: r = torch.rand(1)[0] @@ -1152,8 +1240,8 @@ def inputs_to_tensor(self, inputs: Any = None) -> Tuple[torch.Tensor, torch.Tens if not isinstance(theta, (torch.Tensor, nn.Parameter)): theta = torch.tensor(theta, dtype=torch.float) if self.noise: - r = r + torch.normal(self.mu, self.sigma, size=(1, )).squeeze() - theta = theta + torch.normal(self.mu, self.sigma, size=(1, )).squeeze() + r = r + torch.normal(self.mu, self.sigma, size=(1,)).squeeze() + theta = theta + torch.normal(self.mu, self.sigma, size=(1,)).squeeze() return r, theta def get_matrix(self, r: Any, theta: Any) -> torch.Tensor: @@ -1211,7 +1299,7 @@ def update_matrix_state(self) -> torch.Tensor: """Update the local transformation matrix acting on Fock state tensors.""" return self.get_matrix_state(self.r, self.theta) - def get_transform_xp(self, r: Any, theta: Any) -> Tuple[torch.Tensor, torch.Tensor]: + def get_transform_xp(self, r: Any, theta: Any) -> tuple[torch.Tensor, torch.Tensor]: """Get the local affine symplectic transformation acting on quadrature operators in ``xxpp`` order.""" r, theta = self.inputs_to_tensor([r, theta]) ch = torch.cosh(r) @@ -1225,7 +1313,7 @@ def get_transform_xp(self, r: Any, theta: Any) -> Tuple[torch.Tensor, torch.Tens vector_xp = torch.zeros(4, 1, dtype=r.dtype, device=r.device) return matrix_xp, vector_xp - def update_transform_xp(self) -> Tuple[torch.Tensor, torch.Tensor]: + def update_transform_xp(self) -> tuple[torch.Tensor, torch.Tensor]: """Update the local affine symplectic transformation acting on quadrature operators in ``xxpp`` order.""" matrix_xp, vector_xp = self.get_transform_xp(self.r, self.theta) self.matrix_xp = matrix_xp.detach() @@ -1286,23 +1374,34 @@ class Displacement(SingleGate): mu (float, optional): The mean of Gaussian noise. Default: 0 sigma (float, optional): The standard deviation of Gaussian noise. Default: 0.1 """ + def __init__( self, inputs: Any = None, nmode: int = 1, - wires: Union[int, List[int], None] = None, - cutoff: Optional[int] = None, + wires: int | list[int] | None = None, + cutoff: int | None = None, den_mat: bool = False, requires_grad: bool = False, noise: bool = False, mu: float = 0, - sigma: float = 0.1 + sigma: float = 0.1, ) -> None: - super().__init__(name='Displacement', inputs=inputs, nmode=nmode, wires=wires, cutoff=cutoff, den_mat=den_mat, - requires_grad=requires_grad, noise=noise, mu=mu, sigma=sigma) + super().__init__( + name='Displacement', + inputs=inputs, + nmode=nmode, + wires=wires, + cutoff=cutoff, + den_mat=den_mat, + requires_grad=requires_grad, + noise=noise, + mu=mu, + sigma=sigma, + ) self.npara = 2 - def inputs_to_tensor(self, inputs: Any = None) -> Tuple[torch.Tensor, torch.Tensor]: + def inputs_to_tensor(self, inputs: Any = None) -> tuple[torch.Tensor, torch.Tensor]: """Convert inputs to torch.Tensor.""" if inputs is None: r = torch.rand(1)[0] @@ -1318,10 +1417,10 @@ def inputs_to_tensor(self, inputs: Any = None) -> Tuple[torch.Tensor, torch.Tens r, theta = self._add_noise(r, theta) return r, theta - def _add_noise(self, r: torch.Tensor, theta: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: + def _add_noise(self, r: torch.Tensor, theta: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: """Add Gaussian noise to the parameters.""" - r = r + torch.normal(self.mu, self.sigma, size=(1, )).squeeze() - theta = theta + torch.normal(self.mu, self.sigma, size=(1, )).squeeze() + r = r + torch.normal(self.mu, self.sigma, size=(1,)).squeeze() + theta = theta + torch.normal(self.mu, self.sigma, size=(1,)).squeeze() return r, theta def get_matrix(self, r: Any, theta: Any) -> torch.Tensor: @@ -1346,7 +1445,7 @@ def get_matrix_state(self, r: Any, theta: Any) -> torch.Tensor: alpha = r * torch.exp(1j * theta) alpha_c = r * torch.exp(-1j * theta) tran_mat = alpha.new_zeros([self.cutoff] * 2) - tran_mat[0, 0] = torch.exp(-(r ** 2) / 2) + tran_mat[0, 0] = torch.exp(-(r**2) / 2) # rank 1 for m in range(self.cutoff - 1): tran_mat[m + 1, 0] = alpha / sqrt[m + 1] * tran_mat[m, 0].clone() @@ -1354,8 +1453,7 @@ def get_matrix_state(self, r: Any, theta: Any) -> torch.Tensor: for m in range(self.cutoff): for n in range(self.cutoff - 1): tran_mat[m, n + 1] = ( - - alpha_c / sqrt[n + 1] * tran_mat[m, n].clone() - + sqrt[m] / sqrt[n + 1] * tran_mat[m - 1, n].clone() + -alpha_c / sqrt[n + 1] * tran_mat[m, n].clone() + sqrt[m] / sqrt[n + 1] * tran_mat[m - 1, n].clone() ) return tran_mat @@ -1363,7 +1461,7 @@ def update_matrix_state(self) -> torch.Tensor: """Update the local transformation matrix acting on Fock state tensors.""" return self.get_matrix_state(self.r, self.theta) - def get_transform_xp(self, r: Any, theta: Any) -> Tuple[torch.Tensor, torch.Tensor]: + def get_transform_xp(self, r: Any, theta: Any) -> tuple[torch.Tensor, torch.Tensor]: """Get the local affine symplectic transformation acting on quadrature operators in ``xxpp`` order.""" r, theta = self.inputs_to_tensor([r, theta]) cos = torch.cos(theta) @@ -1372,7 +1470,7 @@ def get_transform_xp(self, r: Any, theta: Any) -> Tuple[torch.Tensor, torch.Tens vector_xp = torch.stack([r * cos, r * sin]).reshape(2, 1) * dqp.hbar**0.5 / dqp.kappa return matrix_xp, vector_xp - def update_transform_xp(self) -> Tuple[torch.Tensor, torch.Tensor]: + def update_transform_xp(self) -> tuple[torch.Tensor, torch.Tensor]: """Update the local affine symplectic transformation acting on quadrature operators in ``xxpp`` order.""" matrix_xp, vector_xp = self.get_transform_xp(self.r, self.theta) self.matrix_xp = matrix_xp.detach() @@ -1432,26 +1530,36 @@ class DisplacementPosition(Displacement): mu (float, optional): The mean of Gaussian noise. Default: 0 sigma (float, optional): The standard deviation of Gaussian noise. Default: 0.1 """ + def __init__( self, inputs: Any = None, nmode: int = 1, - wires: Union[int, List[int], None] = None, - cutoff: Optional[int] = None, + wires: int | list[int] | None = None, + cutoff: int | None = None, den_mat: bool = False, requires_grad: bool = False, noise: bool = False, mu: float = 0, - sigma: float = 0.1 + sigma: float = 0.1, ) -> None: - super().__init__(inputs=inputs, nmode=nmode, wires=wires, cutoff=cutoff, den_mat=den_mat, - requires_grad=requires_grad, noise=noise, mu=mu, sigma=sigma) + super().__init__( + inputs=inputs, + nmode=nmode, + wires=wires, + cutoff=cutoff, + den_mat=den_mat, + requires_grad=requires_grad, + noise=noise, + mu=mu, + sigma=sigma, + ) self.name = 'DisplacementPosition' self.npara = 1 - def _add_noise(self, r: torch.Tensor, theta: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: + def _add_noise(self, r: torch.Tensor, theta: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: """Add Gaussian noise to the parameters.""" - r = r + torch.normal(self.mu, self.sigma, size=(1, )).squeeze() + r = r + torch.normal(self.mu, self.sigma, size=(1,)).squeeze() return r, theta def init_para(self, inputs: Any = None) -> None: @@ -1507,26 +1615,36 @@ class DisplacementMomentum(Displacement): mu (float, optional): The mean of Gaussian noise. Default: 0 sigma (float, optional): The standard deviation of Gaussian noise. Default: 0.1 """ + def __init__( self, inputs: Any = None, nmode: int = 1, - wires: Union[int, List[int], None] = None, - cutoff: Optional[int] = None, + wires: int | list[int] | None = None, + cutoff: int | None = None, den_mat: bool = False, requires_grad: bool = False, noise: bool = False, mu: float = 0, - sigma: float = 0.1 + sigma: float = 0.1, ) -> None: - super().__init__(inputs=inputs, nmode=nmode, wires=wires, cutoff=cutoff, den_mat=den_mat, - requires_grad=requires_grad, noise=noise, mu=mu, sigma=sigma) + super().__init__( + inputs=inputs, + nmode=nmode, + wires=wires, + cutoff=cutoff, + den_mat=den_mat, + requires_grad=requires_grad, + noise=noise, + mu=mu, + sigma=sigma, + ) self.name = 'DisplacementMomentum' self.npara = 1 - def _add_noise(self, r: torch.Tensor, theta: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: + def _add_noise(self, r: torch.Tensor, theta: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: """Add Gaussian noise to the parameters.""" - r = r + torch.normal(self.mu, self.sigma, size=(1, )).squeeze() + r = r + torch.normal(self.mu, self.sigma, size=(1,)).squeeze() return r, theta def init_para(self, inputs: Any = None) -> None: @@ -1616,20 +1734,31 @@ class QuadraticPhase(SingleGate): mu (float, optional): The mean of Gaussian noise. Default: 0 sigma (float, optional): The standard deviation of Gaussian noise. Default: 0.1 """ + def __init__( self, inputs: Any = None, nmode: int = 1, - wires: Union[int, List[int], None] = None, - cutoff: Optional[int] = None, + wires: int | list[int] | None = None, + cutoff: int | None = None, den_mat: bool = False, requires_grad: bool = False, noise: bool = False, mu: float = 0, - sigma: float = 0.1 + sigma: float = 0.1, ) -> None: - super().__init__(name='QuadraticPhase', inputs=inputs, nmode=nmode, wires=wires, cutoff=cutoff, - den_mat=den_mat, requires_grad=requires_grad, noise=noise, mu=mu, sigma=sigma) + super().__init__( + name='QuadraticPhase', + inputs=inputs, + nmode=nmode, + wires=wires, + cutoff=cutoff, + den_mat=den_mat, + requires_grad=requires_grad, + noise=noise, + mu=mu, + sigma=sigma, + ) self.npara = 1 def inputs_to_tensor(self, inputs: Any = None) -> torch.Tensor: @@ -1641,7 +1770,7 @@ def inputs_to_tensor(self, inputs: Any = None) -> torch.Tensor: if not isinstance(inputs, torch.Tensor): inputs = torch.tensor(inputs, dtype=torch.float) if self.noise: - inputs = inputs + torch.normal(self.mu, self.sigma, size=(1, )).squeeze() + inputs = inputs + torch.normal(self.mu, self.sigma, size=(1,)).squeeze() return inputs def get_matrix(self, s: Any) -> torch.Tensor: @@ -1664,7 +1793,7 @@ def get_matrix_state(self, s: Any) -> torch.Tensor: theta = torch.arctan(s / 2) phi = -torch.sign(s) * torch.pi / 2 - theta # noise already in s - mat_s = Squeezing([r, phi], cutoff=self.cutoff).update_matrix_state() + mat_s = Squeezing([r, phi], cutoff=self.cutoff).update_matrix_state() mat_ps = PhaseShift(theta, cutoff=self.cutoff).update_matrix_state() return mat_ps @ mat_s @@ -1672,7 +1801,7 @@ def update_matrix_state(self) -> torch.Tensor: """Update the local transformation matrix acting on Fock state tensors.""" return self.get_matrix_state(self.s) - def get_transform_xp(self, s: Any) -> Tuple[torch.Tensor, torch.Tensor]: + def get_transform_xp(self, s: Any) -> tuple[torch.Tensor, torch.Tensor]: """Get the local affine symplectic transformation acting on quadrature operators in ``xxpp`` order.""" s = self.inputs_to_tensor(s).reshape(-1) one = s.new_ones(1) @@ -1681,7 +1810,7 @@ def get_transform_xp(self, s: Any) -> Tuple[torch.Tensor, torch.Tensor]: vector_xp = s.new_zeros(2, 1) return matrix_xp, vector_xp - def update_transform_xp(self) -> Tuple[torch.Tensor, torch.Tensor]: + def update_transform_xp(self) -> tuple[torch.Tensor, torch.Tensor]: """Update the local affine symplectic transformation acting on quadrature operators in ``xxpp`` order.""" matrix_xp, vector_xp = self.get_transform_xp(self.s) self.matrix_xp = matrix_xp.detach() @@ -1785,20 +1914,31 @@ class ControlledX(DoubleGate): mu (float, optional): The mean of Gaussian noise. Default: 0 sigma (float, optional): The standard deviation of Gaussian noise. Default: 0.1 """ + def __init__( self, inputs: Any = None, nmode: int = 2, - wires: Union[int, List[int], None] = None, - cutoff: Optional[int] = None, + wires: int | list[int] | None = None, + cutoff: int | None = None, den_mat: bool = False, requires_grad: bool = False, noise: bool = False, mu: float = 0, - sigma: float = 0.1 + sigma: float = 0.1, ) -> None: - super().__init__(name='ControlledX', inputs=inputs, nmode=nmode, wires=wires, cutoff=cutoff, - den_mat=den_mat, requires_grad=requires_grad, noise=noise, mu=mu, sigma=sigma) + super().__init__( + name='ControlledX', + inputs=inputs, + nmode=nmode, + wires=wires, + cutoff=cutoff, + den_mat=den_mat, + requires_grad=requires_grad, + noise=noise, + mu=mu, + sigma=sigma, + ) self.npara = 1 def inputs_to_tensor(self, inputs: Any = None) -> torch.Tensor: @@ -1810,7 +1950,7 @@ def inputs_to_tensor(self, inputs: Any = None) -> torch.Tensor: if not isinstance(inputs, torch.Tensor): inputs = torch.tensor(inputs, dtype=torch.float) if self.noise: - inputs = inputs + torch.normal(self.mu, self.sigma, size=(1, )).squeeze() + inputs = inputs + torch.normal(self.mu, self.sigma, size=(1,)).squeeze() return inputs def get_matrix(self, s: Any) -> torch.Tensor: @@ -1820,10 +1960,7 @@ def get_matrix(self, s: Any) -> torch.Tensor: one = s.new_ones(1) zero = s.new_zeros(1) x = s / 2 - mat = torch.stack([one, -x, zero, x, - x, one, x, zero, - zero, x, one, -x, - x, zero, x, one]).reshape(4, 4) + 0j + mat = torch.stack([one, -x, zero, x, x, one, x, zero, zero, x, one, -x, x, zero, x, one]).reshape(4, 4) + 0j return mat def update_matrix(self) -> torch.Tensor: @@ -1850,7 +1987,7 @@ def update_matrix_state(self) -> torch.Tensor: """Update the local transformation matrix acting on Fock state tensors.""" return self.get_matrix_state(self.s) - def get_transform_xp(self, s: Any) -> Tuple[torch.Tensor, torch.Tensor]: + def get_transform_xp(self, s: Any) -> tuple[torch.Tensor, torch.Tensor]: """Get the local affine symplectic transformation acting on quadrature operators in ``xxpp`` order.""" s = self.inputs_to_tensor(s).reshape(-1) one = s.new_ones(1) @@ -1861,7 +1998,7 @@ def get_transform_xp(self, s: Any) -> Tuple[torch.Tensor, torch.Tensor]: vector_xp = s.new_zeros(4, 1) return matrix_xp, vector_xp - def update_transform_xp(self) -> Tuple[torch.Tensor, torch.Tensor]: + def update_transform_xp(self) -> tuple[torch.Tensor, torch.Tensor]: """Update the local affine symplectic transformation acting on quadrature operators in ``xxpp`` order.""" matrix_xp, vector_xp = self.get_transform_xp(self.s) self.matrix_xp = matrix_xp.detach() @@ -1961,20 +2098,31 @@ class ControlledZ(DoubleGate): mu (float, optional): The mean of Gaussian noise. Default: 0 sigma (float, optional): The standard deviation of Gaussian noise. Default: 0.1 """ + def __init__( self, inputs: Any = None, nmode: int = 2, - wires: Union[int, List[int], None] = None, - cutoff: Optional[int] = None, + wires: int | list[int] | None = None, + cutoff: int | None = None, den_mat: bool = False, requires_grad: bool = False, noise: bool = False, mu: float = 0, - sigma: float = 0.1 + sigma: float = 0.1, ) -> None: - super().__init__(name='ControlledZ', inputs=inputs, nmode=nmode, wires=wires, cutoff=cutoff, - den_mat=den_mat, requires_grad=requires_grad, noise=noise, mu=mu, sigma=sigma) + super().__init__( + name='ControlledZ', + inputs=inputs, + nmode=nmode, + wires=wires, + cutoff=cutoff, + den_mat=den_mat, + requires_grad=requires_grad, + noise=noise, + mu=mu, + sigma=sigma, + ) self.npara = 1 def inputs_to_tensor(self, inputs: Any = None) -> torch.Tensor: @@ -1986,7 +2134,7 @@ def inputs_to_tensor(self, inputs: Any = None) -> torch.Tensor: if not isinstance(inputs, torch.Tensor): inputs = torch.tensor(inputs, dtype=torch.float) if self.noise: - inputs = inputs + torch.normal(self.mu, self.sigma, size=(1, )).squeeze() + inputs = inputs + torch.normal(self.mu, self.sigma, size=(1,)).squeeze() return inputs def get_matrix(self, s: Any) -> torch.Tensor: @@ -1996,10 +2144,7 @@ def get_matrix(self, s: Any) -> torch.Tensor: one = s.new_ones(1) zero = s.new_zeros(1) x = 1j * s / 2 - mat = torch.stack([one, x, zero, x, - x, one, x, zero, - zero, -x, one, -x, - -x, zero, -x, one]).reshape(4, 4) + mat = torch.stack([one, x, zero, x, x, one, x, zero, zero, -x, one, -x, -x, zero, -x, one]).reshape(4, 4) return mat def update_matrix(self) -> torch.Tensor: @@ -2023,7 +2168,7 @@ def update_matrix_state(self) -> torch.Tensor: """Update the local transformation matrix acting on Fock state tensors.""" return self.get_matrix_state(self.s) - def get_transform_xp(self, s: Any) -> Tuple[torch.Tensor, torch.Tensor]: + def get_transform_xp(self, s: Any) -> tuple[torch.Tensor, torch.Tensor]: """Get the local affine symplectic transformation acting on quadrature operators in ``xxpp`` order.""" s = self.inputs_to_tensor(s).reshape(-1) zero = s.new_zeros(1) @@ -2033,7 +2178,7 @@ def get_transform_xp(self, s: Any) -> Tuple[torch.Tensor, torch.Tensor]: vector_xp = s.new_zeros(4, 1) return matrix_xp, vector_xp - def update_transform_xp(self) -> Tuple[torch.Tensor, torch.Tensor]: + def update_transform_xp(self) -> tuple[torch.Tensor, torch.Tensor]: """Update the local affine symplectic transformation acting on quadrature operators in ``xxpp`` order.""" matrix_xp, vector_xp = self.get_transform_xp(self.s) self.matrix_xp = matrix_xp.detach() @@ -2090,20 +2235,31 @@ class CubicPhase(SingleGate): mu (float, optional): The mean of Gaussian noise. Default: 0 sigma (float, optional): The standard deviation of Gaussian noise. Default: 0.1 """ + def __init__( self, inputs: Any = None, nmode: int = 1, - wires: Union[int, List[int], None] = None, - cutoff: Optional[int] = None, + wires: int | list[int] | None = None, + cutoff: int | None = None, den_mat: bool = False, requires_grad: bool = False, noise: bool = False, mu: float = 0, - sigma: float = 0.1 + sigma: float = 0.1, ) -> None: - super().__init__(name='CubicPhase', inputs=inputs, nmode=nmode, wires=wires, cutoff=cutoff, - den_mat=den_mat, requires_grad=requires_grad, noise=noise, mu=mu, sigma=sigma) + super().__init__( + name='CubicPhase', + inputs=inputs, + nmode=nmode, + wires=wires, + cutoff=cutoff, + den_mat=den_mat, + requires_grad=requires_grad, + noise=noise, + mu=mu, + sigma=sigma, + ) self.npara = 1 def inputs_to_tensor(self, inputs: Any = None) -> torch.Tensor: @@ -2115,7 +2271,7 @@ def inputs_to_tensor(self, inputs: Any = None) -> torch.Tensor: if not isinstance(inputs, torch.Tensor): inputs = torch.tensor(inputs, dtype=torch.float) if self.noise: - inputs = inputs + torch.normal(self.mu, self.sigma, size=(1, )).squeeze() + inputs = inputs + torch.normal(self.mu, self.sigma, size=(1,)).squeeze() return inputs def get_matrix_state(self, gamma: Any) -> torch.Tensor: @@ -2176,20 +2332,31 @@ class Kerr(SingleGate): mu (float, optional): The mean of Gaussian noise. Default: 0 sigma (float, optional): The standard deviation of Gaussian noise. Default: 0.1 """ + def __init__( self, inputs: Any = None, nmode: int = 1, - wires: Union[int, List[int], None] = None, - cutoff: Optional[int] = None, + wires: int | list[int] | None = None, + cutoff: int | None = None, den_mat: bool = False, requires_grad: bool = False, noise: bool = False, mu: float = 0, - sigma: float = 0.1 + sigma: float = 0.1, ) -> None: - super().__init__(name='Kerr', inputs=inputs, nmode=nmode, wires=wires, cutoff=cutoff, - den_mat=den_mat, requires_grad=requires_grad, noise=noise, mu=mu, sigma=sigma) + super().__init__( + name='Kerr', + inputs=inputs, + nmode=nmode, + wires=wires, + cutoff=cutoff, + den_mat=den_mat, + requires_grad=requires_grad, + noise=noise, + mu=mu, + sigma=sigma, + ) self.npara = 1 def inputs_to_tensor(self, inputs: Any = None) -> torch.Tensor: @@ -2201,7 +2368,7 @@ def inputs_to_tensor(self, inputs: Any = None) -> torch.Tensor: if not isinstance(inputs, torch.Tensor): inputs = torch.tensor(inputs, dtype=torch.float) if self.noise: - inputs = inputs + torch.normal(self.mu, self.sigma, size=(1, )).squeeze() + inputs = inputs + torch.normal(self.mu, self.sigma, size=(1,)).squeeze() return inputs def get_matrix_state(self, kappa: Any) -> torch.Tensor: @@ -2265,20 +2432,31 @@ class CrossKerr(DoubleGate): mu (float, optional): The mean of Gaussian noise. Default: 0 sigma (float, optional): The standard deviation of Gaussian noise. Default: 0.1 """ + def __init__( self, inputs: Any = None, nmode: int = 2, - wires: Union[int, List[int], None] = None, - cutoff: Optional[int] = None, + wires: int | list[int] | None = None, + cutoff: int | None = None, den_mat: bool = False, requires_grad: bool = False, noise: bool = False, mu: float = 0, - sigma: float = 0.1 + sigma: float = 0.1, ) -> None: - super().__init__(name='CrossKerr', inputs=inputs, nmode=nmode, wires=wires, cutoff=cutoff, - den_mat=den_mat, requires_grad=requires_grad, noise=noise, mu=mu, sigma=sigma) + super().__init__( + name='CrossKerr', + inputs=inputs, + nmode=nmode, + wires=wires, + cutoff=cutoff, + den_mat=den_mat, + requires_grad=requires_grad, + noise=noise, + mu=mu, + sigma=sigma, + ) self.npara = 1 def inputs_to_tensor(self, inputs: Any = None) -> torch.Tensor: @@ -2290,7 +2468,7 @@ def inputs_to_tensor(self, inputs: Any = None) -> torch.Tensor: if not isinstance(inputs, torch.Tensor): inputs = torch.tensor(inputs, dtype=torch.float) if self.noise: - inputs = inputs + torch.normal(self.mu, self.sigma, size=(1, )).squeeze() + inputs = inputs + torch.normal(self.mu, self.sigma, size=(1,)).squeeze() return inputs def get_matrix_state(self, kappa: Any) -> torch.Tensor: @@ -2338,27 +2516,55 @@ class DelayBS(Delay): mu (float, optional): The mean of Gaussian noise. Default: 0 sigma (float, optional): The standard deviation of Gaussian noise. Default: 0.1 """ + def __init__( self, inputs: Any = None, ntau: int = 1, nmode: int = 1, - wires: Union[int, List[int], None] = None, - cutoff: Optional[int] = None, + wires: int | list[int] | None = None, + cutoff: int | None = None, den_mat: bool = False, requires_grad: bool = False, - loop_gates: Optional[List] = None, + loop_gates: list | None = None, noise: bool = False, mu: float = 0, - sigma: float = 0.1 + sigma: float = 0.1, ) -> None: - super().__init__(name='DelayBS', ntau=ntau, nmode=nmode, wires=wires, cutoff=cutoff, - den_mat=den_mat, noise=noise, mu=mu, sigma=sigma) + super().__init__( + name='DelayBS', + ntau=ntau, + nmode=nmode, + wires=wires, + cutoff=cutoff, + den_mat=den_mat, + noise=noise, + mu=mu, + sigma=sigma, + ) self.requires_grad = requires_grad - bs = BeamSplitterTheta(inputs=None, nmode=2, wires=None, cutoff=cutoff, den_mat=den_mat, - requires_grad=requires_grad, noise=noise, mu=mu, sigma=sigma) - ps = PhaseShift(inputs=None, nmode=1, wires=None, cutoff=cutoff, den_mat=den_mat, - requires_grad=requires_grad, noise=noise, mu=mu, sigma=sigma) + bs = BeamSplitterTheta( + inputs=None, + nmode=2, + wires=None, + cutoff=cutoff, + den_mat=den_mat, + requires_grad=requires_grad, + noise=noise, + mu=mu, + sigma=sigma, + ) + ps = PhaseShift( + inputs=None, + nmode=1, + wires=None, + cutoff=cutoff, + den_mat=den_mat, + requires_grad=requires_grad, + noise=noise, + mu=mu, + sigma=sigma, + ) self.gates.append(bs) self.gates.append(ps) self.npara = 2 @@ -2398,25 +2604,45 @@ class DelayMZI(Delay): mu (float, optional): The mean of Gaussian noise. Default: 0 sigma (float, optional): The standard deviation of Gaussian noise. Default: 0.1 """ + def __init__( self, inputs: Any = None, ntau: int = 1, nmode: int = 1, - wires: Union[int, List[int], None] = None, - cutoff: Optional[int] = None, + wires: int | list[int] | None = None, + cutoff: int | None = None, den_mat: bool = False, requires_grad: bool = False, - loop_gates: Optional[List] = None, + loop_gates: list | None = None, noise: bool = False, mu: float = 0, - sigma: float = 0.1 + sigma: float = 0.1, ) -> None: - super().__init__(name='DelayMZI', ntau=ntau, nmode=nmode, wires=wires, cutoff=cutoff, - den_mat=den_mat, noise=noise, mu=mu, sigma=sigma) + super().__init__( + name='DelayMZI', + ntau=ntau, + nmode=nmode, + wires=wires, + cutoff=cutoff, + den_mat=den_mat, + noise=noise, + mu=mu, + sigma=sigma, + ) self.requires_grad = requires_grad - mzi = MZI(inputs=inputs, nmode=2, wires=None, cutoff=cutoff, den_mat=den_mat, phi_first=False, - requires_grad=requires_grad, noise=noise, mu=mu, sigma=sigma) + mzi = MZI( + inputs=inputs, + nmode=2, + wires=None, + cutoff=cutoff, + den_mat=den_mat, + phi_first=False, + requires_grad=requires_grad, + noise=noise, + mu=mu, + sigma=sigma, + ) self.gates.append(mzi) self.npara = 2 if loop_gates is not None: @@ -2435,6 +2661,7 @@ def phi(self): def extra_repr(self) -> str: return f'wires={self.wires}, ntau={self.ntau}, theta={self.theta.item()}, phi={self.phi.item()}' + class Barrier(Gate): """Barrier. @@ -2444,12 +2671,8 @@ class Barrier(Gate): Default: ``None`` cutoff (int or None, optional): The Fock space truncation. Default: ``None`` """ - def __init__( - self, - nmode: int = 1, - wires: Union[int, List[int], None] = None, - cutoff: Optional[int] = None - ) -> None: + + def __init__(self, nmode: int = 1, wires: int | list[int] | None = None, cutoff: int | None = None) -> None: if wires is None: wires = list(range(nmode)) super().__init__(name='Barrier', nmode=nmode, wires=wires, cutoff=cutoff) diff --git a/src/deepquantum/photonic/hafnian_.py b/src/deepquantum/photonic/hafnian_.py index 7581f1af..19b560d6 100644 --- a/src/deepquantum/photonic/hafnian_.py +++ b/src/deepquantum/photonic/hafnian_.py @@ -1,10 +1,7 @@ -""" -functions for hafnian -""" +"""Functions for Hafnian""" from collections import Counter -from functools import lru_cache -from typing import List, Union +from functools import cache import numpy as np import torch @@ -13,8 +10,8 @@ from .qmath import get_powerset -@lru_cache(maxsize=None) -def integer_partition(remaining: int, max_num: int) -> List: +@cache +def integer_partition(remaining: int, max_num: int) -> list: """Generate all unique integer partitions of m using integers up to n.""" if remaining == 0: return [[]] @@ -28,7 +25,7 @@ def integer_partition(remaining: int, max_num: int) -> List: return result -def count_unique_permutations(nums: Union[List, np.array]) -> np.float64: +def count_unique_permutations(nums: list | np.ndarray) -> np.float64: """Count the number of unique permutations of a list of numbers.""" total_permutations = factorial(len(nums)) num_counts = Counter(nums) @@ -52,7 +49,7 @@ def get_submat_haf(a: torch.Tensor, z: torch.Tensor) -> torch.Tensor: return submat -def poly_lambda(submat: torch.Tensor, int_partition: List, power: int, loop: bool = False) -> torch.Tensor: +def poly_lambda(submat: torch.Tensor, int_partition: list, power: int, loop: bool = False) -> torch.Tensor: """Get the coefficient of the polynomial. See https://arxiv.org/abs/1805.12498 Eq.(3.26) (noting that Eq.(3.26) contains a typo) and @@ -71,14 +68,14 @@ def poly_lambda(submat: torch.Tensor, int_partition: List, power: int, loop: boo traces.append(torch.trace(x)) trace_list = torch.stack(traces) coeff = 0 - if loop: # loop hafnian case + if loop: # loop hafnian case v = torch.diag(submat) xv = x_mat @ v / 2 # matrix power calculation diag_term = [] x = xaz.new_ones(xaz.shape[-1]).diag() diag_term.append(v @ x @ xv) - for _ in range(power-1): + for _ in range(power - 1): x = x @ xaz diag_term.append(v @ x @ xv) diag_term = torch.stack(diag_term) diff --git a/src/deepquantum/photonic/mapper.py b/src/deepquantum/photonic/mapper.py index 5073a7de..4aed46af 100644 --- a/src/deepquantum/photonic/mapper.py +++ b/src/deepquantum/photonic/mapper.py @@ -1,23 +1,21 @@ -""" -Map the quantum gate to photonic quantum circuit -""" +"""Map the quantum gate to photonic quantum circuit""" import copy import itertools import os import pickle -from typing import Any, List, Optional +from typing import Any import matplotlib.pyplot as plt import numpy as np import torch from scipy import special from scipy.optimize import root -from sympy import symbols, Matrix, simplify +from sympy import Matrix, simplify, symbols from torch import vmap -class UnitaryMapper(): +class UnitaryMapper: """Map the quantum gate to the unitary matrix of the photonic quantum circuit based on dual-rail encoding. Args: @@ -31,38 +29,33 @@ class UnitaryMapper(): aux_pos (List or None, optional): The positions of the auxiliary modes. Default: ``None`` (which means the last two modes). """ + def __init__( - self, - nqubit: int, - nmode: int, - ugate: Any, - success: float, - aux: Optional[List] = None, - aux_pos: Optional[List] = None + self, nqubit: int, nmode: int, ugate: Any, success: float, aux: list | None = None, aux_pos: list | None = None ) -> None: - assert 2*nqubit<=nmode, 'need more modes' + assert 2 * nqubit <= nmode, 'need more modes' self.nmode = nmode self.ugate = ugate # the quantum gate to map self.success = success self.nqubit = nqubit self.aux = aux if aux_pos is None: - aux_pos = [nmode-2, nmode-1] + aux_pos = [nmode - 2, nmode - 1] self.aux_position = aux_pos - self.basis = self.create_basis(aux_pos) # these basis is changed with aux_pos + self.basis = self.create_basis(aux_pos) # these basis is changed with aux_pos - self.u_dim =self.nmode - y_var = symbols('y0:%d'%((self.u_dim)**2)) - y_mat = Matrix(np.array((y_var))) - y_mat = y_mat.reshape(self.u_dim,self.u_dim) # for symbolic computation, the matrix to optimize + self.u_dim = self.nmode + y_var = symbols(f'y0:{self.u_dim**2}') + y_mat = Matrix(np.array(y_var)) + y_mat = y_mat.reshape(self.u_dim, self.u_dim) # for symbolic computation, the matrix to optimize self.y_var = y_var self.u = y_mat - if self.nmode == 2*self.nqubit: # no auxiliary mode case + if self.nmode == 2 * self.nqubit: # no auxiliary mode case self.all_basis = self.create_basis([]) else: # self.all_basis = self.create_basis([nmode-2, nmode-1]) #with auxiliary mode case, - #these basis is fixed for computation + # these basis is fixed for computation self.all_basis = self.create_basis(aux_pos) # num_photons = int(sum(self.all_basis[0])) @@ -70,20 +63,20 @@ def __init__( # L = self.u_dim basic_dic = {} - for k in range(len(y_var)): # here we only consider single term - basic_dic[y_var[k]]=k + for k in range(len(y_var)): # here we only consider single term + basic_dic[y_var[k]] = k self.basic_dic = basic_dic # load indicies data current_path = os.path.abspath(__file__) - cur_sep = os.path.sep # obtain seperation + cur_sep = os.path.sep # obtain seperation # print(cur_sep) path_2 = os.path.abspath(os.path.join(current_path, '..')) try: # load the idx tensor dataf - fn = path_2+cur_sep+'cache'+cur_sep+f'Idx_{nqubit}qb_{nmode}mode_aux_{aux[0]}{aux[1]}.pt' + fn = path_2 + cur_sep + 'cache' + cur_sep + f'Idx_{nqubit}qb_{nmode}mode_aux_{aux[0]}{aux[1]}.pt' idx_ts = torch.load(fn, weights_only=True) - except: + except Exception: idx_ts = None self.idx_ts = idx_ts @@ -91,8 +84,8 @@ def __init__( if self.idx_ts is None: idx_dic, idx_ts = self.indx_eqs() # save the idx tensor data - fn = path_2+cur_sep+'cache'+cur_sep+f'Idx_{nqubit}qb_{nmode}mode_aux_{aux[0]}{aux[1]}.pt' - fn_2 = path_2+cur_sep+'cache'+cur_sep+f'Idx_{nqubit}qb_{nmode}mode_aux_{aux[0]}{aux[1]}.pickle' + fn = path_2 + cur_sep + 'cache' + cur_sep + f'Idx_{nqubit}qb_{nmode}mode_aux_{aux[0]}{aux[1]}.pt' + fn_2 = path_2 + cur_sep + 'cache' + cur_sep + f'Idx_{nqubit}qb_{nmode}mode_aux_{aux[0]}{aux[1]}.pickle' torch.save(idx_ts, fn) UnitaryMapper.save_dict(idx_dic, fn_2) @@ -102,7 +95,7 @@ def create_basis(self, aux_position): # print(main_position) all_basis = [] n = self.nqubit - temp = [[1, 0],[0, 1]] # |0> and |1> + temp = [[1, 0], [0, 1]] # |0> and |1> for state in itertools.product([0, 1], repeat=n): len_state = len(state) dual_code = [] @@ -116,33 +109,28 @@ def create_basis(self, aux_position): all_basis.append(temp_basis) return all_basis - ############################## # using symbolic computation finding indicies to avoid repeat computing - def get_coeff_sym(self, input_state: torch.Tensor, output_states: Optional[List]=None): - """ - Return the transfer state coefficient in a symbolic way. - """ + def get_coeff_sym(self, input_state: torch.Tensor, output_states: list | None = None): + """Return the transfer state coefficient in a symbolic way.""" if output_states is None: output_states = self.all_basis - u =self.u - dic_coeff={} + u = self.u + dic_coeff = {} for output in output_states: mat = self.sub_matrix_sym(u, input_state, output) per = self.permanent(mat) - temp_coeff = per/np.sqrt((self.product_factorial(input_state)*self.product_factorial(output))) - dic_coeff[output]=simplify(temp_coeff) + temp_coeff = per / np.sqrt(self.product_factorial(input_state) * self.product_factorial(output)) + dic_coeff[output] = simplify(temp_coeff) return dic_coeff def get_indx_coeff(self, coeff_i, all_inputs=None): - """ - Get the index of y_var for given state transfer coefficients. - """ + """Get the index of y_var for given state transfer coefficients.""" index_coeff = {} if all_inputs is None: all_inputs = self.all_basis basic_dic = self.basic_dic - temp_ts2 = torch.empty(0, dtype=torch.int32) # set initial value 0 + temp_ts2 = torch.empty(0, dtype=torch.int32) # set initial value 0 for s in range(len(all_inputs)): input_s = all_inputs[s] test = simplify(coeff_i[input_s]) @@ -154,18 +142,15 @@ def get_indx_coeff(self, coeff_i, all_inputs=None): map_tuple = tuple(map(lambda x: basic_dic[x], key.args)) temp.append([map_tuple, float(dict_0[key])]) temp_2.append(list(map_tuple)) - index_coeff[tuple(input_s.numpy())] =temp # save as dictionary - temp_ts = torch.IntTensor(temp_2).unsqueeze(0) # save as torch.tensor - temp_ts2 = torch.cat((temp_ts2, temp_ts),0) - print('finishing find idx for state',tuple(input_s.numpy())) ## for check the result + index_coeff[tuple(input_s.numpy())] = temp # save as dictionary + temp_ts = torch.IntTensor(temp_2).unsqueeze(0) # save as torch.tensor + temp_ts2 = torch.cat((temp_ts2, temp_ts), 0) + print('finishing find idx for state', tuple(input_s.numpy())) ## for check the result print('##########') return index_coeff, temp_ts2 - def indx_eqs(self): - """ - Get the dictinonary of indices of y_mat for each nonlinear equations for n modes. - """ + """Get the dictinonary of indices of y_mat for each nonlinear equations for n modes.""" all_inputs = self.all_basis all_outputs = list(map(self.get_coeff_sym, all_inputs)) indices = list(map(self.get_indx_coeff, all_outputs)) @@ -174,7 +159,7 @@ def indx_eqs(self): idx_dic = [] for idx in indices: idx_dic.append(idx[0]) - temp_ts = torch.cat((temp_ts, idx[1]),0) + temp_ts = torch.cat((temp_ts, idx[1]), 0) self.idx_all = idx_dic self.idx_ts = temp_ts @@ -186,6 +171,7 @@ def indx_eqs(self): def single_prod(single_idx_i, y_test): temp = y_test[single_idx_i] return temp.prod() + @staticmethod def single_output(single_idx, y_test): temp_ = vmap(UnitaryMapper.single_prod, in_dims=(0, None))(single_idx, y_test) @@ -199,18 +185,17 @@ def get_transfer_mat(self, y): all_inputs = self.all_basis num_basis = len(all_inputs) temp_ = vmap(UnitaryMapper.single_output, in_dims=(0, None))(idx_ts, y) - temp_mat = temp_.reshape(num_basis, num_basis) # here already do transpose + temp_mat = temp_.reshape(num_basis, num_basis) # here already do transpose return temp_mat def f_real(self, y): - """ - Construct :math:`2^{nqubit}*2^{nqubit}` equations for :math:`n*n` matrix y, obtain real solutions for real part of u_gate. + """Construct equations to obtain real solutions for the real part of the target quantum gate. Args: - y: an array with :math:`n^2` element + y: Input matrix/array with :math:`n^2` elements, where :math:`n = 2^{nqubit}`. """ transfer_mat = self.get_transfer_mat(y) - u_gate = self.ugate*self.success # the target quantum gate with probability + u_gate = self.ugate * self.success # the target quantum gate with probability if not isinstance(u_gate, torch.Tensor): u_gate = torch.tensor(u_gate) diff_matrix = transfer_mat - u_gate.real @@ -219,21 +204,16 @@ def f_real(self, y): return eqs_list def f_real_unitary(self, y): - """ - Return the quantum gate constrains and the unitary constrains. - """ + """Return the quantum gate constrains and the unitary constrains.""" eqs_1 = self.f_real(y) mat_y = torch.tensor(y).view(self.nmode, self.nmode) eqs_2 = self.unitary_constrains(mat_y) eqs = eqs_1 + eqs_2 - return eqs @staticmethod def unitary_constrains(u_temp: torch.Tensor): - """ - Return :math:`n^2` equations for :math:`n*n` matrix with unitary condition. - """ + """Return :math:`n^2` equations for :math:`n*n` matrix with unitary condition.""" u_size = u_temp.size() u_temp_conj = u_temp.conj() u_temp_dag = u_temp_conj.transpose(0, 1) @@ -245,11 +225,9 @@ def unitary_constrains(u_temp: torch.Tensor): return eqs_list def f_complex_unitary(self, paras): - """ - Return quantum gate constrains and the unitary constrains. - """ + """Return quantum gate constrains and the unitary constrains.""" num_paras = len(paras) - y = np.array(paras)[0 : int(num_paras/2)] + np.array(paras)[int(num_paras/2):]*1j + y = np.array(paras)[0 : int(num_paras / 2)] + np.array(paras)[int(num_paras / 2) :] * 1j eqs_1 = self.f_complex(y) mat_y = torch.tensor(y).view(self.nmode, self.nmode) @@ -259,15 +237,14 @@ def f_complex_unitary(self, paras): return eqs def f_complex(self, y): - """ - Construct :math:`2^{nqubit}*2^{nqubit}` equations for :math:`n*n` matrix y, obtain complex solutions. + """Construct :math:`2^{nqubit}*2^{nqubit}` equations for :math:`n*n` matrix y, obtain complex solutions. Args: y: an array with :math:`2*n^2` element """ - num_paras = len(y)*2 + num_paras = len(y) * 2 transfer_mat = self.get_transfer_mat(y) - u_gate = self.ugate*self.success # the target quantum gate with probability + u_gate = self.ugate * self.success # the target quantum gate with probability if not isinstance(u_gate, torch.Tensor): u_gate = torch.tensor(u_gate) diff_matrix = transfer_mat - u_gate @@ -279,16 +256,15 @@ def f_complex(self, y): eqs_imag_list = eqs_imag.tolist() eqs_all = eqs_real_list + eqs_imag_list - if num_paras > len(eqs_all): # if # parameters > # equations - for t in range(num_paras-len(eqs_all)): - extra_eq = y[0]*y[1]-y[0]*y[1] + if num_paras > len(eqs_all): # if # parameters > # equations + for _ in range(num_paras - len(eqs_all)): + extra_eq = y[0] * y[1] - y[0] * y[1] eqs_all.append(extra_eq) return eqs_all + @staticmethod def unitary_constrains_complex(u_temp: torch.Tensor): - """ - Return :math:`2*n^2` equations for :math:`n*n` complex matrix with unitary condition. - """ + """Return :math:`2*n^2` equations for :math:`n*n` complex matrix with unitary condition.""" u_size = u_temp.size() u_temp_conj = u_temp.conj() u_temp_dag = u_temp_conj.transpose(0, 1) @@ -304,119 +280,103 @@ def unitary_constrains_complex(u_temp: torch.Tensor): return eqs_list - def solve_eqs_real(self, total_trials = 10, trials = 1000, precision = 1e-6): - """ - Solve the non-linear eqautions for matrix satisfying ugate with real solution. - """ + def solve_eqs_real(self, total_trials=10, trials=1000, precision=1e-6): + """Solve the non-linear eqautions for matrix satisfying ugate with real solution.""" func = self.f_real_unitary results = [] for t in range(total_trials): - - result =[] - sum_=[] + result = [] + sum_ = [] for i in range(trials): - - y0 = np.random.uniform(-1, 1, (self.u_dim)**2) + y0 = np.random.uniform(-1, 1, (self.u_dim) ** 2) re1 = root(func, y0, method='lm') - temp_eqs = np.array((func(re1.x))) + temp_eqs = np.array(func(re1.x)) print(re1.success, sum(abs(temp_eqs))) - if re1.success and sum(abs(temp_eqs)) --> s_1!*s_2!*...s_n!`. - """ + """Get the product of the factorial from the state, i.e., :math:`|s_1,s_2,...s_n> --> s_1!*s_2!*...s_n!`.""" temp = special.factorial(state) product_fac = 1 for i in range(len(state)): @@ -440,27 +398,26 @@ def create_subset(num_coincidence): num_arange = np.arange(num_coincidence) all_subset = [] - for index_1 in range(1, 2 ** num_coincidence): + for index_1 in range(1, 2**num_coincidence): all_subset.append([]) for index_2 in range(num_coincidence): if index_1 & (1 << index_2): all_subset[-1].append(num_arange[index_2]) return all_subset + @staticmethod def save_dict(dictionary, file_path): with open(file_path, 'wb') as file: pickle.dump(dictionary, file) - -######################################## -######some additional function########## -#for plotting matrix and check the unitary matrix + ######################################## + ######some additional function########## + # for plotting matrix and check the unitary matrix @staticmethod def plot_u(unitary, vmax=1, vmin=0, fs=20, len_ticks=5, cl='RdBu'): - """ - Plot the matrix in graphic way. + """Plot the matrix in graphic way. Args: unitary: the plotting matrix @@ -470,31 +427,21 @@ def plot_u(unitary, vmax=1, vmin=0, fs=20, len_ticks=5, cl='RdBu'): len_ticks: number of ticks in colorbar cl: color of plotting """ - plt.rcParams ['figure.figsize'] = (8, 8) - step = (vmax -vmin)/(len_ticks-1) - ticks_=[0]*len_ticks - for i in range(len_ticks): - ticks_[i] = vmin + i*step - # print(ticks_) - ax = plt.matshow(np.array(unitary).real, vmax = vmax, vmin =vmin, cmap=cl ) - cb = plt.colorbar(fraction =0.03, ticks=ticks_) - plt.title('U_real', fontsize=fs) - plt.xticks(fontsize=fs-1) - plt.yticks(fontsize=fs-1) - cb.ax.tick_params(labelsize = fs-6) - - ax1 = plt.matshow(np.array(unitary).imag, vmax = vmax, vmin = vmin,cmap=cl ) - cb1 = plt.colorbar( fraction =0.03, ticks=ticks_) - plt.title('U_imag', fontsize=fs) - plt.xticks(fontsize=fs-1) - plt.yticks(fontsize=fs-1) - cb1.ax.tick_params(labelsize = fs-6) + u_np = np.asarray(unitary) + ticks = np.linspace(vmin, vmax, len_ticks) + plots = [('Real', u_np.real), ('Imag', u_np.imag)] + for title_suffix, data in plots: + fig, ax = plt.subplots(figsize=(8, 8)) + im = ax.matshow(data, vmax=vmax, vmin=vmin, cmap=cl) + ax.set_title(f'U_{title_suffix}', fontsize=fs) + ax.tick_params(axis='both', labelsize=fs - 1) + cb = fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04, ticks=ticks) + cb.ax.tick_params(labelsize=fs - 6) + plt.show() @staticmethod def is_unitary(u_temp): - """ - Check the matrix is unitary or not. - """ + """Check the matrix is unitary or not.""" u_size = u_temp.size() u_temp_conj = u_temp.conj() u_temp_dag = u_temp_conj.transpose(0, 1) @@ -505,16 +452,14 @@ def is_unitary(u_temp): return all_sum def state_basis(self): - """ - Map '000' to dual_rail. - """ + """Map '000' to dual_rail.""" state_map = {} map_dic = {(1, 0): '0', (0, 1): '1'} for i in self.all_basis: len_ = len(i) s = '' - for j in range(int(len_/2)): - temp = tuple(i[2*j : 2*j+2].numpy()) + for j in range(int(len_ / 2)): + temp = tuple(i[2 * j : 2 * j + 2].numpy()) s = s + map_dic[temp] state_map[s] = i.tolist() return state_map diff --git a/src/deepquantum/photonic/measurement.py b/src/deepquantum/photonic/measurement.py index 081b1634..d0867efe 100644 --- a/src/deepquantum/photonic/measurement.py +++ b/src/deepquantum/photonic/measurement.py @@ -1,8 +1,6 @@ -""" -Photonic measurements -""" +"""Photonic measurements""" -from typing import Any, List, Optional, Union +from typing import Any import numpy as np import torch @@ -11,8 +9,9 @@ from torch.distributions.multivariate_normal import MultivariateNormal import deepquantum.photonic as dqp -from .gate import PhaseShift, Displacement -from .operation import Operation, evolve_state, evolve_den_mat + +from .gate import Displacement, PhaseShift +from .operation import Operation, evolve_den_mat, evolve_state from .qmath import sample_homodyne_fock, sample_reject_bosonic from .state import FockStateBosonic @@ -32,17 +31,18 @@ class Generaldyne(Operation): mu (float, optional): The mean of Gaussian noise. Default: 0 sigma (float, optional): The standard deviation of Gaussian noise. Default: 0.1 """ + def __init__( self, cov_m: Any, nmode: int = 1, - wires: Union[int, List[int], None] = None, - cutoff: Optional[int] = None, + wires: int | list[int] | None = None, + cutoff: int | None = None, den_mat: bool = False, name: str = 'Generaldyne', noise: bool = False, mu: float = 0, - sigma: float = 0.1 + sigma: float = 0.1, ) -> None: self.nmode = nmode if wires is None: @@ -51,15 +51,16 @@ def __init__( nwire = len(wires) if cutoff is None: cutoff = 2 - super().__init__(name=name, nmode=nmode, wires=wires, cutoff=cutoff, den_mat=den_mat, - noise=noise, mu=mu, sigma=sigma) + super().__init__( + name=name, nmode=nmode, wires=wires, cutoff=cutoff, den_mat=den_mat, noise=noise, mu=mu, sigma=sigma + ) if not isinstance(cov_m, torch.Tensor): cov_m = torch.tensor(cov_m, dtype=torch.float).reshape(2 * nwire, 2 * nwire) assert cov_m.shape[-2] == cov_m.shape[-1] == 2 * nwire, 'The size of cov_m does not match the wires' self.register_buffer('cov_m', cov_m) self.samples = None - def forward(self, x: List[torch.Tensor], samples: Any = None) -> List[torch.Tensor]: + def forward(self, x: list[torch.Tensor], samples: Any = None) -> list[torch.Tensor]: """Perform a forward pass for Gaussian (Bosonic) states. See Quantum Continuous Variables: A Primer of Theoretical Methods (2024) @@ -70,43 +71,43 @@ def forward(self, x: List[torch.Tensor], samples: Any = None) -> List[torch.Tens cov, mean = x[:2] size = cov.size() wires = torch.tensor(self.wires) - idx = torch.cat([wires, wires + self.nmode]) # xxpp order + idx = torch.cat([wires, wires + self.nmode]) # xxpp order idx_all = torch.arange(2 * self.nmode) mask = ~torch.isin(idx_all, idx) idx_rest = idx_all[mask] - cov_a = cov[..., idx_rest[:, None], idx_rest] - cov_b = cov[..., idx[:, None], idx] + cov_a = cov[..., idx_rest[:, None], idx_rest] + cov_b = cov[..., idx[:, None], idx] cov_ab = cov[..., idx_rest[:, None], idx] mean_a = mean[..., idx_rest, :] mean_b = mean[..., idx, :] cov_t = cov_b + self.cov_m - cov_a = cov_a - cov_ab @ torch.linalg.solve(cov_t, cov_ab.mT) # update the unmeasured part + cov_a = cov_a - cov_ab @ torch.linalg.solve(cov_t, cov_ab.mT) # update the unmeasured part cov_out = cov.new_ones(size[:-1].numel()).reshape(size[:-1]).diag_embed() - cov_out[..., idx_rest[:, None], idx_rest] = cov_a # update the total cov mat + cov_out[..., idx_rest[:, None], idx_rest] = cov_a # update the total cov mat - if len(x) == 2: # Gaussian + if len(x) == 2: # Gaussian if samples is None: - mean_m = MultivariateNormal(mean_b.squeeze(-1), cov_t).sample([1])[0] # (batch, 2 * nwire) + mean_m = MultivariateNormal(mean_b.squeeze(-1), cov_t).sample([1])[0] # (batch, 2 * nwire) else: if not isinstance(samples, torch.Tensor): samples = torch.tensor(samples, dtype=cov.dtype, device=cov.device) mean_m = samples.reshape(-1, 2 * len(self.wires)) mean_a = mean_a + cov_ab @ torch.linalg.solve(cov_t, mean_m.unsqueeze(-1) - mean_b) - elif len(x) == 3: # Bosonic + elif len(x) == 3: # Bosonic weight = x[2] if samples is None: - mean_m = sample_reject_bosonic(cov_b, mean_b, weight, self.cov_m, 1)[:, 0] # (batch, 2 * nwire) + mean_m = sample_reject_bosonic(cov_b, mean_b, weight, self.cov_m, 1)[:, 0] # (batch, 2 * nwire) else: if not isinstance(samples, torch.Tensor): samples = torch.tensor(samples, dtype=cov.dtype, device=cov.device) mean_m = samples.reshape(-1, 2 * len(self.wires)) # (batch, ncomb) exp_real = torch.exp(mean_b.imag.mT @ torch.linalg.solve(cov_t, mean_b.imag) / 2).squeeze(-2, -1) - gaus_b = MultivariateNormal(mean_b.squeeze(-1).real, cov_t) # (batch, ncomb, 2 * nwire) - prob_g = gaus_b.log_prob(mean_m.unsqueeze(-2)).exp() # (batch, ncomb, 2 * nwire) -> (batch, ncomb) - rm = mean_m.unsqueeze(-1).unsqueeze(-3) # (batch, 2 * nwire) -> (batch, 1, 2 * nwire, 1) + gaus_b = MultivariateNormal(mean_b.squeeze(-1).real, cov_t) # (batch, ncomb, 2 * nwire) + prob_g = gaus_b.log_prob(mean_m.unsqueeze(-2)).exp() # (batch, ncomb, 2 * nwire) -> (batch, ncomb) + rm = mean_m.unsqueeze(-1).unsqueeze(-3) # (batch, 2 * nwire) -> (batch, 1, 2 * nwire, 1) # (batch, ncomb) exp_imag = torch.exp((rm - mean_b.real).mT @ torch.linalg.solve(cov_t, mean_b.imag) * 1j).squeeze(-2, -1) weight *= exp_real * prob_g * exp_imag @@ -116,7 +117,7 @@ def forward(self, x: List[torch.Tensor], samples: Any = None) -> List[torch.Tens mean_out = torch.zeros_like(mean) mean_out[..., idx_rest, :] = mean_a - self.samples = mean_m # xxpp order + self.samples = mean_m # xxpp order if len(x) == 2: return [cov_out, mean_out] elif len(x) == 3: @@ -141,27 +142,37 @@ class Homodyne(Generaldyne): sigma (float, optional): The standard deviation of Gaussian noise. Default: 0.1 name (str, optional): The name of the measurement. Default: ``'Homodyne'`` """ + def __init__( self, phi: Any = None, nmode: int = 1, - wires: Union[int, List[int], None] = None, - cutoff: Optional[int] = None, + wires: int | list[int] | None = None, + cutoff: int | None = None, den_mat: bool = False, eps: float = 2e-4, requires_grad: bool = False, noise: bool = False, mu: float = 0, sigma: float = 0.1, - name: str = 'Homodyne' + name: str = 'Homodyne', ) -> None: self.nmode = nmode if wires is None: wires = [0] wires = self._convert_indices(wires) - cov_m = torch.diag(torch.tensor([eps ** 2] * len(wires) + [1 / eps ** 2] * len(wires))) # xxpp - super().__init__(cov_m=cov_m, nmode=nmode, wires=wires, cutoff=cutoff, den_mat=den_mat, name=name, - noise=noise, mu=mu, sigma=sigma) + cov_m = torch.diag(torch.tensor([eps**2] * len(wires) + [1 / eps**2] * len(wires))) # xxpp + super().__init__( + cov_m=cov_m, + nmode=nmode, + wires=wires, + cutoff=cutoff, + den_mat=den_mat, + name=name, + noise=noise, + mu=mu, + sigma=sigma, + ) assert len(self.wires) == 1, f'{self.name} must act on one mode' self.requires_grad = requires_grad self.init_para(inputs=phi) @@ -176,7 +187,7 @@ def inputs_to_tensor(self, inputs: Any = None) -> torch.Tensor: inputs = inputs.reshape(-1) assert len(inputs) == len(self.wires) if self.noise: - inputs = inputs + torch.normal(self.mu, self.sigma, size=(len(self.wires), )).squeeze() + inputs = inputs + torch.normal(self.mu, self.sigma, size=(len(self.wires),)).squeeze() return inputs def init_para(self, inputs: Any = None) -> None: @@ -197,47 +208,43 @@ def op_fock(self, x: torch.Tensor, samples: Any = None) -> torch.Tensor: if not isinstance(samples, torch.Tensor): samples = torch.tensor(samples, dtype=x.real.dtype, device=x.device) samples = samples.reshape(-1, 1, 1) - self.samples = samples.squeeze(-2) # with dimension \sqrt{m\omega\hbar} + self.samples = samples.squeeze(-2) # with dimension \sqrt{m\omega\hbar} # projection operator as single gate - vac_state = x.new_zeros(self.cutoff) # (cutoff) + vac_state = x.new_zeros(self.cutoff) # (cutoff) vac_state[0] = 1 - inf_sqz_vac = torch.zeros_like(vac_state) # (cutoff) + inf_sqz_vac = torch.zeros_like(vac_state) # (cutoff) orders = torch.arange(np.ceil(self.cutoff / 2), dtype=x.real.dtype, device=x.device) fac_2n = torch.tensor(factorial(2 * orders), dtype=orders.dtype, device=orders.device) fac_n = torch.tensor(factorial(orders), dtype=orders.dtype, device=orders.device) - inf_sqz_vac[::2] = (-0.5)**orders * fac_2n**0.5 / fac_n # unnormalized + inf_sqz_vac[::2] = (-0.5) ** orders * fac_2n**0.5 / fac_n # unnormalized alpha = self.samples * dqp.kappa / dqp.hbar**0.5 d = Displacement(cutoff=self.cutoff) - d_mat = vmap(d.get_matrix_state, in_dims=(0, None))(alpha, 0) # (batch, cutoff, cutoff) + d_mat = vmap(d.get_matrix_state, in_dims=(0, None))(alpha, 0) # (batch, cutoff, cutoff) r = PhaseShift(inputs=self.phi, nmode=1, wires=0, cutoff=self.cutoff) - eigenstate = r(evolve_state(inf_sqz_vac.unsqueeze(0), d_mat, 1, [0], self.cutoff)) # (batch, cutoff) - project_op = vmap(torch.outer, in_dims=(None, 0))(vac_state, eigenstate.conj()) # (batch, cutoff, cutoff) - if self.den_mat: # (batch, 1, [cutoff] * 2 * nmode) + eigenstate = r(evolve_state(inf_sqz_vac.unsqueeze(0), d_mat, 1, [0], self.cutoff)) # (batch, cutoff) + project_op = vmap(torch.outer, in_dims=(None, 0))(vac_state, eigenstate.conj()) # (batch, cutoff, cutoff) + if self.den_mat: # (batch, 1, [cutoff] * 2 * nmode) evolve = vmap(evolve_den_mat, in_dims=(0, 0, None, None, None)) - else: # (batch, 1, [cutoff] * nmode) + else: # (batch, 1, [cutoff] * nmode) evolve = vmap(evolve_state, in_dims=(0, 0, None, None, None)) x = evolve(x.unsqueeze(1), project_op, self.nmode, self.wires, self.cutoff).squeeze(1) # normalization if self.den_mat: - norm = vmap(torch.trace)(x.reshape(-1, self.cutoff ** self.nmode, self.cutoff ** self.nmode)) # (batch) + norm = vmap(torch.trace)(x.reshape(-1, self.cutoff**self.nmode, self.cutoff**self.nmode)) # (batch) x = x / norm.reshape([-1] + [1] * 2 * self.nmode) else: - norm = (x.reshape(-1, self.cutoff ** self.nmode).abs() ** 2).sum(-1) ** 0.5 # (batch) + norm = (x.reshape(-1, self.cutoff**self.nmode).abs() ** 2).sum(-1) ** 0.5 # (batch) x = x / norm.reshape([-1] + [1] * self.nmode) return x - def op_cv(self, x: List[torch.Tensor], samples: Any = None) -> List[torch.Tensor]: + def op_cv(self, x: list[torch.Tensor], samples: Any = None) -> list[torch.Tensor]: """Perform a forward pass for Gaussian (Bosonic) states.""" r = PhaseShift(inputs=-self.phi, nmode=self.nmode, wires=self.wires, cutoff=self.cutoff) cov, mean = x[:2] cov, mean = r([cov, mean]) return super().forward([cov, mean] + x[2:], samples) - def forward( - self, - x: Union[torch.Tensor, List[torch.Tensor]], - samples: Any = None - ) -> Union[torch.Tensor, List[torch.Tensor]]: + def forward(self, x: torch.Tensor | list[torch.Tensor], samples: Any = None) -> torch.Tensor | list[torch.Tensor]: """Perform a forward pass.""" if isinstance(x, torch.Tensor): return self.op_fock(x, samples) @@ -260,14 +267,15 @@ class GeneralBosonic(Operation): cutoff (int or None, optional): The Fock space truncation. Default: ``None`` name (str, optional): The name of the measurement. Default: ``'GeneralBosonic'`` """ + def __init__( self, cov: Any, weight: Any, nmode: int = 1, - wires: Union[int, List[int], None] = None, - cutoff: Optional[int] = None, - name: str = 'GeneralBosonic' + wires: int | list[int] | None = None, + cutoff: int | None = None, + name: str = 'GeneralBosonic', ) -> None: self.nmode = nmode if wires is None: @@ -290,7 +298,7 @@ def __init__( self.register_buffer('weight', weight) self.samples = None - def forward(self, x: List[torch.Tensor], samples: Any = None) -> List[torch.Tensor]: + def forward(self, x: list[torch.Tensor], samples: Any = None) -> list[torch.Tensor]: """Perform a forward pass for Gaussian (Bosonic) states. See https://arxiv.org/abs/2103.05530 Eq.(30-31) and Eq.(35-37) @@ -298,39 +306,37 @@ def forward(self, x: List[torch.Tensor], samples: Any = None) -> List[torch.Tens cov, mean = x[:2] size = cov.size() wires = torch.tensor(self.wires) - idx = torch.cat([wires, wires + self.nmode]) # xxpp order + idx = torch.cat([wires, wires + self.nmode]) # xxpp order idx_all = torch.arange(2 * self.nmode) mask = ~torch.isin(idx_all, idx) idx_rest = idx_all[mask] # for ncomb_j of the measurement - if len(x) == 2: # Gaussian + if len(x) == 2: # Gaussian cov = cov.unsqueeze(-3).unsqueeze(-3) mean = mean.unsqueeze(-3).unsqueeze(-3) + 0j weight = mean.new_ones(size[0], 1, 1) - elif len(x) == 3: # Bosonic + elif len(x) == 3: # Bosonic cov = cov.unsqueeze(-3) mean = mean.unsqueeze(-3) + 0j weight = x[2].unsqueeze(-1) - cov_a = cov[..., idx_rest[:, None], idx_rest] - cov_b = cov[..., idx[:, None], idx] + cov_a = cov[..., idx_rest[:, None], idx_rest] + cov_b = cov[..., idx[:, None], idx] cov_ab = cov[..., idx_rest[:, None], idx] mean_a = mean[..., idx_rest, :] mean_b = mean[..., idx, :] ncomb_j = self.weight.shape[-1] - if self.cov.shape[0] == 1: - cov_t = cov_b + self.cov.expand(ncomb_j, -1, -1) # (batch, ncomb, ncomb_j, 2 * nwire, 2 * nwire) - else: - cov_t = cov_b + self.cov - cov_new = cov_t.flatten(-4, -3) # (batch, ncomb_new, 2 * nwire, 2 * nwire) + # (batch, ncomb, ncomb_j, 2 * nwire, 2 * nwire) + cov_t = cov_b + self.cov.expand(ncomb_j, -1, -1) if self.cov.shape[0] == 1 else cov_b + self.cov + cov_new = cov_t.flatten(-4, -3) # (batch, ncomb_new, 2 * nwire, 2 * nwire) mean_new = mean_b.expand(-1, -1, ncomb_j, -1, -1).flatten(-4, -3) weight_new = (weight * self.weight).flatten(-2, -1) - size_out = cov_new.size()[:-2] + size[-2:] # (batch, ncomb_new, 2 * nmode, 2 * nmode) + size_out = cov_new.size()[:-2] + size[-2:] # (batch, ncomb_new, 2 * nmode, 2 * nmode) # (batch, ncomb, ncomb_j, 2 * nwire_rest, 2 * nwire_rest) - cov_a = cov_a - cov_ab @ torch.linalg.solve(cov_t, cov_ab.mT) # update the unmeasured part + cov_a = cov_a - cov_ab @ torch.linalg.solve(cov_t, cov_ab.mT) # update the unmeasured part cov_out = cov.new_ones(size_out[:-1].numel()).reshape(size_out[:-1]).diag_embed() - cov_out[..., idx_rest[:, None], idx_rest] = cov_a.flatten(-4, -3) # update the total cov mat + cov_out[..., idx_rest[:, None], idx_rest] = cov_a.flatten(-4, -3) # update the total cov mat if samples is None: # (batch, 2 * nwire) mean_m = sample_reject_bosonic(cov_new, mean_new, weight_new, cov_new.new_zeros(1), 1)[:, 0] @@ -340,9 +346,9 @@ def forward(self, x: List[torch.Tensor], samples: Any = None) -> List[torch.Tens mean_m = samples.reshape(-1, 2 * len(self.wires)) # (batch, ncomb_new) exp_real = torch.exp(mean_new.imag.mT @ torch.linalg.solve(cov_new, mean_new.imag) / 2).squeeze(-2, -1) - gaus_b = MultivariateNormal(mean_new.squeeze(-1).real, cov_new) # (batch, ncomb_new, 2 * nwire) - prob_g = gaus_b.log_prob(mean_m.unsqueeze(-2)).exp() # (batch, ncomb_new, 2 * nwire) -> (batch, ncomb_new) - rm = mean_m.unsqueeze(-1).unsqueeze(-3) # (batch, 2 * nwire) -> (batch, 1, 2 * nwire, 1) + gaus_b = MultivariateNormal(mean_new.squeeze(-1).real, cov_new) # (batch, ncomb_new, 2 * nwire) + prob_g = gaus_b.log_prob(mean_m.unsqueeze(-2)).exp() # (batch, ncomb_new, 2 * nwire) -> (batch, ncomb_new) + rm = mean_m.unsqueeze(-1).unsqueeze(-3) # (batch, 2 * nwire) -> (batch, 1, 2 * nwire, 1) # (batch, ncomb_new) exp_imag = torch.exp((rm - mean_new.real).mT @ torch.linalg.solve(cov_new, mean_new.imag) * 1j).squeeze(-2, -1) weight_out = weight_new * exp_real * prob_g * exp_imag @@ -352,7 +358,7 @@ def forward(self, x: List[torch.Tensor], samples: Any = None) -> List[torch.Tens mean_out = mean.new_zeros(size_out[:-1]).unsqueeze(-1) mean_out[..., idx_rest, :] = mean_a.flatten(-4, -3) - self.samples = mean_m # xxpp order + self.samples = mean_m # xxpp order return [cov_out, mean_out, weight_out] @@ -368,14 +374,15 @@ class PhotonNumberResolvingBosonic(GeneralBosonic): cutoff (int or None, optional): The Fock space truncation. Default: ``None`` name (str, optional): The name of the measurement. Default: ``'PhotonNumberResolvingBosonic'`` """ + def __init__( self, n: int, r: Any = 0.05, nmode: int = 1, - wires: Union[int, List[int], None] = None, - cutoff: Optional[int] = None, - name: str = 'PhotonNumberResolvingBosonic' + wires: int | list[int] | None = None, + cutoff: int | None = None, + name: str = 'PhotonNumberResolvingBosonic', ) -> None: self.nmode = nmode if wires is None: @@ -388,7 +395,7 @@ def __init__( super().__init__(cov=cov, weight=weight, nmode=nmode, wires=wires, cutoff=cutoff, name=name) assert len(self.wires) == 1, f'{self.name} must act on one mode' - def forward(self, x: List[torch.Tensor]) -> List[torch.Tensor]: + def forward(self, x: list[torch.Tensor]) -> list[torch.Tensor]: cov = x[0] batch = cov.shape[0] return super().forward(x, cov.new_zeros(batch, 2)) diff --git a/src/deepquantum/photonic/operation.py b/src/deepquantum/photonic/operation.py index 2f9b5904..2e1b2d26 100644 --- a/src/deepquantum/photonic/operation.py +++ b/src/deepquantum/photonic/operation.py @@ -1,14 +1,12 @@ -""" -Base classes -""" +"""Base classes for photonic quantum operations""" -from typing import Any, List, Optional, Tuple, Union +from typing import Any import numpy as np import torch from torch import nn, vmap -from ..qmath import state_to_tensors, evolve_state, evolve_den_mat +from ..qmath import evolve_den_mat, evolve_state, state_to_tensors from ..state import MatrixProductState from .distributed import dist_gate from .state import DistributedFockState @@ -28,16 +26,17 @@ class Operation(nn.Module): mu (float, optional): The mean of Gaussian noise. Default: 0 sigma (float, optional): The standard deviation of Gaussian noise. Default: 0.1 """ + def __init__( self, - name: Optional[str] = None, + name: str | None = None, nmode: int = 1, - wires: Union[int, List, None] = None, + wires: int | list | None = None, cutoff: int = 2, den_mat: bool = False, noise: bool = False, mu: float = 0, - sigma: float = 0.1 + sigma: float = 0.1, ) -> None: super().__init__() self.name = name @@ -65,7 +64,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: """Perform a forward pass.""" return self.tensor_rep(x) - def _convert_indices(self, indices: Union[int, List[int]]) -> List[int]: + def _convert_indices(self, indices: int | list[int]) -> list[int]: """Convert and check the indices of the modes.""" if isinstance(indices, int): indices = [indices] @@ -76,7 +75,7 @@ def _convert_indices(self, indices: Union[int, List[int]]) -> List[int]: assert len(set(indices)) == len(indices), 'Invalid input' return indices - def _check_minmax(self, minmax: List[int]) -> None: + def _check_minmax(self, minmax: list[int]) -> None: """Check the minimum and maximum indices of the modes.""" assert isinstance(minmax, list) assert len(minmax) == 2 @@ -98,16 +97,17 @@ class Gate(Operation): mu (float, optional): The mean of Gaussian noise. Default: 0 sigma (float, optional): The standard deviation of Gaussian noise. Default: 0.1 """ + def __init__( self, - name: Optional[str] = None, + name: str | None = None, nmode: int = 1, - wires: Union[int, List[int], None] = None, - cutoff: Optional[int] = None, + wires: int | list[int] | None = None, + cutoff: int | None = None, den_mat: bool = False, noise: bool = False, mu: float = 0, - sigma: float = 0.1 + sigma: float = 0.1, ) -> None: self.nmode = nmode if wires is None: @@ -115,8 +115,9 @@ def __init__( wires = self._convert_indices(wires) if cutoff is None: cutoff = 2 - super().__init__(name=name, nmode=nmode, wires=wires, cutoff=cutoff, - den_mat=den_mat, noise=noise, mu=mu, sigma=sigma) + super().__init__( + name=name, nmode=nmode, wires=wires, cutoff=cutoff, den_mat=den_mat, noise=noise, mu=mu, sigma=sigma + ) def update_matrix(self) -> torch.Tensor: """Update the local unitary matrix acting on creation operators.""" @@ -143,16 +144,16 @@ def update_matrix_state(self) -> torch.Tensor: def op_state_tensor(self, x: torch.Tensor) -> torch.Tensor: """Perform a forward pass for state tensors.""" nt = len(self.wires) - matrix = self.update_matrix_state().reshape(self.cutoff ** nt, self.cutoff ** nt) + matrix = self.update_matrix_state().reshape(self.cutoff**nt, self.cutoff**nt) return evolve_state(x, matrix, self.nmode, self.wires, self.cutoff) def op_den_mat(self, x: torch.Tensor) -> torch.Tensor: """Perform a forward pass for density matrices.""" nt = len(self.wires) - matrix = self.update_matrix_state().reshape(self.cutoff ** nt, self.cutoff ** nt) + matrix = self.update_matrix_state().reshape(self.cutoff**nt, self.cutoff**nt) return evolve_den_mat(x, matrix, self.nmode, self.wires, self.cutoff) - def update_transform_xp(self) -> Tuple[torch.Tensor, torch.Tensor]: + def update_transform_xp(self) -> tuple[torch.Tensor, torch.Tensor]: """Update the local affine symplectic transformation acting on quadrature operators in xxpp order.""" return self.matrix_xp, self.vector_xp @@ -175,7 +176,7 @@ def get_displacement(self) -> torch.Tensor: d[np.ix_(wires)] = vector return d - def op_cv(self, x: List[torch.Tensor]) -> List[torch.Tensor]: + def op_cv(self, x: list[torch.Tensor]) -> list[torch.Tensor]: """Perform a forward pass for Gaussian (Bosonic) states.""" cov, mean = x[:2] sp_mat = self.get_symplectic() @@ -183,7 +184,7 @@ def op_cv(self, x: List[torch.Tensor]) -> List[torch.Tensor]: mean = sp_mat.to(mean.dtype) @ mean + self.get_displacement() return [cov, mean] + x[2:] - def get_mpo(self) -> Tuple[List[torch.Tensor], int]: + def get_mpo(self) -> tuple[list[torch.Tensor], int]: r"""Convert gate to MPO form with identities at empty sites. Note: @@ -210,12 +211,12 @@ def get_mpo(self) -> Tuple[List[torch.Tensor], int]: mat = self.update_matrix_state() # transform gate from (out1, out2, ..., in1, in2 ...) to (out1, in1, out2, in2, ...) order = list(np.arange(2 * nindex).reshape((2, nindex)).T.flatten()) - mat = mat.reshape([self.cutoff] * 2 * nindex).permute(order).reshape([self.cutoff ** 2] * nindex) - main_tensors = state_to_tensors(mat, nsite=nindex, qudit=self.cutoff ** 2) + mat = mat.reshape([self.cutoff] * 2 * nindex).permute(order).reshape([self.cutoff**2] * nindex) + main_tensors = state_to_tensors(mat, nsite=nindex, qudit=self.cutoff**2) # each tensor is in shape of (i, a, b, j) tensors = [] previous_i = None - for i, main_tensor in zip(index_sort, main_tensors): + for i, main_tensor in zip(index_sort, main_tensors, strict=True): # insert identities in the middle if previous_i is not None: for _ in range(previous_i + 1, i): @@ -251,14 +252,13 @@ def op_mps(self, mps: MatrixProductState) -> MatrixProductState: def op_dist_state(self, x: DistributedFockState) -> DistributedFockState: """Perform a forward pass for a distributed Fock state tensor.""" nt = len(self.wires) - matrix = self.update_matrix_state().reshape(self.cutoff ** nt, self.cutoff ** nt) + matrix = self.update_matrix_state().reshape(self.cutoff**nt, self.cutoff**nt) targets = [self.nmode - wire - 1 for wire in self.wires] return dist_gate(x, targets, matrix) def forward( - self, - x: Union[torch.Tensor, List[torch.Tensor], MatrixProductState, DistributedFockState] - ) -> Union[torch.Tensor, List[torch.Tensor], MatrixProductState, DistributedFockState]: + self, x: torch.Tensor | list[torch.Tensor] | MatrixProductState | DistributedFockState + ) -> torch.Tensor | list[torch.Tensor] | MatrixProductState | DistributedFockState: """Perform a forward pass.""" if isinstance(x, DistributedFockState): return self.op_dist_state(x) @@ -286,12 +286,13 @@ class Channel(Operation): Default: ``None`` cutoff (int or None, optional): The Fock space truncation. Default: ``None`` """ + def __init__( self, - name: Optional[str] = None, + name: str | None = None, nmode: int = 1, - wires: Union[int, List[int], None] = None, - cutoff: Optional[int] = None, + wires: int | list[int] | None = None, + cutoff: int | None = None, ) -> None: self.nmode = nmode if wires is None: @@ -308,15 +309,15 @@ def update_matrix_state(self) -> torch.Tensor: def op_den_mat(self, x: torch.Tensor) -> torch.Tensor: """Perform a forward pass for density matrices.""" nt = len(self.wires) - matrix = self.update_matrix_state().reshape(-1, self.cutoff ** nt, self.cutoff ** nt) + matrix = self.update_matrix_state().reshape(-1, self.cutoff**nt, self.cutoff**nt) x = vmap(evolve_den_mat, in_dims=(None, 0, None, None, None))(x, matrix, self.nmode, self.wires, self.cutoff) return x.sum(0) - def update_transform_xy(self) -> Tuple[torch.Tensor, torch.Tensor]: + def update_transform_xy(self) -> tuple[torch.Tensor, torch.Tensor]: """Update the local transformation matrices X and Y acting on Gaussian states.""" return self.matrix_x, self.matrix_y - def op_cv(self, x: List[torch.Tensor]) -> List[torch.Tensor]: + def op_cv(self, x: list[torch.Tensor]) -> list[torch.Tensor]: """Perform a forward pass for Gaussian (Bosonic) states. See Quantum Continuous Variables: A Primer of Theoretical Methods (2024) @@ -336,7 +337,7 @@ def op_cv(self, x: List[torch.Tensor]) -> List[torch.Tensor]: mean = mat_x.to(mean.dtype) @ mean return [cov, mean] + x[2:] - def forward(self, x: Union[torch.Tensor, List[torch.Tensor]]) -> Union[torch.Tensor, List[torch.Tensor]]: + def forward(self, x: torch.Tensor | list[torch.Tensor]) -> torch.Tensor | list[torch.Tensor]: """Perform a forward pass.""" if isinstance(x, torch.Tensor): return self.op_den_mat(x) @@ -359,17 +360,18 @@ class Delay(Operation): mu (float, optional): The mean of Gaussian noise. Default: 0 sigma (float, optional): The standard deviation of Gaussian noise. Default: 0.1 """ + def __init__( self, - name = 'Delay', + name='Delay', ntau: int = 1, nmode: int = 1, - wires: Union[int, List[int], None] = None, - cutoff: Optional[int] = None, + wires: int | list[int] | None = None, + cutoff: int | None = None, den_mat: bool = False, noise: bool = False, mu: float = 0, - sigma: float = 0.1 + sigma: float = 0.1, ) -> None: self.nmode = nmode if wires is None: @@ -377,8 +379,9 @@ def __init__( wires = self._convert_indices(wires) if cutoff is None: cutoff = 2 - super().__init__(name=name, nmode=nmode, wires=wires, cutoff=cutoff, den_mat=den_mat, - noise=noise, mu=mu, sigma=sigma) + super().__init__( + name=name, nmode=nmode, wires=wires, cutoff=cutoff, den_mat=den_mat, noise=noise, mu=mu, sigma=sigma + ) assert len(self.wires) == 1, f'{self.name} must act on one mode' self.ntau = ntau self.gates = nn.Sequential() @@ -396,13 +399,12 @@ def init_para(self, inputs: Any = None) -> None: if inputs is None: gate.init_para() else: - gate.init_para(inputs[count:count+gate.npara]) + gate.init_para(inputs[count : count + gate.npara]) count += gate.npara def forward( - self, - x: Union[torch.Tensor, List[torch.Tensor], MatrixProductState, DistributedFockState] - ) -> Union[torch.Tensor, List[torch.Tensor], MatrixProductState, DistributedFockState]: + self, x: torch.Tensor | list[torch.Tensor] | MatrixProductState | DistributedFockState + ) -> torch.Tensor | list[torch.Tensor] | MatrixProductState | DistributedFockState: """Perform a forward pass.""" return self.gates(x) diff --git a/src/deepquantum/photonic/qmath.py b/src/deepquantum/photonic/qmath.py index 89e105b5..2ef4ea3a 100644 --- a/src/deepquantum/photonic/qmath.py +++ b/src/deepquantum/photonic/qmath.py @@ -1,11 +1,9 @@ -""" -Common functions -""" +"""Common functions""" import itertools import warnings from collections import Counter -from typing import Dict, Generator, List, Optional, Tuple, Union +from collections.abc import Generator import matplotlib.pyplot as plt import torch @@ -14,14 +12,15 @@ from torch.distributions.multivariate_normal import MultivariateNormal import deepquantum.photonic as dqp -from ..qmath import list_to_decimal, decimal_to_list, is_unitary, partial_trace, block_sample + +from ..qmath import block_sample, decimal_to_list, is_unitary, list_to_decimal, partial_trace from .utils import mem_to_chunksize -def dirac_ket(matrix: torch.Tensor) -> Dict: +def dirac_ket(matrix: torch.Tensor) -> dict: """Convert the batched Fock state tensor to the dictionary of Dirac ket.""" ket_dict = {} - for i in range(matrix.shape[0]): # consider batch i + for i in range(matrix.shape[0]): # consider batch i state_i = matrix[i] abs_state = abs(state_i) # get the largest k values with abs(amplitudes) @@ -43,8 +42,8 @@ def dirac_ket(matrix: torch.Tensor) -> Dict: return ket_dict -def sort_dict_fock_basis(state_dict: Dict, idx: int = 0) -> Dict: - """Sort the dictionary of Fock basis states in the descending order of probs.""" +def sort_dict_fock_basis(state_dict: dict, idx: int = 0) -> dict: + """Sort the dictionary of Fock basis states in the descending order of probabilities.""" sort_list = sorted(state_dict.items(), key=lambda t: abs(t[1][idx]), reverse=True) sorted_dict = {} for key, value in sort_list: @@ -53,9 +52,9 @@ def sort_dict_fock_basis(state_dict: Dict, idx: int = 0) -> Dict: def sub_matrix(u: torch.Tensor, input_state: torch.Tensor, output_state: torch.Tensor) -> torch.Tensor: - """Get the submatrix for calculating the transfer amplitude and transfer probs from the given matrix, - the input state and the output state. The rows are chosen according to the output state and the columns - are chosen according to the input state. + """Get the submatrix for calculating the transfer amplitude and transfer probabilities. + + The rows are chosen according to the output state and the columns are chosen according to the input state. Args: u (torch.Tensor): The unitary matrix. @@ -63,7 +62,7 @@ def sub_matrix(u: torch.Tensor, input_state: torch.Tensor, output_state: torch.T output_state (torch.Tensor): The output state. """ with warnings.catch_warnings(): - warnings.filterwarnings('ignore') # local warning + warnings.filterwarnings('ignore') # local warning u1 = torch.repeat_interleave(u, output_state, dim=0) u2 = torch.repeat_interleave(u1, input_state, dim=-1) return u2 @@ -84,12 +83,14 @@ def permanent(mat: torch.Tensor) -> torch.Tensor: if shape[0] == 2: return mat[0, 0] * mat[1, 1] + mat[0, 1] * mat[1, 0] if shape[0] == 3: - return (mat[0, 2] * mat[1, 1] * mat[2, 0] - + mat[0, 1] * mat[1, 2] * mat[2, 0] - + mat[0, 2] * mat[1, 0] * mat[2, 1] - + mat[0, 0] * mat[1, 2] * mat[2, 1] - + mat[0, 1] * mat[1, 0] * mat[2, 2] - + mat[0, 0] * mat[1, 1] * mat[2, 2]) + return ( + mat[0, 2] * mat[1, 1] * mat[2, 0] + + mat[0, 1] * mat[1, 2] * mat[2, 0] + + mat[0, 2] * mat[1, 0] * mat[2, 1] + + mat[0, 0] * mat[1, 2] * mat[2, 1] + + mat[0, 1] * mat[1, 0] * mat[2, 2] + + mat[0, 0] * mat[1, 1] * mat[2, 2] + ) return permanent_ryser(mat) @@ -101,7 +102,8 @@ def create_subset(num_coincidence: int) -> Generator[torch.Tensor, None, None]: comb_lst.append(list(comb)) yield torch.tensor(comb_lst).reshape(len(comb_lst), k) -def get_powerset(n: int) -> List: + +def get_powerset(n: int) -> list: r"""Get the powerset of :math:`\{0,1,...,n-1\}`.""" powerset = [] for k in range(n + 1): @@ -114,6 +116,7 @@ def get_powerset(n: int) -> List: def permanent_ryser(mat: torch.Tensor) -> torch.Tensor: """Calculate the permanent by Ryser's formula.""" + def helper(subset: torch.Tensor, mat: torch.Tensor) -> torch.Tensor: num_elements = subset.numel() s = torch.sum(mat[:, subset], dim=-1) @@ -132,10 +135,10 @@ def helper(subset: torch.Tensor, mat: torch.Tensor) -> torch.Tensor: def product_factorial(state: torch.Tensor) -> torch.Tensor: """Get the product of the factorial from the Fock state, i.e., :math:`|s_1,s_2,...s_n> -> s_1!s_2!...s_n!`.""" - return torch.exp(torch.lgamma(state.double() + 1).sum(-1, keepdim=True)) # nature log gamma function + return torch.exp(torch.lgamma(state.double() + 1).sum(-1, keepdim=True)) # nature log gamma function -def fock_combinations(nmode: int, nphoton: int, cutoff: Optional[int] = None, nancilla: int = 0) -> List: +def fock_combinations(nmode: int, nphoton: int, cutoff: int | None = None, nancilla: int = 0) -> list: """Generate all possible combinations of Fock states for a given number of modes, photons, and cutoff. Args: @@ -159,7 +162,8 @@ def fock_combinations(nmode: int, nphoton: int, cutoff: Optional[int] = None, na if cutoff is None: cutoff = nphoton + 1 result = [] - def backtrack(state: List[int], length: int, num_sum: int) -> None: + + def backtrack(state: list[int], length: int, num_sum: int) -> None: """A helper function that uses backtracking to generate all possible Fock states. Args: @@ -183,23 +187,23 @@ def backtrack(state: List[int], length: int, num_sum: int) -> None: return result -def ladder_ops(cutoff: int, dtype = torch.cfloat, device = 'cpu') -> Tuple[torch.Tensor, torch.Tensor]: +def ladder_ops(cutoff: int, dtype=torch.cfloat, device='cpu') -> tuple[torch.Tensor, torch.Tensor]: """Get the matrix representation of the annihilation and creation operators.""" sqrt = torch.arange(1, cutoff, dtype=dtype, device=device) ** 0.5 a = torch.diag(sqrt, diagonal=1) - ad = a.mH # share the memory + ad = a.mH # share the memory return a, ad -def shift_func(l: List, nstep: int) -> List: +def shift_func(lst: list, nstep: int) -> list: """Shift a list by a number of steps. If ``nstep`` is positive, it shifts to the left. """ - if len(l) <= 1: - return l - nstep = nstep % len(l) - return l[nstep:] + l[:nstep] + if len(lst) <= 1: + return lst + nstep = nstep % len(lst) + return lst[nstep:] + lst[:nstep] def xxpp_to_xpxp(matrix: torch.Tensor) -> torch.Tensor: @@ -233,15 +237,14 @@ def quadrature_to_ladder(tensor: torch.Tensor, symplectic: bool = False) -> torc nmode = tensor.shape[-2] // 2 tensor = tensor + 0j identity = torch.eye(nmode, dtype=tensor.dtype, device=tensor.device) - omega = torch.cat([torch.cat([identity, identity * 1j], dim=-1), - torch.cat([identity, identity * -1j], dim=-1)]) + omega = torch.cat([torch.cat([identity, identity * 1j], dim=-1), torch.cat([identity, identity * -1j], dim=-1)]) if tensor.shape[-1] == 2 * nmode: if symplectic: - return omega @ tensor @ omega.mH / 2 # inversed omega + return omega @ tensor @ omega.mH / 2 # inversed omega else: - return omega @ tensor @ omega.mH * dqp.kappa ** 2 / dqp.hbar + return omega @ tensor @ omega.mH * dqp.kappa**2 / dqp.hbar elif tensor.shape[-1] == 1: - return omega @ tensor * dqp.kappa / dqp.hbar ** 0.5 + return omega @ tensor * dqp.kappa / dqp.hbar**0.5 def ladder_to_quadrature(tensor: torch.Tensor, symplectic: bool = False) -> torch.Tensor: @@ -255,32 +258,29 @@ def ladder_to_quadrature(tensor: torch.Tensor, symplectic: bool = False) -> torc nmode = tensor.shape[-2] // 2 tensor = tensor + 0j identity = torch.eye(nmode, dtype=tensor.dtype, device=tensor.device) - omega = torch.cat([torch.cat([identity, identity], dim=-1), - torch.cat([identity * -1j, identity * 1j], dim=-1)]) + omega = torch.cat([torch.cat([identity, identity], dim=-1), torch.cat([identity * -1j, identity * 1j], dim=-1)]) if tensor.shape[-1] == 2 * nmode: if symplectic: - return (omega @ tensor @ omega.mH).real / 2 # inversed omega + return (omega @ tensor @ omega.mH).real / 2 # inversed omega else: - return (omega @ tensor @ omega.mH).real * dqp.hbar / (4 * dqp.kappa ** 2) + return (omega @ tensor @ omega.mH).real * dqp.hbar / (4 * dqp.kappa**2) elif tensor.shape[-1] == 1: - return (omega @ tensor).real * dqp.hbar ** 0.5 / (2 * dqp.kappa) + return (omega @ tensor).real * dqp.hbar**0.5 / (2 * dqp.kappa) -def _photon_number_mean_var_gaussian(cov: torch.Tensor, mean: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: +def _photon_number_mean_var_gaussian(cov: torch.Tensor, mean: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: """Get the expectation value and variance of the photon number for single-mode Gaussian states.""" - coef = dqp.kappa ** 2 / dqp.hbar + coef = dqp.kappa**2 / dqp.hbar cov = cov.reshape(-1, 2, 2) mean = mean.reshape(-1, 2, 1) exp = coef * (vmap(torch.trace)(cov) + (mean.mT @ mean).squeeze()) - 1 / 2 - var = coef ** 2 * (vmap(torch.trace)(cov @ cov) + 2 * (mean.mT @ cov.to(mean.dtype) @ mean).squeeze()) * 2 - 1 / 4 + var = coef**2 * (vmap(torch.trace)(cov @ cov) + 2 * (mean.mT @ cov.to(mean.dtype) @ mean).squeeze()) * 2 - 1 / 4 return exp, var def _photon_number_mean_var_bosonic( - cov: torch.Tensor, - mean: torch.Tensor, - weight: torch.Tensor -) -> Tuple[torch.Tensor, torch.Tensor]: + cov: torch.Tensor, mean: torch.Tensor, weight: torch.Tensor +) -> tuple[torch.Tensor, torch.Tensor]: """Get the expectation value and variance of the photon number for single-mode Bosonic states.""" shape_cov = cov.shape shape_mean = mean.shape @@ -290,7 +290,7 @@ def _photon_number_mean_var_bosonic( exp_gaussian = exp_gaussian.reshape(shape_cov[:2]) var_gaussian = var_gaussian.reshape(shape_cov[:2]) exp = (weight * exp_gaussian).sum(-1) - var = (weight * var_gaussian).sum(-1) + (weight * exp_gaussian ** 2).sum(-1) - exp ** 2 + var = (weight * var_gaussian).sum(-1) + (weight * exp_gaussian**2).sum(-1) - exp**2 zeros = cov.new_zeros(1) assert torch.allclose(exp.imag, zeros, atol=1e-6) assert torch.allclose(var.imag, zeros, atol=1e-6) @@ -298,27 +298,21 @@ def _photon_number_mean_var_bosonic( def photon_number_mean_var_cv( - cov: torch.Tensor, - mean: torch.Tensor, - weight: Optional[torch.Tensor] = None -) -> Tuple[torch.Tensor, torch.Tensor]: + cov: torch.Tensor, mean: torch.Tensor, weight: torch.Tensor | None = None +) -> tuple[torch.Tensor, torch.Tensor]: """Get the expectation value and variance of the photon number for single-mode Gaussian (Bosonic) states.""" if weight is None: - return _photon_number_mean_var_gaussian(cov, mean) + return _photon_number_mean_var_gaussian(cov, mean) else: return _photon_number_mean_var_bosonic(cov, mean, weight) def photon_number_mean_var_fock( - state: torch.Tensor, - nmode: int, - cutoff: int, - wires: List[int], - den_mat: bool = False -) -> Tuple[torch.Tensor, torch.Tensor]: + state: torch.Tensor, nmode: int, cutoff: int, wires: list[int], den_mat: bool = False +) -> tuple[torch.Tensor, torch.Tensor]: """Get the expectation value and variance of the photon number for Fock state tensors.""" if den_mat: - rho = state.reshape(-1, cutoff ** nmode, cutoff ** nmode) + rho = state.reshape(-1, cutoff**nmode, cutoff**nmode) prob = torch.diagonal(rho, dim1=1, dim2=2).reshape([-1] + [cutoff] * nmode) else: if state.ndim == nmode: @@ -328,52 +322,48 @@ def photon_number_mean_var_fock( num_exp_list = [] var_list = [] for i in wires: - p_i = torch.sum(prob, dim=[j+1 for j in range(nmode) if j != i]) - num_exp = (num_op * p_i).sum(dim=-1) # (batch,) - num2_exp = ((num_op ** 2) * p_i).sum(dim=-1) # (batch,) - var = num2_exp - num_exp ** 2 + p_i = torch.sum(prob, dim=[j + 1 for j in range(nmode) if j != i]) + num_exp = (num_op * p_i).sum(dim=-1) # (batch,) + num2_exp = ((num_op**2) * p_i).sum(dim=-1) # (batch,) + var = num2_exp - num_exp**2 num_exp_list.append(num_exp) var_list.append(var) return torch.stack(num_exp_list).real, torch.stack(var_list).real def quadrature_mean_fock( - state: torch.Tensor, - nmode: int, - cutoff: int, - wires: List[int], - den_mat: bool = False + state: torch.Tensor, nmode: int, cutoff: int, wires: list[int], den_mat: bool = False ) -> torch.Tensor: """Get the expectation value of the quadrature x for Fock state tensors.""" - coef = 2 * dqp.kappa ** 2 / dqp.hbar + coef = 2 * dqp.kappa**2 / dqp.hbar factor = torch.sqrt(torch.arange(1, cutoff, device=state.device, dtype=state.real.dtype) / 2) mean = [] if den_mat: - state = state.reshape(-1, cutoff ** nmode, cutoff ** nmode) + state = state.reshape(-1, cutoff**nmode, cutoff**nmode) for wire in wires: trace_lst = [i for i in range(nmode) if i != wire] - reduced_dm = partial_trace(state, nmode, trace_lst, cutoff) # (batch, cutoff, cutoff) + reduced_dm = partial_trace(state, nmode, trace_lst, cutoff) # (batch, cutoff, cutoff) reduced_dm = reduced_dm.reshape(-1, cutoff, cutoff) - off_diag = reduced_dm.diagonal(offset=1, dim1=1, dim2=2) # rho_{n, n+1} - term = factor * 2 * off_diag.real # only with real part contribution + off_diag = reduced_dm.diagonal(offset=1, dim1=1, dim2=2) # rho_{n, n+1} + term = factor * 2 * off_diag.real # only with real part contribution mean.append(term.sum(dim=1)) else: if state.ndim == nmode: state = state.unsqueeze(0) factor = factor.view([1, -1] + [1] * (nmode - 1)) for wire in wires: - pm_shape = list(range(1, nmode+1)) - pm_shape.remove(wire+1) - pm_shape = [0] + [wire+1] + pm_shape + pm_shape = list(range(1, nmode + 1)) + pm_shape.remove(wire + 1) + pm_shape = [0] + [wire + 1] + pm_shape state_i = state.permute(pm_shape) - cn = state_i[:, :-1, ...] # n - cn1 = state_i[:, 1:, ...] # n+1 + cn = state_i[:, :-1, ...] # n + cn1 = state_i[:, 1:, ...] # n+1 term = factor * 2 * (cn.conj() * cn1).real mean.append(term.sum(dim=tuple(range(1, nmode + 1)))) return coef ** (-0.5) * torch.stack(mean) -def takagi(a: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: +def takagi(a: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: """Tagaki decomposition for a symmetric complex matrix. See https://math.stackexchange.com/questions/2026110/ @@ -384,13 +374,13 @@ def takagi(a: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: a_2[:size, size:] = a.imag a_2[size:, :size] = a.imag s, u = torch.linalg.eigh(a_2) - diag = s[size:] # s already sorted + diag = s[size:] # s already sorted v = u[:, size:][size:] + 1j * u[:, size:][:size] if is_unitary(v): return v, diag - else: # consider degeneracy case + else: # consider degeneracy case idx_zero = torch.where(abs(s) < 1e-5)[0] - idx_max = max(idx_zero) + 1 + idx_max = max(idx_zero) + 1 temp = abs(u[:size, idx_max:]) ** 2 + abs(u[size:, idx_max:]) ** 2 sum_rhalf = temp.sum(1) idx_lt_1 = torch.where(abs(sum_rhalf - 1) > 1e-6)[0] @@ -414,7 +404,7 @@ def sqrtm_herm(mat: torch.Tensor) -> torch.Tensor: return mat_q @ lambd.sqrt().diag_embed().to(mat_q.dtype) @ mat_q.mH -def schur_anti_symm_even(mat: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: +def schur_anti_symm_even(mat: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: r"""Schur decomposition for a real antisymmetric and even-dimensional matrix. This function decomposes a real antisymmetric matrix :math:`A` into the form :math:`A = O T O^T`, @@ -429,17 +419,17 @@ def schur_anti_symm_even(mat: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor] idx1 = torch.arange(0, n, 2, device=mat.device) idx2 = torch.arange(1, n, 2, device=mat.device) # positive value is above the diagonal and in ascending order - mat_t[idx1, idx2] = lambd[n//2:] - mat_t[idx2, idx1] = -lambd[n//2:] + mat_t[idx1, idx2] = lambd[n // 2 :] + mat_t[idx2, idx1] = -lambd[n // 2 :] mat_o = torch.zeros_like(mat) - mat_o[:, ::2] = u[:, n//2:].real - mat_o[:, 1::2] = u[:, n//2:].imag + mat_o[:, ::2] = u[:, n // 2 :].real + mat_o[:, 1::2] = u[:, n // 2 :].imag norm = torch.linalg.vector_norm(mat_o, dim=0, keepdim=True) mat_o = mat_o / norm return mat_t, mat_o -def williamson(cov: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: +def williamson(cov: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: """Williamson decomposition. This function decomposes a real symmetric and even-dimensional positive definite matrix :math:`V` @@ -452,12 +442,12 @@ def williamson(cov: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: nmode = cov.shape[-1] // 2 omega = cov.new_ones(nmode) omega = torch.cat([-omega, omega]).diag_embed() - omega = omega.reshape(2, nmode, 2 * nmode).flip(0).reshape(2 * nmode, 2 * nmode) # symplectic form + omega = omega.reshape(2, nmode, 2 * nmode).flip(0).reshape(2 * nmode, 2 * nmode) # symplectic form vals = torch.linalg.eigvalsh(cov) assert torch.all(vals > 0), 'Matrix must be positive definite.' cov_sqrt = sqrtm_herm(cov) cov_sqrt_inv = cov_sqrt.inverse() - psi = cov_sqrt_inv @ omega @ cov_sqrt_inv # antisymmetric + psi = cov_sqrt_inv @ omega @ cov_sqrt_inv # antisymmetric mat_t, o_tilde = schur_anti_symm_even(psi) idx_perm = torch.arange(2 * nmode, device=cov.device).reshape(nmode, 2).T.flatten() mat_t_xxpp = mat_t[:, idx_perm][idx_perm] @@ -474,9 +464,9 @@ def measure_fock_tensor( state: torch.Tensor, shots: int = 1024, with_prob: bool = False, - wires: Union[int, List[int], None] = None, - block_size: int = 2 ** 24 -) -> Union[Dict, List[Dict]]: + wires: int | list[int] | None = None, + block_size: int = 2**24, +) -> dict | list[dict]: r"""Measure the batched Fock state tensors. Args: @@ -490,8 +480,8 @@ def measure_fock_tensor( measured) block_size (int, optional): The block size for sampling. Default: 2 ** 24 """ - # pylint: disable=import-outside-toplevel from .state import FockState + shape = state.shape batch = shape[0] cutoff = shape[-1] @@ -534,39 +524,35 @@ def sample_homodyne_fock( shots: int = 1, den_mat: bool = False, x_range: float = 15, - nbin: int = 100000 + nbin: int = 100000, ) -> torch.Tensor: """Get the samples of homodyne measurement for batched Fock state tensors on one mode.""" - coef = 2 * dqp.kappa ** 2 / dqp.hbar + coef = 2 * dqp.kappa**2 / dqp.hbar if den_mat: - state = state.reshape(-1, cutoff ** nmode, cutoff ** nmode) + state = state.reshape(-1, cutoff**nmode, cutoff**nmode) else: - state = state.reshape(-1, cutoff ** nmode, 1) + state = state.reshape(-1, cutoff**nmode, 1) state = state @ state.mH trace_lst = [i for i in range(nmode) if i != wire] - reduced_dm = partial_trace(state, nmode, trace_lst, cutoff) # (batch, cutoff, cutoff) - orders = torch.arange(cutoff, dtype=state.real.dtype, device=state.device).reshape(-1, 1) # (cutoff, 1) + reduced_dm = partial_trace(state, nmode, trace_lst, cutoff) # (batch, cutoff, cutoff) + orders = torch.arange(cutoff, dtype=state.real.dtype, device=state.device).reshape(-1, 1) # (cutoff, 1) # with dimension \sqrt{m\omega\hbar} - xs = torch.linspace(-x_range, x_range, nbin, dtype=state.real.dtype, device=state.device) # (nbin) - h_vals = torch.special.hermite_polynomial_h(coef ** 0.5 * xs, orders) #(cutoff, nbin) + xs = torch.linspace(-x_range, x_range, nbin, dtype=state.real.dtype, device=state.device) # (nbin) + h_vals = torch.special.hermite_polynomial_h(coef**0.5 * xs, orders) # (cutoff, nbin) # H_n / \sqrt{2^n * n!} - h_vals = h_vals / torch.sqrt(2 ** orders * torch.exp(torch.lgamma(orders.double() + 1))).to(orders.dtype) - h_mat = h_vals.reshape(1, cutoff, nbin) * h_vals.reshape(cutoff, 1, nbin) # (cutoff, cutoff, nbin) - h_terms = reduced_dm.unsqueeze(-1) * h_mat # (batch, cutoff, cutoff, nbin) - probs = (h_terms.sum(dim=[-3, -2]) * torch.exp(-coef * xs ** 2)).real # (batch, nbin) + h_vals = h_vals / torch.sqrt(2**orders * torch.exp(torch.lgamma(orders.double() + 1))).to(orders.dtype) + h_mat = h_vals.reshape(1, cutoff, nbin) * h_vals.reshape(cutoff, 1, nbin) # (cutoff, cutoff, nbin) + h_terms = reduced_dm.unsqueeze(-1) * h_mat # (batch, cutoff, cutoff, nbin) + probs = (h_terms.sum(dim=[-3, -2]) * torch.exp(-coef * xs**2)).real # (batch, nbin) probs = abs(probs) probs[probs < 1e-10] = 0 - indices = torch.multinomial(probs.reshape(-1, nbin), num_samples=shots, replacement=True) # (batch, shots) + indices = torch.multinomial(probs.reshape(-1, nbin), num_samples=shots, replacement=True) # (batch, shots) samples = xs[indices] - return samples.unsqueeze(-1) # (batch, shots, 1) + return samples.unsqueeze(-1) # (batch, shots, 1) def sample_reject_bosonic( - cov: torch.Tensor, - mean: torch.Tensor, - weight: torch.Tensor, - cov_m: torch.Tensor, - shots: int + cov: torch.Tensor, mean: torch.Tensor, weight: torch.Tensor, cov_m: torch.Tensor, shots: int ) -> torch.Tensor: """Get the samples of the Bosonic states via rejection sampling. @@ -592,25 +578,25 @@ def sample_reject_bosonic( cov_rest = cov[batches] mean_rest = mean[batches] cov_t = cov_m + cov_rest - m0 = torch.multinomial(c_tilde[batches], 1).reshape(-1) # (batch) + m0 = torch.multinomial(c_tilde[batches], 1).reshape(-1) # (batch) cov_m0 = cov[batches, m0] mean_m0 = mean[batches, m0].squeeze(-1).real - dist_g = MultivariateNormal(mean_rest.squeeze(-1).real, cov_t) # (batch, ncomb, 2 * nmode) - r0 = MultivariateNormal(mean_m0, cov_m + cov_m0).sample([shots_tmp]) # (shots, batch, 2 * nmode) - prob_g = dist_g.log_prob(r0.unsqueeze(-2)).exp() # (shots, batch, ncomb, 2 * nmode) -> (shots, batch, ncomb) - g_r0 = (c_tilde[batches] * prob_g).sum(-1) # (shots, batch) + dist_g = MultivariateNormal(mean_rest.squeeze(-1).real, cov_t) # (batch, ncomb, 2 * nmode) + r0 = MultivariateNormal(mean_m0, cov_m + cov_m0).sample([shots_tmp]) # (shots, batch, 2 * nmode) + prob_g = dist_g.log_prob(r0.unsqueeze(-2)).exp() # (shots, batch, ncomb, 2 * nmode) -> (shots, batch, ncomb) + g_r0 = (c_tilde[batches] * prob_g).sum(-1) # (shots, batch) y0 = torch.rand_like(g_r0) * g_r0 - rm = r0.unsqueeze(-1).unsqueeze(-3) # (shots, batch, 2 * nmode) -> (shots, batch, 1, 2 * nmode, 1) + rm = r0.unsqueeze(-1).unsqueeze(-3) # (shots, batch, 2 * nmode) -> (shots, batch, 1, 2 * nmode, 1) # (shots, batch, ncomb) exp_imag = torch.exp((rm - mean_rest.real).mT @ torch.linalg.solve(cov_t, mean_rest.imag) * 1j).squeeze() # Eq.(70-71) - p_r0 = (weight[batches] * exp_real[batches] * prob_g * exp_imag).sum(-1) # (shots, batch) + p_r0 = (weight[batches] * exp_real[batches] * prob_g * exp_imag).sum(-1) # (shots, batch) assert torch.allclose(p_r0.imag, p_r0.imag.new_zeros(1)) - idx_shots, idx_batch = torch.where((y0 <= p_r0.real)) + idx_shots, idx_batch = torch.where(y0 <= p_r0.real) batches_done = [] for i in range(len(batches)): idx = batches[i] - rst[idx] = torch.cat([rst[idx], r0[idx_shots[idx_batch==i], i]]) # (shots, 2 * nmode) + rst[idx] = torch.cat([rst[idx], r0[idx_shots[idx_batch == i], i]]) # (shots, 2 * nmode) count_shots[idx] = len(rst[idx]) if count_shots[idx] >= shots: batches_done.append(idx) @@ -618,10 +604,10 @@ def sample_reject_bosonic( for i in batches_done: batches.remove(i) shots_tmp = shots - min(count_shots) - return torch.stack(rst) # (batch, shots, 2 * nmode) + return torch.stack(rst) # (batch, shots, 2 * nmode) -def align_shape(cov: torch.Tensor, mean: torch.Tensor, weight: torch.Tensor) -> List[torch.Tensor]: +def align_shape(cov: torch.Tensor, mean: torch.Tensor, weight: torch.Tensor) -> list[torch.Tensor]: """Align the shape for Bosonic state.""" ncomb = weight.shape[-1] if cov.ndim == mean.ndim == 4 and weight.ndim == 2: @@ -645,47 +631,40 @@ def fock_to_wigner( nmode: int, cutoff: int, den_mat: bool = False, - xrange: Union[int, List] = 10, - prange: Union[int, List] = 10, - npoints: Union[int, List] = 100, + xrange: int | list = 10, + prange: int | list = 10, + npoints: int | list = 100, plot: bool = True, - k: int = 0 + k: int = 0, ) -> torch.Tensor: - """Compute the Wigner function W(q, p) from a Fock state tensor or density matrix - using the iterative method. + """Get the discretized Wigner function of the specified mode from a Fock state using the iterative method. See https://qutip.org/docs/4.7/modules/qutip/wigner.html Args: state (torch.Tensor): The input Fock state tensor or density matrix. - wire (int): The wigner function for given wire. + wire (int): The Wigner function for the given wire. nmode (int): The mode number of the Fock state. cutoff (int): The Fock space truncation. den_mat (bool, optional): Whether to use density matrix representation. Only valid for Fock state tensor. Default: ``False`` - xrange (int or List, optional): The range of quadrature q. Default: 10 + xrange (int or List, optional): The range of quadrature x. Default: 10 prange (int or List, optional): The range of quadrature p. Default: 10 npoints (int or List, optional): The number of discretization points for quadratures. Default: 100 - plot (bool, optional): Whether to plot the wigner function. Default: ``True`` + plot (bool, optional): Whether to plot the Wigner function. Default: ``True`` k (int, optional): The index of the Wigner function within the batch to plot. Default: 0 """ if den_mat: - rho = state.reshape(-1, cutoff ** nmode, cutoff ** nmode) + rho = state.reshape(-1, cutoff**nmode, cutoff**nmode) else: - state = state.reshape(-1, cutoff ** nmode, 1) + state = state.reshape(-1, cutoff**nmode, 1) rho = state @ state.mH trace_lst = [i for i in range(nmode) if i != wire] - reduced_dm = partial_trace(rho, nmode, trace_lst, cutoff) # (batch, cutoff, cutoff) + reduced_dm = partial_trace(rho, nmode, trace_lst, cutoff) # (batch, cutoff, cutoff) if reduced_dm.ndim == 2: reduced_dm = reduced_dm.unsqueeze(0) - if isinstance(xrange, int): - xlist = [-xrange, xrange] - else: - xlist = xrange - if isinstance(prange, int): - plist = [-prange, prange] - else: - plist = prange + xlist = [-xrange, xrange] if isinstance(xrange, int) else xrange + plist = [-prange, prange] if isinstance(prange, int) else prange if isinstance(npoints, int): xlist.append(npoints) plist.append(npoints) @@ -695,10 +674,10 @@ def fock_to_wigner( assert len(xlist) == len(plist) == 3 xvec = torch.linspace(*xlist, dtype=state.real.dtype, device=state.device) pvec = torch.linspace(*plist, dtype=state.real.dtype, device=state.device) - coef = 2 * dqp.kappa ** 2 / dqp.hbar + coef = 2 * dqp.kappa**2 / dqp.hbar xlist, plist = torch.meshgrid(xvec, pvec, indexing='ij') # alpha = (sqrt(2) * kappa / sqrt(hbar)) * (q + i p) / sqrt(2) - alpha = coef ** 0.5 * (xlist + 1.0j * plist) / 2 ** 0.5 + alpha = coef**0.5 * (xlist + 1.0j * plist) / 2**0.5 w_list = xlist.new_zeros(cutoff, xlist.shape[-2], xlist.shape[-1]) * 1j w_00 = coef * torch.exp(-2 * abs(alpha) ** 2) / torch.pi w_list[0] = w_00 @@ -706,19 +685,19 @@ def fock_to_wigner( # First row: W_{0i} for i in range(1, cutoff): # For numerical stability, it is recommended to use cutoff < 80 - w_list[i] = 2 * alpha * w_list[i-1] / rho.new_tensor(i).sqrt() + w_list[i] = 2 * alpha * w_list[i - 1] / rho.new_tensor(i).sqrt() w += 2 * (reduced_dm[:, 0, i].reshape(-1, 1, 1) * w_list[i]).real # Remaining rows: W_{ij}, i ≥ 1 for i in range(1, cutoff): # Diagonal element W_{ii} - sqrt_i = i ** 0.5 + sqrt_i = i**0.5 temp = w_list[i].clone() - w_list[i] = (2 * alpha.conj() * temp - sqrt_i * w_list[i-1]) / sqrt_i + w_list[i] = (2 * alpha.conj() * temp - sqrt_i * w_list[i - 1]) / sqrt_i w += reduced_dm[:, i, i].reshape(-1, 1, 1) * w_list[i] # Off-diagonal elements W_{ij}, j > i - for j in range(i+1, cutoff): - sqrt_j = j ** 0.5 - temp2 = (2 * alpha * w_list[j-1] - sqrt_i * temp) / sqrt_j + for j in range(i + 1, cutoff): + sqrt_j = j**0.5 + temp2 = (2 * alpha * w_list[j - 1] - sqrt_i * temp) / sqrt_j temp = w_list[j].clone() w_list[j] = temp2 w += 2 * (reduced_dm[:, i, j].reshape(-1, 1, 1) * w_list[j]).real @@ -728,36 +707,30 @@ def fock_to_wigner( def cv_to_wigner( - state: List, + state: list, wire: int, - xrange: Union[int, List] = 10, - prange: Union[int, List] = 10, - npoints: Union[int, List] = 100, + xrange: int | list = 10, + prange: int | list = 10, + npoints: int | list = 100, plot: bool = True, k: int = 0, - normalize: bool = True + normalize: bool = True, ): - """Get the discretized Wigner function of the specified mode. + """Get the discretized Wigner function of the specified mode from a CV state. Args: - state (List): The input Gaussianstate or BosonicState. - wire (int): The wigner function for given wire. - xrange (int or List, optional): The range of quadrature q. Default: 10 + state (List): The input ``Gaussianstate`` or ``BosonicState``. + wire (int): The Wigner function for the given wire. + xrange (int or List, optional): The range of quadrature x. Default: 10 prange (int or List, optional): The range of quadrature p. Default: 10 npoints (int or List, optional): The number of discretization points for quadratures. Default: 100 - plot (bool, optional): Whether to plot the wigner function. Default: ``True`` + plot (bool, optional): Whether to plot the Wigner function. Default: ``True`` k (int, optional): The index of the Wigner function within the batch to plot. Default: 0 normalize (bool, optional): Whether to normalize the Wigner function. Default: ``True`` """ cov, mean = state[:2] - if isinstance(xrange, int): - xlist = [-xrange, xrange] - else: - xlist = xrange - if isinstance(prange, int): - plist = [-prange, prange] - else: - plist = prange + xlist = [-xrange, xrange] if isinstance(xrange, int) else xrange + plist = [-prange, prange] if isinstance(prange, int) else prange if isinstance(npoints, int): xlist.append(npoints) plist.append(npoints) @@ -769,7 +742,7 @@ def cv_to_wigner( pvec = torch.linspace(*plist, dtype=cov.dtype, device=cov.device) grid_x, grid_y = torch.meshgrid(xvec, pvec, indexing='ij') coords = torch.stack([grid_x.reshape(-1), grid_y.reshape(-1)]).mT - coords2 = coords.unsqueeze(1).unsqueeze(2) # (npoints, 1, 1, 2) + coords2 = coords.unsqueeze(1).unsqueeze(2) # (npoints, 1, 1, 2) coords3 = coords.unsqueeze(-1).unsqueeze(-3) if not isinstance(wire, torch.Tensor): wire = torch.tensor(wire).reshape(1) @@ -787,34 +760,30 @@ def cv_to_wigner( weight = state[-1] cov, mean, weight = align_shape(cov, mean, weight) nmode = cov.shape[-1] // 2 - idx = torch.cat([wire, wire + nmode]) # xxpp order - cov = cov[..., idx[:, None], idx] - mean = mean[..., idx, :] + 0j # for Gaussian state - gauss_b = MultivariateNormal(mean.squeeze(-1).real, cov) # mean shape: (batch, ncomb, 2) - prob_g = gauss_b.log_prob(coords2).exp() # (npoints, batch, ncomb) - exp_real = torch.exp(mean.imag.mT @ torch.linalg.solve(cov, mean.imag) / 2).squeeze(-2, -1) # (batch, ncomb) + idx = torch.cat([wire, wire + nmode]) # xxpp order + cov = cov[..., idx[:, None], idx] + mean = mean[..., idx, :] + 0j # for Gaussian state + gauss_b = MultivariateNormal(mean.squeeze(-1).real, cov) # mean shape: (batch, ncomb, 2) + prob_g = gauss_b.log_prob(coords2).exp() # (npoints, batch, ncomb) + exp_real = torch.exp(mean.imag.mT @ torch.linalg.solve(cov, mean.imag) / 2).squeeze(-2, -1) # (batch, ncomb) # (batch, npoints, ncomb) - exp_imag = torch.exp((coords3 - mean.real.unsqueeze(1)).mT @ - torch.linalg.solve(cov, mean.imag).unsqueeze(1) * 1j).squeeze(-2, -1) + exp_imag = torch.exp( + (coords3 - mean.real.unsqueeze(1)).mT @ torch.linalg.solve(cov, mean.imag).unsqueeze(1) * 1j + ).squeeze(-2, -1) wigner_vals = exp_real.unsqueeze(-2) * prob_g.permute(1, 0, 2) * exp_imag * weight.unsqueeze(-2) wigner_vals = wigner_vals.sum(dim=2).reshape(-1, len(xvec), len(pvec)).real if normalize: # normalize the wigner function dx = xvec[1] - xvec[0] dp = pvec[1] - pvec[0] - total_integral = torch.sum(wigner_vals, dim=[1,2]) * dx * dp + total_integral = torch.sum(wigner_vals, dim=[1, 2]) * dx * dp wigner_vals = wigner_vals / total_integral.reshape(-1, 1, 1) if plot: plot_wigner(wigner_vals, xvec, pvec, k) return wigner_vals -def plot_wigner( - wigner: torch.Tensor, - xvec: torch.Tensor, - pvec: torch.Tensor, - k: int = 0 -): +def plot_wigner(wigner: torch.Tensor, xvec: torch.Tensor, pvec: torch.Tensor, k: int = 0): """Plot a 2D contour and a 3D surface of a discretized Wigner function W(x, p). Args: diff --git a/src/deepquantum/photonic/state.py b/src/deepquantum/photonic/state.py index 99d27b62..3fc6d47f 100644 --- a/src/deepquantum/photonic/state.py +++ b/src/deepquantum/photonic/state.py @@ -1,8 +1,6 @@ -""" -Quantum states -""" +"""Quantum states""" -from typing import Any, List, Optional, Union +from typing import Any import matplotlib.pyplot as plt import numpy as np @@ -11,9 +9,10 @@ from torch import nn, vmap import deepquantum.photonic as dqp + from ..communication import comm_get_rank, comm_get_world_size from ..qmath import is_power, list_to_decimal, multi_kron -from .qmath import dirac_ket, xpxp_to_xxpp, xxpp_to_xpxp, fock_to_wigner, cv_to_wigner +from .qmath import cv_to_wigner, dirac_ket, fock_to_wigner, xpxp_to_xxpp, xxpp_to_xpxp class FockState(nn.Module): @@ -29,13 +28,9 @@ class FockState(nn.Module): den_mat (bool, optional): Whether to use density matrix representation. Only valid for Fock state tensor. Default: ``False`` """ + def __init__( - self, - state: Any, - nmode: Optional[int] = None, - cutoff: Optional[int] = None, - basis: bool = True, - den_mat: bool = False + self, state: Any, nmode: int | None = None, cutoff: int | None = None, basis: bool = True, den_mat: bool = False ) -> None: super().__init__() self.basis = basis @@ -44,10 +39,7 @@ def __init__( if state in ('vac', 'zeros'): state = [0] * nmode # Process Fock basis state - if not isinstance(state, torch.Tensor): - state = torch.tensor(state, dtype=torch.long) - else: - state = state.long() + state = torch.tensor(state, dtype=torch.long) if not isinstance(state, torch.Tensor) else state.long() if state.ndim == 1: state = state.unsqueeze(0) assert state.ndim == 2 @@ -71,10 +63,7 @@ def __init__( # Process Fock state tensor if isinstance(state, torch.Tensor): # with the dimension of batch size if nmode is None: - if den_mat: - nmode = (state.ndim - 1) // 2 - else: - nmode = state.ndim - 1 + nmode = (state.ndim - 1) // 2 if den_mat else state.ndim - 1 if cutoff is None: cutoff = state.shape[-1] self.nmode = nmode @@ -84,7 +73,7 @@ def __init__( # Process Fock state tensor from Fock basis states assert isinstance(state, list) if all(isinstance(i, int) for i in state): - state = [(1., state)] + state = [(1.0, state)] assert all(isinstance(i, tuple) for i in state) nphoton = 0 # Determine the number of photons and modes @@ -104,7 +93,7 @@ def __init__( state_ts[fock_basis] = amp state_ts = state_ts.unsqueeze(0) # add additional batch size if den_mat: - state_ts = state_ts.reshape([self.cutoff ** self.nmode, 1]) + state_ts = state_ts.reshape([self.cutoff**self.nmode, 1]) state_ts = (state_ts @ state_ts.mH).reshape([-1] + [self.cutoff] * 2 * self.nmode) if den_mat: assert state_ts.ndim == 2 * self.nmode + 1 @@ -128,11 +117,11 @@ def to(self, arg: Any) -> 'FockState': def wigner( self, wire: int, - xrange: Union[int, List] = 10, - prange: Union[int, List] = 10, - npoints: Union[int, List] = 100, + xrange: int | list = 10, + prange: int | list = 10, + npoints: int | list = 100, plot: bool = True, - k: int = 0 + k: int = 0, ): """Get the discretized Wigner function of the specified mode. @@ -204,17 +193,13 @@ class GaussianState(nn.Module): nmode (int or None, optional): The number of modes in the state. Default: ``None`` cutoff (int or None, optional): The Fock space truncation. Default: ``None`` """ - def __init__( - self, - state: Union[str, List] = 'vac', - nmode: Optional[int] = None, - cutoff: Optional[int] = None - ) -> None: + + def __init__(self, state: str | list = 'vac', nmode: int | None = None, cutoff: int | None = None) -> None: super().__init__() if state == 'vac': if nmode is None: nmode = 1 - cov = torch.eye(2 * nmode) * dqp.hbar / (4 * dqp.kappa ** 2) + cov = torch.eye(2 * nmode) * dqp.hbar / (4 * dqp.kappa**2) mean = torch.zeros(2 * nmode, 1) elif isinstance(state, list): cov = state[0] @@ -229,7 +214,8 @@ def __init__( mean = mean.reshape(-1, 2 * nmode, 1) assert cov.ndim == mean.ndim == 3 assert cov.shape[-2] == cov.shape[-1] == 2 * nmode, ( - 'The shape of the covariance matrix should be (2*nmode, 2*nmode)') + 'The shape of the covariance matrix should be (2*nmode, 2*nmode)' + ) assert mean.shape[-2] == 2 * nmode, 'The length of the mean vector should be 2*nmode' self.register_buffer('cov', cov) self.register_buffer('mean', mean) @@ -239,24 +225,24 @@ def __init__( self.cutoff = cutoff self.is_pure = self.check_purity() - def check_purity(self, rtol = 1e-5, atol = 1e-8): + def check_purity(self, rtol=1e-5, atol=1e-8): """Check if the Gaussian state is pure state See https://arxiv.org/pdf/quant-ph/0503237.pdf Eq.(2.5) """ - purity = 1 / torch.sqrt(torch.det(4 * dqp.kappa ** 2 / dqp.hbar * self.cov)) + purity = 1 / torch.sqrt(torch.det(4 * dqp.kappa**2 / dqp.hbar * self.cov)) unity = torch.tensor(1.0, dtype=purity.dtype, device=purity.device) return torch.allclose(purity, unity, rtol=rtol, atol=atol) def wigner( self, wire: int, - xrange: Union[int, List] = 10, - prange: Union[int, List] = 10, - npoints: Union[int, List] = 100, + xrange: int | list = 10, + prange: int | list = 10, + npoints: int | list = 100, plot: bool = True, k: int = 0, - normalize: bool = True + normalize: bool = True, ): """Get the discretized Wigner function of the specified mode. @@ -284,17 +270,13 @@ class BosonicState(nn.Module): nmode (int or None, optional): The number of modes in the state. Default: ``None`` cutoff (int or None, optional): The Fock space truncation. Default: ``None`` """ - def __init__( - self, - state: Union[str, List] = 'vac', - nmode: Optional[int] = None, - cutoff: Optional[int] = None - ) -> None: + + def __init__(self, state: str | list = 'vac', nmode: int | None = None, cutoff: int | None = None) -> None: super().__init__() if state == 'vac': if nmode is None: nmode = 1 - cov = torch.eye(2 * nmode) * dqp.hbar / (4 * dqp.kappa ** 2) + cov = torch.eye(2 * nmode) * dqp.hbar / (4 * dqp.kappa**2) mean = torch.zeros(2 * nmode, 1) + 0j weight = torch.tensor([1]) + 0j elif isinstance(state, list): @@ -325,7 +307,8 @@ def __init__( assert cov.ndim == mean.ndim == 4 assert cov.shape[0] == mean.shape[0] assert cov.shape[-2] == cov.shape[-1] == 2 * nmode, ( - 'The shape of the covariance matrix should be (2*nmode, 2*nmode)') + 'The shape of the covariance matrix should be (2*nmode, 2*nmode)' + ) assert mean.shape[-2] == 2 * nmode, 'The length of the mean vector should be 2*nmode' self.register_buffer('cov', cov) self.register_buffer('mean', mean) @@ -358,12 +341,12 @@ def tensor_product(self, state: 'BosonicState') -> 'BosonicState': def wigner( self, wire: int, - xrange: Union[int, List] = 10, - prange: Union[int, List] = 10, - npoints: Union[int, List] = 100, + xrange: int | list = 10, + prange: int | list = 10, + npoints: int | list = 100, plot: bool = True, k: int = 0, - normalize: bool = True + normalize: bool = True, ): """Get the discretized Wigner function of the specified mode. @@ -379,13 +362,7 @@ def wigner( return cv_to_wigner([self.cov, self.mean, self.weight], wire, xrange, prange, npoints, plot, k, normalize) def marginal( - self, - wire: int, - phi: float = 0., - xrange: Union[int, List] = 10, - npoints: int = 100, - plot: bool = True, - k: int = 0 + self, wire: int, phi: float = 0.0, xrange: int | list = 10, npoints: int = 100, plot: bool = True, k: int = 0 ): r"""Get the discretized marginal distribution of the specified mode along :math:`x\cos\phi + p\sin\phi`. @@ -397,28 +374,25 @@ def marginal( plot (bool, optional): Whether to plot the marginal function. Default: ``True`` k (int, optional): The index of the marginal function within the batch to plot. Default: 0 """ - # pylint: disable=import-outside-toplevel from .gate import PhaseShift - if isinstance(xrange, int): - xlist = [-xrange, xrange] - else: - xlist = xrange + + xlist = [-xrange, xrange] if isinstance(xrange, int) else xrange xlist.append(npoints) assert len(xlist) == 3 xvec = torch.linspace(*xlist) if not isinstance(wire, torch.Tensor): wire = torch.tensor(wire).reshape(1) - idx = torch.cat([wire, wire + self.nmode]) # xxpp order - cov = self.cov[..., idx[:, None], idx] + idx = torch.cat([wire, wire + self.nmode]) # xxpp order + cov = self.cov[..., idx[:, None], idx] mean = self.mean[..., idx, :] r = PhaseShift(inputs=-phi, nmode=1, wires=[0], cutoff=self.cutoff) r.to(cov.dtype).to(cov.device) - cov, mean = r([cov, mean]) # (batch, ncomb, 2, 2) + cov, mean = r([cov, mean]) # (batch, ncomb, 2, 2) cov = cov[..., 0, 0].unsqueeze(1) mean = mean[..., 0, 0].unsqueeze(1) - prefactor = 1 / (torch.sqrt(2 * torch.pi * cov)) # (batch, 1, ncomb) + prefactor = 1 / (torch.sqrt(2 * torch.pi * cov)) # (batch, 1, ncomb) # (batch, npoints, ncomb) - log_marg_vals = torch.log(self.weight.unsqueeze(1) * prefactor) - 0.5 * (xvec.reshape(-1, 1) - mean)**2 / cov + log_marg_vals = torch.log(self.weight.unsqueeze(1) * prefactor) - 0.5 * (xvec.reshape(-1, 1) - mean) ** 2 / cov marginal_vals = torch.exp(log_marg_vals).sum(2).real if plot: plt.subplots(1, 1, figsize=(12, 10)) @@ -443,7 +417,8 @@ class CatState(BosonicState): cat state, and ``p=1`` an odd cat state. Default: 1 cutoff (int or None, optional): The Fock space truncation. Default: ``None`` """ - def __init__(self, r: Any = None, theta: Any = None, p: int = 1, cutoff: Optional[int] = None) -> None: + + def __init__(self, r: Any = None, theta: Any = None, p: int = 1, cutoff: int | None = None) -> None: nmode = 1 covs = torch.eye(2) * dqp.hbar / (4 * dqp.kappa**2) if r is None: @@ -458,10 +433,18 @@ def __init__(self, r: Any = None, theta: Any = None, p: int = 1, cutoff: Optiona p = torch.tensor(p, dtype=torch.long) real_part = r * torch.cos(theta) imag_part = r * torch.sin(theta) - means = torch.stack([torch.stack([real_part, imag_part]), - -torch.stack([real_part, imag_part]), - torch.stack([imag_part, -real_part]) * 1j, - -torch.stack([imag_part, -real_part]) * 1j]) * dqp.hbar**0.5 / dqp.kappa + means = ( + torch.stack( + [ + torch.stack([real_part, imag_part]), + -torch.stack([real_part, imag_part]), + torch.stack([imag_part, -real_part]) * 1j, + -torch.stack([imag_part, -real_part]) * 1j, + ] + ) + * dqp.hbar**0.5 + / dqp.kappa + ) temp = torch.exp(-2 * r**2) w0 = 0.5 / (1 + temp * torch.cos(p * torch.pi)) w1 = w0 @@ -472,6 +455,7 @@ def __init__(self, r: Any = None, theta: Any = None, p: int = 1, cutoff: Optiona super().__init__(state, nmode, cutoff) +# ruff: noqa: E741 class GKPState(BosonicState): r"""Finite-energy single-mode GKP state. @@ -487,13 +471,14 @@ class GKPState(BosonicState): epsilon (float, optional): The finite energy damping parameter. Default: 0.05 cutoff (int or None, optional): The Fock space truncation. Default: ``None`` """ + def __init__( self, theta: Any = None, phi: Any = None, amp_cutoff: float = 0.1, epsilon: float = 0.05, - cutoff: Optional[int] = None + cutoff: int | None = None, ) -> None: nmode = 1 if theta is None: @@ -527,7 +512,7 @@ def __init__( weights = weights[filt] + 0j weights /= torch.sum(weights) means = means[filt] - means = means * torch.exp(-epsilon) / (1 + exp_eps) * (torch.pi * dqp.hbar / 2)**0.5 / dqp.kappa + 0j + means = means * torch.exp(-epsilon) / (1 + exp_eps) * (torch.pi * dqp.hbar / 2) ** 0.5 / dqp.kappa + 0j covs = torch.eye(2) * dqp.hbar / (4 * dqp.kappa**2) * (1 - exp_eps) / (1 + exp_eps) state = [covs, means, weights] super().__init__(state, nmode, cutoff) @@ -593,9 +578,9 @@ def _update_weight(self, k, l, theta, phi): result[mask5_4] = torch.sin(theta[mask5_4]) * torch.sin(phi[mask5_4]) exp_eps = torch.exp(-2 * self.epsilon) - prefactor = torch.exp(-0.25 * torch.pi * (l**2 + k**2) * (1 - exp_eps)/(1 + exp_eps)) + prefactor = torch.exp(-0.25 * torch.pi * (l**2 + k**2) * (1 - exp_eps) / (1 + exp_eps)) - weight = result * prefactor # update coefficient + weight = result * prefactor # update coefficient return weight @@ -609,12 +594,13 @@ class FockStateBosonic(BosonicState): r (float, optional): The quality parameter for the approximation. Default: 0.05 cutoff (int or None, optional): The Fock space truncation. Default: ``None`` """ - def __init__(self, n: int, r: Any = 0.05, cutoff: Optional[int] = None) -> None: - assert r ** 2 < 1 / n, 'NOT a physical state' + + def __init__(self, n: int, r: Any = 0.05, cutoff: int | None = None) -> None: + assert r**2 < 1 / n, 'NOT a physical state' nmode = 1 m = np.arange(n + 1) combs = comb(n, m) - weight = (1 - n * r**2) / (1 - (n - m) * r**2) * combs * (-1)**(n - m) + weight = (1 - n * r**2) / (1 - (n - m) * r**2) * combs * (-1) ** (n - m) weight = torch.tensor(weight / weight.sum(), dtype=torch.float) + 0j mean = torch.zeros([n + 1, 2, 1]) + 0j m = torch.tensor(m).reshape(-1, 1, 1) @@ -635,7 +621,8 @@ class DistributedFockState(nn.Module): nmode (int or None, optional): The number of modes in the state. Default: ``None`` cutoff (int or None, optional): The Fock space truncation. Default: ``None`` """ - def __init__(self, state: Any, nmode: Optional[int] = None, cutoff: Optional[int] = None) -> None: + + def __init__(self, state: Any, nmode: int | None = None, cutoff: int | None = None) -> None: super().__init__() self.world_size = comm_get_world_size() self.rank = comm_get_rank() @@ -644,7 +631,7 @@ def __init__(self, state: Any, nmode: Optional[int] = None, cutoff: Optional[int state = [(1, [0] * nmode)] assert isinstance(state, list) if all(isinstance(i, int) for i in state): - state = [(1., state)] + state = [(1.0, state)] assert all(isinstance(i, tuple) for i in state) nphoton = 0 for s in state: @@ -653,14 +640,14 @@ def __init__(self, state: Any, nmode: Optional[int] = None, cutoff: Optional[int nmode = len(s[1]) if cutoff is None: cutoff = nphoton + 1 - self.state = state # list of tuples + self.state = state # list of tuples self.nmode = nmode self.cutoff = cutoff assert is_power(self.world_size, self.cutoff) - assert cutoff ** nmode >= self.world_size + assert cutoff**nmode >= self.world_size self.nmode_global = int(np.round(np.emath.logn(self.cutoff, self.world_size))) self.nmode_local = self.nmode - self.nmode_global - self.num_amps_per_node = self.cutoff ** self.nmode_local + self.num_amps_per_node = self.cutoff**self.nmode_local amps = torch.zeros([self.cutoff] * self.nmode_local, dtype=torch.cfloat) buffer = torch.zeros_like(amps) self.register_buffer('amps', amps) @@ -686,13 +673,13 @@ def reset(self): self.buffer.zero_() for s in self.state: amp = s[0] - rank = list_to_decimal(s[1][:self.nmode_global], self.cutoff) + rank = list_to_decimal(s[1][: self.nmode_global], self.cutoff) if self.rank == rank: - fock_basis = tuple(s[1][self.nmode_global:]) + fock_basis = tuple(s[1][self.nmode_global :]) self.amps[fock_basis] = amp -def combine_tensors(tensors: List[torch.Tensor], ndim_ds: int = 2) -> torch.Tensor: +def combine_tensors(tensors: list[torch.Tensor], ndim_ds: int = 2) -> torch.Tensor: """Combine a list of 3D tensors for Bosonic states according to the dimension of direct sum. Args: @@ -704,7 +691,7 @@ def combine_tensors(tensors: List[torch.Tensor], ndim_ds: int = 2) -> torch.Tens # Get number of tensors and their shapes n = len(tensors) shape_lst = [tensor.shape for tensor in tensors] - len_lst, hs, ws = map(list, zip(*shape_lst)) + len_lst, hs, ws = map(list, zip(*shape_lst, strict=True)) size_h = sum(hs) if ndim_ds == 1: size_w = ws[0] @@ -714,7 +701,7 @@ def combine_tensors(tensors: List[torch.Tensor], ndim_ds: int = 2) -> torch.Tens expanded_tensors = [] for i in range(n): # Insert new dimensions and expand for combination - view_shape = [1] * n + list(tensors[i].shape[1:]) # tensors[i]: (len_i, h_i, w_i) + view_shape = [1] * n + list(tensors[i].shape[1:]) # tensors[i]: (len_i, h_i, w_i) view_shape[i] = tensors[i].shape[0] expand_shape = len_lst + list(tensors[i].shape[1:]) expanded = tensors[i].view(*view_shape).expand(*expand_shape) @@ -730,15 +717,15 @@ def combine_tensors(tensors: List[torch.Tensor], ndim_ds: int = 2) -> torch.Tens h, w = hs[i], ws[i] row_start = row_offsets[i] if ndim_ds == 1: - result[..., row_start:row_start+h, :w] = expanded_tensors[i] + result[..., row_start : row_start + h, :w] = expanded_tensors[i] elif ndim_ds == 2: col_start = col_offsets[i] - result[..., row_start:row_start+h, col_start:col_start+w] = expanded_tensors[i] + result[..., row_start : row_start + h, col_start : col_start + w] = expanded_tensors[i] # Flatten result to (len_1*len_2*...*len_n, size_h, size_w) return result.view(-1, size_h, size_w) -def combine_bosonic_states(states: List[BosonicState], cutoff: Optional[int] = None) -> BosonicState: +def combine_bosonic_states(states: list[BosonicState], cutoff: int | None = None) -> BosonicState: """Combine multiple Bosonic states into a single state. Args: diff --git a/src/deepquantum/photonic/tdm.py b/src/deepquantum/photonic/tdm.py index 3607f049..71449b99 100644 --- a/src/deepquantum/photonic/tdm.py +++ b/src/deepquantum/photonic/tdm.py @@ -1,8 +1,6 @@ -""" -Time domain multiplexing -""" +"""Time domain multiplexing""" -from typing import Any, List, Optional, Union +from typing import Any import torch @@ -29,28 +27,38 @@ class QumodeCircuitTDM(QumodeCircuit): mu (float, optional): The mean of Gaussian noise. Default: 0 sigma (float, optional): The standard deviation of Gaussian noise. Default: 0.1 """ + def __init__( self, nmode: int, init_state: Any, - cutoff: Optional[int] = None, + cutoff: int | None = None, backend: str = 'gaussian', - name: Optional[str] = None, + name: str | None = None, noise: bool = False, mu: float = 0, - sigma: float = 0.1 + sigma: float = 0.1, ) -> None: assert backend in ('gaussian', 'bosonic') - super().__init__(nmode=nmode, init_state=init_state, cutoff=cutoff, backend=backend, basis=False, - detector='pnrd', name=name, mps=False, chi=None, noise=noise, mu=mu, sigma=sigma) + super().__init__( + nmode=nmode, + init_state=init_state, + cutoff=cutoff, + backend=backend, + basis=False, + detector='pnrd', + name=name, + mps=False, + chi=None, + noise=noise, + mu=mu, + sigma=sigma, + ) self.samples = None def forward( - self, - data: Optional[torch.Tensor] = None, - state: Any = None, - nstep: Optional[int] = None - ) -> List[torch.Tensor]: + self, data: torch.Tensor | None = None, state: Any = None, nstep: int | None = None + ) -> list[torch.Tensor]: r"""Perform a forward pass of the TDM photonic quantum circuit and return the final state. Args: @@ -83,10 +91,10 @@ def forward( self.state = super().forward(data_i, self.state) samples.append(self.measure_homodyne(shots=1)) self.state = self.state_measured - self.samples = torch.stack(samples, dim=-1) # (batch, nwire, nstep) + self.samples = torch.stack(samples, dim=-1) # (batch, nwire, nstep) return self.state - def get_samples(self, wires: Union[int, List[int], None] = None) -> torch.Tensor: + def get_samples(self, wires: int | list[int] | None = None) -> torch.Tensor: """Get the measured samples according to the given ``wires``.""" if wires is None: wires = self.wires diff --git a/src/deepquantum/photonic/torontonian_.py b/src/deepquantum/photonic/torontonian_.py index 1d369429..6240008c 100644 --- a/src/deepquantum/photonic/torontonian_.py +++ b/src/deepquantum/photonic/torontonian_.py @@ -1,8 +1,4 @@ -""" -functions for torontonian -""" - -from typing import Optional +"""Functions for Torontonian""" import torch @@ -28,7 +24,7 @@ def _tor_helper(submat: torch.Tensor, sub_gamma: torch.Tensor) -> torch.Tensor: return torch.exp(exp_term) / torch.sqrt(torch.linalg.det(cov_q_inv)) -def torontonian(o_mat: torch.Tensor, gamma: Optional[torch.Tensor] = None) -> torch.Tensor: +def torontonian(o_mat: torch.Tensor, gamma: torch.Tensor | None = None) -> torch.Tensor: """Calculate the torontonian function for the given matrix. See https://research-information.bris.ac.uk/ws/portalfiles/portal/329011096/thesis.pdf Eq.(3.54) @@ -50,13 +46,13 @@ def torontonian(o_mat: torch.Tensor, gamma: Optional[torch.Tensor] = None) -> to return tor -def torontonian_batch(o_mat: torch.Tensor, gamma: Optional[torch.Tensor] = None) -> torch.Tensor: +def torontonian_batch(o_mat: torch.Tensor, gamma: torch.Tensor | None = None) -> torch.Tensor: """Calculate the batch torontonian.""" assert o_mat.dim() == 3, 'Input tensor should be in batched size' assert o_mat.shape[-2] == o_mat.shape[-1] assert o_mat.shape[-1] % 2 == 0, 'Input matrix dimension should be even' - if gamma is None: # torontonian case + if gamma is None: # torontonian case tors = torch.vmap(torontonian, in_dims=(0, None))(o_mat, gamma) - else: # loop torontonian case + else: # loop torontonian case tors = torch.vmap(torontonian, in_dims=(0, 0))(o_mat, gamma) return tors diff --git a/src/deepquantum/photonic/utils.py b/src/deepquantum/photonic/utils.py index 945e0e8d..a4d19f30 100644 --- a/src/deepquantum/photonic/utils.py +++ b/src/deepquantum/photonic/utils.py @@ -1,10 +1,7 @@ -""" -Utilities -""" +"""Utilities""" import gzip import pickle -from typing import Optional import numpy as np import psutil @@ -17,35 +14,41 @@ def set_hbar(hbar: float) -> None: """Set the global reduced Planck constant.""" dqp.hbar = hbar + def set_kappa(kappa: float) -> None: """Set the global kappa.""" dqp.kappa = kappa + def load_sample(filename): - """load the sample data with the given filename""" - with gzip.open('./data/' + filename + '.pkl.gz','rb') as f: + """Load the sample data with the given filename""" + with gzip.open('./data/' + filename + '.pkl.gz', 'rb') as f: sample = pickle.load(f) return sample + def save_sample(filename, data): - """save the sample data with the given filename""" - with gzip.open('./data/' + filename + '.pkl.gz','wb') as f: + """Save the sample data with the given filename""" + with gzip.open('./data/' + filename + '.pkl.gz', 'wb') as f: pickle.dump(data, f) return + def load_adj(filename): - """load the adjacent matrix with the given filename""" + """Load the adjacent matrix with the given filename""" mat = np.load('./data/' + filename + '.npy') return mat + def save_adj(filename, data): - """save the adjacent matrix with the given filename""" + """Save the adjacent matrix with the given filename""" np.save('./data/' + filename + '.npy', data) return -def mem_to_chunksize(device: torch.device, dtype: torch.dtype) -> Optional[int]: + +def mem_to_chunksize(device: torch.device, dtype: torch.dtype) -> int | None: """Return the chunk size of vmap according to device free memory and dtype. - + Note: Currently only optimized for permanent and complex dtype. """ if (device, dtype) in dqp.perm_chunksize_dict: @@ -86,6 +89,7 @@ def mem_to_chunksize(device: torch.device, dtype: torch.dtype) -> Optional[int]: dqp.perm_chunksize_dict[device, dtype] = chunksize return chunksize -def set_perm_chunksize(device: torch.device, dtype: torch.dtype, chunksize: Optional[int]) -> None: + +def set_perm_chunksize(device: torch.device, dtype: torch.dtype, chunksize: int | None) -> None: """Set the global chunk size for permanent calculations.""" dqp.perm_chunksize_dict[device, dtype] = chunksize diff --git a/src/deepquantum/qasm3.py b/src/deepquantum/qasm3.py index f198c543..32fd0870 100644 --- a/src/deepquantum/qasm3.py +++ b/src/deepquantum/qasm3.py @@ -1,51 +1,87 @@ +"""Converter between QubitCircuit and QASM3""" + import re -from typing import List, Dict, Any +from typing import Any import numpy as np import torch from .circuit import QubitCircuit from .gate import ( - U3Gate, PhaseShift, PauliX, PauliY, PauliZ, Hadamard, SGate, SDaggerGate, TGate, TDaggerGate, - Rx, Ry, Rz, CNOT, Swap, Rxx, Ryy, Rzz, Toffoli, Fredkin, Barrier + Barrier, + CNOT, + Fredkin, + Hadamard, + PauliX, + PauliY, + PauliZ, + PhaseShift, + Rx, + Rxx, + Ry, + Ryy, + Rz, + Rzz, + SDaggerGate, + SGate, + Swap, + TDaggerGate, + TGate, + Toffoli, + U3Gate, ) -from .operation import Gate, Layer, Channel, Operation +from .operation import Channel, Gate, Layer, Operation # ============================================================================== # DeepQuantum Circuit to OpenQASM 3.0 Converter # ============================================================================== + def _op_to_qasm3(op: Operation) -> str: - """ - Helper function to convert a single deepquantum operation to an OpenQASM 3.0 string. - """ + """Helper function to convert a single deepquantum operation to an OpenQASM 3.0 string.""" if isinstance(op, Layer): - return "\n".join([_op_to_qasm3(gate) for gate in op.gates]) + return '\n'.join([_op_to_qasm3(gate) for gate in op.gates]) if isinstance(op, Barrier): - qubits_str = ", ".join([f"q[{w}]" for w in op.wires]) - return f"barrier {qubits_str};" + qubits_str = ', '.join([f'q[{w}]' for w in op.wires]) + return f'barrier {qubits_str};' if not isinstance(op, Gate): - return f"// Unsupported operation type: {op.__class__.__name__}" + return f'// Unsupported operation type: {op.__class__.__name__}' if isinstance(op, Channel): - return f"// Quantum channels like {op.name} are not part of the OpenQASM 3.0 core specification." + return f'// Quantum channels like {op.name} are not part of the OpenQASM 3.0 core specification.' # Gate name mapping name_map = { - U3Gate: "u", PhaseShift: "p", PauliX: "x", PauliY: "y", PauliZ: "z", - Hadamard: "h", SGate: "s", SDaggerGate: "sdg", TGate: "t", TDaggerGate: "tdg", - Rx: "rx", Ry: "ry", Rz: "rz", Swap: "swap", CNOT: "cx", Toffoli: "ccx", - Fredkin: "cswap", Rxx: "rxx", Ryy: "ryy", Rzz: "rzz" + U3Gate: 'u', + PhaseShift: 'p', + PauliX: 'x', + PauliY: 'y', + PauliZ: 'z', + Hadamard: 'h', + SGate: 's', + SDaggerGate: 'sdg', + TGate: 't', + TDaggerGate: 'tdg', + Rx: 'rx', + Ry: 'ry', + Rz: 'rz', + Swap: 'swap', + CNOT: 'cx', + Toffoli: 'ccx', + Fredkin: 'cswap', + Rxx: 'rxx', + Ryy: 'ryy', + Rzz: 'rzz', } qasm_name = name_map.get(type(op)) if not qasm_name: - return f"// Unsupported gate: {op.name}" + return f'// Unsupported gate: {op.name}' # Parameters - param_str = "" + param_str = '' if hasattr(op, 'npara') and op.npara > 0: params = [] if isinstance(op, U3Gate): @@ -58,7 +94,7 @@ def _op_to_qasm3(op: Operation) -> str: if hasattr(op, 'inv_mode') and op.inv_mode: params = [-p for p in params] - param_str = f"({', '.join(map(str, params))})" + param_str = f'({", ".join(map(str, params))})' # Qubits and Controls controls = op.controls @@ -66,16 +102,16 @@ def _op_to_qasm3(op: Operation) -> str: # Implicit controls in gates like CNOT, Toffoli if isinstance(op, (CNOT, Toffoli, Fredkin)): - qubits_str = ", ".join([f"q[{w}]" for w in targets]) - return f"{qasm_name} {qubits_str};" + qubits_str = ', '.join([f'q[{w}]' for w in targets]) + return f'{qasm_name} {qubits_str};' all_qubits = controls + targets - qubits_str = ", ".join([f"q[{w}]" for w in all_qubits]) + qubits_str = ', '.join([f'q[{w}]' for w in all_qubits]) # Control modifiers - ctrl_modifiers = "ctrl @ " * len(controls) + ctrl_modifiers = 'ctrl @ ' * len(controls) - return f"{ctrl_modifiers}{qasm_name}{param_str} {qubits_str};" + return f'{ctrl_modifiers}{qasm_name}{param_str} {qubits_str};' def cir_to_qasm3(circuit: QubitCircuit) -> str: @@ -87,13 +123,10 @@ def cir_to_qasm3(circuit: QubitCircuit) -> str: Returns: str: A string containing the OpenQASM 3.0 representation of the circuit. """ - qasm_parts = [ - "OPENQASM 3.0;", - 'include "stdgates.inc";' - ] + qasm_parts = ['OPENQASM 3.0;', 'include "stdgates.inc";'] num_qubits = circuit.nqubit - qasm_parts.append(f"qubit[{num_qubits}] q;") + qasm_parts.append(f'qubit[{num_qubits}] q;') # Declare classical bits if any measurements are defined if circuit.wires_measure: @@ -101,7 +134,7 @@ def cir_to_qasm3(circuit: QubitCircuit) -> str: max_measured_wire = max(circuit.wires_measure) if circuit.wires_measure else -1 num_classical_bits = max_measured_wire + 1 if num_classical_bits > 0: - qasm_parts.append(f"bit[{num_classical_bits}] c;") + qasm_parts.append(f'bit[{num_classical_bits}] c;') # Convert operations for op in circuit.operators: @@ -111,37 +144,41 @@ def cir_to_qasm3(circuit: QubitCircuit) -> str: # Add measurements if circuit.wires_measure: - qasm_parts.append("\n// Measurements") + qasm_parts.append('\n// Measurements') for wire in sorted(circuit.wires_measure): - qasm_parts.append(f"c[{wire}] = measure q[{wire}];") + qasm_parts.append(f'c[{wire}] = measure q[{wire}];') - return "\n".join(qasm_parts) + return '\n'.join(qasm_parts) # ============================================================================== # OpenQASM 3.0 to DeepQuantum Circuit Converter # ============================================================================== + class GateDefinition: - def __init__(self, name: str, params: List[str], qubits: List[str], body: List[str]): + """Gate definition in OpenQASM 3.0""" + + def __init__(self, name: str, params: list[str], qubits: list[str], body: list[str]): self.name, self.params, self.qubits, self.body = name, params, qubits, body def qasm3_to_cir(qasm_string: str) -> QubitCircuit: """Converts a full-featured OpenQASM 3.0 string to ``QubitCircuit``. + Supports: `def`, `inv @`, `ctrl @`, and floating-point/negative `pow() @`. """ lines = [line.split('//')[0].strip() for line in qasm_string.strip().splitlines() if line.strip()] - if not any(line.startswith("OPENQASM 3.0") for line in lines): - raise ValueError("Input is not a valid OpenQASM 3.0 string (Header missing).") + if not any(line.startswith('OPENQASM 3.0') for line in lines): + raise ValueError('Input is not a valid OpenQASM 3.0 string (Header missing).') - gate_definitions: Dict[str, GateDefinition] = {} + gate_definitions: dict[str, GateDefinition] = {} main_body_lines = [] i = 0 while i < len(lines): line = lines[i] - if line.startswith("def "): + if line.startswith('def '): header_line = line open_braces = header_line.count('{') body_start_line_offset = 1 @@ -161,19 +198,19 @@ def qasm3_to_cir(qasm_string: str) -> QubitCircuit: break open_braces += lines[brace_scan_index].count('{') open_braces -= lines[brace_scan_index].count('}') - body_lines = [l.strip() for l in lines[body_start_index:brace_scan_index]] + body_lines = [l_.strip() for l_ in lines[body_start_index:brace_scan_index]] i = brace_scan_index + 1 try: header_content = header_line[3:].strip() if header_content.endswith('{'): header_content = header_content[:-1].strip() - paren_match = re.match(r"(\w+)\s*\((.*?)\)\s*(.*)", header_content) + paren_match = re.match(r'(\w+)\s*\((.*?)\)\s*(.*)', header_content) if paren_match: name, params_str, qubits_str = paren_match.groups() else: - name_match = re.match(r"(\w+)\s*(.*)", header_content) + name_match = re.match(r'(\w+)\s*(.*)', header_content) name, qubits_str = name_match.groups() - params_str = "" + params_str = '' params = [p.strip() for p in params_str.split(',')] if params_str else [] qubits = [q.strip() for q in qubits_str.strip().split(',')] if qubits_str.strip() else [] qubits = [q for q in qubits if q] @@ -186,52 +223,64 @@ def qasm3_to_cir(qasm_string: str) -> QubitCircuit: num_qubits = 0 for line in main_body_lines: - match = re.search(r"qubit\[(\d+)\]", line) + match = re.search(r'qubit\[(\d+)\]', line) if match: num_qubits = int(match.group(1)) break if num_qubits == 0: - raise ValueError("Qubit declaration not found or zero qubits specified.") + raise ValueError('Qubit declaration not found or zero qubits specified.') circuit = QubitCircuit(nqubit=num_qubits) # --- Helper Function to get gate matrix --- - def get_gate_matrix(gate_name: str, params_str: str, gate_qubits_str: List[str], scope: Dict[str, Any]) -> torch.Tensor: + def get_gate_matrix( + gate_name: str, params_str: str, gate_qubits_str: list[str], scope: dict[str, Any] + ) -> torch.Tensor: """Dynamically builds the unitary matrix for a given gate call.""" n_gate_qubits = len(gate_qubits_str) # Create a fake QASM program to parse - local_qubit_def = f"qubit[{n_gate_qubits}] q;" - local_qubits_str = ", ".join([f"q[{i}]" for i in range(n_gate_qubits)]) - param_part = f"({params_str})" if params_str else "" - fake_line = f"{gate_name}{param_part} {local_qubits_str};" + local_qubit_def = f'qubit[{n_gate_qubits}] q;' + local_qubits_str = ', '.join([f'q[{i}]' for i in range(n_gate_qubits)]) + param_part = f'({params_str})' if params_str else '' + fake_line = f'{gate_name}{param_part} {local_qubits_str};' # Build a temporary full QASM string for the sub-parser - temp_qasm = ["OPENQASM 3.0;", local_qubit_def] + temp_qasm = ['OPENQASM 3.0;', local_qubit_def] # We need the definitions to be available to the sub-parser for name, definition in gate_definitions.items(): - params_def = f"({','.join(definition.params)})" if definition.params else "" - qubits_def = ",".join(definition.qubits) - body_def = "\n ".join(definition.body) - temp_qasm.append(f"def {name}{params_def} {qubits_def} {{\n {body_def}\n}}") + params_def = f'({",".join(definition.params)})' if definition.params else '' + qubits_def = ','.join(definition.qubits) + body_def = '\n '.join(definition.body) + temp_qasm.append(f'def {name}{params_def} {qubits_def} {{\n {body_def}\n}}') temp_qasm.append(fake_line) - temp_circ = qasm3_to_cir("\n".join(temp_qasm)) + temp_circ = qasm3_to_cir('\n'.join(temp_qasm)) return temp_circ.get_unitary() - def _process_qasm_lines(lines_to_process: List[str], circuit_obj: QubitCircuit, scope: Dict[str, Any] = {}, external_controls: List[int] = [], is_inverted: bool = False): - gate_pattern = re.compile(r"((?:(?:inv|ctrl|pow\s*\(.*?\))\s*@\s*)*)(\w+)(?:\((.*?)\))?\s+(.*?);") + def _process_qasm_lines( + lines_to_process: list[str], + circuit_obj: QubitCircuit, + scope: dict[str, Any] | None = None, + external_controls: list[int] | None = None, + is_inverted: bool = False, + ): + if scope is None: + scope = {} + if external_controls is None: + external_controls = [] + gate_pattern = re.compile(r'((?:(?:inv|ctrl|pow\s*\(.*?\))\s*@\s*)*)(\w+)(?:\((.*?)\))?\s+(.*?);') processing_order = reversed(lines_to_process) if is_inverted else lines_to_process for line in processing_order: line = line.strip() - if not line or line.startswith(("OPENQASM", "include", "qubit", "bit", "defcal")): + if not line or line.startswith(('OPENQASM', 'include', 'qubit', 'bit', 'defcal')): continue - if "measure" in line: - for m in re.findall(r"q\[(\d+)\]", line): + if 'measure' in line: + for m in re.findall(r'q\[(\d+)\]', line): if int(m) not in circuit_obj.wires_measure: circuit_obj.wires_measure.append(int(m)) continue - if line.startswith("barrier"): - qubits_str = line.replace("barrier", "").replace(";", "").strip() + if line.startswith('barrier'): + qubits_str = line.replace('barrier', '').replace(';', '').strip() wires = [int(q.strip()[2:-1]) for q in qubits_str.split(',')] if qubits_str else [] circuit_obj.barrier(wires=wires if wires else None) continue @@ -242,9 +291,9 @@ def _process_qasm_lines(lines_to_process: List[str], circuit_obj: QubitCircuit, continue modifiers_str, gate_name, params_str, qubits_str = match.groups() - num_inv = modifiers_str.count("inv") - num_ctrls = modifiers_str.count("ctrl") - pow_match = re.search(r"pow\s*\((.*?)\)", modifiers_str) + num_inv = modifiers_str.count('inv') + num_ctrls = modifiers_str.count('ctrl') + pow_match = re.search(r'pow\s*\((.*?)\)', modifiers_str) effectively_inverted = is_inverted ^ (num_inv % 2 == 1) power = 1.0 @@ -268,7 +317,7 @@ def _process_qasm_lines(lines_to_process: List[str], circuit_obj: QubitCircuit, try: base_unitary = get_gate_matrix(gate_name, params_str, gate_qubits_actual_str, scope) eigvals, eigvecs = torch.linalg.eig(base_unitary.to(dtype=torch.cfloat)) - eigvals_pow = eigvals ** power + eigvals_pow = eigvals**power final_unitary = eigvecs @ torch.diag(eigvals_pow) @ torch.linalg.inv(eigvecs) circuit_obj.any(final_unitary, wires=gate_qubits, controls=total_controls) except Exception as e: @@ -288,28 +337,50 @@ def _process_qasm_lines(lines_to_process: List[str], circuit_obj: QubitCircuit, if len(gate_qubits_actual_str) != len(definition.qubits): print(f"Warning: Mismatched qubit count for gate '{gate_name}'.") continue - qubit_map = dict(zip(definition.qubits, gate_qubits_actual_str)) + qubit_map = dict(zip(definition.qubits, gate_qubits_actual_str, strict=True)) eval_scope = {'pi': np.pi, 'np': np} eval_scope.update(scope) - call_params_evaluated = [eval(p.strip(), eval_scope) for p in params_str.split(',')] if params_str else [] + call_params_evaluated = ( + [eval(p.strip(), eval_scope) for p in params_str.split(',')] if params_str else [] + ) if len(call_params_evaluated) != len(definition.params): print(f"Warning: Mismatched parameter count for gate '{gate_name}'.") continue new_scope = scope.copy() - new_scope.update(zip(definition.params, call_params_evaluated)) + new_scope.update(zip(definition.params, call_params_evaluated, strict=True)) expanded_body = [] for body_line in definition.body: new_line = body_line for formal_param in definition.params: - new_line = re.sub(r'\b' + re.escape(formal_param) + r'\b', str(new_scope.get(formal_param, formal_param)), new_line) + new_line = re.sub( + r'\b' + re.escape(formal_param) + r'\b', + str(new_scope.get(formal_param, formal_param)), + new_line, + ) for formal_qubit, actual_qubit in qubit_map.items(): new_line = re.sub(r'\b' + re.escape(formal_qubit) + r'\b', actual_qubit, new_line) expanded_body.append(new_line) - _process_qasm_lines(expanded_body, circuit_obj, new_scope, external_controls=total_external_controls, is_inverted=loop_inverted) + _process_qasm_lines( + expanded_body, + circuit_obj, + new_scope, + external_controls=total_external_controls, + is_inverted=loop_inverted, + ) else: - _apply_builtin_gate(circuit_obj, gate_name, params_str, call_qubits_all_str, num_ctrls, external_controls, loop_inverted) - - def _apply_builtin_gate(circuit_obj, gate_name, params_str, call_qubits_all_str, num_ctrls, external_controls, is_inverted): + _apply_builtin_gate( + circuit_obj, + gate_name, + params_str, + call_qubits_all_str, + num_ctrls, + external_controls, + loop_inverted, + ) + + def _apply_builtin_gate( + circuit_obj, gate_name, params_str, call_qubits_all_str, num_ctrls, external_controls, is_inverted + ): try: qubit_indices = [int(q[2:-1]) for q in call_qubits_all_str] params = [float(eval(p, {'pi': np.pi})) for p in params_str.split(',')] if params_str else [] diff --git a/src/deepquantum/qmath.py b/src/deepquantum/qmath.py index 87927dac..623920e1 100644 --- a/src/deepquantum/qmath.py +++ b/src/deepquantum/qmath.py @@ -1,11 +1,9 @@ -""" -Common functions -""" +"""Common functions""" import copy from collections import Counter, defaultdict -from typing import Any, Callable, Dict, List, Optional, Tuple, Union -from typing import TYPE_CHECKING +from collections.abc import Callable +from typing import Any, TYPE_CHECKING import numpy as np import torch @@ -18,10 +16,11 @@ def is_power_of_two(n: int) -> bool: """Check if an integer is a power of two.""" + def f(x): if x < 2: return False - elif x & (x-1) == 0: + elif x & (x - 1) == 0: return True return False @@ -43,7 +42,7 @@ def int_to_bitstring(x: int, n: int, debug: bool = False) -> str: """Convert from integer to bit string.""" assert isinstance(x, int) assert isinstance(n, int) - if x < 2 ** n: + if x < 2**n: # remove '0b' s = bin(x)[2:] if len(s) <= n: @@ -55,7 +54,7 @@ def int_to_bitstring(x: int, n: int, debug: bool = False) -> str: return s -def list_to_decimal(digits: List[int], base: int) -> int: +def list_to_decimal(digits: list[int], base: int) -> int: """Convert from list of digits to decimal integer.""" result = 0 for digit in digits: @@ -64,7 +63,7 @@ def list_to_decimal(digits: List[int], base: int) -> int: return result -def decimal_to_list(n: int, base: int, ndigit: Optional[int] = None) -> List[int]: +def decimal_to_list(n: int, base: int, ndigit: int | None = None) -> list[int]: """Convert from decimal integer to list of digits.""" assert base >= 2, 'Base must be at least 2' if n == 0: @@ -82,7 +81,7 @@ def decimal_to_list(n: int, base: int, ndigit: Optional[int] = None) -> List[int return digits -def inverse_permutation(permute_shape: List[int]) -> List[int]: +def inverse_permutation(permute_shape: list[int]) -> list[int]: """Calculate the inversed permutation. Args: @@ -100,6 +99,8 @@ def is_unitary(matrix: torch.Tensor, rtol: float = 1e-5, atol: float = 1e-4) -> Args: matrix (torch.Tensor): Square matrix. + rtol (float, optional): Relative tolerance. Default: 1e-5 + atol (float, optional): Absolute tolerance. Default: 1e-4 Returns: bool: ``True`` if ``matrix`` is unitary, ``False`` otherwise. @@ -108,8 +109,9 @@ def is_unitary(matrix: torch.Tensor, rtol: float = 1e-5, atol: float = 1e-4) -> return False conj_trans = matrix.t().conj() product = torch.matmul(matrix, conj_trans) - return torch.allclose(product, torch.eye(matrix.shape[0], dtype=matrix.dtype, device=matrix.device), - rtol=rtol, atol=atol) + return torch.allclose( + product, torch.eye(matrix.shape[0], dtype=matrix.dtype, device=matrix.device), rtol=rtol, atol=atol + ) def is_density_matrix(rho: torch.Tensor) -> bool: @@ -144,9 +146,7 @@ def is_density_matrix(rho: torch.Tensor) -> bool: return False # Check if the eigenvalues of each matrix are non-negative positive_semi_definite = torch.all(torch.linalg.eig(rho)[0].real >= 0).item() - if not positive_semi_definite: - return False - return True + return positive_semi_definite def is_positive_definite(mat: torch.Tensor) -> bool: @@ -158,7 +158,7 @@ def is_positive_definite(mat: torch.Tensor) -> bool: def safe_inverse(x: Any, epsilon: float = 1e-12) -> Any: """Safe inversion.""" - return x / (x ** 2 + epsilon) + return x / (x**2 + epsilon) class SVD(torch.autograd.Function): @@ -167,14 +167,13 @@ class SVD(torch.autograd.Function): Modified from https://github.com/wangleiphy/tensorgrad/blob/master/tensornets/adlib/svd.py See https://readpaper.com/paper/2971614414 """ + generate_vmap_rule = True - # pylint: disable=arguments-renamed @staticmethod def forward(a): u, s, vh = torch.linalg.svd(a, full_matrices=False) s = s.to(u.dtype) - # ctx.save_for_backward(u, s, vh) return u, s, vh # setup_context is responsible for calling methods and/or assigning to @@ -203,10 +202,10 @@ def backward(ctx, du, ds, dvh): j = f * (uh @ du) k = f * (vh @ dv) - l = (vh @ dv).diagonal(dim1=-2, dim2=-1).diag_embed() + l = (vh @ dv).diagonal(dim1=-2, dim2=-1).diag_embed() # noqa: E741 s_inv = safe_inverse(s).diag_embed() - # pylint: disable=line-too-long - da = u @ (ds.diag_embed() + (j + j.mH) @ s.diag_embed() + s.diag_embed() @ (k + k.mH) + s_inv @ (l.mH - l) / 2) @ vh + mat_s = s.diag_embed() + da = u @ (ds.diag_embed() + (j + j.mH) @ mat_s + mat_s @ (k + k.mH) + s_inv @ (l.mH - l) / 2) @ vh if m > ns: da += (torch.eye(m, dtype=du.dtype, device=du.device) - u @ uh) @ du @ s_inv @ vh if n > ns: @@ -229,13 +228,10 @@ def torchqr_grad(a, q, r, dq, dr): def _triangular_solve(x, r): """Equivalent to matmul(x, adjoint(matrix_inverse(r))) if r is upper-tri.""" - return torch.linalg.solve_triangular( - r, x.adjoint(), upper=True, unitriangular=False - ).adjoint() + return torch.linalg.solve_triangular(r, x.adjoint(), upper=True, unitriangular=False).adjoint() def _qr_grad_square_and_deep_matrices(q, r, dq, dr): """Get the gradient for matrix orders num_rows >= num_cols and full_matrices is false.""" - # Modification begins rdiag = torch.linalg.diagonal(r) # if abs(rdiag[i]) < qr_epsilon then rdiag[i] = qr_epsilon otherwise keep the old value @@ -258,9 +254,7 @@ def _qr_grad_square_and_deep_matrices(q, r, dq, dr): if q.is_complex(): m = rdr - qdq.adjoint() - eyem = torch.diagonal_scatter( - torch.zeros_like(m), torch.linalg.diagonal(m), dim1=-2, dim2=-1 - ) + eyem = torch.diagonal_scatter(torch.zeros_like(m), torch.linalg.diagonal(m), dim1=-2, dim2=-1) correction = eyem - torch.real(eyem).to(dtype=q.dtype) ret = ret + _triangular_solve(torch.matmul(q, correction.adjoint()), r) @@ -283,9 +277,9 @@ def _qr_grad_square_and_deep_matrices(q, r, dq, dr): # from tensorcircuit class QR(torch.autograd.Function): """Customized backward of QR for better numerical stability.""" + generate_vmap_rule = True - # pylint: disable=arguments-renamed @staticmethod def forward(a): q, r = torch.linalg.qr(a, mode='reduced') @@ -315,7 +309,8 @@ def backward(ctx, dq, dr): svd = SVD.apply qr = QR.apply -def split_tensor(tensor: torch.Tensor, center_left: bool = True) -> Tuple[torch.Tensor, torch.Tensor]: + +def split_tensor(tensor: torch.Tensor, center_left: bool = True) -> tuple[torch.Tensor, torch.Tensor]: """Split a tensor by QR.""" if center_left: q, r = qr(tensor.mH) @@ -324,7 +319,7 @@ def split_tensor(tensor: torch.Tensor, center_left: bool = True) -> Tuple[torch. return qr(tensor) -def state_to_tensors(state: torch.Tensor, nsite: int, qudit: int = 2) -> List[torch.Tensor]: +def state_to_tensors(state: torch.Tensor, nsite: int, qudit: int = 2) -> list[torch.Tensor]: """Convert a quantum state to a list of tensors.""" state = state.reshape([qudit] * nsite) tensors = [] @@ -340,11 +335,7 @@ def state_to_tensors(state: torch.Tensor, nsite: int, qudit: int = 2) -> List[to def slice_state_vector( - state: torch.Tensor, - nqubit: int, - wires: List[int], - bits: str, - normalize: bool = True + state: torch.Tensor, nqubit: int, wires: list[int], bits: str, normalize: bool = True ) -> torch.Tensor: """Get the sliced state vectors according to ``wires`` and ``bits``.""" if len(bits) == 1: @@ -368,7 +359,7 @@ def slice_state_vector( return state -def multi_kron(lst: List[torch.Tensor]) -> torch.Tensor: +def multi_kron(lst: list[torch.Tensor]) -> torch.Tensor: """Calculate the Kronecker/tensor/outer product for a list of tensors. Args: @@ -386,7 +377,7 @@ def multi_kron(lst: List[torch.Tensor]) -> torch.Tensor: return rst.contiguous() -def partial_trace(rho: torch.Tensor, nqudit: int, trace_lst: List[int], qudit: int = 2) -> torch.Tensor: +def partial_trace(rho: torch.Tensor, nqudit: int, trace_lst: list[int], qudit: int = 2) -> torch.Tensor: r"""Calculate the partial trace for a batch of density matrices. Args: @@ -402,7 +393,7 @@ def partial_trace(rho: torch.Tensor, nqudit: int, trace_lst: List[int], qudit: i if rho.ndim == 2: rho = rho.unsqueeze(0) assert rho.ndim == 3 - assert rho.shape[1] == rho.shape[2] == qudit ** nqudit + assert rho.shape[1] == rho.shape[2] == qudit**nqudit b = rho.shape[0] n = len(trace_lst) trace_lst = [i + 1 for i in trace_lst] @@ -412,7 +403,7 @@ def partial_trace(rho: torch.Tensor, nqudit: int, trace_lst: List[int], qudit: i for i in trace_lst: permute_shape.remove(i) permute_shape += trace_lst - rho = rho.reshape([b] + [qudit] * 2 * nqudit).permute(permute_shape).reshape(-1, qudit ** n, qudit ** n) + rho = rho.reshape([b] + [qudit] * 2 * nqudit).permute(permute_shape).reshape(-1, qudit**n, qudit**n) rho = rho.diagonal(dim1=-2, dim2=-1).sum(-1) return rho.reshape(b, qudit ** (nqudit - n), qudit ** (nqudit - n)).squeeze(0) @@ -448,15 +439,13 @@ def amplitude_encoding(data: Any, nqubit: int) -> torch.Tensor: [0.0000+0.j], [0.0000+0.j]]]) """ - if not isinstance(data, (torch.Tensor, nn.Parameter)): + if not isinstance(data, torch.Tensor): data = torch.tensor(data) - if data.ndim == 1 or (data.ndim == 2 and data.shape[-1] == 1): - batch = 1 - else: - batch = data.shape[0] + is_single_state = data.ndim == 1 or (data.ndim == 2 and data.shape[-1] == 1) + batch = 1 if is_single_state else data.shape[0] data = data.reshape(batch, -1) size = data.shape[1] - n = 2 ** nqubit + n = 2**nqubit state = torch.zeros(batch, n, dtype=data.dtype, device=data.device) + 0j data = nn.functional.normalize(data[:, :n], p=2, dim=-1) if n > size: @@ -467,11 +456,7 @@ def amplitude_encoding(data: Any, nqubit: int) -> torch.Tensor: def evolve_state( - state: torch.Tensor, - matrix: torch.Tensor, - nqudit: int, - wires: List[int], - qudit: int = 2 + state: torch.Tensor, matrix: torch.Tensor, nqudit: int, wires: list[int], qudit: int = 2 ) -> torch.Tensor: """Perform the evolution of quantum states. @@ -488,18 +473,14 @@ def evolve_state( for i in wires: pm_shape.remove(i) pm_shape = wires + pm_shape - state = state.permute(pm_shape).reshape(qudit ** nt, -1) + state = state.permute(pm_shape).reshape(qudit**nt, -1) state = (matrix @ state).reshape([qudit] * nt + [-1] + [qudit] * (nqudit - nt)) state = state.permute(inverse_permutation(pm_shape)) return state def evolve_den_mat( - state: torch.Tensor, - matrix: torch.Tensor, - nqudit: int, - wires: List[int], - qudit: int = 2 + state: torch.Tensor, matrix: torch.Tensor, nqudit: int, wires: list[int], qudit: int = 2 ) -> torch.Tensor: """Perform the evolution of density matrices. @@ -517,7 +498,7 @@ def evolve_den_mat( for i in wires1: pm_shape.remove(i) pm_shape = wires1 + pm_shape - state = state.permute(pm_shape).reshape(qudit ** nt, -1) + state = state.permute(pm_shape).reshape(qudit**nt, -1) state = (matrix @ state).reshape([qudit] * nt + [-1] + [qudit] * (2 * nqudit - nt)) state = state.permute(inverse_permutation(pm_shape)) # right multiply @@ -526,13 +507,13 @@ def evolve_den_mat( for i in wires2: pm_shape.remove(i) pm_shape = wires2 + pm_shape - state = state.permute(pm_shape).reshape(qudit ** nt, -1) + state = state.permute(pm_shape).reshape(qudit**nt, -1) state = (matrix.conj() @ state).reshape([qudit] * nt + [-1] + [qudit] * (2 * nqudit - nt)) state = state.permute(inverse_permutation(pm_shape)) return state -def block_sample(probs: torch.Tensor, shots: int = 1024, block_size: int = 2 ** 24) -> List: +def block_sample(probs: torch.Tensor, shots: int = 1024, block_size: int = 2**24) -> list: """Sample from a probability distribution using block sampling. Args: @@ -561,10 +542,10 @@ def measure( state: torch.Tensor, shots: int = 1024, with_prob: bool = False, - wires: Union[int, List[int], None] = None, + wires: int | list[int] | None = None, den_mat: bool = False, - block_size: int = 2 ** 24 -) -> Union[Dict, List[Dict]]: + block_size: int = 2**24, +) -> dict | list[dict]: r"""A function that performs a measurement on a quantum state and returns the results. The measurement is done by sampling from the probability distribution of the quantum state. The results @@ -597,10 +578,8 @@ def measure( if den_mat: assert is_density_matrix(state), 'Please input density matrices' state = state.diagonal(dim1=-2, dim2=-1) - if state.ndim == 1 or (state.ndim == 2 and state.shape[-1] == 1): - batch = 1 - else: - batch = state.shape[0] + is_single_state = state.ndim == 1 or (state.ndim == 2 and state.shape[-1] == 1) + batch = 1 if is_single_state else state.shape[0] state = state.reshape(batch, -1) assert is_power_of_two(state.shape[-1]), 'The length of the quantum state is not in the form of 2^n' n = int(np.log2(state.shape[-1])) @@ -616,10 +595,7 @@ def measure( num_bits = len(wires) if wires else n results_tot = [] for i in range(batch): - if den_mat: - probs = torch.abs(state[i]) - else: - probs = torch.abs(state[i]) ** 2 + probs = torch.abs(state[i]) if den_mat else torch.abs(state[i]) ** 2 if wires is not None: probs = probs.reshape([2] * n).permute(pm_shape).reshape([2] * len(wires) + [-1]).sum(-1).reshape(-1) # Perform block sampling to reduce memory consumption @@ -637,10 +613,7 @@ def measure( def sample_sc_mcmc( - prob_func: Callable, - proposal_sampler: Callable, - shots: int = 1024, - num_chain: int = 5 + prob_func: Callable, proposal_sampler: Callable, shots: int = 1024, num_chain: int = 5 ) -> defaultdict: """Get the samples of the probability distribution function via SC-MCMC method.""" samples_chain = [] @@ -660,7 +633,7 @@ def sample_sc_mcmc( # random start sample_0 = proposal_sampler() if not isinstance(sample_0, str): - if prob_func(sample_0) < 1e-12: # avoid the samples with almost-zero probability + if prob_func(sample_0) < 1e-12: # avoid the samples with almost-zero probability sample_0 = tuple([0] * len(sample_0)) while prob_func(sample_0) < 1e-9: sample_0 = proposal_sampler() @@ -672,7 +645,7 @@ def sample_sc_mcmc( prob_max = prob_func(sample_0) cache_prob[sample_max] = prob_max dict_sample = defaultdict(int) - for i in tqdm(range(1, shots_lst[trial]), desc=f'chain {trial+1}', ncols=80, colour='green'): + for i in tqdm(range(1, shots_lst[trial]), desc=f'chain {trial + 1}', ncols=80, colour='green'): sample_i = proposal_sampler() if sample_i in cache_prob: prob_i = cache_prob[sample_i] @@ -684,9 +657,9 @@ def sample_sc_mcmc( if prob_i / prob_max > rand_num: sample_max = sample_i prob_max = prob_i - if i < len_cache: # cache not full + if i < len_cache: # cache not full cache.append(sample_max) - else: # full + else: # full idx = np.random.randint(0, len_cache) out_sample = copy.deepcopy(cache[idx]) cache[idx] = sample_max @@ -709,7 +682,7 @@ def sample_sc_mcmc( return merged_samples -def get_prob_mps(mps_lst: List[torch.Tensor], wire: int) -> torch.Tensor: +def get_prob_mps(mps_lst: list[torch.Tensor], wire: int) -> torch.Tensor: """Calculate the probability distribution (|0⟩ and |1⟩ probabilities) for a specific wire in an MPS. This function computes the probability of measuring |0⟩ and |1⟩ for the k-th qubit in a quantum state @@ -726,7 +699,8 @@ def get_prob_mps(mps_lst: List[torch.Tensor], wire: int) -> torch.Tensor: Returns: torch.Tensor: A tensor containing [P(|0⟩), P(|1⟩)] probabilities for the target qubit """ - def contract_conjugate_pair(tensors: List[torch.Tensor]) -> torch.Tensor: + + def contract_conjugate_pair(tensors: list[torch.Tensor]) -> torch.Tensor: """Contract a list of MPS tensors with their conjugates. This helper function performs the contraction between a list of MPS tensors @@ -743,7 +717,7 @@ def contract_conjugate_pair(tensors: List[torch.Tensor]) -> torch.Tensor: # Contract first tensor with its conjugate contracted = torch.tensordot(tensors[0].conj(), tensors[0], dims=([1], [1])) - contracted = contracted.permute(0, 2, 1, 3) # (left_c, left, right_c, right) + contracted = contracted.permute(0, 2, 1, 3) # (left_c, left, right_c, right) # Iteratively contract remaining tensors for tensor in tensors[1:]: @@ -755,7 +729,7 @@ def contract_conjugate_pair(tensors: List[torch.Tensor]) -> torch.Tensor: # Split MPS into left and right parts relative to target qubit left_tensors = mps_lst[:wire] if wire > 0 else [] - right_tensors = mps_lst[wire + 1:] if wire < len(mps_lst) - 1 else [] + right_tensors = mps_lst[wire + 1 :] if wire < len(mps_lst) - 1 else [] target_tensor = mps_lst[wire] # Contract left and right parts separately @@ -773,10 +747,8 @@ def contract_conjugate_pair(tensors: List[torch.Tensor]) -> torch.Tensor: def inner_product_mps( - tensors0: List[torch.Tensor], - tensors1: List[torch.Tensor], - form: str = 'norm' -) -> Union[torch.Tensor, List[torch.Tensor]]: + tensors0: list[torch.Tensor], tensors1: list[torch.Tensor], form: str = 'norm' +) -> torch.Tensor | list[torch.Tensor]: r"""Computes the inner product of two matrix product states. Args: @@ -802,12 +774,13 @@ def inner_product_mps( v0 = torch.eye(tensors0[0].shape[-3], dtype=tensors0[0].dtype, device=tensors0[0].device) v1 = torch.eye(tensors1[0].shape[-3], dtype=tensors0[0].dtype, device=tensors0[0].device) - v = torch.kron(v0, v1).reshape([tensors0[0].shape[-3], tensors1[0].shape[-3], - tensors0[0].shape[-3], tensors1[0].shape[-3]]) + v = torch.kron(v0, v1).reshape( + [tensors0[0].shape[-3], tensors1[0].shape[-3], tensors0[0].shape[-3], tensors1[0].shape[-3]] + ) norm_list = [] for n in range(len(tensors0)): v = torch.einsum('...uvap,...adb,...pdq->...uvbq', v, tensors0[n].conj(), tensors1[n]) - norm_v = v.norm(p=2, dim=[-4,-3,-2,-1], keepdim=True) + norm_v = v.norm(p=2, dim=[-4, -3, -2, -1], keepdim=True) v = v / norm_v norm_list.append(norm_v.squeeze()) if v.numel() > 1: @@ -829,10 +802,7 @@ def inner_product_mps( def expectation( - state: Union[torch.Tensor, List[torch.Tensor]], - observable: 'Observable', - den_mat: bool = False, - chi: Optional[int] = None + state: torch.Tensor | list[torch.Tensor], observable: 'Observable', den_mat: bool = False, chi: int | None = None ) -> torch.Tensor: """A function that calculates the expectation value of an observable on a quantum state. @@ -852,9 +822,9 @@ def expectation( torch.Tensor: The expectation value of the observable on the quantum state. It is a scalar tensor with real values. """ - # pylint: disable=import-outside-toplevel if isinstance(state, list): from .state import MatrixProductState + mps = MatrixProductState(nsite=len(state), state=state, chi=chi) return inner_product_mps(state, observable(mps).tensors).real if den_mat: @@ -865,7 +835,7 @@ def expectation( return expval -def sample2expval(sample: Dict) -> torch.Tensor: +def sample2expval(sample: dict) -> torch.Tensor: """Get the expectation value according to the measurement results.""" total = 0 exp = 0 diff --git a/src/deepquantum/qpd.py b/src/deepquantum/qpd.py index 06c979cd..b1053e0e 100644 --- a/src/deepquantum/qpd.py +++ b/src/deepquantum/qpd.py @@ -1,12 +1,8 @@ -""" -Quasiprobability-decomposition gates -""" - -from typing import List, Optional, Tuple +"""Quasiprobability-decomposition gates""" from torch import nn -from .gate import PauliX, Hadamard, SGate, SDaggerGate +from .gate import Hadamard, PauliX, SDaggerGate, SGate from .operation import GateQPD, MeasureQPD @@ -27,24 +23,33 @@ class SingleGateQPD(GateQPD): tsr_mode (bool, optional): Whether the quantum operation is in tensor mode, which means the input and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + def __init__( self, - bases: List[Tuple[nn.Sequential, ...]], - coeffs: List[float], - label: Optional[int] = None, - name: Optional[str] = None, + bases: list[tuple[nn.Sequential, ...]], + coeffs: list[float], + label: int | None = None, + name: str | None = None, nqubit: int = 1, - wires: Optional[List[int]] = None, + wires: list[int] | None = None, den_mat: bool = False, - tsr_mode: bool = False + tsr_mode: bool = False, ) -> None: if wires is None: wires = [0] assert len(wires) == 1 for basis in bases: assert len(basis) == 1 - super().__init__(bases=bases, coeffs=coeffs, label=label, name=name, nqubit=nqubit, wires=wires, - den_mat=den_mat, tsr_mode=tsr_mode) + super().__init__( + bases=bases, + coeffs=coeffs, + label=label, + name=name, + nqubit=nqubit, + wires=wires, + den_mat=den_mat, + tsr_mode=tsr_mode, + ) class DoubleGateQPD(GateQPD): @@ -64,26 +69,35 @@ class DoubleGateQPD(GateQPD): tsr_mode (bool, optional): Whether the quantum operation is in tensor mode, which means the input and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + def __init__( self, - bases: List[Tuple[nn.Sequential, ...]], - coeffs: List[float], - label: Optional[int] = None, - name: Optional[str] = None, + bases: list[tuple[nn.Sequential, ...]], + coeffs: list[float], + label: int | None = None, + name: str | None = None, nqubit: int = 2, - wires: Optional[List[int]] = None, + wires: list[int] | None = None, den_mat: bool = False, - tsr_mode: bool = False + tsr_mode: bool = False, ) -> None: if wires is None: wires = [0, 1] assert len(wires) == 2 for basis in bases: assert len(basis) == 2 - super().__init__(bases=bases, coeffs=coeffs, label=label, name=name, nqubit=nqubit, wires=wires, - den_mat=den_mat, tsr_mode=tsr_mode) - - def decompose(self) -> Tuple[SingleGateQPD, SingleGateQPD]: + super().__init__( + bases=bases, + coeffs=coeffs, + label=label, + name=name, + nqubit=nqubit, + wires=wires, + den_mat=den_mat, + tsr_mode=tsr_mode, + ) + + def decompose(self) -> tuple[SingleGateQPD, SingleGateQPD]: """Decompose the gate into two single-qubit QPD gates.""" bases1 = [] bases2 = [] @@ -91,10 +105,12 @@ def decompose(self) -> Tuple[SingleGateQPD, SingleGateQPD]: bases1.append(tuple([basis[0]])) bases2.append(tuple([basis[1]])) name = self.name + f'_label{self.label}_' - gate1 = SingleGateQPD(bases1, self.coeffs, self.label, name+'1', self.nqubit, [self.wires[0]], - self.den_mat, self.tsr_mode) - gate2 = SingleGateQPD(bases2, self.coeffs, self.label, name+'2', self.nqubit, [self.wires[1]], - self.den_mat, self.tsr_mode) + gate1 = SingleGateQPD( + bases1, self.coeffs, self.label, name + '1', self.nqubit, [self.wires[0]], self.den_mat, self.tsr_mode + ) + gate2 = SingleGateQPD( + bases2, self.coeffs, self.label, name + '2', self.nqubit, [self.wires[1]], self.den_mat, self.tsr_mode + ) return gate1, gate2 @@ -111,13 +127,14 @@ class MoveQPD(DoubleGateQPD): tsr_mode (bool, optional): Whether the quantum operation is in tensor mode, which means the input and output are represented by a tensor of shape :math:`(\text{batch}, 2, ..., 2)`. Default: ``False`` """ + def __init__( self, nqubit: int = 2, - wires: Optional[List[int]] = None, - label: Optional[int] = None, + wires: list[int] | None = None, + label: int | None = None, den_mat: bool = False, - tsr_mode: bool = False + tsr_mode: bool = False, ) -> None: if wires is None: wires = [0, 1] @@ -140,14 +157,24 @@ def __init__( prep_iplus = nn.Sequential(h2, s2) prep_iminus = nn.Sequential(x2, h2, s2) - bases = [(measure_i, prep_0), - (measure_i, prep_1), - (measure_x, prep_plus), - (measure_x, prep_minus), - (measure_y, prep_iplus), - (measure_y, prep_iminus), - (measure_z, prep_0), - (measure_z, prep_1)] + bases = [ + (measure_i, prep_0), + (measure_i, prep_1), + (measure_x, prep_plus), + (measure_x, prep_minus), + (measure_y, prep_iplus), + (measure_y, prep_iminus), + (measure_z, prep_0), + (measure_z, prep_1), + ] coeffs = [0.5, 0.5, 0.5, -0.5, 0.5, -0.5, 0.5, -0.5] - super().__init__(bases=bases, coeffs=coeffs, label=label, name='MoveQPD', nqubit=nqubit, wires=wires, - den_mat=den_mat, tsr_mode=tsr_mode) + super().__init__( + bases=bases, + coeffs=coeffs, + label=label, + name='MoveQPD', + nqubit=nqubit, + wires=wires, + den_mat=den_mat, + tsr_mode=tsr_mode, + ) diff --git a/src/deepquantum/state.py b/src/deepquantum/state.py index 56a29bd3..cc8e1683 100644 --- a/src/deepquantum/state.py +++ b/src/deepquantum/state.py @@ -1,15 +1,13 @@ -""" -Quantum states -""" +"""Quantum states""" -from typing import Any, List, Optional, Union +from typing import Any, Union import torch from torch import nn -from .bitmath import power_of_2, is_power_of_2, log_base2 +from .bitmath import is_power_of_2, log_base2, power_of_2 from .communication import comm_get_rank, comm_get_world_size -from .qmath import is_density_matrix, amplitude_encoding, inner_product_mps, svd, qr +from .qmath import amplitude_encoding, inner_product_mps, is_density_matrix, qr, svd class QubitState(nn.Module): @@ -22,26 +20,27 @@ class QubitState(nn.Module): a tensor that represents a custom state vector or density matrix. Default: ``'zeros'`` den_mat (bool, optional): Whether the state is a density matrix or not. Default: ``False`` """ + def __init__(self, nqubit: int = 1, state: Any = 'zeros', den_mat: bool = False) -> None: super().__init__() self.nqubit = nqubit self.den_mat = den_mat if state == 'zeros': - state = torch.zeros((2 ** nqubit, 1), dtype=torch.cfloat) + state = torch.zeros((2**nqubit, 1), dtype=torch.cfloat) state[0] = 1 if den_mat: state = state @ state.mH self.register_buffer('state', state) elif state == 'equal': - state = torch.ones((2 ** nqubit, 1), dtype=torch.cfloat) + state = torch.ones((2**nqubit, 1), dtype=torch.cfloat) state = nn.functional.normalize(state, p=2, dim=-2) if den_mat: state = state @ state.mH self.register_buffer('state', state) elif state in ('entangle', 'GHZ', 'ghz'): - state = torch.zeros((2 ** nqubit, 1), dtype=torch.cfloat) - state[0] = 1 / 2 ** 0.5 - state[-1] = 1 / 2 ** 0.5 + state = torch.zeros((2**nqubit, 1), dtype=torch.cfloat) + state[0] = 1 / 2**0.5 + state[-1] = 1 / 2**0.5 if den_mat: state = state @ state.mH self.register_buffer('state', state) @@ -50,7 +49,7 @@ def __init__(self, nqubit: int = 1, state: Any = 'zeros', den_mat: bool = False) state = torch.tensor(state, dtype=torch.cfloat) ndim = state.ndim s = state.shape - if den_mat and s[-1] == 2 ** nqubit and is_density_matrix(state): + if den_mat and s[-1] == 2**nqubit and is_density_matrix(state): self.register_buffer('state', state) else: state = amplitude_encoding(data=state, nqubit=nqubit) @@ -93,13 +92,14 @@ class MatrixProductState(nn.Module): qudit (int, optional): The local Hilbert space dimension of each qudit. Default: 2 normalize (bool, optional): Whether to normalize the MPS after each operation. Default: ``True`` """ + def __init__( self, nsite: int = 1, - state: Union[str, List[torch.Tensor], List[int]] = 'zeros', - chi: Optional[int] = None, + state: str | list[torch.Tensor] | list[int] = 'zeros', + chi: int | None = None, qudit: int = 2, - normalize: bool = True + normalize: bool = True, ) -> None: super().__init__() if chi is None: @@ -124,7 +124,7 @@ def to(self, arg: Any) -> 'MatrixProductState': return self @property - def tensors(self) -> List[torch.Tensor]: + def tensors(self) -> list[torch.Tensor]: """Get the tensors of the matrix product state. Note: @@ -136,7 +136,7 @@ def tensors(self) -> List[torch.Tensor]: tensors.append(getattr(self, f'tensor{j}')) return tensors - def set_tensors(self, state: Union[str, List[torch.Tensor], List[int]]) -> None: + def set_tensors(self, state: str | list[torch.Tensor] | list[int]) -> None: """Set the tensors of the matrix product state.""" if state in ('zeros', 'vac'): state = [0] * self.nsite @@ -150,7 +150,7 @@ def set_tensors(self, state: Union[str, List[torch.Tensor], List[int]]) -> None: elif isinstance(state[i], int): assert 0 <= state[i] < self.qudit, 'Invalid input' tensor = torch.zeros(self.qudit, dtype=torch.cfloat) - tensor[state[i]] = 1. + tensor[state[i]] = 1.0 # the bond dimension is 1 self.register_buffer(f'tensor{i}', tensor.reshape(1, self.qudit, 1)) @@ -167,7 +167,7 @@ def center_orthogonalization(self, c: int, dc: int = -1, normalize: bool = False if normalize: self.normalize_central_tensor() - def check_center_orthogonality(self, prt: bool = False) -> List[torch.Tensor]: + def check_center_orthogonality(self, prt: bool = False) -> list[torch.Tensor]: """Check if the MPS is in center-orthogonal form.""" tensors = self.tensors assert tensors[0].ndim == 3 @@ -180,14 +180,12 @@ def check_center_orthogonality(self, prt: bool = False) -> List[torch.Tensor]: s = tensors[i].shape tmp = tensors[i].reshape(-1, s[-1]) tmp = tmp.mH @ tmp - err[i] = (tmp - torch.eye(tmp.shape[0], device=tmp.device, - dtype=tmp.dtype)).norm(p=1).item() + err[i] = (tmp - torch.eye(tmp.shape[0], device=tmp.device, dtype=tmp.dtype)).norm(p=1).item() for i in range(self.nsite - 1, self.center, -1): s = tensors[i].shape tmp = tensors[i].reshape(s[0], -1) tmp = tmp @ tmp.mH - err[i] = (tmp - torch.eye(tmp.shape[0], device=tmp.device, - dtype=tmp.dtype)).norm(p=1).item() + err[i] = (tmp - torch.eye(tmp.shape[0], device=tmp.device, dtype=tmp.dtype)).norm(p=1).item() if prt: print('Orthogonality check:') print('=' * 35) @@ -210,14 +208,12 @@ def full_tensor(self) -> torch.Tensor: for i in range(1, self.nsite): psi = torch.einsum('...abc,...cde->...abde', psi, tensors[i]) s = psi.shape - psi = psi.reshape(-1, s[-4], s[-3]*s[-2], s[-1]) + psi = psi.reshape(-1, s[-4], s[-3] * s[-2], s[-1]) return psi.squeeze() def inner( - self, - tensors: Union[List[torch.Tensor], 'MatrixProductState'], - form: str = 'norm' - ) -> Union[torch.Tensor, List[torch.Tensor]]: + self, tensors: Union[list[torch.Tensor], 'MatrixProductState'], form: str = 'norm' + ) -> torch.Tensor | list[torch.Tensor]: """Get the inner product with another matrix product state.""" # form: 'log' or 'list' if isinstance(tensors, list): @@ -232,7 +228,7 @@ def normalize_central_tensor(self) -> None: if tensors[self.center].ndim == 3: norm = tensors[self.center].norm() elif tensors[self.center].ndim == 4: - norm = tensors[self.center].norm(p=2, dim=[1,2,3], keepdim=True) + norm = tensors[self.center].norm(p=2, dim=[1, 2, 3], keepdim=True) self._buffers[f'tensor{self.center}'] = self._buffers[f'tensor{self.center}'] / norm def orthogonalize_left2right(self, site: int, dc: int = -1, normalize: bool = False) -> None: @@ -251,10 +247,7 @@ def orthogonalize_left2right(self, site: int, dc: int = -1, normalize: bool = Fa assert site < self.nsite - 1 tensors = self.tensors shape = tensors[site].shape - if len(shape) == 3: - batch = 1 - else: - batch = shape[0] + batch = 1 if len(shape) == 3 else shape[0] if_trun = 0 < dc < shape[-1] if if_trun: u, s, vh = svd(tensors[site].reshape(batch, -1, shape[-1])) @@ -264,7 +257,7 @@ def orthogonalize_left2right(self, site: int, dc: int = -1, normalize: bool = Fa u, r = qr(tensors[site].reshape(batch, -1, shape[-1])) self._buffers[f'tensor{site}'] = u.reshape(batch, shape[-3], shape[-2], -1) if normalize: - norm = r.norm(dim=[-2,-1], keepdim=True) + norm = r.norm(dim=[-2, -1], keepdim=True) r = r / norm self._buffers[f'tensor{site + 1}'] = torch.einsum('...ab,...bcd->...acd', r, tensors[site + 1]) if len(shape) == 3: @@ -272,6 +265,7 @@ def orthogonalize_left2right(self, site: int, dc: int = -1, normalize: bool = Fa self._buffers[f'tensor{site}'] = tensors[site].squeeze(0) self._buffers[f'tensor{site + 1}'] = tensors[site + 1].squeeze(0) + # ruff: noqa: E741 def orthogonalize_right2left(self, site: int, dc: int = -1, normalize: bool = False) -> None: r"""Orthogonalize the tensor at ``site`` and update the next one at ``site`` - 1. @@ -289,10 +283,7 @@ def orthogonalize_right2left(self, site: int, dc: int = -1, normalize: bool = Fa assert site > 0 tensors = self.tensors shape = tensors[site].shape - if len(shape) == 3: - batch = 1 - else: - batch = shape[0] + batch = 1 if len(shape) == 3 else shape[0] if_trun = 0 < dc < shape[-3] if if_trun: u, s, vh = svd(tensors[site].reshape(batch, shape[-3], -1)) @@ -304,7 +295,7 @@ def orthogonalize_right2left(self, site: int, dc: int = -1, normalize: bool = Fa l = r.mH self._buffers[f'tensor{site}'] = vh.reshape(batch, -1, shape[-2], shape[-1]) if normalize: - norm = l.norm(dim=[-2,-1], keepdim=True) + norm = l.norm(dim=[-2, -1], keepdim=True) l = l / norm self._buffers[f'tensor{site - 1}'] = torch.einsum('...abc,...cd->...abd', tensors[site - 1], l) if len(shape) == 3: @@ -321,16 +312,16 @@ def orthogonalize_n1_n2(self, n1: int, n2: int, dc: int, normalize: bool) -> Non for site in range(n1, n2, -1): self.orthogonalize_right2left(site, dc, normalize) - def apply_mpo(self, mpo: List[torch.Tensor], sites: List[int]) -> None: + def apply_mpo(self, mpo: list[torch.Tensor], sites: list[int]) -> None: """Use TEBD algorithm to contract tensors (contract local states with local operators), i.e., - >>> a - >>> | - >>> i-----O-----j a - >>> | -> | - >>> b ik---X---jl - >>> | - >>> k-----T-----l + >>> a + >>> | + >>> i-----O-----j a + >>> | -> | + >>> b ik---X---jl + >>> | + >>> k-----T-----l """ assert len(mpo) == len(sites) for i, site in enumerate(sites): @@ -352,6 +343,7 @@ class DistributedQubitState(nn.Module): Args: nqubit (int): The number of qubits in the state. """ + def __init__(self, nqubit: int) -> None: super().__init__() self.world_size = comm_get_world_size() diff --git a/src/deepquantum/utils.py b/src/deepquantum/utils.py index 78699588..3d7e9471 100644 --- a/src/deepquantum/utils.py +++ b/src/deepquantum/utils.py @@ -1,14 +1,13 @@ -""" -Utilities -""" +"""Utilities""" import time +from collections.abc import Callable from functools import wraps -from typing import Callable def record_time(func: Callable) -> Callable: """A decorator that records the running time of a function.""" + @wraps(func) def wrapped_function(*args, **kwargs): t1 = time.time() @@ -16,11 +15,13 @@ def wrapped_function(*args, **kwargs): t2 = time.time() print(f'running time of "{func.__name__}": {t2 - t1}') return rst + return wrapped_function -class Time(object): +class Time: """A decorator that records the running time of a function.""" + def __init__(self) -> None: pass @@ -32,4 +33,5 @@ def wrapped_function(*args, **kwargs): t2 = time.time() print(f'running time of "{func.__name__}": {t2 - t1}') return rst + return wrapped_function diff --git a/src/ruff.toml b/src/ruff.toml new file mode 100644 index 00000000..de2159c7 --- /dev/null +++ b/src/ruff.toml @@ -0,0 +1,8 @@ +extend = '../pyproject.toml' + +[lint] +extend-select = ['D'] +ignore = ['D102', 'D105', 'D107', 'D415'] + +[lint.pydocstyle] +convention = 'google' diff --git a/tests/test_ansatz.py b/tests/test_ansatz.py index 7f1e8789..f32853e2 100644 --- a/tests/test_ansatz.py +++ b/tests/test_ansatz.py @@ -2,7 +2,6 @@ from fractions import Fraction import deepquantum as dq -import pytest def test_quantum_phase_estimation_single_qubit(): @@ -12,7 +11,7 @@ def test_quantum_phase_estimation_single_qubit(): qpe() res = qpe.measure(wires=list(range(t))) max_key = max(res, key=res.get) - phase_est = int(max_key, 2) / 2 ** t + phase_est = int(max_key, 2) / 2**t assert phase_est == phase @@ -61,7 +60,7 @@ def test_controlled_multiplier(): nqubit = nx + nb + 1 minmax1 = [0, nx - 1] minmax2 = [nx, nqubit - 2] - ancilla = [nqubit -1] + ancilla = [nqubit - 1] enc1 = dq.NumberEncoder(nqubit, n3, minmax1) enc2 = dq.NumberEncoder(nqubit, n1, minmax2) cmult = dq.ControlledMultiplier(nqubit, n2, mod, [0, nqubit - 2], nx, ancilla) @@ -107,7 +106,7 @@ def test_shor_general(): cir() res = cir.measure(wires=list(range(ncount)), shots=1) max_key = max(res, key=res.get) - phase = int(max_key, 2) / 2 ** ncount + phase = int(max_key, 2) / 2**ncount frac = Fraction(phase).limit_denominator(mod) r = frac.denominator print(f'Result: r = {r}') @@ -134,7 +133,7 @@ def test_shor_special(): cir() res = cir.measure(wires=list(range(ncount)), shots=1) max_key = max(res, key=res.get) - phase = int(max_key, 2) / 2 ** ncount + phase = int(max_key, 2) / 2**ncount frac = Fraction(phase).limit_denominator(mod) r = frac.denominator print(f'Result: r = {r}') diff --git a/tests/test_auto_grad.py b/tests/test_auto_grad.py index 6cb4cd43..9cc61e21 100644 --- a/tests/test_auto_grad.py +++ b/tests/test_auto_grad.py @@ -1,7 +1,7 @@ -import deepquantum as dq -import pytest import torch +import deepquantum as dq + def test_gaussian_backend_auto_grad(): def get_vac_prob(paras): @@ -11,31 +11,31 @@ def get_vac_prob(paras): cir.s(wires=1, r=paras[1]) cir.d(wires=0, r=paras[2]) cir.d(wires=1, r=paras[3]) - cir.bs(wires=[0,1], inputs=[paras[4], paras[5]]) + cir.bs(wires=[0, 1], inputs=[paras[4], paras[5]]) state = cir(is_prob=True) - target_state = dq.FockState([0,0]) + target_state = dq.FockState([0, 0]) vac_prob = state[target_state] return vac_prob - r_s1 = torch.tensor([1.], requires_grad=True) - r_s2 = torch.tensor([1.], requires_grad=True) - r_d1 = torch.tensor([1.], requires_grad=True) - r_d2 = torch.tensor([1.], requires_grad=True) + r_s1 = torch.tensor([1.0], requires_grad=True) + r_s2 = torch.tensor([1.0], requires_grad=True) + r_d1 = torch.tensor([1.0], requires_grad=True) + r_d2 = torch.tensor([1.0], requires_grad=True) theta = torch.tensor([0.1], requires_grad=True) phi = torch.tensor([0.1], requires_grad=True) para_ini = [r_s1, r_s2, r_d1, r_d2, theta, phi] - target_prob = 0.5 # set vacuum state prob + target_prob = 0.5 # set vacuum state prob optimizer = torch.optim.Adam(para_ini, lr=0.05) best_para = [] for _ in range(500): optimizer.zero_grad() - vac_prob = get_vac_prob(para_ini) # forward + vac_prob = get_vac_prob(para_ini) # forward loss = abs(target_prob - vac_prob) if loss < 1e-4: best_para.append([i.detach().clone() for i in para_ini]) - loss.backward() # backpropagation - optimizer.step() # update parameters + loss.backward() # backpropagation + optimizer.step() # update parameters best_result = get_vac_prob(best_para[0]) assert abs(best_result - target_prob) < 1e-4 diff --git a/tests/test_channel.py b/tests/test_channel.py index 8afe0367..5746a7d7 100644 --- a/tests/test_channel.py +++ b/tests/test_channel.py @@ -1,7 +1,7 @@ -import deepquantum as dq -import pytest import torch +import deepquantum as dq + def test_qubit_channel(): cir = dq.QubitCircuit(2, den_mat=True) @@ -13,4 +13,4 @@ def test_qubit_channel(): cir.amp_damp(0) cir.phase_damp(1) cir.gen_amp_damp(0) - assert torch.allclose(torch.trace(cir()), torch.tensor(1.) + 0j) + assert torch.allclose(torch.trace(cir()), torch.tensor(1.0) + 0j) diff --git a/tests/test_circuit.py b/tests/test_circuit.py index d4e27283..938b9520 100644 --- a/tests/test_circuit.py +++ b/tests/test_circuit.py @@ -1,7 +1,7 @@ -import deepquantum as dq -import pytest import torch +import deepquantum as dq + def test_qubit_mps(): nqubit = 3 @@ -28,16 +28,16 @@ def test_fock_mps(): cir.s(0, 0.1) cir.s(1, 0.2) cir.s(2, 0.3) - cir.bs([0,1], [0.1,0.2]) - cir.bs([1,2], [0.3,0.4]) + cir.bs([0, 1], [0.1, 0.2]) + cir.bs([1, 2], [0.3, 0.4]) state1 = dq.MatrixProductState(nmode, cir()).full_tensor().reshape(-1) cir = dq.QumodeCircuit(nmode, init_state='zeros', cutoff=cutoff, backend='fock', basis=False) cir.s(0, 0.1) cir.s(1, 0.2) cir.s(2, 0.3) - cir.bs([0,1], [0.1,0.2]) - cir.bs([1,2], [0.3,0.4]) + cir.bs([0, 1], [0.1, 0.2]) + cir.bs([1, 2], [0.3, 0.4]) state2 = cir().reshape(-1) assert torch.allclose(state1, state2, rtol=1e-5, atol=1e-5) @@ -51,16 +51,16 @@ def test_qubit_dist(): cir.u3layer(encode=True) cir.hlayer() cir.cnot_ring() - cir.toffoli(0,1,2) - cir.fredkin(2,1,0) - cir.swap([2,3]) - cir.rx(0, controls=[1,2,3], encode=True) - cir.ry(1, controls=[0,2,3], encode=True) - cir.rz(2, controls=[0,1,3], encode=True) - cir.rxx([0,1], controls=[2,3], encode=True) - cir.ryy([1,2], controls=[0,3], encode=True) - cir.rzz([2,3], controls=[0,1], encode=True) - cir.rxy([3,0], controls=[1,2], encode=True) + cir.toffoli(0, 1, 2) + cir.fredkin(2, 1, 0) + cir.swap([2, 3]) + cir.rx(0, controls=[1, 2, 3], encode=True) + cir.ry(1, controls=[0, 2, 3], encode=True) + cir.rz(2, controls=[0, 1, 3], encode=True) + cir.rxx([0, 1], controls=[2, 3], encode=True) + cir.ryy([1, 2], controls=[0, 3], encode=True) + cir.rzz([2, 3], controls=[0, 1], encode=True) + cir.rxy([3, 0], controls=[1, 2], encode=True) state1 = cir(data=data).amps cir = dq.QubitCircuit(4, reupload=True) @@ -70,16 +70,16 @@ def test_qubit_dist(): cir.u3layer(encode=True) cir.hlayer() cir.cnot_ring() - cir.toffoli(0,1,2) - cir.fredkin(2,1,0) - cir.swap([2,3]) - cir.rx(0, controls=[1,2,3], encode=True) - cir.ry(1, controls=[0,2,3], encode=True) - cir.rz(2, controls=[0,1,3], encode=True) - cir.rxx([0,1], controls=[2,3], encode=True) - cir.ryy([1,2], controls=[0,3], encode=True) - cir.rzz([2,3], controls=[0,1], encode=True) - cir.rxy([3,0], controls=[1,2], encode=True) + cir.toffoli(0, 1, 2) + cir.fredkin(2, 1, 0) + cir.swap([2, 3]) + cir.rx(0, controls=[1, 2, 3], encode=True) + cir.ry(1, controls=[0, 2, 3], encode=True) + cir.rz(2, controls=[0, 1, 3], encode=True) + cir.rxx([0, 1], controls=[2, 3], encode=True) + cir.ryy([1, 2], controls=[0, 3], encode=True) + cir.rzz([2, 3], controls=[0, 1], encode=True) + cir.rxy([3, 0], controls=[1, 2], encode=True) state2 = cir(data=data).reshape(-1) assert torch.allclose(state1, state2) @@ -93,19 +93,19 @@ def test_qubit_expectation_and_differentiation_dist(): cir1.u3layer(encode=True) cir1.hlayer() cir1.cnot_ring() - cir1.toffoli(0,1,2) - cir1.fredkin(2,1,0) - cir1.swap([2,3]) - cir1.rx(0, controls=[1,2,3], encode=True) - cir1.ry(1, controls=[0,2,3], encode=True) - cir1.rz(2, controls=[0,1,3], encode=True) - cir1.rxx([0,1], controls=[2,3], encode=True) - cir1.ryy([1,2], controls=[0,3], encode=True) - cir1.rzz([2,3], controls=[0,1], encode=True) - cir1.rxy([3,0], controls=[1,2], encode=True) + cir1.toffoli(0, 1, 2) + cir1.fredkin(2, 1, 0) + cir1.swap([2, 3]) + cir1.rx(0, controls=[1, 2, 3], encode=True) + cir1.ry(1, controls=[0, 2, 3], encode=True) + cir1.rz(2, controls=[0, 1, 3], encode=True) + cir1.rxx([0, 1], controls=[2, 3], encode=True) + cir1.ryy([1, 2], controls=[0, 3], encode=True) + cir1.rzz([2, 3], controls=[0, 1], encode=True) + cir1.rxy([3, 0], controls=[1, 2], encode=True) cir1.observable(0) cir1.observable(1, 'x') - cir1.observable([2,3], 'xy') + cir1.observable([2, 3], 'xy') cir1(data=data1) exp1 = cir1.expectation().sum() exp1.backward() @@ -118,19 +118,19 @@ def test_qubit_expectation_and_differentiation_dist(): cir2.u3layer(encode=True) cir2.hlayer() cir2.cnot_ring() - cir2.toffoli(0,1,2) - cir2.fredkin(2,1,0) - cir2.swap([2,3]) - cir2.rx(0, controls=[1,2,3], encode=True) - cir2.ry(1, controls=[0,2,3], encode=True) - cir2.rz(2, controls=[0,1,3], encode=True) - cir2.rxx([0,1], controls=[2,3], encode=True) - cir2.ryy([1,2], controls=[0,3], encode=True) - cir2.rzz([2,3], controls=[0,1], encode=True) - cir2.rxy([3,0], controls=[1,2], encode=True) + cir2.toffoli(0, 1, 2) + cir2.fredkin(2, 1, 0) + cir2.swap([2, 3]) + cir2.rx(0, controls=[1, 2, 3], encode=True) + cir2.ry(1, controls=[0, 2, 3], encode=True) + cir2.rz(2, controls=[0, 1, 3], encode=True) + cir2.rxx([0, 1], controls=[2, 3], encode=True) + cir2.ryy([1, 2], controls=[0, 3], encode=True) + cir2.rzz([2, 3], controls=[0, 1], encode=True) + cir2.rxy([3, 0], controls=[1, 2], encode=True) cir2.observable(0) cir2.observable(1, 'x') - cir2.observable([2,3], 'xy') + cir2.observable([2, 3], 'xy') cir2(data=data2) exp2 = cir2.expectation().sum() exp2.backward() diff --git a/tests/test_get_amplitude.py b/tests/test_get_amplitude.py index 2676e290..58c58569 100644 --- a/tests/test_get_amplitude.py +++ b/tests/test_get_amplitude.py @@ -1,7 +1,7 @@ -import deepquantum as dq -import pytest import torch +import deepquantum as dq + def test_get_amplitude(): n = 10 diff --git a/tests/test_mapper.py b/tests/test_mapper.py index dfac7d69..089de8c3 100644 --- a/tests/test_mapper.py +++ b/tests/test_mapper.py @@ -1,27 +1,23 @@ -import deepquantum as dq import numpy as np -import pytest import torch +import deepquantum as dq + def test_mapper(): - cnot = np.array([[1,0,0,0], - [0,1,0,0], - [0,0,0,1], - [0,0,1,0]]) + cnot = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]]) nqubit = 2 nmode = 6 ugate = cnot aux = [0, 0] aux_pos = [4, 5] success = 1 / 3 - umap = dq.UnitaryMapper(nqubit=nqubit, nmode=nmode, ugate=ugate, - success=success, aux=aux, aux_pos=aux_pos) + umap = dq.UnitaryMapper(nqubit=nqubit, nmode=nmode, ugate=ugate, success=success, aux=aux, aux_pos=aux_pos) basis = umap.basis - Re3 = umap.solve_eqs_real(total_trials=1, trials=10, precision=1e-5) # for real solution + re = umap.solve_eqs_real(total_trials=1, trials=10, precision=1e-5) # for real solution # check the result - cnot_test = Re3[0][0][0] - init_state = [1,0,1,0,0,0] + cnot_test = re[0][0][0] + init_state = [1, 0, 1, 0, 0, 0] test_circuit = dq.QumodeCircuit(nmode=6, init_state=init_state, basis=True) test_circuit.any(cnot_test, list(range(6))) temp_cnot = torch.zeros((4, 4), dtype=torch.cdouble) diff --git a/tests/test_mbqc_transpile.py b/tests/test_mbqc_transpile.py index 5ce3d39a..be38dfa5 100644 --- a/tests/test_mbqc_transpile.py +++ b/tests/test_mbqc_transpile.py @@ -1,9 +1,9 @@ import random -import deepquantum as dq -import pytest import torch +import deepquantum as dq + def test_random_circuit_transpilation(): # Create a circuit with random number of qubits (2-5) @@ -19,7 +19,7 @@ def test_random_circuit_transpilation(): lambda q: cir.x(q), lambda q: cir.rx(q, inputs=torch.rand(1) * 2 * torch.pi), lambda q: cir.ry(q, inputs=torch.rand(1) * 2 * torch.pi), - lambda q: cir.rz(q, inputs=torch.rand(1) * 2 * torch.pi) + lambda q: cir.rz(q, inputs=torch.rand(1) * 2 * torch.pi), ] # Add random number of gates (3-10) @@ -29,15 +29,15 @@ def test_random_circuit_transpilation(): # 70% chance for single-qubit gate, 30% for CNOT if random.random() < 0.7: # Random single-qubit gate - qubit = random.randint(0, n_qubits-1) + qubit = random.randint(0, n_qubits - 1) random.choice(single_gates)(qubit) else: # Random CNOT - control = random.randint(0, n_qubits-1) - target = random.randint(0, n_qubits-1) + control = random.randint(0, n_qubits - 1) + target = random.randint(0, n_qubits - 1) # Ensure control and target are different while target == control: - target = random.randint(0, n_qubits-1) + target = random.randint(0, n_qubits - 1) cir.cnot(control=control, target=target) # Transpile circuit to measurement pattern @@ -47,11 +47,7 @@ def test_random_circuit_transpilation(): state_cir = cir() state_pattern = pattern() # Assert that both states are equal (up to global phase) - assert torch.allclose( - torch.abs(state_cir), - torch.abs(state_pattern.graph.full_state), - atol=1e-6 - ) + assert torch.allclose(torch.abs(state_cir), torch.abs(state_pattern.graph.full_state), atol=1e-6) def test_encode_transpilation(): @@ -68,13 +64,13 @@ def test_encode_transpilation(): lambda q: cir.x(q), lambda q: cir.rx(q, inputs=torch.rand(1) * 2 * torch.pi), lambda q: cir.ry(q, inputs=torch.rand(1) * 2 * torch.pi), - lambda q: cir.rz(q, inputs=torch.rand(1) * 2 * torch.pi) + lambda q: cir.rz(q, inputs=torch.rand(1) * 2 * torch.pi), ] parametric_single_gates = [ - lambda q: cir.rx(q, encode=True), - lambda q: cir.ry(q, encode=True), - lambda q: cir.rz(q, encode=True) + lambda q: cir.rx(q, encode=True), + lambda q: cir.ry(q, encode=True), + lambda q: cir.rz(q, encode=True), ] # Add random number of gates (3-10) @@ -85,20 +81,20 @@ def test_encode_transpilation(): random_number = random.random() if random_number < 0.3: # Random single-qubit gate - qubit = random.randint(0, n_qubits-1) + qubit = random.randint(0, n_qubits - 1) random.choice(single_gates)(qubit) elif random_number < 0.8: # Random parametric-single-qubit gate - qubit = random.randint(0, n_qubits-1) + qubit = random.randint(0, n_qubits - 1) random.choice(parametric_single_gates)(qubit) n_para += 1 else: # Random CNOT - control = random.randint(0, n_qubits-1) - target = random.randint(0, n_qubits-1) + control = random.randint(0, n_qubits - 1) + target = random.randint(0, n_qubits - 1) # Ensure control and target are different while target == control: - target = random.randint(0, n_qubits-1) + target = random.randint(0, n_qubits - 1) cir.cnot(control=control, target=target) # Transpile circuit to measurement pattern @@ -109,11 +105,7 @@ def test_encode_transpilation(): state_cir = cir(data=para) state_pattern = pattern(data=-para) # Assert that both states are equal (up to global phase) - assert torch.allclose( - torch.abs(state_cir), - torch.abs(state_pattern.graph.full_state), - atol=1e-6 - ) + assert torch.allclose(torch.abs(state_cir), torch.abs(state_pattern.graph.full_state), atol=1e-6) def test_batch_data_transpilation(): @@ -130,13 +122,13 @@ def test_batch_data_transpilation(): lambda q: cir.x(q), lambda q: cir.rx(q, inputs=torch.rand(1) * 2 * torch.pi), lambda q: cir.ry(q, inputs=torch.rand(1) * 2 * torch.pi), - lambda q: cir.rz(q, inputs=torch.rand(1) * 2 * torch.pi) + lambda q: cir.rz(q, inputs=torch.rand(1) * 2 * torch.pi), ] parametric_single_gates = [ - lambda q: cir.rx(q, encode=True), - lambda q: cir.ry(q, encode=True), - lambda q: cir.rz(q, encode=True) + lambda q: cir.rx(q, encode=True), + lambda q: cir.ry(q, encode=True), + lambda q: cir.rz(q, encode=True), ] # Add random number of gates (3-10) @@ -147,20 +139,20 @@ def test_batch_data_transpilation(): random_number = random.random() if random_number < 0.3: # Random single-qubit gate - qubit = random.randint(0, n_qubits-1) + qubit = random.randint(0, n_qubits - 1) random.choice(single_gates)(qubit) elif random_number < 0.8: # Random parametric-single-qubit gate - qubit = random.randint(0, n_qubits-1) + qubit = random.randint(0, n_qubits - 1) random.choice(parametric_single_gates)(qubit) n_para += 1 else: # Random CNOT - control = random.randint(0, n_qubits-1) - target = random.randint(0, n_qubits-1) + control = random.randint(0, n_qubits - 1) + target = random.randint(0, n_qubits - 1) # Ensure control and target are different while target == control: - target = random.randint(0, n_qubits-1) + target = random.randint(0, n_qubits - 1) cir.cnot(control=control, target=target) # Transpile circuit to measurement pattern @@ -171,11 +163,7 @@ def test_batch_data_transpilation(): state_cir = cir(data=para) state_pattern = pattern(data=-para).graph.full_state # Assert that both states are equal (up to global phase) - assert torch.allclose( - torch.abs(state_cir), - torch.abs(state_pattern), - atol=1e-6 - ) + assert torch.allclose(torch.abs(state_cir), torch.abs(state_pattern), atol=1e-6) def test_data_reupload_transpilation(): @@ -192,13 +180,13 @@ def test_data_reupload_transpilation(): lambda q: cir.x(q), lambda q: cir.rx(q, inputs=torch.rand(1) * 2 * torch.pi), lambda q: cir.ry(q, inputs=torch.rand(1) * 2 * torch.pi), - lambda q: cir.rz(q, inputs=torch.rand(1) * 2 * torch.pi) + lambda q: cir.rz(q, inputs=torch.rand(1) * 2 * torch.pi), ] parametric_single_gates = [ - lambda q: cir.rx(q, encode=True), - lambda q: cir.ry(q, encode=True), - lambda q: cir.rz(q, encode=True) + lambda q: cir.rx(q, encode=True), + lambda q: cir.ry(q, encode=True), + lambda q: cir.rz(q, encode=True), ] # Add random number of gates (3-10) @@ -209,35 +197,28 @@ def test_data_reupload_transpilation(): random_number = random.random() if random_number < 0.3: # Random single-qubit gate - qubit = random.randint(0, n_qubits-1) + qubit = random.randint(0, n_qubits - 1) random.choice(single_gates)(qubit) elif random_number < 0.8: # Random parametric-single-qubit gate - qubit = random.randint(0, n_qubits-1) + qubit = random.randint(0, n_qubits - 1) random.choice(parametric_single_gates)(qubit) n_para += 1 else: # Random CNOT - control = random.randint(0, n_qubits-1) - target = random.randint(0, n_qubits-1) + control = random.randint(0, n_qubits - 1) + target = random.randint(0, n_qubits - 1) # Ensure control and target are different while target == control: - target = random.randint(0, n_qubits-1) + target = random.randint(0, n_qubits - 1) cir.cnot(control=control, target=target) # Transpile circuit to measurement pattern pattern = cir.pattern() # prepare batched input data - if n_para > 3: - para = torch.randn(batch_size, n_para-2) - else: - para = torch.randn(batch_size, n_para) + para = torch.randn(batch_size, n_para - 2) if n_para > 3 else torch.randn(batch_size, n_para) # Execute both circuits state_cir = cir(data=para) state_pattern = pattern(data=-para).graph.full_state # Assert that both states are equal (up to global phase) - assert torch.allclose( - torch.abs(state_cir), - torch.abs(state_pattern), - atol=1e-6 - ) + assert torch.allclose(torch.abs(state_cir), torch.abs(state_pattern), atol=1e-6) diff --git a/tests/test_mps.py b/tests/test_mps.py index aaa23b37..53c6e7c5 100644 --- a/tests/test_mps.py +++ b/tests/test_mps.py @@ -1,9 +1,9 @@ import random -import deepquantum as dq -import pytest import torch -from deepquantum.qmath import slice_state_vector, get_prob_mps + +import deepquantum as dq +from deepquantum.qmath import get_prob_mps, slice_state_vector def test_cir_get_prob(): @@ -57,8 +57,7 @@ def test_get_prob_mps(): cir2.rxlayer(encode=True) sv = cir2(data=data).reshape([2] * n) - offset = 0 - for i, b in zip(wires, bits): + for offset, (i, b) in enumerate(zip(wires, bits, strict=True)): prob0_sv = (slice_state_vector(sv, n - offset, [i - offset], '0', False).abs() ** 2).sum() prob1_sv = (slice_state_vector(sv, n - offset, [i - offset], '1', False).abs() ** 2).sum() probs_mps = get_prob_mps(mps, i) @@ -66,4 +65,3 @@ def test_get_prob_mps(): assert torch.allclose(prob1_sv, probs_mps[1]) sv = slice_state_vector(sv, n - offset, [i - offset], b, False) mps[i] = mps[i][:, [int(b)], :] - offset += 1 diff --git a/tests/test_photonic_batch_shape.py b/tests/test_photonic_batch_shape.py index 33f0b3e1..d3aa5178 100644 --- a/tests/test_photonic_batch_shape.py +++ b/tests/test_photonic_batch_shape.py @@ -1,22 +1,23 @@ -import deepquantum as dq -import pytest import torch +import deepquantum as dq + def test_forward_unitary(): batch = 4 - cir = dq.QumodeCircuit(2, init_state=[1,0]) - cir.bs([0,1]) + cir = dq.QumodeCircuit(2, init_state=[1, 0]) + cir.bs([0, 1]) cir.ps(0, encode=True) x = torch.randn(batch, 1) u = cir(x) assert u.shape == (batch, 2, 2) + def test_gaussian_shape(): cir = dq.QumodeCircuit(nmode=1, init_state='vac', cutoff=3, backend='gaussian') - cir.s(0, 0., encode=True) + cir.s(0, 0.0, encode=True) - data2 = torch.tensor([[0,0], [0,1]]) + data2 = torch.tensor([[0, 0], [0, 1]]) state = cir() assert tuple(state[0].shape) == (1, 2, 2) and tuple(state[1].shape) == (1, 2, 1) state = cir(data=data2) @@ -28,9 +29,9 @@ def test_gaussian_batch_shape(): covs = torch.stack([torch.eye(2)] * batch) means = torch.tensor([[0, 0]] * batch) cir = dq.QumodeCircuit(nmode=1, init_state=[covs, means], cutoff=3, backend='gaussian') - cir.s(0, 0., encode=True) + cir.s(0, 0.0, encode=True) - data2 = torch.tensor([[0,0]] * batch) + data2 = torch.tensor([[0, 0]] * batch) state = cir() assert tuple(state[0].shape) == (batch, 2, 2) and tuple(state[1].shape) == (batch, 2, 1) state = cir(data=data2) @@ -39,36 +40,44 @@ def test_gaussian_batch_shape(): def test_bosonic_shape(): cir = dq.QumodeCircuit(nmode=2, init_state='vac', cutoff=3, backend='bosonic') - cir.cat(0, r=1, theta=0.) - cir.gkp(1, theta=0., phi=0.) - cir.s(0, 0., encode=True) + cir.cat(0, r=1, theta=0.0) + cir.gkp(1, theta=0.0, phi=0.0) + cir.s(0, 0.0, encode=True) - data2 = torch.tensor([[0,0], [0,1]]) + data2 = torch.tensor([[0, 0], [0, 1]]) state = cir() - assert (tuple(state[0].shape) == (1, 1, 4, 4) and - tuple(state[1].shape) == (1, 356, 4, 1) and - tuple(state[2].shape) == (1, 356)) + assert ( + tuple(state[0].shape) == (1, 1, 4, 4) + and tuple(state[1].shape) == (1, 356, 4, 1) + and tuple(state[2].shape) == (1, 356) + ) state = cir(data=data2) - assert (tuple(state[0].shape) == (2, 1, 4, 4) and - tuple(state[1].shape) == (2, 356, 4, 1) and - tuple(state[2].shape) == (1, 356)) + assert ( + tuple(state[0].shape) == (2, 1, 4, 4) + and tuple(state[1].shape) == (2, 356, 4, 1) + and tuple(state[2].shape) == (1, 356) + ) def test_bosonic_batch_shape(): batch = torch.randint(1, 10, size=[1])[0] - cat = dq.CatState(r=1., theta=0., p=1) + cat = dq.CatState(r=1.0, theta=0.0, p=1) cov_in = cat.cov.expand(batch, 1, 2, 2) mean_in = cat.mean.expand(batch, 4, 2, 1) weight_in = cat.weight.expand(batch, 4) cir = dq.QumodeCircuit(nmode=1, init_state=[cov_in, mean_in, weight_in], cutoff=3, backend='bosonic') - cir.s(0, 0., encode=True) + cir.s(0, 0.0, encode=True) - data2 = torch.tensor([[0,0]] * batch) + data2 = torch.tensor([[0, 0]] * batch) state = cir() - assert (tuple(state[0].shape) == (batch, 1, 2, 2) and - tuple(state[1].shape) == (batch, 4, 2, 1) and - tuple(state[2].shape) == (batch, 4)) + assert ( + tuple(state[0].shape) == (batch, 1, 2, 2) + and tuple(state[1].shape) == (batch, 4, 2, 1) + and tuple(state[2].shape) == (batch, 4) + ) state = cir(data=data2) - assert (tuple(state[0].shape) == (batch, 1, 2, 2) and - tuple(state[1].shape) == (batch, 4, 2, 1) and - tuple(state[2].shape) == (batch, 4)) + assert ( + tuple(state[0].shape) == (batch, 1, 2, 2) + and tuple(state[1].shape) == (batch, 4, 2, 1) + and tuple(state[2].shape) == (batch, 4) + ) diff --git a/tests/test_photonic_bosonic.py b/tests/test_photonic_bosonic.py index 2a777aca..a8544c03 100644 --- a/tests/test_photonic_bosonic.py +++ b/tests/test_photonic_bosonic.py @@ -1,8 +1,8 @@ -import deepquantum as dq import numpy as np -import pytest import strawberryfields as sf import torch + +import deepquantum as dq from deepquantum.photonic import xxpp_to_xpxp @@ -13,8 +13,8 @@ def test_catstate(): prog_cat_bosonic = sf.Program(nmodes) hbar = 2 with prog_cat_bosonic.context as q: - sf.ops.Catstate(a=r, phi=theta, p=1) | q[0] # superposition of 4 states - eng = sf.Engine("bosonic", backend_options={"hbar": hbar}) # xpxp order + sf.ops.Catstate(a=r, phi=theta, p=1) | q[0] # superposition of 4 states + eng = sf.Engine('bosonic', backend_options={'hbar': hbar}) # xpxp order state = eng.run(prog_cat_bosonic).state means_sf = state.means() covs_sf = state.covs() @@ -23,7 +23,7 @@ def test_catstate(): cat = dq.CatState(r=r, theta=theta, p=1) err1 = abs(cat.cov - covs_sf).sum() err2 = abs(cat.mean[0].squeeze() - means_sf).sum() - err3 = abs(cat.weight - weights_sf).sum() / abs(weights_sf).sum() # relative error + err3 = abs(cat.weight - weights_sf).sum() / abs(weights_sf).sum() # relative error assert err1 + err2 + err3 < 3e-4 @@ -34,26 +34,26 @@ def test_forward_cov_mean(): vac = dq.BosonicState(state='vac', nmode=1) cir = dq.QumodeCircuit(nmode=2, init_state=[cat, vac], backend='bosonic') angles = 2 * np.pi * np.random.rand(2) - cir.s(1, r=2.) - cir.bs([0,1], angles) + cir.s(1, r=2.0) + cir.bs([0, 1], angles) test = cir() nmodes = 2 prog_cat_bosonic = sf.Program(nmodes) hbar = 2 with prog_cat_bosonic.context as q: - sf.ops.Catstate(a=r, phi=theta, p=1) | q[0] # superposition of 4 states - # sf.ops.Squeezed(r=1) | q[0] # catstate 不能加压缩门 + sf.ops.Catstate(a=r, phi=theta, p=1) | q[0] # superposition of 4 states + # sf.ops.Squeezed(r=1) | q[0] # catstate 不能加压缩门 sf.ops.Squeezed(r=2) | q[1] sf.ops.BSgate(angles[0], angles[1]) | [q[0], q[1]] - eng = sf.Engine("bosonic", backend_options={"hbar": hbar}) # xpxp order + eng = sf.Engine('bosonic', backend_options={'hbar': hbar}) # xpxp order state = eng.run(prog_cat_bosonic).state means_sf = state.means() covs_sf = state.covs() weights_sf = state.weights() err1 = abs(xxpp_to_xpxp(test[0][0]) - covs_sf).sum() err2 = abs(xxpp_to_xpxp(test[1][0]).squeeze() - means_sf).sum() - err3 = abs(test[2] - weights_sf).sum() / abs(weights_sf).sum() # relative error + err3 = abs(test[2] - weights_sf).sum() / abs(weights_sf).sum() # relative error assert err1 + err2 + err3 < 3e-4 @@ -64,8 +64,8 @@ def test_photon_number_mean_var(): prog_cat_bosonic = sf.Program(nmodes) hbar = 2 with prog_cat_bosonic.context as q: - sf.ops.Catstate(a=r, phi=theta, p=1) | q[0] # superposition of 4 states - eng = sf.Engine("bosonic", backend_options={"hbar": hbar}) # xpxp order + sf.ops.Catstate(a=r, phi=theta, p=1) | q[0] # superposition of 4 states + eng = sf.Engine('bosonic', backend_options={'hbar': hbar}) # xpxp order state = eng.run(prog_cat_bosonic).state cir = dq.QumodeCircuit(nmode=1, init_state='vac', backend='bosonic') @@ -91,8 +91,8 @@ def test_wigner(): nmodes = 1 prog_cat_bosonic = sf.Program(nmodes) with prog_cat_bosonic.context as q: - sf.ops.GKP(state=[theta, phi], epsilon=0.05, ampl_cutoff=0.01) | q[0] # superposition of 4 states - eng = sf.Engine("bosonic", backend_options={"hbar": 2}) # xpxp order + sf.ops.GKP(state=[theta, phi], epsilon=0.05, ampl_cutoff=0.01) | q[0] # superposition of 4 states + eng = sf.Engine('bosonic', backend_options={'hbar': 2}) # xpxp order state = eng.run(prog_cat_bosonic).state xvec = torch.linspace(-xrange, xrange, npoints) pvec = torch.linspace(-prange, prange, npoints) diff --git a/tests/test_photonic_fock.py b/tests/test_photonic_fock.py index 13c7dea1..332afe5b 100644 --- a/tests/test_photonic_fock.py +++ b/tests/test_photonic_fock.py @@ -1,7 +1,7 @@ -import deepquantum as dq -import pytest import torch +import deepquantum as dq + def test_batched_fock_basis_states(): batch = 3 @@ -32,12 +32,12 @@ def test_batched_fock_basis_states(): # test is_prob=None assert torch.equal(res1, re1[i]) - for key in res2.keys(): + for key in res2: # test is_prob=False assert torch.allclose(res2[key], re2[key][i], atol=1e-6) - for key in res3.keys(): - # test is prob = True + for key in res3: + # test is prob=True assert torch.allclose(res3[key], re3[key][i], atol=1e-6) @@ -70,11 +70,11 @@ def test_batched_fock_basis_states_and_data(): # test is_prob=None assert torch.allclose(res1, re1[i], atol=1e-6) - for key in res2.keys(): + for key in res2: # test is_prob=False assert torch.allclose(res2[key], re2[key][i], atol=1e-5) - for key in res3.keys(): + for key in res3: # test is_prob=True assert torch.allclose(res3[key], re3[key][i], atol=1e-6) @@ -113,6 +113,6 @@ def test_loss_batched_fock_basis_states(): # test is_prob=None assert torch.equal(res1, re1[i]) - for key in res2.keys(): - # test is prob = True + for key in res2: + # test is prob=True assert torch.allclose(res2[key], re2[key][i], atol=1e-6) diff --git a/tests/test_photonic_gate.py b/tests/test_photonic_gate.py index 6399738b..4cb3857c 100644 --- a/tests/test_photonic_gate.py +++ b/tests/test_photonic_gate.py @@ -1,22 +1,22 @@ -import deepquantum as dq -import pytest import torch +import deepquantum as dq + def test_2_mode_squeezing_gate(): r = torch.rand(1)[0] theta = torch.rand(1)[0] * 2 * torch.pi cutoff = 5 cir1 = dq.QumodeCircuit(nmode=2, init_state='vac', cutoff=cutoff, backend='gaussian') - cir1.s2([0,1], r, theta) + cir1.s2([0, 1], r, theta) cov1, mean1 = cir1() sym1 = cir1.get_symplectic() cir2 = dq.QumodeCircuit(nmode=2, init_state='vac', cutoff=cutoff, backend='gaussian') - cir2.bs([0,1], [torch.pi / 4, 0]) + cir2.bs([0, 1], [torch.pi / 4, 0]) cir2.s(0, r, theta) cir2.s(1, -r, theta) - cir2.bs([0,1], [-torch.pi / 4, 0]) + cir2.bs([0, 1], [-torch.pi / 4, 0]) cov2, mean2 = cir2() sym2 = cir2.get_symplectic() assert torch.allclose(cov1, cov2, atol=1e-6) @@ -44,13 +44,13 @@ def test_2_mode_squeezing_gate_numerical_stability(): cutoff = 64 r = 1 cir1 = dq.QumodeCircuit(nmode=2, init_state='vac', cutoff=cutoff, backend='fock', basis=False) - cir1.s2([0,1], r=r) - cir1.s2([0,1], r=r) + cir1.s2([0, 1], r=r) + cir1.s2([0, 1], r=r) cir1.to(torch.double) state1 = cir1() cir2 = dq.QumodeCircuit(nmode=2, init_state='vac', cutoff=cutoff, backend='fock', basis=False) - cir2.s2([0,1], r=2 * r) + cir2.s2([0, 1], r=2 * r) cir2.to(torch.double) state2 = cir2() assert torch.allclose(state1, state2) diff --git a/tests/test_photonic_qmath.py b/tests/test_photonic_qmath.py index 70638323..9e0ed34f 100644 --- a/tests/test_photonic_qmath.py +++ b/tests/test_photonic_qmath.py @@ -1,13 +1,12 @@ -import deepquantum as dq -import deepquantum.photonic as dqp import networkx as nx import numpy as np -import pytest import torch -from deepquantum.photonic import Squeezing2 -from deepquantum.photonic import xxpp_to_xpxp, xpxp_to_xxpp, quadrature_to_ladder, ladder_to_quadrature from scipy.stats import unitary_group +import deepquantum as dq +import deepquantum.photonic as dqp +from deepquantum.photonic import Squeezing2, ladder_to_quadrature, quadrature_to_ladder, xpxp_to_xxpp, xxpp_to_xpxp + def test_quadrature_ladder_transform(): batch = 2 @@ -70,9 +69,9 @@ def test_williamson(): cir.any(unitary=u, wires=list(range(nmode))) cov, _ = cir() t, s = dq.williamson(cov[0]) - err1 = abs((s @ t @ s.mT) - cov[0]).sum() # 验证分解正确性 + err1 = abs((s @ t @ s.mT) - cov[0]).sum() # 验证分解正确性 omega = cov.new_ones(nmode) omega = torch.cat([-omega, omega]).diag_embed() - omega = omega.reshape(2, nmode, 2 * nmode).flip(0).reshape(2 * nmode, 2 * nmode) # symplectic form - err2 = abs((s.mT @ omega @ s) - omega).sum() # 验证辛形式 + omega = omega.reshape(2, nmode, 2 * nmode).flip(0).reshape(2 * nmode, 2 * nmode) # symplectic form + err2 = abs((s.mT @ omega @ s) - omega).sum() # 验证辛形式 assert err1 + err2 < 5e-4 diff --git a/tests/test_photonic_random_circuit.py b/tests/test_photonic_random_circuit.py index c6f65178..311f818f 100644 --- a/tests/test_photonic_random_circuit.py +++ b/tests/test_photonic_random_circuit.py @@ -1,6 +1,6 @@ -import deepquantum as dq import numpy as np -import pytest + +import deepquantum as dq def test_random_circuit_two_approaches(): @@ -19,7 +19,7 @@ def test_random_circuit_two_approaches(): dq_gate_1 = dq.QumodeCircuit(nmode=nmode, init_state=ini_state_1, basis=True) dq_gate_2 = dq.QumodeCircuit(nmode=nmode, init_state=ini_state_2, basis=False) - for _ in range(ndevice): # take the random circuit + for _ in range(ndevice): # take the random circuit j = np.random.uniform(-2, 5) if j > 4: temp_1 = int(np.random.choice(np.arange(nmode))) @@ -29,28 +29,28 @@ def test_random_circuit_two_approaches(): if 3 < j < 4: k = int(np.random.choice(np.arange(nmode - 1))) angle_1 = np.random.uniform(0, 2 * np.pi) - dq_gate_1.bs_theta([k, k+1], angle_1) - dq_gate_2.bs_theta([k, k+1], angle_1) + dq_gate_1.bs_theta([k, k + 1], angle_1) + dq_gate_2.bs_theta([k, k + 1], angle_1) if 2 < j < 3: k = int(np.random.choice(np.arange(nmode - 1))) angle_1 = np.random.uniform(0, 2 * np.pi) - dq_gate_1.bs_rx([k, k+1], angle_1) - dq_gate_2.bs_rx([k, k+1], angle_1) + dq_gate_1.bs_rx([k, k + 1], angle_1) + dq_gate_2.bs_rx([k, k + 1], angle_1) if 1 < j < 2: k = int(np.random.choice(np.arange(nmode - 1))) angle_1 = np.random.uniform(0, 2 * np.pi) - dq_gate_1.bs_ry([k, k+1], angle_1) - dq_gate_2.bs_ry([k, k+1], angle_1) + dq_gate_1.bs_ry([k, k + 1], angle_1) + dq_gate_2.bs_ry([k, k + 1], angle_1) if 0 < j < 1: k = int(np.random.choice(np.arange(nmode - 1))) angle_1 = np.random.uniform(0, 2 * np.pi) - dq_gate_1.bs_h([k, k+1], angle_1) - dq_gate_2.bs_h([k, k+1], angle_1) + dq_gate_1.bs_h([k, k + 1], angle_1) + dq_gate_2.bs_h([k, k + 1], angle_1) if j < 0: k = int(np.random.choice(np.arange(nmode - 1))) angle_1 = np.random.uniform(0, 2 * np.pi, 2) - dq_gate_1.mzi([k, k+1], angle_1) - dq_gate_2.mzi([k, k+1], angle_1) + dq_gate_1.mzi([k, k + 1], angle_1) + dq_gate_2.mzi([k, k + 1], angle_1) re1 = dq_gate_1(is_prob=False) re2 = dq_gate_2() max_error = -1.0 diff --git a/tests/test_with_graphix.py b/tests/test_with_graphix.py index 92143a76..997069c7 100644 --- a/tests/test_with_graphix.py +++ b/tests/test_with_graphix.py @@ -1,82 +1,87 @@ -import deepquantum as dq import numpy as np import torch from graphix import Pattern, command from graphix.fundamentals import Plane +import deepquantum as dq + def test_random_with_graphix(): n = np.random.randint(4, 10) results_1 = 0 results_2 = 1 while results_1 != results_2: - pat_gx = Pattern([0,1]) # initial state - for i in range(2, n+1): + pat_gx = Pattern([0, 1]) # initial state + for i in range(2, n + 1): pat_gx.add(command.N(i)) - for i in range(2, n+1): + for i in range(2, n + 1): pat_gx.add(command.E(nodes=(0, i))) - pat_gx.add(command.E(nodes=(n-1, n))) + pat_gx.add(command.E(nodes=(n - 1, n))) pat_gx.add(command.M(node=0, angle=1, plane=Plane.XY)) pat_gx.add(command.M(node=1, angle=1, plane=Plane.XY, s_domain={0})) - pat_gx.add(command.M(node=n-1, angle=1, plane=Plane.XY, s_domain={0}, t_domain={1})) - pat_gx.add(command.X(node=n, domain={0,1, n-1})) - out_state = pat_gx.simulate_pattern(backend="statevector") + pat_gx.add(command.M(node=n - 1, angle=1, plane=Plane.XY, s_domain={0}, t_domain={1})) + pat_gx.add(command.X(node=n, domain={0, 1, n - 1})) + out_state = pat_gx.simulate_pattern(backend='statevector') results_1 = pat_gx.results - pat_dq = dq.Pattern(nodes_state=[0,1]) - for i in range(2, n+1): + pat_dq = dq.Pattern(nodes_state=[0, 1]) + for i in range(2, n + 1): pat_dq.n(i) - for i in range(2, n+1): + for i in range(2, n + 1): pat_dq.e(0, i) - pat_dq.e(n-1, n) + pat_dq.e(n - 1, n) pat_dq.m(node=0, angle=np.pi) pat_dq.m(node=1, angle=np.pi, s_domain=[0]) - pat_dq.m(node=n-1, angle=np.pi, s_domain=[0], t_domain=[1]) - pat_dq.x(node=n, domain=[0, 1, n-1]) + pat_dq.m(node=n - 1, angle=np.pi, s_domain=[0], t_domain=[1]) + pat_dq.x(node=n, domain=[0, 1, n - 1]) state = pat_dq().full_state results_2 = pat_dq.state.measure_dict - assert torch.allclose(torch.abs(torch.tensor(out_state.flatten(), dtype=torch.complex64)), torch.abs(state.flatten()), atol=1e-6) + assert torch.allclose( + torch.abs(torch.tensor(out_state.flatten(), dtype=torch.complex64)), torch.abs(state.flatten()), atol=1e-6 + ) def test_batch_init_state(): n = np.random.randint(4, 10) - init_state = [[1.,0.,0.,0.], - [0.5,0.5,0.5,0.5], - [0.,0.,1.,0.]] - pat_dq = dq.Pattern(nodes_state=[0,1], state=torch.tensor(init_state)) - for i in range(2, n+1): + init_state = [[1.0, 0.0, 0.0, 0.0], [0.5, 0.5, 0.5, 0.5], [0.0, 0.0, 1.0, 0.0]] + pat_dq = dq.Pattern(nodes_state=[0, 1], state=torch.tensor(init_state)) + for i in range(2, n + 1): pat_dq.n(i) - for i in range(2, n+1): + for i in range(2, n + 1): pat_dq.e(0, i) - pat_dq.e(n-1, n) + pat_dq.e(n - 1, n) pat_dq.m(node=0, angle=np.pi) pat_dq.m(node=1, angle=np.pi, s_domain=[0]) - pat_dq.m(node=n-1, angle=np.pi, s_domain=[0], t_domain=[1]) - pat_dq.x(node=n, domain=[0, 1, n-1]) + pat_dq.m(node=n - 1, angle=np.pi, s_domain=[0], t_domain=[1]) + pat_dq.x(node=n, domain=[0, 1, n - 1]) state = pat_dq().full_state results = pat_dq.state.measure_dict for i in range(3): rst = {} - for key in results.keys(): + for key in results: rst[key] = [results[key][i]] results_1 = 0 while results_1 != rst: - pat_gx = Pattern([0,1]) # initial state - for j in range(2, n+1): + pat_gx = Pattern([0, 1]) # initial state + for j in range(2, n + 1): pat_gx.add(command.N(j)) - for j in range(2, n+1): + for j in range(2, n + 1): pat_gx.add(command.E(nodes=(0, j))) - pat_gx.add(command.E(nodes=(n-1, n))) + pat_gx.add(command.E(nodes=(n - 1, n))) pat_gx.add(command.M(node=0, angle=1, plane=Plane.XY)) pat_gx.add(command.M(node=1, angle=1, plane=Plane.XY, s_domain={0})) - pat_gx.add(command.M(node=n-1, angle=1, plane=Plane.XY, s_domain={0}, t_domain={1})) - pat_gx.add(command.X(node=n, domain={0,1, n-1})) - out_state = pat_gx.simulate_pattern(backend="statevector", input_state=init_state[i]) + pat_gx.add(command.M(node=n - 1, angle=1, plane=Plane.XY, s_domain={0}, t_domain={1})) + pat_gx.add(command.X(node=n, domain={0, 1, n - 1})) + out_state = pat_gx.simulate_pattern(backend='statevector', input_state=init_state[i]) results_1 = pat_gx.results print(out_state.flatten()) print(state[0].flatten()) - assert torch.allclose(torch.abs(torch.tensor(out_state.flatten(), dtype=torch.complex64)), torch.abs(state[i].flatten()), atol=1e-6) + assert torch.allclose( + torch.abs(torch.tensor(out_state.flatten(), dtype=torch.complex64)), + torch.abs(state[i].flatten()), + atol=1e-6, + ) def test_standardize(): @@ -86,37 +91,37 @@ def test_standardize(): while results_1 != results_2: pat_gx = Pattern([0]) - for l in range(3): - pat_gx.add(command.N(1+2*l)) - pat_gx.add(command.N(2+2*l)) - pat_gx.add(command.E(nodes=(0+2*l, 1+2*l))) - pat_gx.add(command.E(nodes=(1+2*l, 2+2*l))) - if l == 0: - pat_gx.add(command.M(node=0+2*l, angle=alpha[50])) - pat_gx.add(command.M(node=1+2*l, angle=alpha[51], s_domain={0})) + for i in range(3): + pat_gx.add(command.N(1 + 2 * i)) + pat_gx.add(command.N(2 + 2 * i)) + pat_gx.add(command.E(nodes=(0 + 2 * i, 1 + 2 * i))) + pat_gx.add(command.E(nodes=(1 + 2 * i, 2 + 2 * i))) + if i == 0: + pat_gx.add(command.M(node=0 + 2 * i, angle=alpha[50])) + pat_gx.add(command.M(node=1 + 2 * i, angle=alpha[51], s_domain={0})) else: - pat_gx.add(command.M(node=0+2*l, angle=alpha[2*l], s_domain={2*l-1})) - pat_gx.add(command.M(node=1+2*l, angle=alpha[2*l+1], s_domain={(2*l-1),2*l})) - pat_gx.add(command.X(node=2 +2*l, domain={0+2*l, 1+2*l})) - pat_gx.add(command.Z(node=2 +2*l, domain={1+2*l})) + pat_gx.add(command.M(node=0 + 2 * i, angle=alpha[2 * i], s_domain={2 * i - 1})) + pat_gx.add(command.M(node=1 + 2 * i, angle=alpha[2 * i + 1], s_domain={(2 * i - 1), 2 * i})) + pat_gx.add(command.X(node=2 + 2 * i, domain={0 + 2 * i, 1 + 2 * i})) + pat_gx.add(command.Z(node=2 + 2 * i, domain={1 + 2 * i})) pat_gx.standardize() - state = pat_gx.simulate_pattern(backend="statevector") + state = pat_gx.simulate_pattern(backend='statevector') results_1 = pat_gx.results pat_dq = dq.Pattern(nodes_state=[0]) - for l in range(3): - pat_dq.n(1+2*l) - pat_dq.n(2+2*l) - pat_dq.e(0+2*l, 1+2*l) - pat_dq.e(1+2*l, 2+2*l) - if l == 0: - pat_dq.m(node=0+2*l, angle=alpha[50]*torch.pi) - pat_dq.m(node=1+2*l, angle=alpha[51]*torch.pi, s_domain=[0]) + for i in range(3): + pat_dq.n(1 + 2 * i) + pat_dq.n(2 + 2 * i) + pat_dq.e(0 + 2 * i, 1 + 2 * i) + pat_dq.e(1 + 2 * i, 2 + 2 * i) + if i == 0: + pat_dq.m(node=0 + 2 * i, angle=alpha[50] * torch.pi) + pat_dq.m(node=1 + 2 * i, angle=alpha[51] * torch.pi, s_domain=[0]) else: - pat_dq.m(node=0+2*l, angle=alpha[2*l]*torch.pi, s_domain=[2*l-1]) - pat_dq.m(node=1+2*l, angle=alpha[2*l+1]*torch.pi, s_domain=[2*l-1, 2*l]) - pat_dq.x(node=2+2*l, domain=[0+2*l, 1+2*l]) - pat_dq.z(node=2+2*l, domain=[1+2*l]) + pat_dq.m(node=0 + 2 * i, angle=alpha[2 * i] * torch.pi, s_domain=[2 * i - 1]) + pat_dq.m(node=1 + 2 * i, angle=alpha[2 * i + 1] * torch.pi, s_domain=[2 * i - 1, 2 * i]) + pat_dq.x(node=2 + 2 * i, domain=[0 + 2 * i, 1 + 2 * i]) + pat_dq.z(node=2 + 2 * i, domain=[1 + 2 * i]) pat_dq.standardize() state2 = pat_dq().full_state results_2 = pat_dq.state.measure_dict @@ -130,38 +135,38 @@ def test_signal_shifting(): while results_1 != results_2: pat_gx = Pattern([0]) - for l in range(3): - pat_gx.add(command.N(1+2*l)) - pat_gx.add(command.N(2+2*l)) - pat_gx.add(command.E(nodes=(0+2*l, 1+2*l))) - pat_gx.add(command.E(nodes=(1+2*l, 2+2*l))) - if l == 0: - pat_gx.add(command.M(node=0+2*l, angle=alpha[50])) - pat_gx.add(command.M(node=1+2*l, angle=alpha[51], s_domain={0}, t_domain={0})) + for i in range(3): + pat_gx.add(command.N(1 + 2 * i)) + pat_gx.add(command.N(2 + 2 * i)) + pat_gx.add(command.E(nodes=(0 + 2 * i, 1 + 2 * i))) + pat_gx.add(command.E(nodes=(1 + 2 * i, 2 + 2 * i))) + if i == 0: + pat_gx.add(command.M(node=0 + 2 * i, angle=alpha[50])) + pat_gx.add(command.M(node=1 + 2 * i, angle=alpha[51], s_domain={0}, t_domain={0})) else: - pat_gx.add(command.M(node=0+2*l, angle=alpha[2*l], s_domain={2*l-1}, t_domain={2*l-1})) - pat_gx.add(command.M(node=1+2*l, angle=alpha[2*l+1], s_domain={(2*l-1),2*l})) - pat_gx.add(command.X(node=2 +2*l, domain={0+2*l, 1+2*l})) - pat_gx.add(command.Z(node=2 +2*l, domain={1+2*l})) + pat_gx.add(command.M(node=0 + 2 * i, angle=alpha[2 * i], s_domain={2 * i - 1}, t_domain={2 * i - 1})) + pat_gx.add(command.M(node=1 + 2 * i, angle=alpha[2 * i + 1], s_domain={(2 * i - 1), 2 * i})) + pat_gx.add(command.X(node=2 + 2 * i, domain={0 + 2 * i, 1 + 2 * i})) + pat_gx.add(command.Z(node=2 + 2 * i, domain={1 + 2 * i})) pat_gx.standardize() pat_gx.shift_signals() - state = pat_gx.simulate_pattern(backend="statevector") + state = pat_gx.simulate_pattern(backend='statevector') results_1 = pat_gx.results pat_dq = dq.Pattern(nodes_state=[0]) - for l in range(3): - pat_dq.n(1+2*l) - pat_dq.n(2+2*l) - pat_dq.e(0+2*l, 1+2*l) - pat_dq.e(1+2*l, 2+2*l) - if l == 0: - pat_dq.m(node=0+2*l, angle=alpha[50]*torch.pi) - pat_dq.m(node=1+2*l, angle=alpha[51]*torch.pi, s_domain=[0], t_domain=[0]) + for i in range(3): + pat_dq.n(1 + 2 * i) + pat_dq.n(2 + 2 * i) + pat_dq.e(0 + 2 * i, 1 + 2 * i) + pat_dq.e(1 + 2 * i, 2 + 2 * i) + if i == 0: + pat_dq.m(node=0 + 2 * i, angle=alpha[50] * torch.pi) + pat_dq.m(node=1 + 2 * i, angle=alpha[51] * torch.pi, s_domain=[0], t_domain=[0]) else: - pat_dq.m(node=0+2*l, angle=alpha[2*l]*torch.pi, s_domain=[2*l-1], t_domain=[2*l-1]) - pat_dq.m(node=1+2*l, angle=alpha[2*l+1]*torch.pi, s_domain=[2*l-1, 2*l]) - pat_dq.x(node=2+2*l, domain=[0+2*l, 1+2*l]) - pat_dq.z(node=2+2*l, domain=[1+2*l]) + pat_dq.m(node=0 + 2 * i, angle=alpha[2 * i] * torch.pi, s_domain=[2 * i - 1], t_domain=[2 * i - 1]) + pat_dq.m(node=1 + 2 * i, angle=alpha[2 * i + 1] * torch.pi, s_domain=[2 * i - 1, 2 * i]) + pat_dq.x(node=2 + 2 * i, domain=[0 + 2 * i, 1 + 2 * i]) + pat_dq.z(node=2 + 2 * i, domain=[1 + 2 * i]) pat_dq.standardize() pat_dq.shift_signals() state2 = pat_dq().full_state @@ -176,38 +181,48 @@ def test_signal_shifting_plane_yz(): while results_1 != results_2: pat_gx = Pattern([0]) - for l in range(3): - pat_gx.add(command.N(1+2*l)) - pat_gx.add(command.N(2+2*l)) - pat_gx.add(command.E(nodes=(0+2*l, 1+2*l))) - pat_gx.add(command.E(nodes=(1+2*l, 2+2*l))) - if l == 0: - pat_gx.add(command.M(node=0+2*l, angle=alpha[50])) - pat_gx.add(command.M(node=1+2*l, angle=alpha[51], s_domain={0}, t_domain={0})) + for i in range(3): + pat_gx.add(command.N(1 + 2 * i)) + pat_gx.add(command.N(2 + 2 * i)) + pat_gx.add(command.E(nodes=(0 + 2 * i, 1 + 2 * i))) + pat_gx.add(command.E(nodes=(1 + 2 * i, 2 + 2 * i))) + if i == 0: + pat_gx.add(command.M(node=0 + 2 * i, angle=alpha[50])) + pat_gx.add(command.M(node=1 + 2 * i, angle=alpha[51], s_domain={0}, t_domain={0})) else: - pat_gx.add(command.M(node=0+2*l, angle=alpha[2*l], s_domain={2*l-1}, t_domain={2*l-1}, plane=Plane.YZ)) - pat_gx.add(command.M(node=1+2*l, angle=alpha[2*l+1], s_domain={(2*l-1),2*l})) - pat_gx.add(command.X(node=2+2*l, domain={0+2*l, 1+2*l})) - pat_gx.add(command.Z(node=2+2*l, domain={1+2*l})) + pat_gx.add( + command.M( + node=0 + 2 * i, angle=alpha[2 * i], s_domain={2 * i - 1}, t_domain={2 * i - 1}, plane=Plane.YZ + ) + ) + pat_gx.add(command.M(node=1 + 2 * i, angle=alpha[2 * i + 1], s_domain={(2 * i - 1), 2 * i})) + pat_gx.add(command.X(node=2 + 2 * i, domain={0 + 2 * i, 1 + 2 * i})) + pat_gx.add(command.Z(node=2 + 2 * i, domain={1 + 2 * i})) pat_gx.standardize() pat_gx.shift_signals() - state = pat_gx.simulate_pattern(backend="statevector") + state = pat_gx.simulate_pattern(backend='statevector') results_1 = pat_gx.results pat_dq = dq.Pattern(nodes_state=[0]) - for l in range(3): - pat_dq.n(1+2*l) - pat_dq.n(2+2*l) - pat_dq.e(0+2*l, 1+2*l) - pat_dq.e(1+2*l, 2+2*l) - if l == 0: - pat_dq.m(node=0+2*l, angle=alpha[50]*torch.pi) - pat_dq.m(node=1+2*l, angle=alpha[51]*torch.pi, s_domain=[0], t_domain=[0]) + for i in range(3): + pat_dq.n(1 + 2 * i) + pat_dq.n(2 + 2 * i) + pat_dq.e(0 + 2 * i, 1 + 2 * i) + pat_dq.e(1 + 2 * i, 2 + 2 * i) + if i == 0: + pat_dq.m(node=0 + 2 * i, angle=alpha[50] * torch.pi) + pat_dq.m(node=1 + 2 * i, angle=alpha[51] * torch.pi, s_domain=[0], t_domain=[0]) else: - pat_dq.m(node=0+2*l, angle=torch.pi/2 - alpha[2*l]*torch.pi, plane='yz', s_domain =[2*l-1], t_domain=[2*l-1]) - pat_dq.m(node=1+2*l, angle=alpha[2*l+1]*torch.pi, s_domain =[2*l-1, 2*l]) - pat_dq.x(node=2+2*l, domain=[0+2*l, 1+2*l]) - pat_dq.z(node=2+2*l, domain=[1+2*l]) + pat_dq.m( + node=0 + 2 * i, + angle=torch.pi / 2 - alpha[2 * i] * torch.pi, + plane='yz', + s_domain=[2 * i - 1], + t_domain=[2 * i - 1], + ) + pat_dq.m(node=1 + 2 * i, angle=alpha[2 * i + 1] * torch.pi, s_domain=[2 * i - 1, 2 * i]) + pat_dq.x(node=2 + 2 * i, domain=[0 + 2 * i, 1 + 2 * i]) + pat_dq.z(node=2 + 2 * i, domain=[1 + 2 * i]) pat_dq.standardize() pat_dq.shift_signals() state2 = pat_dq().full_state @@ -220,40 +235,50 @@ def test_signal_shifting_plane_xz(): results_1 = 0 results_2 = 1 - while results_1 !=results_2: + while results_1 != results_2: pat_gx = Pattern([0]) - for l in range(3): - pat_gx.add(command.N(1+2*l)) - pat_gx.add(command.N(2+2*l)) - pat_gx.add(command.E(nodes=(0+2*l, 1+2*l))) - pat_gx.add(command.E(nodes=(1+2*l, 2+2*l))) - if l == 0: - pat_gx.add(command.M(node=0+2*l, angle=alpha[50])) - pat_gx.add(command.M(node=1+2*l, angle=alpha[51], s_domain={0}, t_domain={0})) + for i in range(3): + pat_gx.add(command.N(1 + 2 * i)) + pat_gx.add(command.N(2 + 2 * i)) + pat_gx.add(command.E(nodes=(0 + 2 * i, 1 + 2 * i))) + pat_gx.add(command.E(nodes=(1 + 2 * i, 2 + 2 * i))) + if i == 0: + pat_gx.add(command.M(node=0 + 2 * i, angle=alpha[50])) + pat_gx.add(command.M(node=1 + 2 * i, angle=alpha[51], s_domain={0}, t_domain={0})) else: - pat_gx.add(command.M(node=0+2*l, angle=alpha[2*l], s_domain={2*l-1}, t_domain={2*l-1}, plane=Plane.XZ)) - pat_gx.add(command.M(node=1+2*l, angle=alpha[2*l+1], s_domain={(2*l-1),2*l})) - pat_gx.add(command.X(node=2+2*l, domain={0+2*l, 1+2*l})) - pat_gx.add(command.Z(node=2+2*l, domain={1+2*l})) + pat_gx.add( + command.M( + node=0 + 2 * i, angle=alpha[2 * i], s_domain={2 * i - 1}, t_domain={2 * i - 1}, plane=Plane.XZ + ) + ) + pat_gx.add(command.M(node=1 + 2 * i, angle=alpha[2 * i + 1], s_domain={(2 * i - 1), 2 * i})) + pat_gx.add(command.X(node=2 + 2 * i, domain={0 + 2 * i, 1 + 2 * i})) + pat_gx.add(command.Z(node=2 + 2 * i, domain={1 + 2 * i})) pat_gx.standardize() pat_gx.shift_signals() - state = pat_gx.simulate_pattern(backend="statevector") + state = pat_gx.simulate_pattern(backend='statevector') results_1 = pat_gx.results pat_dq = dq.Pattern(nodes_state=[0]) - for l in range(3): - pat_dq.n(1+2*l) - pat_dq.n(2+2*l) - pat_dq.e(0+2*l, 1+2*l) - pat_dq.e(1+2*l, 2+2*l) - if l == 0: - pat_dq.m(node=0+2*l, angle=alpha[50]*torch.pi) - pat_dq.m(node=1+2*l, angle=alpha[51]*torch.pi, s_domain=[0], t_domain=[0]) + for i in range(3): + pat_dq.n(1 + 2 * i) + pat_dq.n(2 + 2 * i) + pat_dq.e(0 + 2 * i, 1 + 2 * i) + pat_dq.e(1 + 2 * i, 2 + 2 * i) + if i == 0: + pat_dq.m(node=0 + 2 * i, angle=alpha[50] * torch.pi) + pat_dq.m(node=1 + 2 * i, angle=alpha[51] * torch.pi, s_domain=[0], t_domain=[0]) else: - pat_dq.m(node=0+2*l, angle=alpha[2*l]*torch.pi, plane='xz', s_domain=[2*l-1], t_domain=[2*l-1]) - pat_dq.m(node=1+2*l, angle=alpha[2*l+1]*torch.pi, s_domain=[2*l-1, 2*l]) - pat_dq.x(node=2+2*l, domain=[0+2*l, 1+2*l]) - pat_dq.z(node=2+2*l, domain=[1+2*l]) + pat_dq.m( + node=0 + 2 * i, + angle=alpha[2 * i] * torch.pi, + plane='xz', + s_domain=[2 * i - 1], + t_domain=[2 * i - 1], + ) + pat_dq.m(node=1 + 2 * i, angle=alpha[2 * i + 1] * torch.pi, s_domain=[2 * i - 1, 2 * i]) + pat_dq.x(node=2 + 2 * i, domain=[0 + 2 * i, 1 + 2 * i]) + pat_dq.z(node=2 + 2 * i, domain=[1 + 2 * i]) pat_dq.standardize() pat_dq.shift_signals() state2 = pat_dq().full_state diff --git a/tests/test_with_pennylane.py b/tests/test_with_pennylane.py index 0cf27c76..cff5cb82 100644 --- a/tests/test_with_pennylane.py +++ b/tests/test_with_pennylane.py @@ -1,34 +1,37 @@ -import deepquantum as dq import numpy as np import pennylane as qml import torch +import deepquantum as dq + def test_number_operator_exp(): nmode = 4 cutoff = 4 sq = np.random.rand(nmode) - bs_angles = np.random.rand(nmode-1, 2) * 2 * np.pi + bs_angles = np.random.rand(nmode - 1, 2) * 2 * np.pi dev = qml.device('strawberryfields.fock', wires=nmode, cutoff_dim=cutoff) + @qml.qnode(dev) def circuit(): for i in range(nmode): qml.Squeezing(sq[i], 0.0, wires=i) - if i < nmode-1: - qml.Beamsplitter(theta=bs_angles[i][0], phi=bs_angles[i][1], wires=[i, i+1]) - exp = qml.expval(qml.NumberOperator(0)) # number operator + if i < nmode - 1: + qml.Beamsplitter(theta=bs_angles[i][0], phi=bs_angles[i][1], wires=[i, i + 1]) + exp = qml.expval(qml.NumberOperator(0)) # number operator exp1 = qml.expval(qml.NumberOperator(1)) exp2 = qml.expval(qml.NumberOperator(2)) exp3 = qml.expval(qml.NumberOperator(3)) return exp, exp1, exp2, exp3 + exp_qml = circuit() cir = dq.QumodeCircuit(nmode=nmode, backend='fock', basis=False, cutoff=cutoff, init_state='vac', den_mat=True) for i in range(nmode): cir.s(i, sq[i], encode=True) - if i < nmode-1: - cir.bs(wires=[i, i+1], inputs=bs_angles[i]) + if i < nmode - 1: + cir.bs(wires=[i, i + 1], inputs=bs_angles[i]) cir() exp_dq, _ = cir.photon_number_mean_var() assert (exp_dq.flatten() - exp_qml.numpy()).sum() < 1e-6 @@ -38,27 +41,29 @@ def test_quadrature_operator_exp(): nmode = 4 cutoff = 4 sq = np.random.rand(nmode) - bs_angles = np.random.rand(nmode-1, 2) * 2 * np.pi + bs_angles = np.random.rand(nmode - 1, 2) * 2 * np.pi phi = np.random.rand(nmode) * 2 * np.pi dev = qml.device('strawberryfields.fock', wires=nmode, cutoff_dim=cutoff) + @qml.qnode(dev) def circuit(): for i in range(nmode): qml.Squeezing(sq[i], 0.0, wires=i) - if i < nmode-1: - qml.Beamsplitter(theta=bs_angles[i][0], phi=bs_angles[i][1], wires=[i, i+1]) + if i < nmode - 1: + qml.Beamsplitter(theta=bs_angles[i][0], phi=bs_angles[i][1], wires=[i, i + 1]) exp = [] for i in range(nmode): - exp.append(qml.expval(qml.QuadOperator(phi=phi[i], wires=i))) # Quadrature X, P + exp.append(qml.expval(qml.QuadOperator(phi=phi[i], wires=i))) # Quadrature X, P return exp + exp_qml = circuit() cir = dq.QumodeCircuit(nmode=nmode, backend='fock', basis=False, cutoff=cutoff, init_state='vac', den_mat=True) for i in range(nmode): cir.s(i, sq[i], encode=True) - if i < nmode-1: - cir.bs(wires=[i, i+1], inputs=bs_angles[i]) + if i < nmode - 1: + cir.bs(wires=[i, i + 1], inputs=bs_angles[i]) cir.to(torch.double) cir() exp_dq = cir.quadrature_mean(wires=list(range(nmode)), phi=list(phi)) diff --git a/tests/test_with_perceval.py b/tests/test_with_perceval.py index f1da41dc..dc9eced7 100644 --- a/tests/test_with_perceval.py +++ b/tests/test_with_perceval.py @@ -1,11 +1,11 @@ -import deepquantum as dq import numpy as np import perceval as pcvl import perceval.components as comp -import pytest import torch from perceval.components import BS +import deepquantum as dq + def test_random_circuit(): """Compare with Perceval.""" @@ -21,68 +21,66 @@ def test_random_circuit(): ini_state_pre[ini_state_pre.argmax()] += nmode - ini_state_pre.sum() ini_state = [int(i) for i in ini_state_pre] assert np.sum(ini_state_pre) == nmode - test_gate = pcvl.Circuit(nmode, name='test1') - dq_gate = dq.QumodeCircuit(nmode=nmode, init_state=ini_state, name='test', - cutoff=sum(ini_state)+1, basis=True) + cir_pcvl = pcvl.Circuit(nmode) + cir_dq = dq.QumodeCircuit(nmode=nmode, init_state=ini_state, cutoff=sum(ini_state) + 1, basis=True) encode = True - for _ in range(ndevice): # take the random circuit + for _ in range(ndevice): # take the random circuit j = np.random.uniform(-1, 5) - if 4 < j < 5: # add H + if 4 < j < 5: # add H k = int(np.random.choice(np.arange(nmode - 1))) angle_1 = np.random.uniform(0, 2 * np.pi) - test_gate.add([k, k+1], BS.H(angle_1)) - dq_gate.bs_h([k, k+1], angle_1) - if 2 < j < 3: # add Rx + cir_pcvl.add([k, k + 1], BS.H(angle_1)) + cir_dq.bs_h([k, k + 1], angle_1) + if 2 < j < 3: # add Rx k = int(np.random.choice(np.arange(nmode - 1))) angle_1 = np.random.uniform(0, 2 * np.pi) - test_gate.add([k, k+1], BS.Rx(angle_1)) - dq_gate.bs_rx([k, k+1], angle_1) - if 1 < j < 2: # add Ry + cir_pcvl.add([k, k + 1], BS.Rx(angle_1)) + cir_dq.bs_rx([k, k + 1], angle_1) + if 1 < j < 2: # add Ry k = int(np.random.choice(np.arange(nmode - 1))) angle_1 = np.random.uniform(0, 2 * np.pi) - test_gate.add([k, k+1], BS.Ry(angle_1)) - dq_gate.bs_ry([k, k+1], angle_1) + cir_pcvl.add([k, k + 1], BS.Ry(angle_1)) + cir_dq.bs_ry([k, k + 1], angle_1) if 0 < j < 1: temp_1 = int(np.random.choice(np.arange(nmode))) angle_1 = np.random.uniform(0, 2 * np.pi) - test_gate.add((temp_1), comp.PS(angle_1)) - dq_gate.ps([temp_1], angle_1, encode=encode) + cir_pcvl.add((temp_1), comp.PS(angle_1)) + cir_dq.ps([temp_1], angle_1, encode=encode) else: k = int(np.random.choice(np.arange(nmode - 1))) angle_2 = np.random.uniform(0, 2 * np.pi) - test_gate.add((k, k+1), BS.Rx(angle_2)) - dq_gate.bs_theta([k, k+1], angle_2 / 2, encode=encode) + cir_pcvl.add((k, k + 1), BS.Rx(angle_2)) + cir_dq.bs_theta([k, k + 1], angle_2 / 2, encode=encode) backend = pcvl.BackendFactory().get_backend('Naive') - backend.set_circuit(test_gate) + backend.set_circuit(cir_pcvl) input_state = pcvl.BasicState(ini_state) backend.set_input_state(input_state) - re1 = backend.evolve() - re2 = dq_gate(is_prob=False) + re_pcvl = backend.evolve() + re_dq = cir_dq(is_prob=False) # calculating the difference for two simu approach max_error = -1.0 - for key in re1.keys(): - key2 = list(key) - key3 = dq.FockState(key2) - tmp_error = abs(re2[key3] - re1[key]) - # tmp_error = abs(re2[tuple(key2)] - re1[(key)]) + for basis_state in re_pcvl: + key_pcvl = basis_state[0] + key_dq = dq.FockState(list(key_pcvl)) + tmp_error = abs(re_dq[key_dq] - re_pcvl[key_pcvl]) if tmp_error > max_error: max_error = tmp_error assert max_error < 1e-4 -def test_loss_fock_basis_True(): +def test_loss_fock_basis(): n = 3 angles = np.random.rand(6) * np.pi transmittance = np.random.rand(6) - cir = pcvl.Processor("SLOS",n) + cir = pcvl.Processor('SLOS', n) cir.add(0, pcvl.LC(loss=1 - transmittance[0])) cir.add(0, pcvl.PS(phi=angles[0])) cir.add(1, pcvl.PS(phi=angles[1])) - cir.add((0,1), pcvl.BS(theta=angles[2])) + cir.add((0, 1), pcvl.BS(theta=angles[2])) cir.add(0, pcvl.LC(loss=1 - transmittance[1])) cir.add(2, pcvl.LC(loss=1 - transmittance[2])) - cir.add((1,2), pcvl.BS(theta=angles[3])) + cir.add((1, 2), pcvl.BS(theta=angles[3])) cir.add(0, pcvl.LC(loss=1 - transmittance[3])) cir.add(1, pcvl.LC(loss=1 - transmittance[4])) cir.add(2, pcvl.LC(loss=1 - transmittance[5])) @@ -91,25 +89,25 @@ def test_loss_fock_basis_True(): cir.min_detected_photons_filter(0) imperfect_sampler = pcvl.algorithm.Sampler(cir) - output = imperfect_sampler.probs()["results"] + output = imperfect_sampler.probs()['results'] nmode = n - cir = dq.QumodeCircuit(nmode=nmode, init_state=[1,1,1], backend='fock', basis=True) + cir = dq.QumodeCircuit(nmode=nmode, init_state=[1, 1, 1], backend='fock', basis=True) cir.loss_t(0, transmittance[0]) cir.ps(0, angles[0]) cir.ps(1, angles[1]) - cir.bs_rx([0,1], [angles[2]]) + cir.bs_rx([0, 1], [angles[2]]) cir.loss_t(0, transmittance[1]) cir.loss_t(2, transmittance[2]) - cir.bs_rx([1,2], [angles[3]]) + cir.bs_rx([1, 2], [angles[3]]) cir.loss_t(0, transmittance[3]) cir.loss_t(1, transmittance[4]) cir.loss_t(2, transmittance[5]) cir.to(torch.float64) state = cir(is_prob=True) - for key in state.keys(): + for key in state: dq_prob = state[key] fock_lst = key.state.tolist() pcvl_prob = output[pcvl.BasicState(fock_lst)] err = abs(dq_prob - pcvl_prob) - assert err < 1e-4,f'key={key},dq_prob={dq_prob},pcvl_prob={pcvl_prob}' + assert err < 1e-4, f'key={key}, dq_prob={dq_prob}, pcvl_prob={pcvl_prob}' diff --git a/tests/test_with_qutip.py b/tests/test_with_qutip.py index 98730292..c15efef8 100644 --- a/tests/test_with_qutip.py +++ b/tests/test_with_qutip.py @@ -1,9 +1,9 @@ -import deepquantum as dq import numpy as np -import pytest import qutip as qp import torch +import deepquantum as dq + def test_with_qutip_fock_wigner(): r, d = torch.rand(2) @@ -24,7 +24,7 @@ def test_with_qutip_fock_wigner(): pvec = np.linspace(-prange, prange, npoints) wigner_qp = qp.wigner(qp.Qobj(psi), xvec, pvec, g=1) wigner_dq = fock_state.wigner(0, xrange, prange, npoints, plot=False) - err = torch.sum(abs(wigner_dq - torch.tensor(wigner_qp.mT)), dim=[1,2]) + err = torch.sum(abs(wigner_dq - torch.tensor(wigner_qp.mT)), dim=[1, 2]) assert err < 1e-6 @@ -53,5 +53,5 @@ def test_with_qutip_gaussian_wigner(): pvec = np.linspace(-prange, prange, npoints) wigner_qp = qp.wigner(qp.Qobj(psi), xvec, pvec, g=1) wigner_dq = gaussian_state.wigner(0, xrange, prange, npoints, plot=False, normalize=True) - err = torch.sum(abs(wigner_dq - torch.tensor(wigner_qp.mT)), dim=[1,2]) + err = torch.sum(abs(wigner_dq - torch.tensor(wigner_qp.mT)), dim=[1, 2]) assert err < 1e-2 diff --git a/tests/test_with_xanadu.py b/tests/test_with_xanadu.py index 21808053..c09fa929 100644 --- a/tests/test_with_xanadu.py +++ b/tests/test_with_xanadu.py @@ -1,11 +1,11 @@ -import deepquantum as dq import numpy as np -import pytest import strawberryfields as sf import thewalrus import torch -from deepquantum.photonic import quadrature_to_ladder, hafnian, torontonian -from strawberryfields.ops import Sgate, BSgate, Rgate, MeasureHomodyne, Dgate, Fock +from strawberryfields.ops import BSgate, Dgate, Fock, MeasureHomodyne, Rgate, Sgate + +import deepquantum as dq +from deepquantum.photonic import hafnian, quadrature_to_ladder, torontonian def test_hafnian(): @@ -35,12 +35,11 @@ def test_torontonian(): cir.s(wires=i) cir.d(wires=i) for i in range(nmode - 1): - cir.bs(wires=[i,i+1]) + cir.bs(wires=[i, i + 1]) cir.to(torch.double) - covs, means = cir() + covs, _ = cir() cov_ladder = quadrature_to_ladder(covs[0]) - mean_ladder = quadrature_to_ladder(means[0]) q = cov_ladder + torch.eye(2 * nmode) / 2 o_mat = torch.eye(2 * nmode) - torch.inverse(q) tor1 = torontonian(o_mat) @@ -55,7 +54,7 @@ def test_torontonian_loop(): cir.s(wires=i) cir.d(wires=i) for i in range(nmode - 1): - cir.bs(wires=[i,i+1]) + cir.bs(wires=[i, i + 1]) cir.to(torch.double) covs, means = cir() @@ -79,7 +78,7 @@ def test_gaussian_prob_random_circuit(): cir.s(1, para_r[1], para_theta[1]) cir.d(0, para_r[2], para_theta[2]) cir.d(1, para_r[3], para_theta[3]) - cir.bs([0,1], [para_theta[4], para_theta[5]]) + cir.bs([0, 1], [para_theta[4], para_theta[5]]) cir.to(torch.double) cov, mean = cir(is_prob=False) @@ -87,7 +86,7 @@ def test_gaussian_prob_random_circuit(): test_prob = thewalrus.quantum.probabilities(mu=mean[0].squeeze().numpy(), cov=cov[0].numpy(), cutoff=5) error = [] - for i in state.keys(): + for i in state: idx = i.state.tolist() error.append(abs(test_prob[tuple(idx)] - state[i].item())) assert sum(error) < 1e-10 @@ -118,15 +117,15 @@ def test_measure_homodyne(): cir.s(0, r=r1) cir.d(1, r=r2) cir.s(1, r=r3) - cir.bs([0,1], inputs=bs1) - cir.bs([1,2], inputs=bs2) + cir.bs([0, 1], inputs=bs1) + cir.bs([1, 2], inputs=bs2) cir.homodyne(wires=0, phi=phi[0]) cir.homodyne(wires=1, phi=phi[1]) cir.to(torch.double) cir() - sample = cir.measure_homodyne() + cir.measure_homodyne() state = cir.state_measured - err = abs(state[0] - result.state.cov()).max() # compare the covariance matrix after the measurement + err = abs(state[0] - result.state.cov()).max() # compare the covariance matrix after the measurement assert err < 1e-6 @@ -146,14 +145,14 @@ def test_non_adjacent_bs_fock(): result = eng.run(prog) nmode = n - cir = dq.QumodeCircuit(nmode=nmode, init_state=[1,1,1], cutoff=4, backend='fock', basis=True) + cir = dq.QumodeCircuit(nmode=nmode, init_state=[1, 1, 1], cutoff=4, backend='fock', basis=True) cir.ps(0, angles[0]) cir.ps(1, angles[1]) - cir.bs([0,2], [angles[2], angles[3]]) - cir.bs([1,2], [angles[4], angles[5]]) + cir.bs([0, 2], [angles[2], angles[3]]) + cir.bs([1, 2], [angles[4], angles[5]]) state = cir(is_prob=True) err = 0 - for key in state.keys(): + for key in state: dq_prob = state[key] fock_st = key.state.tolist() sf_prob = result.state.fock_prob(fock_st) @@ -192,8 +191,8 @@ def test_non_adjacent_bs_gaussian(): cir.d(2, r=angles[6]) cir.d(3, r=angles[7]) - cir.bs([0,2], [angles[8], angles[9]]) - cir.bs([1,3], [angles[10], angles[11]]) + cir.bs([0, 2], [angles[8], angles[9]]) + cir.bs([1, 3], [angles[10], angles[11]]) state = cir() err = abs(state[0].squeeze() - cov_sf).sum() + abs(state[1].squeeze() - mean_sf).sum() diff --git a/tests/test_with_xanadu_gate.py b/tests/test_with_xanadu_gate.py index ebb2ce02..8eb14db8 100644 --- a/tests/test_with_xanadu_gate.py +++ b/tests/test_with_xanadu_gate.py @@ -1,8 +1,9 @@ -import deepquantum as dq import numpy as np import strawberryfields as sf import torch -from strawberryfields.ops import Pgate, CXgate, CZgate, Vgate, Kgate, CKgate +from strawberryfields.ops import CKgate, CXgate, CZgate, Kgate, Pgate, Vgate + +import deepquantum as dq def test_quadratic_phase_gate(): @@ -36,7 +37,7 @@ def test_cx_gate(): result = eng.run(prog) cir = dq.QumodeCircuit(nmode=nmode, init_state='vac', cutoff=cutoff, backend='fock', basis=False) - cir.cx([0,1], params[0]) + cir.cx([0, 1], params[0]) cir.to(torch.double) state = cir() @@ -55,7 +56,7 @@ def test_cz_gate(): result = eng.run(prog) cir = dq.QumodeCircuit(nmode=nmode, init_state='vac', cutoff=cutoff, backend='fock', basis=False) - cir.cz([0,1], params[0]) + cir.cz([0, 1], params[0]) cir.to(torch.double) state = cir() @@ -112,7 +113,7 @@ def test_cross_kerr_gate(): result = eng.run(prog) cir = dq.QumodeCircuit(nmode=nmode, init_state='vac', cutoff=cutoff, backend='fock', basis=False) - cir.ck([0,1], params[0]) + cir.ck([0, 1], params[0]) cir.to(torch.double) state = cir() diff --git a/tests/test_with_xanadu_loss.py b/tests/test_with_xanadu_loss.py index 481c3efa..9ccabd75 100644 --- a/tests/test_with_xanadu_loss.py +++ b/tests/test_with_xanadu_loss.py @@ -1,13 +1,14 @@ import itertools -import deepquantum as dq import numpy as np import strawberryfields as sf import torch -from strawberryfields.ops import Fock, Rgate, BSgate, LossChannel +from strawberryfields.ops import BSgate, Fock, LossChannel, Rgate + +import deepquantum as dq -def test_loss_fock_basis_True(): +def test_loss_fock_basis(): n = 3 angles = np.random.rand(6) * np.pi transmittance = np.random.rand(6) @@ -30,21 +31,21 @@ def test_loss_fock_basis_True(): result = eng.run(prog) nmode = n - cir = dq.QumodeCircuit(nmode=nmode, init_state=[1,1,1], cutoff=4, backend='fock', basis=True) + cir = dq.QumodeCircuit(nmode=nmode, init_state=[1, 1, 1], cutoff=4, backend='fock', basis=True) cir.loss_t(0, transmittance[0]) cir.ps(0, angles[0]) cir.ps(1, angles[1]) - cir.bs([0,2], [angles[2], angles[3]]) + cir.bs([0, 2], [angles[2], angles[3]]) cir.loss_t(0, transmittance[1]) cir.loss_t(2, transmittance[2]) - cir.bs([1,2], [angles[4], angles[5]]) + cir.bs([1, 2], [angles[4], angles[5]]) cir.loss_t(0, transmittance[3]) cir.loss_t(1, transmittance[4]) cir.loss_t(2, transmittance[5]) # cir.to(torch.float64) state = cir(is_prob=True) err = 0 - for key in state.keys(): + for key in state: dq_prob = state[key] fock_st = key.state.tolist() sf_prob = result.state.fock_prob(fock_st) @@ -52,7 +53,7 @@ def test_loss_fock_basis_True(): assert err < 1e-6 -def test_loss_fock_basis_False(): +def test_loss_fock_tensor(): n = 3 angles = np.random.rand(6) * np.pi transmittance = np.random.rand(6) @@ -75,14 +76,16 @@ def test_loss_fock_basis_False(): result = eng.run(prog) nmode = n - cir = dq.QumodeCircuit(nmode=nmode, init_state=[(1, [1,1,1])], cutoff=4, backend='fock', basis=False, den_mat=True) + cir = dq.QumodeCircuit( + nmode=nmode, init_state=[(1, [1, 1, 1])], cutoff=4, backend='fock', basis=False, den_mat=True + ) cir.loss_t(0, transmittance[0]) cir.ps(0, angles[0]) cir.ps(1, angles[1]) - cir.bs([0,2], [angles[2], angles[3]]) + cir.bs([0, 2], [angles[2], angles[3]]) cir.loss_t(0, transmittance[1]) cir.loss_t(2, transmittance[2]) - cir.bs([1,2], [angles[4], angles[5]]) + cir.bs([1, 2], [angles[4], angles[5]]) cir.loss_t(0, transmittance[3]) cir.loss_t(1, transmittance[4]) cir.loss_t(2, transmittance[5]) @@ -122,10 +125,10 @@ def test_loss_gaussian(): cir.loss_t(0, transmittance[0]) cir.ps(0, angles[0]) cir.ps(1, angles[1]) - cir.bs([0,1], [angles[2], angles[3]]) # only support adjacent wires for gaussian loss channel + cir.bs([0, 1], [angles[2], angles[3]]) # only support adjacent wires for gaussian loss channel cir.loss_t(0, transmittance[1]) cir.loss_t(2, transmittance[2]) - cir.bs([1,2], [angles[4], angles[5]]) + cir.bs([1, 2], [angles[4], angles[5]]) cir.loss_t(0, transmittance[3]) cir.loss_t(1, transmittance[4]) cir.loss_t(2, transmittance[5]) diff --git a/tutorials/basics.ipynb b/tutorials/basics.ipynb index 00f702a3..3dce410c 100644 --- a/tutorials/basics.ipynb +++ b/tutorials/basics.ipynb @@ -23,9 +23,9 @@ "outputs": [], "source": [ "import deepquantum as dq\n", + "import numpy as np\n", "import torch\n", - "import torch.nn as nn\n", - "import numpy as np" + "import torch.nn as nn" ] }, { @@ -65,7 +65,7 @@ } ], "source": [ - "qstate = dq.QubitState(nqubit=1, state=[0,1])\n", + "qstate = dq.QubitState(nqubit=1, state=[0, 1])\n", "state1 = qstate.state\n", "print(state1)" ] @@ -80,7 +80,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -97,7 +97,7 @@ ], "source": [ "x = dq.PauliX()\n", - "x.matrix" + "print(x.matrix)" ] }, { @@ -154,7 +154,7 @@ } ], "source": [ - "rx = dq.Rx(torch.pi/2)\n", + "rx = dq.Rx(torch.pi / 2)\n", "print(rx.matrix @ state1)\n", "print(rx(state1))" ] @@ -215,8 +215,8 @@ } ], "source": [ - "rx = dq.Rx(torch.pi/2, requires_grad=True)\n", - "theta = nn.Parameter(torch.tensor(torch.pi/2))\n", + "rx = dq.Rx(torch.pi / 2, requires_grad=True)\n", + "theta = nn.Parameter(torch.tensor(torch.pi / 2))\n", "print(rx.matrix)\n", "print(rx.update_matrix())\n", "print(rx.get_matrix(theta))" @@ -282,9 +282,9 @@ } ], "source": [ - "state2 = dq.QubitState(nqubit=2, state=[0,0,0,1]).state\n", + "state2 = dq.QubitState(nqubit=2, state=[0, 0, 0, 1]).state\n", "print(state2)\n", - "rx = dq.Rx(torch.pi/2, nqubit=2, wires=[1], requires_grad=True)\n", + "rx = dq.Rx(torch.pi / 2, nqubit=2, wires=[1], requires_grad=True)\n", "print(rx.get_unitary() @ state2)\n", "print(rx(state2))" ] @@ -303,14 +303,14 @@ "metadata": {}, "outputs": [], "source": [ - "cx1 = dq.CNOT(wires=[0,1])\n", + "cx1 = dq.CNOT(wires=[0, 1])\n", "cx2 = dq.PauliX(nqubit=2, wires=[1], controls=[0])\n", "\n", - "ccx1 = dq.Toffoli(wires=[0,1,2])\n", - "ccx2 = dq.PauliX(nqubit=3, wires=[2], controls=[0,1])\n", + "ccx1 = dq.Toffoli(wires=[0, 1, 2])\n", + "ccx2 = dq.PauliX(nqubit=3, wires=[2], controls=[0, 1])\n", "\n", - "cswap1 = dq.Fredkin(wires=[0,1,2])\n", - "cswap2 = dq.Swap(nqubit=3, wires=[1,2], controls=[0])" + "cswap1 = dq.Fredkin(wires=[0, 1, 2])\n", + "cswap2 = dq.Swap(nqubit=3, wires=[1, 2], controls=[0])" ] }, { @@ -447,11 +447,8 @@ } ], "source": [ - "unitary = [[0, 0, 0, 1],\n", - " [0, 0, 1, 0],\n", - " [1, 0, 0, 0],\n", - " [0, 1, 0, 0]]\n", - "u = dq.UAnyGate(unitary=unitary, nqubit=3, minmax=[1,2])\n", + "unitary = [[0, 0, 0, 1], [0, 0, 1, 0], [1, 0, 0, 0], [0, 1, 0, 0]]\n", + "u = dq.UAnyGate(unitary=unitary, nqubit=3, minmax=[1, 2])\n", "print(u.get_unitary())" ] }, @@ -488,8 +485,8 @@ "source": [ "state3 = dq.QubitState(3).state\n", "h = dq.Hadamard(nqubit=3, wires=0)\n", - "cx1 = dq.CNOT(nqubit=3, wires=[0,1])\n", - "cx2 = dq.CNOT(nqubit=3, wires=[0,2])\n", + "cx1 = dq.CNOT(nqubit=3, wires=[0, 1])\n", + "cx2 = dq.CNOT(nqubit=3, wires=[0, 2])\n", "print(cx2(cx1(h(state3))))" ] }, @@ -524,7 +521,299 @@ }, { "data": { - "image/svg+xml": "\n\n\n \n \n \n \n 2023-04-17T18:04:24.672494\n image/svg+xml\n \n \n Matplotlib v3.7.1, https://matplotlib.org/\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2023-04-17T18:04:24.672494\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.7.1, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], "text/plain": [ "
" ] @@ -565,7 +854,564 @@ }, { "data": { - "image/svg+xml": "\n\n\n \n \n \n \n 2023-04-17T18:04:25.018621\n image/svg+xml\n \n \n Matplotlib v3.7.1, https://matplotlib.org/\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2023-04-17T18:04:25.018621\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.7.1, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], "text/plain": [ "
" ] @@ -603,7 +1449,518 @@ }, { "data": { - "image/svg+xml": "\n\n\n \n \n \n \n 2023-04-17T18:04:25.249261\n image/svg+xml\n \n \n Matplotlib v3.7.1, https://matplotlib.org/\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2023-04-17T18:04:25.249261\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.7.1, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], "text/plain": [ "
" ] @@ -614,7 +1971,7 @@ } ], "source": [ - "print(cir.measure(shots=100, wires=[1,2], with_prob=True))\n", + "print(cir.measure(shots=100, wires=[1, 2], with_prob=True))\n", "cir.draw()" ] }, @@ -643,7 +2000,416 @@ }, { "data": { - "image/svg+xml": "\n\n\n \n \n \n \n 2023-04-17T18:04:25.489161\n image/svg+xml\n \n \n Matplotlib v3.7.1, https://matplotlib.org/\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2023-04-17T18:04:25.489161\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.7.1, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], "text/plain": [ "
" ] @@ -655,11 +2421,11 @@ ], "source": [ "cir = dq.QubitCircuit(2)\n", - "cir.ry(0, torch.pi/2)\n", - "cir.rxx([0,1], torch.pi/2)\n", - "cir.rx(1, -torch.pi/2)\n", - "cir.rx(0, -torch.pi/2)\n", - "cir.ry(0, -torch.pi/2)\n", + "cir.ry(0, torch.pi / 2)\n", + "cir.rxx([0, 1], torch.pi / 2)\n", + "cir.rx(1, -torch.pi / 2)\n", + "cir.rx(0, -torch.pi / 2)\n", + "cir.ry(0, -torch.pi / 2)\n", "print(np.exp(-1j * np.pi / 4) * cir.get_unitary())\n", "cir.draw()" ] @@ -679,7 +2445,598 @@ "outputs": [ { "data": { - "image/svg+xml": "\n\n\n \n \n \n \n 2023-04-17T18:04:25.934906\n image/svg+xml\n \n \n Matplotlib v3.7.1, https://matplotlib.org/\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2023-04-17T18:04:25.934906\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.7.1, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], "text/plain": [ "
" ] @@ -697,11 +3054,11 @@ "\n", "cir.toffoli(0, 1, 2)\n", "cir.ccx(0, 1, 2)\n", - "cir.x(2, [0,1])\n", + "cir.x(2, [0, 1])\n", "\n", "cir.fredkin(0, 1, 2)\n", "cir.cswap(0, 1, 2)\n", - "cir.swap([1,2], 0)\n", + "cir.swap([1, 2], 0)\n", "\n", "cir.draw()" ] @@ -727,7 +3084,773 @@ "outputs": [ { "data": { - "image/svg+xml": "\n\n\n \n \n \n \n 2023-04-17T18:04:26.185452\n image/svg+xml\n \n \n Matplotlib v3.7.1, https://matplotlib.org/\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2023-04-17T18:04:26.185452\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.7.1, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], "text/plain": [ "
" ] @@ -740,12 +3863,12 @@ "source": [ "cir = dq.QubitCircuit(4)\n", "cir.rx(0)\n", - "cir.rxx([1,2])\n", + "cir.rxx([1, 2])\n", "cir.u3(3)\n", "cir.p(0)\n", "cir.cu(3, 0)\n", "cir.cp(1, 2)\n", - "cir.draw()\n" + "cir.draw()" ] }, { @@ -763,7 +3886,922 @@ "outputs": [ { "data": { - "image/svg+xml": "\n\n\n \n \n \n \n 2023-04-17T18:04:26.459088\n image/svg+xml\n \n \n Matplotlib v3.7.1, https://matplotlib.org/\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2023-04-17T18:04:26.459088\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.7.1, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], "text/plain": [ "
" ] @@ -776,8 +4814,8 @@ "source": [ "cir = dq.QubitCircuit(4)\n", "cir.hlayer()\n", - "cir.rxlayer([0,2])\n", - "cir.rylayer([1,3])\n", + "cir.rxlayer([0, 2])\n", + "cir.rylayer([1, 3])\n", "cir.u3layer()\n", "cir.cxlayer()\n", "cir.draw()" @@ -798,7 +4836,701 @@ "outputs": [ { "data": { - "image/svg+xml": "\n\n\n \n \n \n \n 2023-04-17T18:04:26.869870\n image/svg+xml\n \n \n Matplotlib v3.7.1, https://matplotlib.org/\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2023-04-17T18:04:26.869870\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.7.1, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], "text/plain": [ "
" ] @@ -812,7 +5544,7 @@ "cir = dq.QubitCircuit(5)\n", "cir.cnot_ring()\n", "cir.barrier()\n", - "cir.cnot_ring(minmax=[1,4], step=3, reverse=True)\n", + "cir.cnot_ring(minmax=[1, 4], step=3, reverse=True)\n", "cir.draw()" ] }, @@ -834,7 +5566,7 @@ "source": [ "nqubit = 4\n", "batch = 2\n", - "data = torch.randn(batch, 2 ** nqubit)" + "data = torch.randn(batch, 2**nqubit)" ] }, { @@ -1029,7 +5761,9 @@ { "cell_type": "code", "execution_count": 29, - "metadata": {}, + "metadata": { + "lines_to_next_cell": 2 + }, "outputs": [ { "name": "stdout", @@ -1088,7 +5822,9 @@ { "attachments": {}, "cell_type": "markdown", - "metadata": {}, + "metadata": { + "lines_to_next_cell": 2 + }, "source": [ "### 混合量子-经典模型\n", "\n", @@ -1164,13 +5900,14 @@ " for i in range(nqubit):\n", " cir.observable(wires=i)\n", " return cir\n", - " \n", + "\n", " def forward(self, x):\n", " x = torch.arctan(self.fc(x))\n", " self.cir(x)\n", " exp = self.cir.expectation()\n", " return exp\n", "\n", + "\n", "nqubit = 4\n", "batch = 2\n", "nfeat = 8\n", @@ -1359,8 +6096,7 @@ "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.16" - }, - "orig_nbformat": 4 + } }, "nbformat": 4, "nbformat_minor": 2 diff --git a/tutorials/basics.py b/tutorials/basics.py new file mode 100644 index 00000000..53773843 --- /dev/null +++ b/tutorials/basics.py @@ -0,0 +1,425 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.19.1 +# kernelspec: +# display_name: dq +# language: python +# name: python3 +# --- + +# %% [markdown] +# # Tutorial + +# %% [markdown] +# import DeepQuantum以及相关的库。 + +# %% +import deepquantum as dq +import numpy as np +import torch +import torch.nn as nn + +# %% [markdown] +# ## 基本的量子门 +# +# 所有的Gate都是Operation的子类,都有布尔变量`den_mat`和`tsr_mode`。 +# +# 参数`den_mat`表示这个操作是处理密度矩阵,还是态矢。 +# +# 参数`tsr_mode`表示这个操作的输入输出是张量态(形状为(batch, 2, ..., 2)的tensor),还是态矢或密度矩阵。 + +# %% [markdown] +# 我们利用`QubitState`准备一个单比特量子态(默认为'zeros'表示全0态,此外,有'equal'表示等权叠加态,'ghz'表示GHZ态),其数据是torch的tensor,存在属性`state`中。 + +# %% +qstate = dq.QubitState(nqubit=1, state=[0, 1]) +state1 = qstate.state +print(state1) + +# %% [markdown] +# 实例化一个单比特量子门,可以从`matrix`属性获得量子门的基础表示(相对于指定任意控制位的量子门而言)。 + +# %% +x = dq.PauliX() +print(x.matrix) + +# %% [markdown] +# 用量子门对量子态进行操作,既可以通过手动的矩阵乘法,也可以通过把量子门直接作用在量子态上。 + +# %% +print(x.matrix @ state1) +print(x(state1)) + +# %% [markdown] +# 我们再看带参数的量子门的例子。 + +# %% +rx = dq.Rx(torch.pi / 2) +print(rx.matrix @ state1) +print(rx(state1)) + +# %% [markdown] +# 可以利用`get_matrix()`获取带参数量子门的计算过程。 + +# %% +rx.get_matrix(torch.pi) @ state1 + +# %% [markdown] +# 注意`matrix`只是纯粹的矩阵表示,因此当需要记录计算图来求参数的梯度时,必须使用`update_matrix()`或`get_matrix()`,区别在于前者使用该量子门本身的参数,而后者需要输入参数,适用于外部的指定参数。 + +# %% +rx = dq.Rx(torch.pi / 2, requires_grad=True) +theta = nn.Parameter(torch.tensor(torch.pi / 2)) +print(rx.matrix) +print(rx.update_matrix()) +print(rx.get_matrix(theta)) + +# %% +print(rx.matrix @ state1) +print(rx.update_matrix() @ state1) +print(rx.get_matrix(theta) @ state1) +print(rx(state1)) + +# %% [markdown] +# 处理多比特量子态时,量子门的`nqubit`需要与量子态的一致,用`wires`来指定量子门作用于哪些线路上。同样,既可以通过`get_unitary()`得到完整的酉矩阵后进行手动的矩阵乘法,也可以通过把量子门直接作用在量子态上。需要注意,前者的计算效率远低于后者。 + +# %% +state2 = dq.QubitState(nqubit=2, state=[0, 0, 0, 1]).state +print(state2) +rx = dq.Rx(torch.pi / 2, nqubit=2, wires=[1], requires_grad=True) +print(rx.get_unitary() @ state2) +print(rx(state2)) + +# %% [markdown] +# DeepQuantum中几乎所有的量子门都支持额外指定任意多的控制位(例外有`CNOT`、`Toffoli`、`Fredkin`等)。因此,同一个量子门可能会有不止一种调用方法。比如cnot和Toffoli可以用`PauliX`实现,Fredkin可以用`Swap`实现。 + +# %% +cx1 = dq.CNOT(wires=[0, 1]) +cx2 = dq.PauliX(nqubit=2, wires=[1], controls=[0]) + +ccx1 = dq.Toffoli(wires=[0, 1, 2]) +ccx2 = dq.PauliX(nqubit=3, wires=[2], controls=[0, 1]) + +cswap1 = dq.Fredkin(wires=[0, 1, 2]) +cswap2 = dq.Swap(nqubit=3, wires=[1, 2], controls=[0]) + +# %% [markdown] +# 注意,它们的`matrix`是不同的。并且,前者的计算效率略高于后者。 + +# %% +print(cx1.matrix) +print(cx2.matrix) + +print(ccx1.matrix) +print(ccx2.matrix) + +print(cswap1.matrix) +print(cswap2.matrix) + +# %% [markdown] +# 后者可以用`get_unitary()`来检查。 + +# %% +print(cx2.get_unitary()) +print(ccx2.get_unitary()) +print(cswap2.get_unitary()) + +# %% [markdown] +# 用户还可以通过`UAnyGate`来封装任意酉矩阵。比如,把$4\times4$的酉矩阵作用在三比特量子态的后两个量子比特上,其中用`minmax`指定作用范围,需要和酉矩阵的大小匹配。 + +# %% +unitary = [[0, 0, 0, 1], [0, 0, 1, 0], [1, 0, 0, 0], [0, 1, 0, 0]] +u = dq.UAnyGate(unitary=unitary, nqubit=3, minmax=[1, 2]) +print(u.get_unitary()) + +# %% [markdown] +# ## 制备GHZ态 +# +# 我们来实现一下制备GHZ态($|\psi\rangle = \left(|000\rangle+|111\rangle\right)/\sqrt{2}$)这个经典的例子。 + +# %% +state3 = dq.QubitState(3).state +h = dq.Hadamard(nqubit=3, wires=0) +cx1 = dq.CNOT(nqubit=3, wires=[0, 1]) +cx2 = dq.CNOT(nqubit=3, wires=[0, 2]) +print(cx2(cx1(h(state3)))) + +# %% [markdown] +# ## 量子线路:QubitCircuit +# +# 量子线路是DeepQuantum的核心对象。通过`QubitCircuit`进行初始化,然后可以在实例对象上添加各种量子门,最后进行演化和测量。我们把上面这个例子用量子线路来实现。并且可以对线路进行可视化。 + +# %% +cir = dq.QubitCircuit(3) +cir.h(0) +cir.cnot(0, 1) +cir.cnot(0, 2) +print(cir()) +cir.draw() + +# %% [markdown] +# 我们可以对线路进行测量,返回的结果是字典或者字典的列表,字典的key是比特串,value是对应测量到的次数,shots默认为1024。 + +# %% +cir.barrier() +print(cir.measure()) +cir.draw() + +# %% [markdown] +# 也可以设定采样次数、进行部分测量以及显示理想的概率。 + +# %% +print(cir.measure(shots=100, wires=[1, 2], with_prob=True)) +cir.draw() + +# %% [markdown] +# 再来看一个对CNOT门实现分解的例子:$\text{CNOT}=e^{-i{\frac {\pi }{4}}}R_{y_{1}}(-\pi /2)R_{x_{1}}(-\pi /2)R_{x_{2}}(-\pi /2)R_{xx}(\pi /2)R_{y_{1}}(\pi /2)$ + +# %% +cir = dq.QubitCircuit(2) +cir.ry(0, torch.pi / 2) +cir.rxx([0, 1], torch.pi / 2) +cir.rx(1, -torch.pi / 2) +cir.rx(0, -torch.pi / 2) +cir.ry(0, -torch.pi / 2) +print(np.exp(-1j * np.pi / 4) * cir.get_unitary()) +cir.draw() + +# %% [markdown] +# CNOT、Toffoli、Fredkin也有不同的API去添加。 + +# %% +cir = dq.QubitCircuit(3) +cir.cnot(0, 1) +cir.cx(0, 1) +cir.x(1, 0) + +cir.toffoli(0, 1, 2) +cir.ccx(0, 1, 2) +cir.x(2, [0, 1]) + +cir.fredkin(0, 1, 2) +cir.cswap(0, 1, 2) +cir.swap([1, 2], 0) + +cir.draw() + +# %% [markdown] +# ## 参数化量子线路 +# +# DeepQuantum可以帮助用户很方便地实现参数化量子线路,从而进行量子机器学习。 +# +# `QubitCircuit`的实例中添加的带参数的量子门,如果没有指定输入参数,会自动初始化变分参数。 +# +# 补充说明:如果指定了输入参数,那么输入参数会在量子门中被记录为buffer,从而保留其原来的性质。比如,参数不需要求梯度时,就会保持不变。又比如,参数是上一层神经网络的输出,那么在backward过程中就会记录梯度,但它的更新不是通过`QubitCircuit`而是上一层神经网络本身。 + +# %% +cir = dq.QubitCircuit(4) +cir.rx(0) +cir.rxx([1, 2]) +cir.u3(3) +cir.p(0) +cir.cu(3, 0) +cir.cp(1, 2) +cir.draw() + +# %% [markdown] +# 也可以直接添加一层量子门,并通过`wires`指定放置于哪几条线路。 + +# %% +cir = dq.QubitCircuit(4) +cir.hlayer() +cir.rxlayer([0, 2]) +cir.rylayer([1, 3]) +cir.u3layer() +cir.cxlayer() +cir.draw() + +# %% [markdown] +# `cnot_ring()`可以用`minmax`参数指定线路范围,`step`设定每一对control和target相隔的距离,`reverse`指定是否从大到小。 + +# %% +cir = dq.QubitCircuit(5) +cir.cnot_ring() +cir.barrier() +cir.cnot_ring(minmax=[1, 4], step=3, reverse=True) +cir.draw() + +# %% [markdown] +# ### 振幅编码 +# +# 下面我们展示一个振幅编码的例子,先准备一些数据。 + +# %% +nqubit = 4 +batch = 2 +data = torch.randn(batch, 2**nqubit) + +# %% [markdown] +# 然后构建量子线路,并通过`observable`指定测量线路和测量基底。测量线路和测量基底也可以使用列表形式的组合,如`wires=[0,1,2]`、`basis='xyz'`。 + +# %% +cir = dq.QubitCircuit(nqubit) +cir.rxlayer() +cir.cnot_ring() +cir.observable(wires=0, basis='z') + +# %% [markdown] +# 量子门和观测量分别被记录在`operators`和`observables`中。 + +# %% +print(cir) + +# %% [markdown] +# 振幅编码会自动补0或者舍弃多余的数据,以及进行归一化。 +# +# 通过线路的`forward()`得到末态,`forward()`有`data`和`state`两个参数,分别对应放入量子门的数据,以及线路作用的初态,即分别对应角度编码和振幅编码。 +# +# 测量期望,输出的形状为(batch, 观测量的数量)。 + +# %% +state = cir.amplitude_encoding(data) +state = cir(state=state) +exp = cir.expectation() +print(state.shape) +print(state.norm(dim=-2)) +print(exp) + +# %% [markdown] +# ### 角度编码 +# +# 角度编码只需要对相应的Gate或Layer指定`encode=True`,会自动将数据的特征依次加入编码层,多余的会被舍弃。 + +# %% +nqubit = 4 +batch = 2 +data = torch.sin(torch.tensor(list(range(batch * nqubit)))).reshape(batch, nqubit) +print(data) + +# %% [markdown] +# 这次我们对每条线路都进行一次测量。 + +# %% +cir = dq.QubitCircuit(nqubit) +cir.hlayer() +cir.rxlayer(encode=True) +cir.cnot_ring() +for i in range(nqubit): + cir.observable(wires=i) +state = cir(data) +exp = cir.expectation() +print(state.shape) +print(state.norm(dim=-2)) +print(exp) + +# %% [markdown] +# `QubitCircuit`支持data re-uploading,只需要初始化时指定`reupload=True`,数据就会被循环地放入线路中。 +# +# 补充说明:`encode`只能针对一条一维的数据,线路对batch的支持是通过`torch.vmap`,并且计算完一次前向过程会自动初始化`encoders`,因为量子门无法保存多组参数。 + +# %% +cir = dq.QubitCircuit(nqubit, reupload=True) +cir.rxlayer(encode=True) +cir.cnot_ring() +cir.rxlayer(encode=True) +cir.cnot_ring() +cir.encode(data[0]) +print(cir) + + +# %% [markdown] +# ### 混合量子-经典模型 +# +# DeepQuantum基于PyTorch,能够方便自然地实现量子模型和经典模型的混合计算。 + + +# %% +class Net(nn.Module): + def __init__(self, dim_in, nqubit) -> None: + super().__init__() + self.fc = nn.Linear(dim_in, nqubit) + self.cir = self.circuit(nqubit) + + def circuit(self, nqubit): + cir = dq.QubitCircuit(nqubit) + cir.hlayer() + cir.rxlayer(encode=True) + cir.cnot_ring() + for i in range(nqubit): + cir.observable(wires=i) + return cir + + def forward(self, x): + x = torch.arctan(self.fc(x)) + self.cir(x) + exp = self.cir.expectation() + return exp + + +nqubit = 4 +batch = 2 +nfeat = 8 +x = torch.sin(torch.tensor(list(range(batch * nfeat)))).reshape(batch, nfeat) +net = Net(nfeat, nqubit) +y = net(x) +print(net.state_dict()) +print('y', y) + +# %% [markdown] +# ### 线路拼接以及更灵活地使用数据 + +# %% +nqubit = 2 +batch = 2 +data1 = torch.sin(torch.tensor(list(range(batch * nqubit)))).reshape(batch, nqubit) +data2 = torch.cos(torch.tensor(list(range(batch * nqubit)))).reshape(batch, nqubit) +cir1 = dq.QubitCircuit(nqubit) +cir1.rxlayer(encode=True) +cir2 = dq.QubitCircuit(nqubit) +cir2.rylayer(encode=True) +cir3 = dq.QubitCircuit(nqubit) +cir3.rzlayer() + +# %% [markdown] +# 通过线路加法来共享变分参数。 +# +# 注意,不建议对encoder部分进行复杂的线路加法来共享数据,因为需要保证数据的顺序与encoders完全一致。一旦出现错位,由于共享了encoders,会造成全局的影响。 + +# %% +data = torch.cat([data1, data2], dim=-1) +cir = cir1 + cir3 + cir2 + cir3 +cir.observable(0) +cir.encode(data[0]) +print(cir) +cir(data) +print(cir.expectation()) + +# %% [markdown] +# 上面的结果也可以由多个线路的分段演化得到。 + +# %% +state = cir1(data1) +state = cir3(state=state) +state = cir2(data2, state=state) +state = cir3(state=state) +cir3.reset_observable() +cir3.observable(0) +print(cir3.expectation()) + +# %% [markdown] +# 这种方式当然就可以更灵活地使用数据。 + +# %% +state = cir1(data1) +state = cir2(data2, state=state) +state = cir3(state=state) +state = cir1(data2, state=state) +state = cir2(data1, state=state) +cir2.reset_observable() +cir2.observable(0) +print(cir2.expectation()) diff --git a/tutorials/mbqc_basics.ipynb b/tutorials/mbqc_basics.ipynb index 4bc9a61f..ea354042 100644 --- a/tutorials/mbqc_basics.ipynb +++ b/tutorials/mbqc_basics.ipynb @@ -22,7 +22,8 @@ "import deepquantum as dq\n", "import numpy as np\n", "import torch\n", - "print('version',dq.__version__)" + "\n", + "print('version', dq.__version__)" ] }, { @@ -87,7 +88,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -130,7 +131,7 @@ "source": [ "# Transpile circuit to measurement pattern\n", "pattern = cir.pattern()\n", - "pattern" + "print(pattern)" ] }, { @@ -142,7 +143,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -189,7 +190,7 @@ "source": [ "n_qubits = 2\n", "batch_size = 5\n", - "init_state = torch.rand(batch_size, 2**n_qubits) # 输入QubitCiurcuit后会自动归一化\n", + "init_state = torch.rand(batch_size, 2**n_qubits) # 输入QubitCiurcuit后会自动归一化\n", "\n", "cir = dq.QubitCircuit(n_qubits, init_state=init_state)\n", "cir.h(0)\n", @@ -198,7 +199,7 @@ "\n", "pattern = cir.pattern()\n", "print(pattern.init_state.full_state.shape)\n", - "pattern.init_state.full_state" + "print(pattern.init_state.full_state)" ] }, { @@ -375,7 +376,7 @@ "| Command | 定义 | Pattern函数 |\n", "|------|------|------|\n", "| $N_i$ | Node (qubit) preparation command with node index $i$ | $n(i)$ |\n", - "| $E_{ij}$ | Entanglement command which apply $CZ$ gate to nodes $(i, j)$ | $e(i, j)$ | \n", + "| $E_{ij}$ | Entanglement command which apply $CZ$ gate to nodes $(i, j)$ | $e(i, j)$ |\n", "| $^t[M_i^{\\lambda, \\alpha}]^s$ | Measurement command which perform measurement of node $i$ ,with
measurement plane $\\lambda = XY, YZ$ or $XZ$,
measurement angle $\\alpha$ defined on the plane $\\lambda$,
$s$ and $t$ feedforward domains that adaptively changes the measurement angles to $\\alpha' = (-1)^{q_s}\\alpha + \\pi q_t$,
where $q_s, q_t$ are the sum of all measurement outcomes in the $s$ and $t$ domains. | $m(i, \\alpha, \\lambda, t, s)$ |\n", "| $X_i^s$ | Correction X command applied to qubit $i$ with signal domain $s$ | $x(i, s)$ |\n", "| $Z_i^s$ | Correction Z command applied to qubit $i$ with signal domain $s$ | $z(i, s)$ |" @@ -415,7 +416,7 @@ "pattern.e(2, 3)\n", "pattern.m(node=2, angle=np.pi, s_domain=[0], t_domain=[1])\n", "pattern.x(node=3, domain=[0, 1])\n", - "pattern.draw()\n" + "pattern.draw()" ] }, { @@ -436,7 +437,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -469,7 +470,7 @@ ], "source": [ "pattern.standardize()\n", - "pattern" + "print(pattern)" ] }, { @@ -481,7 +482,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -514,7 +515,7 @@ ], "source": [ "pattern.shift_signals()\n", - "pattern" + "print(pattern)" ] }, { @@ -593,7 +594,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -610,7 +611,7 @@ ], "source": [ "state = pattern().full_state\n", - "state" + "print(state)" ] }, { @@ -622,7 +623,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -637,7 +638,7 @@ } ], "source": [ - "pattern.state.measure_dict" + "print(pattern.state.measure_dict)" ] }, { @@ -649,7 +650,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -674,7 +675,7 @@ "pattern.x(node=2, domain=[0, 1])\n", "\n", "angle = torch.randn(2)\n", - "pattern(data=angle).full_state" + "print(pattern(data=angle).full_state)" ] }, { @@ -686,7 +687,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -704,7 +705,7 @@ "source": [ "init_graph_state = dq.GraphState([0, 1], state=[1, 0, 0, 0])\n", "\n", - "pattern(data=angle, state=init_graph_state).full_state" + "print(pattern(data=angle, state=init_graph_state).full_state)" ] }, { @@ -723,7 +724,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -755,7 +756,7 @@ ], "source": [ "angle = torch.randn(6, 2)\n", - "pattern(data=angle).full_state" + "print(pattern(data=angle).full_state)" ] }, { @@ -767,7 +768,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -786,10 +787,9 @@ } ], "source": [ - "init_graph_state = dq.GraphState([0, 1], state=[[1, 0, 0, 0],\n", - " [0, 1, 0, 0]])\n", + "init_graph_state = dq.GraphState([0, 1], state=[[1, 0, 0, 0], [0, 1, 0, 0]])\n", "\n", - "pattern(state=init_graph_state).full_state" + "print(pattern(state=init_graph_state).full_state)" ] }, { @@ -876,7 +876,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -895,7 +895,7 @@ ], "source": [ "data = torch.randn(2, requires_grad=True)\n", - "pattern(data=data).full_state" + "print(pattern(data=data).full_state)" ] } ], diff --git a/tutorials/mbqc_basics.py b/tutorials/mbqc_basics.py new file mode 100644 index 00000000..a6b37d72 --- /dev/null +++ b/tutorials/mbqc_basics.py @@ -0,0 +1,260 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.19.1 +# kernelspec: +# display_name: dq_cuda_251 +# language: python +# name: python3 +# --- + +# %% +import deepquantum as dq +import numpy as np +import torch + +print('version', dq.__version__) + +# %% [markdown] +# # 基于测量的量子计算(MBQC)模块 + +# %% [markdown] +# ## 构建Pattern + +# %% [markdown] +# ### 通过转译QubitCircuit线路构建Pattern + +# %% [markdown] +# 构建``QubitCircuit`` + +# %% +cir = dq.QubitCircuit(2) +cir.h(0) +cir.h(1) +cir.cnot(0, 1) +cir.draw() + +# %% [markdown] +# 转译为MBQC的``Pattern``类 + +# %% +# Transpile circuit to measurement pattern +pattern = cir.pattern() +print(pattern) + +# %% [markdown] +# 如果``QubitCircuit``初态是batch形式,转译后的``Pattern``初态依然是batch形式。 + +# %% +n_qubits = 2 +batch_size = 5 +init_state = torch.rand(batch_size, 2**n_qubits) # 输入QubitCiurcuit后会自动归一化 + +cir = dq.QubitCircuit(n_qubits, init_state=init_state) +cir.h(0) +cir.h(1) +cir.cnot(0, 1) + +pattern = cir.pattern() +print(pattern.init_state.full_state.shape) +print(pattern.init_state.full_state) + +# %% [markdown] +# 可视化构建的图态。其中,方形node表示输入,edge表示node间存在CZ纠缠,蓝色node表示待测,剩余的灰色node表示输出。 +# +# 绿色/红色虚线表示测量角度对于t domain/s domain的依赖。因为转译完得到的是未经优化的wild pattern,中间的Z/X修正尚未转移到测量角度上,所以图中并没有显示。 + +# %% +pattern.draw() + +# %% [markdown] +# ### 手动构建Pattern + +# %% [markdown] +# 除了通过``QubitCircuit``的转译,用户可以初始化``Pattern``后,通过手动添加``NEMC``commands,构建自定义的``Pattern``。 + +# %% [markdown] +# 输入的节点可以用初始化参数中``nodes_state``设置,类型可以是``int``,代表初始化节点的数量,也可以用``List``指定节点的编号。 +# +# 而初态用参数``state``设置,默认为全 $ \left |+\right \rangle$ 态。除了输入自定义的态矢,可以用``str``类型的输入,支持``'plus'``, ``'minus'``, ``'zero'``和``'one'``。 + +# %% +pattern = dq.Pattern(nodes_state=[0, 1]) +## 等效为 pattern = dq.Pattern(nodes_state=2) + +print(pattern.init_state.full_state) + +pattern.draw() + +# %% +# 自定义初态 +pattern = dq.Pattern(nodes_state=[0, 1], state=[1, 0, 0, 0]) +print(pattern.init_state.full_state) + +# 初态str表示 +pattern = dq.Pattern(nodes_state=[0, 1], state='minus') +print(pattern.init_state.full_state) + +pattern = dq.Pattern(nodes_state=[0, 1], state='zero') +print(pattern.init_state.full_state) + +pattern = dq.Pattern(nodes_state=[0, 1], state='one') +print(pattern.init_state.full_state) + +# %% [markdown] +# 可以在`nodes_state`的基础上,额外加入`edges`和`nodes`作为输入的初始图态。 + +# %% +pattern = dq.Pattern(nodes_state=[0, 1], nodes=[2, 3], edges=[[2, 3]]) + +pattern.draw() + +# %% [markdown] +# 根据NEMC Commands序列,生成特定的``Pattern`` + +# %% [markdown] +# | Command | 定义 | Pattern函数 | +# |------|------|------| +# | $N_i$ | Node (qubit) preparation command with node index $i$ | $n(i)$ | +# | $E_{ij}$ | Entanglement command which apply $CZ$ gate to nodes $(i, j)$ | $e(i, j)$ | +# | $^t[M_i^{\lambda, \alpha}]^s$ | Measurement command which perform measurement of node $i$ ,with
measurement plane $\lambda = XY, YZ$ or $XZ$,
measurement angle $\alpha$ defined on the plane $\lambda$,
$s$ and $t$ feedforward domains that adaptively changes the measurement angles to $\alpha' = (-1)^{q_s}\alpha + \pi q_t$,
where $q_s, q_t$ are the sum of all measurement outcomes in the $s$ and $t$ domains. | $m(i, \alpha, \lambda, t, s)$ | +# | $X_i^s$ | Correction X command applied to qubit $i$ with signal domain $s$ | $x(i, s)$ | +# | $Z_i^s$ | Correction Z command applied to qubit $i$ with signal domain $s$ | $z(i, s)$ | + +# %% [markdown] +# 例如,生成 $ X_3^{s_0+s_1}\ ^{t_1} [M_2^{\pi}]^{s_0}E_{23}N_3[M_1^{\pi}]^{s_0} M_0^{\pi}E_{12}E_{02}N_2E_{01}N_1N_0$ + +# %% +pattern = dq.Pattern(nodes_state=[0, 1]) +pattern.n(2) +pattern.e(0, 2) +pattern.e(1, 2) +pattern.m(node=0, angle=np.pi) +pattern.m(node=1, angle=np.pi, s_domain=[0]) +pattern.n(3) +pattern.e(2, 3) +pattern.m(node=2, angle=np.pi, s_domain=[0], t_domain=[1]) +pattern.x(node=3, domain=[0, 1]) +pattern.draw() + +# %% [markdown] +# ## 优化Pattern + +# %% [markdown] +# 进行``standardize``操作,将``Pattern``从右向左按NEMC的指令类型进行排列,形成标准形式。 +# +# 注意,相比wild pattern,standard form会占用更多内存。如果标准化后出现内存溢出的报错,可以尝试对wild pattern直接进行前向演化。 + +# %% +pattern.standardize() +print(pattern) + +# %% [markdown] +# 通过signal shifting 进一步优化,目的是消除测量角度对于``t_domain``的依赖(Z-dependency),从而降低量子深度。 + +# %% +pattern.shift_signals() +print(pattern) + +# %% [markdown] +# 通过图态的可视化,可以观察到signal shifting后的测量不再依赖于绿色的Z修正。 + +# %% +pattern.draw() + +# %% [markdown] +# ## 执行MBQC模拟 + +# %% [markdown] +# ### 前向演化 +# +# 和DeepQuantum其它模块一样,仅需一个前向函数即可执行对``Pattern``的模拟。 + +# %% +pattern() + +# %% [markdown] +# 返回的结果是末态输出的``GraphState``,使用属性``full_state``可以得到末态的态矢。 + +# %% +state = pattern().full_state +print(state) + +# %% [markdown] +# 对应的测量结果由``GraphState``中``measure_dict``保存: + +# %% +print(pattern.state.measure_dict) + +# %% [markdown] +# 支持通过 ``data`` 输入``Pattern``中 ``encode=True`` 的command参数(即测量角度),通过 ``state`` 指定初态,对``Pattern``进行演化: + +# %% +pattern = dq.Pattern(nodes_state=[0, 1]) +pattern.n(2) +pattern.e(0, 2) +pattern.e(1, 2) +pattern.m(node=0, encode=True) +pattern.m(node=1, encode=True, s_domain=[0]) +pattern.x(node=2, domain=[0, 1]) + +angle = torch.randn(2) +print(pattern(data=angle).full_state) + +# %% [markdown] +# 初态的类型需要是``GraphState``: + +# %% +init_graph_state = dq.GraphState([0, 1], state=[1, 0, 0, 0]) + +print(pattern(data=angle, state=init_graph_state).full_state) + +# %% [markdown] +# ### 支持batch输入 + +# %% [markdown] +# 测量角度的batch输入: + +# %% +angle = torch.randn(6, 2) +print(pattern(data=angle).full_state) + +# %% [markdown] +# 初态的batch输入: + +# %% +init_graph_state = dq.GraphState([0, 1], state=[[1, 0, 0, 0], [0, 1, 0, 0]]) + +print(pattern(state=init_graph_state).full_state) + +# %% [markdown] +# 也支持转译前``QubitCircuit``中参数化门的encode: + +# %% +cir = dq.QubitCircuit(2) +cir.h(0) +cir.rz(0, encode=True) +cir.ry(1, encode=True) +cir.cnot(0, 1) + +pattern = cir.pattern() + +data = torch.randn(2) +print(pattern(data=data).full_state) + +data = torch.randn(6, 2) +print(pattern(data=data).full_state) + +# %% [markdown] +# ### 支持自动微分 + +# %% [markdown] +# MBQC模块支持基于``PyTorch``的自动微分,用户可以利用这一特性设计和模拟含梯度优化的变分算法(VMBQC)。 + +# %% +data = torch.randn(2, requires_grad=True) +print(pattern(data=data).full_state) diff --git a/tutorials/photonic_basics.ipynb b/tutorials/photonic_basics.ipynb index bc539014..78e0ed36 100644 --- a/tutorials/photonic_basics.ipynb +++ b/tutorials/photonic_basics.ipynb @@ -53,9 +53,10 @@ "source": [ "## Fock态\n", "\n", - "光量子线路Fock态(量子数态)可以表示为 $ \\left |n_1,n_2,...,n_m\\right \\rangle$,其中 $ n_k $ 表示第 $k$ 模的光子数。在DeepQuantum的Photonic模块中,Fock态用 ``FockState`` 表示。\n", + "光量子线路Fock态(量子数态)可以表示为 $ \\left |n_1,n_2,...,n_m\\right \\rangle$,其中 $ n_k $ 表示第 $k$ 模的光子数。\n", + "在DeepQuantum的Photonic模块中,Fock态用 ``FockState`` 表示。\n", "\n", - "我们利用 ``FockState`` 准备一个3模的量子态,其中第一、三模光子数为1,第二模为0:\n" + "我们利用 ``FockState`` 准备一个3模的量子态,其中第一、三模光子数为1,第二模为0:" ] }, { @@ -77,7 +78,7 @@ } ], "source": [ - "qstate = dq.FockState(state=[1,0,1])\n", + "qstate = dq.FockState(state=[1, 0, 1])\n", "print(qstate)" ] }, @@ -119,7 +120,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2025-04-25T09:06:35.524348Z", @@ -145,8 +146,8 @@ } ], "source": [ - "tc_state = torch.tensor([1, 0, 1])\n", - "qstate = dq.FockState(state=tc_state)\n", + "state = torch.tensor([1, 0, 1])\n", + "qstate = dq.FockState(state=state)\n", "print(qstate)\n", "\n", "batch_state = torch.randint(0, 2, (4, 3))\n", @@ -252,10 +253,10 @@ } ], "source": [ - "qstate = dq.FockState(state=[(np.sqrt(2)/2, [1,0,1,0]), (np.sqrt(2)/2, [0,1,1,0])], basis=False)\n", + "qstate = dq.FockState(state=[(np.sqrt(2) / 2, [1, 0, 1, 0]), (np.sqrt(2) / 2, [0, 1, 1, 0])], basis=False)\n", "print(qstate)\n", "\n", - "#得到batch形式的Fock state tensor\n", + "# 得到batch形式的Fock state tensor\n", "print(qstate.state.size())" ] }, @@ -288,18 +289,18 @@ } ], "source": [ - "state_a = dq.FockState(state=[0,0,1])\n", - "state_a2 = dq.FockState(state=[0,0,1])\n", - "state_b = dq.FockState(state=[0,0,2])\n", + "state_a = dq.FockState(state=[0, 0, 1])\n", + "state_a2 = dq.FockState(state=[0, 0, 1])\n", + "state_b = dq.FockState(state=[0, 0, 2])\n", "\n", "print(state_a == state_a2)\n", "print(state_a < state_b)\n", "\n", "# 构建一个简单Qumode线路\n", - "cir = dq.QumodeCircuit(nmode=3, init_state=[1,2,0])\n", - "cir.bs(wires=[0,1], inputs=[np.pi/3, np.pi/3])\n", - "cir.bs(wires=[1,2], inputs=[np.pi/3, np.pi/3])\n", - "#前向函数\n", + "cir = dq.QumodeCircuit(nmode=3, init_state=[1, 2, 0])\n", + "cir.bs(wires=[0, 1], inputs=[np.pi / 3, np.pi / 3])\n", + "cir.bs(wires=[1, 2], inputs=[np.pi / 3, np.pi / 3])\n", + "# 前向函数\n", "cir()\n", "# 采样测量\n", "result_dict = cir.measure()\n", @@ -337,7 +338,7 @@ }, "outputs": [], "source": [ - "cir = dq.QumodeCircuit(nmode=3, init_state=[1,0,1])" + "cir = dq.QumodeCircuit(nmode=3, init_state=[1, 0, 1])" ] }, { @@ -358,9 +359,9 @@ }, "outputs": [], "source": [ - "cir1 = dq.QumodeCircuit(nmode=3, init_state=torch.tensor([1,0,1]))\n", + "cir1 = dq.QumodeCircuit(nmode=3, init_state=torch.tensor([1, 0, 1]))\n", "\n", - "fockstate = dq.FockState(state=[1,0,1])\n", + "fockstate = dq.FockState(state=[1, 0, 1])\n", "cir2 = dq.QumodeCircuit(nmode=3, init_state=fockstate)" ] }, @@ -394,11 +395,11 @@ ], "source": [ "# 无cutoff时\n", - "cir = dq.QumodeCircuit(nmode=3, init_state=[2,1,0])\n", + "cir = dq.QumodeCircuit(nmode=3, init_state=[2, 1, 0])\n", "print('默认cutff时,末态概率分布为:\\n', cir(is_prob=True))\n", "\n", "# 设定cutoff为3,即每模不超过2光子\n", - "cir = dq.QumodeCircuit(nmode=3, init_state=[2,1,0], cutoff=3)\n", + "cir = dq.QumodeCircuit(nmode=3, init_state=[2, 1, 0], cutoff=3)\n", "print('cutff=3时,末态概率分布为:\\n', cir(is_prob=True))" ] }, @@ -423,7 +424,7 @@ "$${PS}(\\theta)\\hat{a}^\\dagger{PS}^\\dagger(\\theta) = U^T(\\theta)\\hat{a}^\\dagger$$\n", "$$U(\\theta) = e^{i\\theta}$$\n", "\n", - "``ps`` 中, ``wires`` 参数表示作用的空间mode(数据类型为 ``int`` ), ``inputs`` 输入移相器对应的相位角 $\\theta$ \n" + "``ps`` 中, ``wires`` 参数表示作用的空间mode(数据类型为 ``int`` ), ``inputs`` 输入移相器对应的相位角 $\\theta$\n" ] }, { @@ -518,7 +519,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "- ``dc`` :50:50 定向耦合器, 即指定 $ \\phi = \\pi/2, \\theta = \\pi/4 $ 时的 ``bs``。 \n", + "- ``dc`` :50:50 定向耦合器, 即指定 $ \\phi = \\pi/2, \\theta = \\pi/4 $ 时的 ``bs``。\n", "\n", "作用在生成算符 $\\hat{a}^\\dagger$ 上的酉变换矩阵如下\n", "\n", @@ -557,8 +558,8 @@ } ], "source": [ - "cir = dq.QumodeCircuit(2, init_state=[1,0])\n", - "cir.h(wires=[0,1])\n", + "cir = dq.QumodeCircuit(2, init_state=[1, 0])\n", + "cir.h(wires=[0, 1])\n", "prob_h = cir(is_prob=True)\n", "amplitude_h = cir(is_prob=False)\n", "\n", @@ -566,7 +567,7 @@ "print('通过H型50:50分束器后末态振幅为:', amplitude_h)\n", "\n", "cir = dq.QumodeCircuit(2, init_state=[1, 0])\n", - "cir.dc(wires=[0,1])\n", + "cir.dc(wires=[0, 1])\n", "prob_dc = cir(is_prob=True)\n", "amplitude_dc = cir(is_prob=False)\n", "\n", @@ -596,7 +597,7 @@ "可以通过参数 ``phi_first=False`` 切换至另一种分束器-移相器-分束器-移相器的结构 (先 $\\theta$后 $\\phi$), 对应的酉变换矩阵如下\n", "\n", "$$\n", - "MZI_{TP} = \n", + "MZI_{TP} =\n", "ie^{i\\theta/2}\n", "\\begin{pmatrix}\n", "e^{i\\phi}\\sin{\\frac{\\theta}{2}}&e^{i\\phi}\\cos{\\frac{\\theta}{2}}\\\\\n", @@ -616,15 +617,15 @@ }, "outputs": [], "source": [ - "cir = dq.QumodeCircuit(nmode=3, init_state=[1,0,1])\n", - "cir.ps(wires=0, inputs=0) # 相位角为0,作用第0个mode上\n", - "cir.ps(1, 0) # 也可省略参数名\n", - "cir.bs_theta([0,1], torch.pi/4) # 单参数theta可调分束器,theta为pi/4 作用在第0、1个mode上\n", - "cir.h(wires=[1,2]) # H型50:50分束器\n", - "cir.bs_rx(wires=[1,2]) # Rx型分束器\n", - "cir.bs_ry(wires=[0,1]) # Ry型分束器\n", - "cir.dc(wires=[1,2]) # 50:50定向耦合器\n", - "cir.mzi(wires=[0,1]) # mzi干涉仪" + "cir = dq.QumodeCircuit(nmode=3, init_state=[1, 0, 1])\n", + "cir.ps(wires=0, inputs=0) # 相位角为0,作用第0个mode上\n", + "cir.ps(1, 0) # 也可省略参数名\n", + "cir.bs_theta([0, 1], torch.pi / 4) # 单参数theta可调分束器,theta为pi/4 作用在第0、1个mode上\n", + "cir.h(wires=[1, 2]) # H型50:50分束器\n", + "cir.bs_rx(wires=[1, 2]) # Rx型分束器\n", + "cir.bs_ry(wires=[0, 1]) # Ry型分束器\n", + "cir.dc(wires=[1, 2]) # 50:50定向耦合器\n", + "cir.mzi(wires=[0, 1]) # mzi干涉仪" ] }, { @@ -676,7 +677,8 @@ "ExecuteTime": { "end_time": "2025-04-25T09:06:41.524304Z", "start_time": "2025-04-25T09:06:41.469229Z" - } + }, + "lines_to_next_cell": 2 }, "outputs": [ { @@ -699,7 +701,9 @@ }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "lines_to_next_cell": 2 + }, "source": [ "另外,支持用户添加任意自定义的酉矩阵门,通过 ``any`` 或 ``clements``来实现。``any`` 直接将酉矩阵作用于线路,支持自动微分梯度的运算;而 ``clements`` 会先将酉矩阵分解为Clements架构的线路参数,再把对应的Clements网络作用在线路上,是一种对应的物理实现,暂不支持自动微分。\n", "\n", @@ -708,7 +712,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2025-04-25T09:06:42.832655Z", @@ -739,19 +743,53 @@ } ], "source": [ + "from deepquantum.qmath import is_unitary\n", + "from torch import nn\n", + "\n", + "\n", "# 通过旋转矩阵构建 3x3 酉矩阵\n", "def unitary_3x3(alpha, beta, gamma, delta):\n", - " r1 = torch.stack([torch.cos(alpha), -torch.sin(alpha), torch.tensor(0),\n", - " torch.sin(alpha), torch.cos(alpha), torch.tensor(0),\n", - " torch.tensor(0), torch.tensor(0), torch.tensor(1)]).reshape(3,3)\n", + " r1 = torch.stack(\n", + " [\n", + " torch.cos(alpha),\n", + " -torch.sin(alpha),\n", + " torch.tensor(0),\n", + " torch.sin(alpha),\n", + " torch.cos(alpha),\n", + " torch.tensor(0),\n", + " torch.tensor(0),\n", + " torch.tensor(0),\n", + " torch.tensor(1),\n", + " ]\n", + " ).reshape(3, 3)\n", "\n", - " r2 = torch.stack([torch.cos(beta), torch.tensor(0), torch.sin(beta),\n", - " torch.tensor(0), torch.tensor(1), torch.tensor(0),\n", - " -torch.sin(beta), torch.tensor(0), torch.cos(beta)]).reshape(3,3)\n", + " r2 = torch.stack(\n", + " [\n", + " torch.cos(beta),\n", + " torch.tensor(0),\n", + " torch.sin(beta),\n", + " torch.tensor(0),\n", + " torch.tensor(1),\n", + " torch.tensor(0),\n", + " -torch.sin(beta),\n", + " torch.tensor(0),\n", + " torch.cos(beta),\n", + " ]\n", + " ).reshape(3, 3)\n", "\n", - " r3 = torch.stack([torch.tensor(1), torch.tensor(0), torch.tensor(0),\n", - " torch.tensor(0), torch.cos(gamma), -torch.sin(gamma),\n", - " torch.tensor(0), torch.sin(gamma), torch.cos(gamma)]).reshape(3,3)\n", + " r3 = torch.stack(\n", + " [\n", + " torch.tensor(1),\n", + " torch.tensor(0),\n", + " torch.tensor(0),\n", + " torch.tensor(0),\n", + " torch.cos(gamma),\n", + " -torch.sin(gamma),\n", + " torch.tensor(0),\n", + " torch.sin(gamma),\n", + " torch.cos(gamma),\n", + " ]\n", + " ).reshape(3, 3)\n", "\n", " u = torch.matmul(torch.matmul(r1, r2), r3)\n", "\n", @@ -759,24 +797,25 @@ " unitary = u * torch.exp(1j * delta)\n", " return unitary\n", "\n", + "\n", "# 生成随机角度作为初始参数,构建酉矩阵。any门支持梯度运算\n", - "import torch.nn as nn\n", "para = nn.Parameter(torch.rand(4, requires_grad=True))\n", "unitary = unitary_3x3(para[0], para[1], para[2], para[3])\n", "\n", "# 检查是否为酉矩阵\n", - "from deepquantum.qmath import is_unitary\n", "print(is_unitary(unitary))\n", "\n", + "\n", "def get_prob_110(para):\n", " unitary = unitary_3x3(para[0], para[1], para[2], para[3])\n", "\n", - " cir = dq.QumodeCircuit(nmode=3, init_state=[1,0,1])\n", - " cir.any(wires=[0,1,2], unitary=unitary)\n", + " cir = dq.QumodeCircuit(nmode=3, init_state=[1, 0, 1])\n", + " cir.any(wires=[0, 1, 2], unitary=unitary)\n", " x = cir(is_prob=True)\n", "\n", " # 得到末态为1,1,0的概率\n", - " return x[dq.FockState([1,1,0])]\n", + " return x[dq.FockState([1, 1, 0])]\n", + "\n", "\n", "# 使用Adam优化器\n", "optimizer = torch.optim.Adam([para], lr=0.1)\n", @@ -786,8 +825,8 @@ " loss = torch.abs(1 - prob_110)\n", " if loss < 1e-5:\n", " break\n", - " loss.backward() # 反向传播\n", - " optimizer.step() # 更新参数\n", + " loss.backward() # 反向传播\n", + " optimizer.step() # 更新参数\n", "print('梯度下降优化后末态为110的概率为:', get_prob_110(para))\n", "\n", "cir.draw()" @@ -795,7 +834,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2025-04-25T09:06:42.859658Z", @@ -823,11 +862,10 @@ "unitary = unitary_3x3(para[0], para[1], para[2], para[3])\n", "\n", "# 检查是否为酉矩阵\n", - "from deepquantum.qmath import is_unitary\n", "is_unitary(unitary)\n", "\n", - "cir = dq.QumodeCircuit(nmode=3, init_state=[1,0,1])\n", - "cir.clements(wires=[0,1,2], unitary=unitary)\n", + "cir = dq.QumodeCircuit(nmode=3, init_state=[1, 0, 1])\n", + "cir.clements(wires=[0, 1, 2], unitary=unitary)\n", "cir.draw()" ] }, @@ -872,15 +910,16 @@ "source": [ "# 实例化一个单参数theta可调分束器,theta为pi/4 作用在第0、1个mode上\n", "from deepquantum.photonic.gate import BeamSplitterTheta\n", - "bs_theta = BeamSplitterTheta(nmode=3, wires=[0,1], inputs=torch.pi/4)\n", + "\n", + "bs_theta = BeamSplitterTheta(nmode=3, wires=[0, 1], inputs=torch.pi / 4)\n", "print('实例化的单参数theta可调分束器为:', bs_theta)\n", "print('对应的酉矩阵为:', bs_theta.get_unitary())\n", "\n", - "cir = dq.QumodeCircuit(nmode=3, init_state=[1,0,1])\n", - "cir.ps(wires=0, inputs=0) # 相位角为0,作用第0个mode上\n", - "cir.ps(1, 0) # 也可省略参数名\n", - "cir.bs_theta([0,1], torch.pi/4) # 单参数theta可调分束器,theta为pi/4 作用在第0、1个mode上\n", - "cir.h(wires=[1,2]) # H型50:50分束器\n", + "cir = dq.QumodeCircuit(nmode=3, init_state=[1, 0, 1])\n", + "cir.ps(wires=0, inputs=0) # 相位角为0,作用第0个mode上\n", + "cir.ps(1, 0) # 也可省略参数名\n", + "cir.bs_theta([0, 1], torch.pi / 4) # 单参数theta可调分束器,theta为pi/4 作用在第0、1个mode上\n", + "cir.h(wires=[1, 2]) # H型50:50分束器\n", "\n", "print(cir.operators)\n", "\n", @@ -928,11 +967,11 @@ } ], "source": [ - "cir = dq.QumodeCircuit(nmode=3, init_state=[1,0,1])\n", - "cir.ps(wires=0, inputs=0) # 相位角为0,作用第0个mode上\n", - "cir.ps(1, 0) # 也可省略参数名\n", - "cir.bs_theta([0,1], torch.pi/4) # 单参数theta可调分束器,theta为pi/4 作用在第0、1个mode上\n", - "cir.h(wires=[1,2]) # H型50:50分束器\n", + "cir = dq.QumodeCircuit(nmode=3, init_state=[1, 0, 1])\n", + "cir.ps(wires=0, inputs=0) # 相位角为0,作用第0个mode上\n", + "cir.ps(1, 0) # 也可省略参数名\n", + "cir.bs_theta([0, 1], torch.pi / 4) # 单参数theta可调分束器,theta为pi/4 作用在第0、1个mode上\n", + "cir.h(wires=[1, 2]) # H型50:50分束器\n", "\n", "print(cir())\n", "print(cir(is_prob=True))\n", @@ -978,10 +1017,10 @@ } ], "source": [ - "cir2 = dq.QumodeCircuit(2, init_state=[1,0,1])\n", - "cir2.ps(wires=0, encode=True) # 相位角为参数,作用第0个mode上\n", - "cir2.ps(1, encode =True) # 也可省略参数名\n", - "cir2.bs_theta([0,1], encode=True) # theta为参数,phi为定值pi/2 作用在第0、1个mode上\n", + "cir2 = dq.QumodeCircuit(2, init_state=[1, 0, 1])\n", + "cir2.ps(wires=0, encode=True) # 相位角为参数,作用第0个mode上\n", + "cir2.ps(1, encode=True) # 也可省略参数名\n", + "cir2.bs_theta([0, 1], encode=True) # theta为参数,phi为定值pi/2 作用在第0、1个mode上\n", "\n", "# 构建data作为输入参数\n", "npara = 3\n", @@ -1024,10 +1063,10 @@ ], "source": [ "# 指定初态前向演化\n", - "print(cir2(state=[0,2,0]))\n", + "print(cir2(state=[0, 2, 0]))\n", "\n", "# 同时指定线路参数和初态前向演化,并返回概率分布\n", - "cir2(data, state=[1,1,1], is_prob=True)" + "cir2(data, state=[1, 1, 1], is_prob=True)" ] }, { @@ -1061,11 +1100,11 @@ } ], "source": [ - "cir = dq.QumodeCircuit(nmode=3, init_state=[1,0,1])\n", - "cir.ps(wires=0) # 作用第0个mode上\n", - "cir.ps(1, 0) # 也可省略参数名\n", - "cir.bs_theta([0,1]) # 单参数theta可调分束器,作用在第0、1个mode上\n", - "cir.h(wires=[1,2]) # H型50:50分束器\n", + "cir = dq.QumodeCircuit(nmode=3, init_state=[1, 0, 1])\n", + "cir.ps(wires=0) # 作用第0个mode上\n", + "cir.ps(1, 0) # 也可省略参数名\n", + "cir.bs_theta([0, 1]) # 单参数theta可调分束器,作用在第0、1个mode上\n", + "cir.h(wires=[1, 2]) # H型50:50分束器\n", "cir()\n", "\n", "cir.measure()" @@ -1135,7 +1174,7 @@ } ], "source": [ - "cir.measure(wires=[0,1])" + "cir.measure(wires=[0, 1])" ] }, { @@ -1211,8 +1250,8 @@ } ], "source": [ - "print(cir.get_amplitude([1,1,0]))\n", - "print(cir.get_prob([1,0,1]))" + "print(cir.get_amplitude([1, 1, 0]))\n", + "print(cir.get_prob([1, 0, 1]))" ] }, { @@ -1288,7 +1327,7 @@ "print('初态为:', batch_state)\n", "\n", "cir = dq.QumodeCircuit(nmode=2, init_state=batch_state)\n", - "cir.bs_theta([0,1])\n", + "cir.bs_theta([0, 1])\n", "\n", "unitary = cir()\n", "print('线路对应酉矩阵为:', unitary)\n", @@ -1359,7 +1398,7 @@ "cir.ps(0, encode=True)\n", "\n", "# 构建一组线路参数\n", - "para = torch.randn(1, 2) # 1组数据,2个参数\n", + "para = torch.randn(1, 2) # 1组数据,2个参数\n", "unitary = cir(para)\n", "print('线路对应酉矩阵为:', unitary)\n", "\n", @@ -1426,8 +1465,8 @@ "batch_para = torch.randn(batch_size, npara)\n", "print('线路参数为:', batch_para)\n", "\n", - "cir = dq.QumodeCircuit(nmode=2, init_state=[0,1])\n", - "cir.bs_theta([0,1], encode=True)\n", + "cir = dq.QumodeCircuit(nmode=2, init_state=[0, 1])\n", + "cir.bs_theta([0, 1], encode=True)\n", "\n", "unitary = cir(data=batch_para)\n", "print('线路对应酉矩阵为:', unitary)\n", @@ -1493,14 +1532,14 @@ "nmode = 2\n", "npara = 1\n", "batch_state = torch.randint(0, 2, (batch_size, nmode))\n", - "batch_para = torch.randn(batch_size,npara)\n", + "batch_para = torch.randn(batch_size, npara)\n", "print('batch数为:', batch_size)\n", "print('初态shape为:', batch_state.shape)\n", "print('线路参数shape为:', batch_para.shape)\n", "\n", "\n", "cir = dq.QumodeCircuit(nmode=nmode, init_state=batch_state)\n", - "cir.bs_theta([0,1], encode=True)\n", + "cir.bs_theta([0, 1], encode=True)\n", "\n", "unitary = cir(data=batch_para)\n", "print('线路对应酉矩阵为:', unitary)\n", @@ -1537,10 +1576,10 @@ } ], "source": [ - "cir_n = dq.QumodeCircuit(nmode=3, init_state=[1,0,1], noise=True, mu=0.3, sigma=0.2)\n", + "cir_n = dq.QumodeCircuit(nmode=3, init_state=[1, 0, 1], noise=True, mu=0.3, sigma=0.2)\n", "cir_n.ps(wires=0, encode=True)\n", "cir_n.ps(1, encode=True)\n", - "cir_n.bs_theta([0,1], encode=True)\n", + "cir_n.bs_theta([0, 1], encode=True)\n", "\n", "# 定义线路参数,并前向演化保存末态\n", "para = torch.randn(3)\n", @@ -1577,13 +1616,13 @@ } ], "source": [ - "cir_n = dq.QumodeCircuit(nmode=3, init_state=[1,0,1], noise=True, mu=0.3, sigma=0.2)\n", - "cir_n.ps(wires=0, encode=True, mu=0.2, sigma=0) # 支持自定义mu和sigma的值,默认为0、0.1\n", + "cir_n = dq.QumodeCircuit(nmode=3, init_state=[1, 0, 1], noise=True, mu=0.3, sigma=0.2)\n", + "cir_n.ps(wires=0, encode=True, mu=0.2, sigma=0) # 支持自定义mu和sigma的值,默认为0、0.1\n", "cir_n.ps(1, encode=True)\n", - "cir_n.bs_theta([0,1], encode=True, mu=0, sigma=0.3)\n", + "cir_n.bs_theta([0, 1], encode=True, mu=0, sigma=0.3)\n", "\n", - "print(f'第一个ps门mu = {cir_n.operators[0].mu}, sigma = {cir_n.operators[0].sigma}') # 覆盖全局mu、sigma\n", - "print(f'第二个ps门mu = {cir_n.operators[1].mu}, sigma = {cir_n.operators[1].sigma}') # 全局mu、sigma\n", + "print(f'第一个ps门mu = {cir_n.operators[0].mu}, sigma = {cir_n.operators[0].sigma}') # 覆盖全局mu、sigma\n", + "print(f'第二个ps门mu = {cir_n.operators[1].mu}, sigma = {cir_n.operators[1].sigma}') # 全局mu、sigma\n", "\n", "# 定义线路参数,并前向演化保存末态\n", "para = torch.randn(3)\n", @@ -1601,7 +1640,7 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2025-04-25T09:06:44.147123Z", @@ -1620,10 +1659,10 @@ ], "source": [ "# 设置无噪声线路\n", - "cir = dq.QumodeCircuit(nmode=3, init_state=[1,0,1])\n", + "cir = dq.QumodeCircuit(nmode=3, init_state=[1, 0, 1])\n", "cir.ps(0, encode=True)\n", "cir.ps(1, encode=True)\n", - "cir.bs_theta([0,1], encode=True)\n", + "cir.bs_theta([0, 1], encode=True)\n", "\n", "final_state = cir(para, is_prob=False)\n", "\n", @@ -1631,7 +1670,7 @@ "\n", "# 计算fidelity\n", "f = 0\n", - "for key in final_state_n.keys():\n", + "for key in final_state_n:\n", " f += final_state_n[key].conj() * final_state[key]\n", "fidelity = torch.abs(f) ** 2\n", "print('加入噪声后的保真度为:', fidelity)" @@ -1739,8 +1778,8 @@ "if torch.cuda.is_available():\n", " device = torch.device('cuda:0')\n", "\n", - "cir = dq.QumodeCircuit(nmode=nmode, init_state=[2,0])\n", - "cir.bs_theta([0,1], encode=True)\n", + "cir = dq.QumodeCircuit(nmode=nmode, init_state=[2, 0])\n", + "cir.bs_theta([0, 1], encode=True)\n", "\n", "# 将线路转移到cuda上\n", "cir.to(device)\n", @@ -1804,7 +1843,7 @@ } ], "source": [ - "clements = dq.Clements(nmode=6, init_state=[1,0,1,0,0,0], cutoff=3)\n", + "clements = dq.Clements(nmode=6, init_state=[1, 0, 1, 0, 0, 0], cutoff=3)\n", "clements.draw()" ] }, @@ -1841,17 +1880,21 @@ ], "source": [ "# 使用Clements架构实现CNOT门\n", - "u6x6 = np.array([[1, 0, 1, -1, 0, 0],\n", - " [0, 1, 0 ,0, 0, np.sqrt(2)],\n", - " [1, 0, 0, 1, 1, 0],\n", - " [-1, 0, 1, 0, 1, 0],\n", - " [0, 0, 1, 1, -1,0],\n", - " [0, np.sqrt(2), 0,0,0,-1]])/np.sqrt(3)\n", + "u6x6 = np.array(\n", + " [\n", + " [1, 0, 1, -1, 0, 0],\n", + " [0, 1, 0, 0, 0, np.sqrt(2)],\n", + " [1, 0, 0, 1, 1, 0],\n", + " [-1, 0, 1, 0, 1, 0],\n", + " [0, 0, 1, 1, -1, 0],\n", + " [0, np.sqrt(2), 0, 0, 0, -1],\n", + " ]\n", + ") / np.sqrt(3)\n", "# 将酉矩阵分解成clements对应的参数\n", "ud = dq.UnitaryDecomposer(u6x6)\n", "mzi_info = ud.decomp()\n", "# 构造clements线路实现CNOT门\n", - "data = clements.dict2data(mzi_info[2]) # encoding the 6x6 data\n", + "data = clements.dict2data(mzi_info[2]) # encoding the 6x6 data\n", "clements(data=data)\n", "# 线路可视化\n", "clements.draw()" @@ -1922,8 +1965,8 @@ ], "source": [ "# 打开mps,chi设为5\n", - "cir = dq.QumodeCircuit(6, init_state=[0,1,1,1,0,0], basis=False, mps=True, chi=5, cutoff=4)\n", - "clements = dq.Clements(nmode=6, init_state=[0,1,1,1,0,0])\n", + "cir = dq.QumodeCircuit(6, init_state=[0, 1, 1, 1, 0, 0], basis=False, mps=True, chi=5, cutoff=4)\n", + "clements = dq.Clements(nmode=6, init_state=[0, 1, 1, 1, 0, 0])\n", "\n", "# 将clements ansatz合并进线路\n", "cir += clements\n", @@ -1932,7 +1975,13 @@ "final_state_tensor = cir(data=data)\n", "\n", "# 转换成Fock state vector,方便比较\n", - "final_state_mps = dq.MatrixProductState(6, state=final_state_tensor, chi=cir.chi, qudit=cir.cutoff, normalize=cir.init_state.normalize).full_tensor().reshape([4] * 6)\n", + "final_state_mps = (\n", + " dq.MatrixProductState(\n", + " 6, state=final_state_tensor, chi=cir.chi, qudit=cir.cutoff, normalize=cir.init_state.normalize\n", + " )\n", + " .full_tensor()\n", + " .reshape([4] * 6)\n", + ")\n", "print(final_state_mps.shape)" ] }, @@ -1945,7 +1994,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2025-04-25T09:06:47.828817Z", @@ -1962,7 +2011,7 @@ } ], "source": [ - "cir_precise = dq.QumodeCircuit(6, init_state=[0,1,1,1,0,0])\n", + "cir_precise = dq.QumodeCircuit(6, init_state=[0, 1, 1, 1, 0, 0])\n", "cir_precise += clements\n", "\n", "precise_dict = cir_precise(data=data, is_prob=False)\n", @@ -1970,7 +2019,7 @@ "# 计算保真度\n", "f = 0\n", "\n", - "for key in precise_dict.keys():\n", + "for key in precise_dict:\n", " f += final_state_mps[tuple(key.state)].conj() * precise_dict[key]\n", "fidelity = torch.abs(f) ** 2\n", "print('保真度为:', fidelity)" @@ -1985,7 +2034,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2025-04-25T09:06:48.074642Z", @@ -2003,18 +2052,24 @@ ], "source": [ "# 打开mps,chi设为7\n", - "cir = dq.QumodeCircuit(6, init_state=[0,1,1,1,0,0], basis=False, mps=True, chi=7, cutoff=4)\n", + "cir = dq.QumodeCircuit(6, init_state=[0, 1, 1, 1, 0, 0], basis=False, mps=True, chi=7, cutoff=4)\n", "cir += clements\n", "\n", "# 得到Fock state tensor形式的末态\n", "final_state_tensor = cir(data=data)\n", "\n", "# 转换成Fock state vector,方便比较\n", - "final_state_mps = dq.MatrixProductState(6, state=final_state_tensor, chi=cir.chi, qudit=cir.cutoff, normalize=cir.init_state.normalize).full_tensor().reshape([4] * 6)\n", + "final_state_mps = (\n", + " dq.MatrixProductState(\n", + " 6, state=final_state_tensor, chi=cir.chi, qudit=cir.cutoff, normalize=cir.init_state.normalize\n", + " )\n", + " .full_tensor()\n", + " .reshape([4] * 6)\n", + ")\n", "\n", "# 计算保真度\n", "f = 0\n", - "for key in precise_dict.keys():\n", + "for key in precise_dict:\n", " f += final_state_mps[tuple(key.state)].conj() * precise_dict[key]\n", "fidelity = torch.abs(f) ** 2\n", "print('保真度为:', fidelity)" @@ -2041,12 +2096,12 @@ } ], "source": [ - "cir = dq.QumodeCircuit(4, init_state=[1,1,1,1], basis=False, mps=True, chi=5, cutoff=4)\n", + "cir = dq.QumodeCircuit(4, init_state=[1, 1, 1, 1], basis=False, mps=True, chi=5, cutoff=4)\n", "for i in range(4):\n", " cir.ps(i)\n", - "cir.bs([0,1])\n", - "cir.bs([2,3])\n", - "cir.bs([1,2])\n", + "cir.bs([0, 1])\n", + "cir.bs([2, 3])\n", + "cir.bs([1, 2])\n", "cir()\n", "samples = cir.measure(shots=100)\n", "print(samples)" @@ -2104,7 +2159,7 @@ "dqp.set_hbar(hbar=1)\n", "dqp.set_kappa(kappa=np.sqrt(2) / 2)\n", "print(dqp.hbar, dqp.kappa)\n", - "dqp.set_hbar(hbar=2) # 重新设置回原来值\n", + "dqp.set_hbar(hbar=2) # 重新设置回原来值\n", "dqp.set_kappa(kappa=np.sqrt(2) / 2)" ] }, @@ -2151,7 +2206,7 @@ ], "source": [ "gaussian_state_1 = dq.GaussianState(state='vac', nmode=2, cutoff=3)\n", - "gaussian_state_2 = dq.GaussianState(state=[torch.eye(4), torch.tensor([0]*4)], nmode=2, cutoff=3)\n", + "gaussian_state_2 = dq.GaussianState(state=[torch.eye(4), torch.tensor([0] * 4)], nmode=2, cutoff=3)\n", "print('state_1_cov_mean', gaussian_state_1.cov, gaussian_state_1.mean)\n", "print('state_2_cov_mean', gaussian_state_2.cov, gaussian_state_2.mean)" ] @@ -2178,10 +2233,10 @@ "\\hat{x} \\\\\n", "\\hat{p}\n", "\\end{pmatrix}$$\n", - "$$S(r, \\theta) = \n", + "$$S(r, \\theta) =\n", "\\begin{pmatrix}\n", "\\cosh r-\\sinh r\\cos \\theta &-\\sinh r\\sin \\theta\\\\\n", - "-\\sinh r\\sin \\theta & \n", + "-\\sinh r\\sin \\theta &\n", "\\cosh r+\\sinh r\\cos \\theta\n", "\\end{pmatrix}$$\n", "\n", @@ -2195,7 +2250,7 @@ "\\hat{x}\\\\\n", "\\hat{p}\n", "\\end{pmatrix}\n", - "D(\\alpha) = \n", + "D(\\alpha) =\n", "\\begin{pmatrix}\n", "\\hat{x}\\\\\n", "\\hat{p}\n", @@ -2206,7 +2261,7 @@ "r\\sin\\theta\n", "\\end{pmatrix}$$\n", "\n", - "- r:旋转门, \n", + "- r:旋转门,\n", "对应的酉算符表示如下\n", "$$R(\\theta) = \\exp(i\\theta \\hat{a}^\\dagger \\hat{a})$$\n", "作用在正交算符$\\hat{x}$和$\\hat{p}$上的辛变换如下\n", @@ -2225,7 +2280,7 @@ "\\sin\\theta & \\cos\\theta\n", "\\end{pmatrix}$$\n", "\n", - "- s2:双模压缩门, \n", + "- s2:双模压缩门,\n", "对应的酉算符表示如下\n", "$$S_2(z) = \\exp(z\\hat{a}_1^\\dagger\\hat{a}_2^\\dagger-z^*\\hat{a}_1\\hat{a}_2)$$\n", "$$z=re^{i\\theta}$$\n", @@ -2304,10 +2359,10 @@ "cir.s(wires=1, r=1, theta=0)\n", "cir.r(0, inputs=0)\n", "cir.r(1, inputs=0)\n", - "cir.s2([0,1], theta=0)\n", + "cir.s2([0, 1], theta=0)\n", "cir.d(wires=0, r=1, theta=0)\n", "cir.d(wires=1, r=1, theta=0)\n", - "cir.bs(wires=[0,1], inputs=[np.pi/4, np.pi/4])\n", + "cir.bs(wires=[0, 1], inputs=[np.pi / 4, np.pi / 4])\n", "cir.draw()" ] }, @@ -2424,8 +2479,8 @@ "cir.s(wires=1, r=1)\n", "cir.d(wires=0, r=1)\n", "cir.d(wires=1, r=1)\n", - "cir.bs(wires=[0,1], inputs=[np.pi/4, np.pi/4])\n", - "state = cir() # 线路演化后得到的高斯态用协方差矩阵和平均值刻画\n", + "cir.bs(wires=[0, 1], inputs=[np.pi / 4, np.pi / 4])\n", + "state = cir() # 线路演化后得到的高斯态用协方差矩阵和平均值刻画\n", "print(state)" ] }, @@ -2438,7 +2493,7 @@ }, { "cell_type": "code", - "execution_count": 44, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2025-04-25T09:06:48.186619Z", @@ -2458,8 +2513,8 @@ } ], "source": [ - "measure_re = cir.measure_homodyne(shots=1024, wires=[0,1]) # 对正交分量进行测量,这里的每个模对应一组正交分量X、P。\n", - "measure_re.shape" + "measure_re = cir.measure_homodyne(shots=1024, wires=[0, 1]) # 对正交分量进行测量,这里的每个模对应一组正交分量X、P。\n", + "print(measure_re.shape)" ] }, { @@ -2471,7 +2526,7 @@ }, { "cell_type": "code", - "execution_count": 45, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2025-04-25T09:06:48.198048Z", @@ -2492,7 +2547,7 @@ ], "source": [ "photon_mean, photon_var = cir.photon_number_mean_var()\n", - "photon_mean, photon_var" + "print(photon_mean, photon_var)" ] }, { @@ -2521,7 +2576,7 @@ } ], "source": [ - "state = cir(is_prob=True, detector='threshold') # 线路演化后得到的高斯态用协方差矩阵和平均值刻画\n", + "state = cir(is_prob=True, detector='threshold') # 线路演化后得到的高斯态用协方差矩阵和平均值刻画\n", "print(state)" ] }, @@ -2595,7 +2650,7 @@ "cir.s(wires=1, r=1)\n", "cir.d(wires=0, r=1)\n", "cir.d(wires=1, r=1)\n", - "cir.bs(wires=[0,1], inputs=[np.pi/4, np.pi/4])\n", + "cir.bs(wires=[0, 1], inputs=[np.pi / 4, np.pi / 4])\n", "state = cir(is_prob=True, detector='pnrd')\n", "samples = cir.measure(shots=100)\n", "print('使用概率分布进行采样', samples)\n", @@ -2606,7 +2661,7 @@ "cir.s(wires=1, r=1)\n", "cir.d(wires=0, r=1)\n", "cir.d(wires=1, r=1)\n", - "cir.bs(wires=[0,1], inputs=[np.pi/4, np.pi/4])\n", + "cir.bs(wires=[0, 1], inputs=[np.pi / 4, np.pi / 4])\n", "cir()\n", "samples = cir.measure(shots=100, mcmc=True)\n", "print('使用mcmc进行采样', samples)\n", @@ -2616,7 +2671,7 @@ "cir.s(wires=1, r=1)\n", "cir.d(wires=0, r=1)\n", "cir.d(wires=1, r=1)\n", - "cir.bs(wires=[0,1], inputs=[np.pi/4, np.pi/4])\n", + "cir.bs(wires=[0, 1], inputs=[np.pi / 4, np.pi / 4])\n", "cir()\n", "samples = cir.measure(shots=100, mcmc=False)\n", "print('使用chain-rule进行采样', samples)" @@ -2723,12 +2778,16 @@ ], "source": [ "# 采用6个节点的图演示简单的高斯玻色采样\n", - "a = np.array([[0., 1., 1., 0., 0., 0.],\n", - " [1., 0., 0., 1., 0., 1.],\n", - " [1., 0., 0., 0., 0., 0.],\n", - " [0., 1., 0., 0., 1., 1.],\n", - " [0., 0., 0., 1., 0., 0.],\n", - " [0., 1., 0., 1., 0., 0.]])\n", + "a = np.array(\n", + " [\n", + " [0.0, 1.0, 1.0, 0.0, 0.0, 0.0],\n", + " [1.0, 0.0, 0.0, 1.0, 0.0, 1.0],\n", + " [1.0, 0.0, 0.0, 0.0, 0.0, 0.0],\n", + " [0.0, 1.0, 0.0, 0.0, 1.0, 1.0],\n", + " [0.0, 0.0, 0.0, 1.0, 0.0, 0.0],\n", + " [0.0, 1.0, 0.0, 1.0, 0.0, 0.0],\n", + " ]\n", + ")\n", "gbs = dqp.GBS_Graph(adj_mat=a, cutoff=2)\n", "state = gbs()\n", "sample = gbs.measure(shots=5000, mcmc=True)\n", @@ -2737,7 +2796,7 @@ }, { "cell_type": "code", - "execution_count": 53, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2025-04-25T09:06:49.772823Z", @@ -2771,7 +2830,7 @@ } ], "source": [ - "sample" + "print(sample)" ] }, { @@ -2783,21 +2842,7 @@ }, { "cell_type": "code", - "execution_count": 54, - "metadata": { - "ExecuteTime": { - "end_time": "2025-04-25T09:06:49.783225Z", - "start_time": "2025-04-25T09:06:49.774836Z" - } - }, - "outputs": [], - "source": [ - "import networkx as nx" - ] - }, - { - "cell_type": "code", - "execution_count": 55, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2025-04-25T09:06:50.135453Z", @@ -2817,29 +2862,35 @@ } ], "source": [ - "graph_adj = np.array([[0., 1., 1., 0., 0., 0., 1., 0., 1., 1., 0., 0., 0., 0., 0., 0.],\n", - " [1., 0., 0., 0., 0., 0., 1., 1., 1., 0., 0., 0., 0., 0., 0., 0.],\n", - " [1., 0., 0., 1., 0., 1., 1., 0., 1., 1., 0., 0., 0., 0., 1., 0.],\n", - " [0., 0., 1., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n", - " [0., 0., 0., 1., 0., 1., 1., 0., 1., 0., 0., 0., 0., 1., 0., 0.],\n", - " [0., 0., 1., 0., 1., 0., 1., 0., 0., 1., 0., 0., 0., 0., 0., 1.],\n", - " [1., 1., 1., 0., 1., 1., 0., 0., 1., 0., 0., 1., 0., 0., 0., 0.],\n", - " [0., 1., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0.],\n", - " [1., 1., 1., 0., 1., 0., 1., 1., 0., 1., 0., 0., 0., 0., 0., 0.],\n", - " [1., 0., 1., 0., 0., 1., 0., 0., 1., 0., 0., 0., 0., 0., 1., 0.],\n", - " [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 1., 1., 1., 1.],\n", - " [0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 1., 0., 0., 1., 1., 1.],\n", - " [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 1., 1., 1.],\n", - " [0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 1., 1., 1., 0., 1., 1.],\n", - " [0., 0., 1., 0., 0., 0., 0., 0., 0., 1., 1., 1., 1., 1., 0., 1.],\n", - " [0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1., 1., 1., 1., 1., 0.]])\n", + "import networkx as nx\n", + "\n", + "graph_adj = np.array(\n", + " [\n", + " [0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],\n", + " [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],\n", + " [1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0],\n", + " [0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],\n", + " [0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0],\n", + " [0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0],\n", + " [1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0],\n", + " [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],\n", + " [1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],\n", + " [1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0],\n", + " [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0],\n", + " [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0],\n", + " [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0],\n", + " [0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0],\n", + " [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0],\n", + " [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0],\n", + " ]\n", + ")\n", "graph = nx.from_numpy_array(graph_adj)\n", "nx.draw(graph, with_labels=True)" ] }, { "cell_type": "code", - "execution_count": 56, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2025-04-25T09:14:10.637601Z", @@ -2867,7 +2918,7 @@ } ], "source": [ - "gbs = dqp.GBS_Graph(adj_mat=graph_adj, cutoff=2)\n", + "gbs = dqp.GraphGBS(adj_mat=graph_adj, cutoff=2)\n", "state = gbs()\n", "sample = gbs.measure(shots=50000, mcmc=True)" ] @@ -2881,7 +2932,7 @@ }, { "cell_type": "code", - "execution_count": 57, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2025-04-25T09:14:10.693980Z", @@ -2900,13 +2951,13 @@ "source": [ "subgraph_sample = gbs.postselect(sample, [6])\n", "subgraph_density = gbs.graph_density(graph, subgraph_sample[0])\n", - "key = list(subgraph_density.keys())\n", + "key = list(subgraph_density)\n", "print(key[0], subgraph_density[key[0]])" ] }, { "cell_type": "code", - "execution_count": 58, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2025-04-25T09:14:10.718226Z", @@ -3027,7 +3078,7 @@ } ], "source": [ - "subgraph_density" + "print(subgraph_density)" ] }, { @@ -3074,7 +3125,7 @@ "cir.s(wires=1, r=1)\n", "cir.d(wires=0, r=1)\n", "cir.d(wires=1, r=1)\n", - "cir.bs(wires=[0,1], inputs=[np.pi/4, np.pi/4])\n", + "cir.bs(wires=[0, 1], inputs=[np.pi / 4, np.pi / 4])\n", "cir.homodyne(wires=0, phi=0)\n", "# 线路可视化\n", "cir.draw()" @@ -3082,7 +3133,7 @@ }, { "cell_type": "code", - "execution_count": 49, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2025-04-25T09:14:10.768704Z", @@ -3092,41 +3143,9 @@ "outputs": [], "source": [ "state = cir()\n", - "sample = cir.measure_homodyne(shots=1) # 运行线路并进行一次采样\n", - "state_measured = cir.state_measured # 得到采样后坍缩的量子态" - ] - }, - { - "cell_type": "code", - "execution_count": 50, - "metadata": { - "ExecuteTime": { - "end_time": "2025-04-25T09:14:10.781043Z", - "start_time": "2025-04-25T09:14:10.770712Z" - } - }, - "outputs": [ - { - "data": { - "text/plain": [ - "(tensor(-0.3463),\n", - " [tensor([[[ 1.0000, 0.0000, 0.0000, 0.0000],\n", - " [ 0.0000, 1.9488, 0.0000, -1.8134],\n", - " [ 0.0000, 0.0000, 1.0000, 0.0000],\n", - " [ 0.0000, -1.8134, 0.0000, 2.2006]]]),\n", - " tensor([[[0.0000],\n", - " [2.4142],\n", - " [0.0000],\n", - " [2.0008]]])])" - ] - }, - "execution_count": 50, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "sample, state_measured" + "sample = cir.measure_homodyne(shots=1) # 运行线路并进行一次采样\n", + "state_measured = cir.state_measured # 得到采样后坍缩的量子态\n", + "print(sample, state_measured)" ] }, { @@ -3173,7 +3192,7 @@ "cir = dq.QumodeCircuit(nmode=nmode, init_state='vac', cutoff=3, backend='gaussian')\n", "cir.s(0)\n", "cir.s(1)\n", - "cir.bs_theta([0,1], [3])\n", + "cir.bs_theta([0, 1], [3])\n", "cir.delay(0, ntau=1, inputs=[3, 0], encode=True)\n", "cir.delay(1, ntau=1, inputs=[3, 0], encode=True)\n", "cir.homodyne(0)\n", @@ -3218,7 +3237,7 @@ }, { "cell_type": "code", - "execution_count": 53, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2025-04-25T09:14:10.842332Z", @@ -3262,7 +3281,7 @@ ], "source": [ "state = cir()\n", - "state" + "print(state)" ] }, { @@ -3288,7 +3307,7 @@ "cir = dq.QumodeCircuit(nmode=nmode, init_state='vac', cutoff=3, backend='gaussian')\n", "cir.s(0)\n", "cir.s(1)\n", - "cir.bs_theta([0,1], [3])\n", + "cir.bs_theta([0, 1], [3])\n", "cir.delay(0, ntau=1, inputs=[3, 0], encode=True)\n", "cir.delay(1, ntau=1, inputs=[3, 0], encode=True)\n", "cir.homodyne(0)\n", @@ -3321,12 +3340,12 @@ ], "source": [ "cir_global = cir.global_circuit(nstep=3, use_deepcopy=True)\n", - "cir_global.draw(unroll=True) # draw global circuit" + "cir_global.draw(unroll=True) # draw global circuit" ] }, { "cell_type": "code", - "execution_count": 56, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2025-04-25T09:14:10.917998Z", @@ -3346,7 +3365,7 @@ } ], "source": [ - "cir_global.ndata" + "print(cir_global.ndata)" ] }, { @@ -3383,7 +3402,7 @@ "source": [ "data = torch.tensor([0] * cir_global.ndata)\n", "state = cir_global(data=data)\n", - "cir_global.draw(unroll=True) # draw global circuit" + "cir_global.draw(unroll=True) # draw global circuit" ] }, { @@ -3423,7 +3442,7 @@ "cir = dq.QumodeCircuitTDM(nmode=nmode, init_state='vac', cutoff=3)\n", "cir.s(0)\n", "cir.s(1)\n", - "cir.bs_theta([0,1], [3])\n", + "cir.bs_theta([0, 1], [3])\n", "cir.delay(0, ntau=1, inputs=[3, 0])\n", "cir.delay(1, ntau=1, inputs=[3, 0])\n", "cir.homodyne(0)\n", @@ -3440,7 +3459,7 @@ }, { "cell_type": "code", - "execution_count": 59, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2025-04-25T09:14:11.076963Z", @@ -3462,7 +3481,7 @@ ], "source": [ "cir(nstep=3)\n", - "cir.samples" + "print(cir.samples)" ] }, { @@ -3475,7 +3494,7 @@ }, { "cell_type": "code", - "execution_count": 60, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2025-04-25T09:14:11.203978Z", @@ -3501,12 +3520,11 @@ "nmode = 1\n", "cir = dq.QumodeCircuitTDM(nmode=nmode, init_state='vac', cutoff=3)\n", "cir.s(0, r=r)\n", - "cir.delay(0, ntau=1, inputs=[np.pi/2, np.pi/2], encode=True) # 参数编码\n", + "cir.delay(0, ntau=1, inputs=[np.pi / 2, np.pi / 2], encode=True) # 参数编码\n", "cir.homodyne_x(0)\n", - "data = torch.tensor([[np.pi/2, np.pi/2],\n", - " [np.pi/4, 0]]).unsqueeze(0) # 周期性参数组合\n", + "data = torch.tensor([[np.pi / 2, np.pi / 2], [np.pi / 4, 0]]).unsqueeze(0) # 周期性参数组合\n", "cir(data=data, nstep=13)\n", - "cir.samples" + "print(cir.samples)" ] }, { @@ -3602,7 +3620,9 @@ ], "source": [ "bosonic_state_1 = dq.BosonicState(state='vac', nmode=2)\n", - "bosonic_state_2 = dq.BosonicState(state=[torch.eye(4).expand(2,4,4), torch.zeros(4).expand(2,4) + 0j, torch.ones(2) / 2 + 0j], nmode=2)\n", + "bosonic_state_2 = dq.BosonicState(\n", + " state=[torch.eye(4).expand(2, 4, 4), torch.zeros(4).expand(2, 4) + 0j, torch.ones(2) / 2 + 0j], nmode=2\n", + ")\n", "print('state_1_cov_mean', bosonic_state_1.cov, bosonic_state_1.mean, bosonic_state_1.weight)\n", "print('state_2_cov_mean', bosonic_state_2.cov, bosonic_state_2.mean, bosonic_state_2.weight)" ] @@ -3642,8 +3662,8 @@ "qrange = 5\n", "prange = 5\n", "npoints = 100\n", - "wigner = bosonic_state_1.wigner(wire=0, qrange=qrange, prange=prange, npoints=npoints) # Wigner函数可视化\n", - "marginal = bosonic_state_1.marginal(wire=0, qrange=qrange) # 边缘分布p(x)可视化" + "wigner = bosonic_state_1.wigner(wire=0, qrange=qrange, prange=prange, npoints=npoints) # Wigner函数可视化\n", + "marginal = bosonic_state_1.marginal(wire=0, qrange=qrange) # 边缘分布p(x)可视化" ] }, { @@ -3717,8 +3737,8 @@ "source": [ "# 猫态实例化\n", "cat = dq.CatState(r=1, theta=0)\n", - "wigner = cat.wigner(wire=0, qrange=5, prange=5, npoints=100) # Wigner函数可视化\n", - "marginal = cat.marginal(wire=0, qrange=5) # 边缘分布p(x)可视化" + "wigner = cat.wigner(wire=0, qrange=5, prange=5, npoints=100) # Wigner函数可视化\n", + "marginal = cat.marginal(wire=0, qrange=5) # 边缘分布p(x)可视化" ] }, { @@ -3754,9 +3774,9 @@ ], "source": [ "# GKP态实例化\n", - "gkp_0 = dq.GKPState(theta=0., phi=0.)\n", - "wigner = gkp_0.wigner(wire=0, qrange=10, prange=10, npoints=500) # Wigner函数可视化\n", - "marginal = gkp_0.marginal(wire=0, qrange=10)# 边缘分布p(x)可视化" + "gkp_0 = dq.GKPState(theta=0.0, phi=0.0)\n", + "wigner = gkp_0.wigner(wire=0, qrange=10, prange=10, npoints=500) # Wigner函数可视化\n", + "marginal = gkp_0.marginal(wire=0, qrange=10) # 边缘分布p(x)可视化" ] }, { @@ -3775,7 +3795,7 @@ }, { "cell_type": "code", - "execution_count": 65, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2025-04-25T09:14:18.288240Z", @@ -3812,7 +3832,7 @@ ], "source": [ "bosonic_fock2 = dq.FockStateBosonic(n=2, r=0.1)\n", - "bosonic_fock2.cov, bosonic_fock2.mean, bosonic_fock2.weight" + "print(bosonic_fock2.cov, bosonic_fock2.mean, bosonic_fock2.weight)" ] }, { @@ -3890,13 +3910,13 @@ ], "source": [ "cir = dq.QumodeCircuit(nmode=2, init_state='vac', backend='bosonic')\n", - "cir.cat(wires=0, r=1) # 设置猫态\n", - "cir.gkp(wires=1, theta=0., phi=0.) # 设置GKP态\n", + "cir.cat(wires=0, r=1) # 设置猫态\n", + "cir.gkp(wires=1, theta=0.0, phi=0.0) # 设置GKP态\n", "cir.s(wires=0, r=1)\n", "cir.s(wires=1, r=1)\n", "cir.d(wires=0, r=1)\n", "cir.d(wires=1, r=1)\n", - "cir.bs(wires=[0,1], inputs=[np.pi/4, np.pi/4])\n", + "cir.bs(wires=[0, 1], inputs=[np.pi / 4, np.pi / 4])\n", "state = cir()\n", "for i in state:\n", " print(i.shape)" @@ -3918,7 +3938,7 @@ }, { "cell_type": "code", - "execution_count": 68, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2025-04-25T09:14:20.120955Z", @@ -3941,8 +3961,8 @@ "cir = dq.QumodeCircuit(nmode=2, init_state='vac', backend='bosonic')\n", "cir.cat(wires=0, r=1)\n", "state = cir()\n", - "samples = cir.measure_homodyne(wires=0, shots=100) # 对正交分量进行测量,这里的每一条线路对应一组正交分量X、P。\n", - "samples.shape" + "samples = cir.measure_homodyne(wires=0, shots=100) # 对正交分量进行测量,这里的每一条线路对应一组正交分量X、P。\n", + "print(samples.shape)" ] }, { @@ -3954,7 +3974,7 @@ }, { "cell_type": "code", - "execution_count": 69, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2025-04-25T09:14:20.277902Z", @@ -3978,13 +3998,13 @@ "cir.cat(wires=0, r=1)\n", "cir.homodyne(wires=0, phi=0)\n", "state = cir()\n", - "samples = cir.measure_homodyne(shots=100) # 对正交分量进行测量,这里的每一条线路对应一个正交分量。\n", - "samples.shape" + "samples = cir.measure_homodyne(shots=100) # 对正交分量进行测量,这里的每一条线路对应一个正交分量。\n", + "print(samples.shape)" ] }, { "cell_type": "code", - "execution_count": 70, + "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2025-04-25T09:14:20.321300Z", @@ -4005,7 +4025,7 @@ ], "source": [ "state_measured = cir.state_measured\n", - "state_measured[0].shape" + "print(state_measured[0].shape)" ] }, { @@ -4094,8 +4114,8 @@ ], "source": [ "cir = dq.QumodeCircuit(nmode=2, init_state='vac', backend='bosonic')\n", - "cir.s(1, 1) # squeezing vac state\n", - "cir.s2([0,1], r=0.1) # SPDC operator\n", + "cir.s(1, 1) # squeezing vac state\n", + "cir.s2([0, 1], r=0.1) # SPDC operator\n", "state = cir()\n", "cir.draw()" ] @@ -4119,8 +4139,8 @@ "outputs": [], "source": [ "prnd = dqp.PhotonNumberResolvingBosonic(n=1, r=0.05, nmode=2, wires=0)\n", - "bs_lst = dqp.qmath.align_shape(*state) # 维度对齐\n", - "bs_lst2 = prnd(bs_lst) # 量子态测量坍缩\n", + "bs_lst = dqp.qmath.align_shape(*state) # 维度对齐\n", + "bs_lst2 = prnd(bs_lst) # 量子态测量坍缩\n", "bs = dq.BosonicState(bs_lst2, nmode=2)" ] }, @@ -4158,15 +4178,15 @@ "source": [ "# 可视化验证\n", "marginal_q = bs.marginal(wire=1, qrange=5, npoints=500)\n", - "marginal_p = bs.marginal(wire=1, phi=np.pi/2, qrange=10, npoints=500)" + "marginal_p = bs.marginal(wire=1, phi=np.pi / 2, qrange=10, npoints=500)" ] } ], "metadata": { "kernelspec": { - "display_name": "dq_draw", + "display_name": "dq", "language": "python", - "name": "dq_draw" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -4178,7 +4198,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.0" + "version": "3.12.0" }, "toc": { "base_numbering": 1, diff --git a/tutorials/photonic_basics.py b/tutorials/photonic_basics.py new file mode 100644 index 00000000..f3d30123 --- /dev/null +++ b/tutorials/photonic_basics.py @@ -0,0 +1,1476 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.19.1 +# kernelspec: +# display_name: dq +# language: python +# name: python3 +# --- + +# %% +import random + +import deepquantum as dq +import deepquantum.photonic as dqp +import numpy as np +import torch + +print('version', dq.__version__) + +seed = 42 + +random.seed(seed) +np.random.seed(seed) +torch.manual_seed(seed) +if torch.cuda.is_available(): + torch.cuda.manual_seed(seed) + torch.cuda.manual_seed_all(seed) + torch.backends.cudnn.deterministic = True + torch.backends.cudnn.benchmark = False + +# %% [markdown] +# # 基于Fock后端构建光量子线路 + +# %% [markdown] +# ## Fock态 +# +# 光量子线路Fock态(量子数态)可以表示为 $ \left |n_1,n_2,...,n_m\right \rangle$,其中 $ n_k $ 表示第 $k$ 模的光子数。 +# 在DeepQuantum的Photonic模块中,Fock态用 ``FockState`` 表示。 +# +# 我们利用 ``FockState`` 准备一个3模的量子态,其中第一、三模光子数为1,第二模为0: + +# %% +qstate = dq.FockState(state=[1, 0, 1]) +print(qstate) + +# %% [markdown] +# 其数据为torch的tensor,存在属性 ``state`` 中: + +# %% +print(qstate.state) + +# %% [markdown] +# 输入态的类型也可以是 ``torch.Tensor`` ,并支持 ``batch`` 输入: + +# %% +state = torch.tensor([1, 0, 1]) +qstate = dq.FockState(state=state) +print(qstate) + +batch_state = torch.randint(0, 2, (4, 3)) +print(batch_state) +qstate = dq.FockState(state=batch_state) +print(qstate) + +# %% [markdown] +# ``state`` 的输入也可以是真空态'vac'或全零态'zeros',表示所有模式均无光子: + +# %% +vacuum_state = dq.FockState(state='vac', nmode=3) +print(vacuum_state) + +zeros_state = dq.FockState(state='zeros', nmode=3) +print(zeros_state) + +# %% [markdown] +# 其中,可选输入参数 ``nmode`` 表示模式数。当模式数小于 ``state`` 代表的模式数时,会根据 ``nmode`` 截断;当模式数更大时,会自动补0: + +# %% +tc_state = torch.tensor([1, 0, 1, 0]) +qstate = dq.FockState(state=tc_state, nmode=3) +print(qstate) + +qstate = dq.FockState(state=tc_state, nmode=5) +print(qstate) + +# %% [markdown] +# 最后一个可选参数 ``basis`` 默认为 ``True``,当输入为叠加态(如 $\frac{\sqrt{2}}{2}\left |1,0,1,0\right \rangle + \frac{\sqrt2}{2}\left |0,1,1,0\right \rangle $)时,需要设置为 ``False``。此时Fock态由带batch的张量积表示,cutoff为总光子数和+1: + +# %% +qstate = dq.FockState(state=[(np.sqrt(2) / 2, [1, 0, 1, 0]), (np.sqrt(2) / 2, [0, 1, 1, 0])], basis=False) +print(qstate) + +# 得到batch形式的Fock state tensor +print(qstate.state.size()) + +# %% [markdown] +# 另外, ``FockState`` 支持根据字典序比较大小,从而实现对采样后的字典根据Fock态进行排序: + +# %% +state_a = dq.FockState(state=[0, 0, 1]) +state_a2 = dq.FockState(state=[0, 0, 1]) +state_b = dq.FockState(state=[0, 0, 2]) + +print(state_a == state_a2) +print(state_a < state_b) + +# 构建一个简单Qumode线路 +cir = dq.QumodeCircuit(nmode=3, init_state=[1, 2, 0]) +cir.bs(wires=[0, 1], inputs=[np.pi / 3, np.pi / 3]) +cir.bs(wires=[1, 2], inputs=[np.pi / 3, np.pi / 3]) +# 前向函数 +cir() +# 采样测量 +result_dict = cir.measure() +print('采样结果(默认按样本数由高到低排序):', result_dict) + +# 对采样结果按Fock态排序 +sorted_dict = dict(sorted(result_dict.items(), key=lambda x: x[0], reverse=True)) +print('采样结果(按样本态字典序排序):', sorted_dict) + +# %% [markdown] +# ## 构建光量子线路 + +# %% [markdown] +# ### 初始化 +# +# 光量子线路对应着DeepQuantum Photonic模块的 ``QumodeCircuit`` 类。仅需指定空间模式数 ``nmode`` 和初态 ``init_state``,即可完成初始化: + +# %% +cir = dq.QumodeCircuit(nmode=3, init_state=[1, 0, 1]) + +# %% [markdown] +# 类似 ``FockState`` ,``init_state`` 也支持输入为torch.tensor。输入还支持使用 ``FockState``: + +# %% +cir1 = dq.QumodeCircuit(nmode=3, init_state=torch.tensor([1, 0, 1])) + +fockstate = dq.FockState(state=[1, 0, 1]) +cir2 = dq.QumodeCircuit(nmode=3, init_state=fockstate) + +# %% [markdown] +# 当量子态空间较大时,可以在初始化时,设定光子截断数 ``cutoff`` 来降低计算复杂度,表示每模的光子数上限+1。默认值为总光子数+1。 + +# %% +# 无cutoff时 +cir = dq.QumodeCircuit(nmode=3, init_state=[2, 1, 0]) +print('默认cutff时,末态概率分布为:\n', cir(is_prob=True)) + +# 设定cutoff为3,即每模不超过2光子 +cir = dq.QumodeCircuit(nmode=3, init_state=[2, 1, 0], cutoff=3) +print('cutff=3时,末态概率分布为:\n', cir(is_prob=True)) + +# %% [markdown] +# ### 量子门 +# +# 初始化后,可以加入各种光量子门 +# +# 当前模拟器支持加入下列门: +# + +# %% [markdown] +# - ``ps``:移相器,对应的酉算符表示如下, +# $${PS}(\theta) = \exp(i \theta \hat{a}^\dagger \hat{a})$$ +# 作用在生成算符 $\hat{a}^\dagger$ 上的酉变换矩阵如下, +# $${PS}(\theta)\hat{a}^\dagger{PS}^\dagger(\theta) = U^T(\theta)\hat{a}^\dagger$$ +# $$U(\theta) = e^{i\theta}$$ +# +# ``ps`` 中, ``wires`` 参数表示作用的空间mode(数据类型为 ``int`` ), ``inputs`` 输入移相器对应的相位角 $\theta$ +# + +# %% [markdown] +# - ``bs``:可调分束器, 对应的酉算符表示如下 +# $$BS(\theta, \phi) = \exp[\theta(e^{i\phi }\hat{a}_1\hat{a}_2^\dagger-e^{-i\phi}\hat{a}_1^\dagger \hat{a}_2)]$$ +# 作用在生成算符 $\hat{a}^\dagger$ 上的酉变换矩阵如下 +# $$BS(\theta,\phi)\hat{a}^\dagger BS^\dagger(\theta,\phi) = U^T(\theta,\phi)\hat{a}^\dagger$$ +# $$U(\theta, \phi) = \begin{pmatrix} \cos\left(\theta\right) & -e^{-i\phi} \sin\left(\theta\right) \\ e^{i\phi} \sin\left(\theta\right) & \cos\left(\theta\right) \\ \end{pmatrix}$$ +# 其中,$\theta, \phi$ 对应 ``inputs`` 两个参数,而``wires``的参数表示作用的两个空间mode(数据类型为``List[int]``)。 +# +# 特殊的,定义了若干``inputs``只有一个参数的bs门: + +# %% [markdown] +# - ``bs_theta``:指定 $\phi = \pi/2$ 时,单参数 $\theta$ 可调分束器。 +# +# 作用在生成算符 $\hat{a}^\dagger$ 上的酉变换矩阵如下, +# $$BS(\theta) \hat{a}^\dagger BS^\dagger(\theta) = U^T(\theta)\hat{a}^\dagger$$ +# +# $$U(\theta) = \begin{pmatrix} \cos\left(\theta\right) & i\sin\left(\theta\right) \\ i\sin\left(\theta\right) & \cos\left(\theta\right) \\ \end{pmatrix}$$ + +# %% [markdown] +# - ``bs_phi``:指定 $\theta = \pi/4$ 时,单参数 $\phi$ 可调分束器。 +# +# 作用在生成算符 $\hat{a}^\dagger$ 上的酉变换矩阵如下 +# $$BS(\phi)\hat{a}^\dagger BS^\dagger (\phi) = U^T(\phi)\hat{a}^\dagger$$ +# $$ U(\phi) = \begin{pmatrix} \frac{\sqrt{2}}{2} & -\frac{\sqrt{2}}{2}e^{-i\phi} \\ \frac{\sqrt{2}}{2}e^{i\phi} & \frac{\sqrt{2}}{2} \\ \end{pmatrix}$$ + +# %% [markdown] +# - ``bs_rx`` :Rx 型分束器, +# +# 作用在生成算符 $\hat{a}^\dagger$ 上的酉变换矩阵如下, +# $${R_x}(\theta)\hat{a}^\dagger{R_x}^\dagger(\theta) = U^T(\theta)\hat{a}^\dagger$$ +# +# $$ U(\theta) = \begin{pmatrix} \cos\left(\frac{\theta}{2}\right) & i\sin\left(\frac{\theta}{2}\right) \\ i\sin\left(\frac{\theta}{2}\right) & \cos\left(\frac{\theta}{2}\right) \\ \end{pmatrix}$$ + +# %% [markdown] +# - ``bs_ry`` : Ry 型分束器, +# +# 作用在生成算符 $\hat{a}^\dagger$ 上的酉变换矩阵如下, +# $$R_y(\theta)\hat{a}^\dagger R_y^\dagger(\theta) = U^T(\theta)\hat{a}^\dagger$$ +# +# $$U(\theta) = \begin{pmatrix} \cos\left(\frac{\theta}{2}\right) & -\sin\left(\frac{\theta}{2}\right) \\ \sin\left(\frac{\theta}{2}\right) & \cos\left(\frac{\theta}{2}\right) \\ \end{pmatrix}$$ + +# %% [markdown] +# - ``bs_h`` :H型分束器, 即指定 $\phi = 0$ +# +# 作用在生成算符 $\hat{a}^\dagger$ 上的酉变换矩阵如下, +# +# $$H(\theta)\hat{a}^\dagger H^\dagger(\theta) = U^T(\theta)\hat{a}^\dagger$$ +# $$U(\theta) = \begin{pmatrix} \cos\left(\frac{\theta}{2}\right) & \sin\left(\frac{\theta}{2}\right) \\ \sin\left(\frac{\theta}{2}\right) & -\cos\left(\frac{\theta}{2}\right) \\ \end{pmatrix}$$ + +# %% [markdown] +# - ``h``:H型50:50分束器, 即指定 $ \theta = \pi/2$ 时的 ``bs_h`` (或 $ \phi = 0, \theta = \pi/4 $ 时的 ``bs``) +# +# 作用在生成算符 $\hat{a}^\dagger$ 上的酉变换矩阵如下, +# +# $$H\hat{a}^\dagger H^\dagger = U^T\hat{a}^\dagger$$ +# $$U = \frac{\sqrt{2}}{2}\begin{pmatrix} +# 1&1\\ +# 1&-1\\ +# \end{pmatrix}$$ + +# %% [markdown] +# - ``dc`` :50:50 定向耦合器, 即指定 $ \phi = \pi/2, \theta = \pi/4 $ 时的 ``bs``。 +# +# 作用在生成算符 $\hat{a}^\dagger$ 上的酉变换矩阵如下 +# +# $$DC\ \hat{a}^\dagger DC^\dagger = U^T\hat{a}^\dagger$$ +# $$ U =\frac{\sqrt{2}}{2}\begin{pmatrix} +# 1&i\\ +# i&1 \end{pmatrix}$$ + +# %% [markdown] +# 与 ``h`` 相比,``dc`` 会改变相位,因而虽然分束比例都是50:50,末态振幅并不相同。 + +# %% +cir = dq.QumodeCircuit(2, init_state=[1, 0]) +cir.h(wires=[0, 1]) +prob_h = cir(is_prob=True) +amplitude_h = cir(is_prob=False) + +print('通过H型50:50分束器后末态概率分布为:', prob_h) +print('通过H型50:50分束器后末态振幅为:', amplitude_h) + +cir = dq.QumodeCircuit(2, init_state=[1, 0]) +cir.dc(wires=[0, 1]) +prob_dc = cir(is_prob=True) +amplitude_dc = cir(is_prob=False) + +print('通过50:50 定向耦合器后末态概率分布为:', prob_dc) +print('通过50:50 定向耦合器后末态振幅分布为:', amplitude_dc) + +# %% [markdown] +# - ``mzi`` :马赫-曾德尔干涉仪(Mach-Zehnder interferometer)采用移相器-分束器-移相器-分束器的结构(先 $\phi$后 $\theta$), +# 对应的酉算符表示如下 +# +# $$MZI(\theta, \phi) = BS(\frac{\pi}{4}, \frac{\pi}{2})(PS(\theta)\otimes I)BS(\frac{\pi}{4}, \frac{\pi}{2}) +# (PS(\phi)\otimes I)$$ +# 作用在生成算符 $\hat{a}^\dagger$ 上的酉变换矩阵如下 +# $$MZI(\theta,\phi)\hat{a}^\dagger MZI^\dagger(\theta,\phi) = U^T(\theta,\phi)\hat{a}^\dagger$$ +# $$MZI_{PT} = ie^{i\theta/2} \begin{pmatrix} e^{i\phi} \sin\left(\frac{\theta}{2}\right) & \cos\left(\frac{\theta}{2}\right) \\ e^{i\phi} \cos\left(\frac{\theta}{2}\right) & -\sin\left(\frac{\theta}{2}\right) \\ \end{pmatrix}$$ + +# %% [markdown] +# +# 可以通过参数 ``phi_first=False`` 切换至另一种分束器-移相器-分束器-移相器的结构 (先 $\theta$后 $\phi$), 对应的酉变换矩阵如下 +# +# $$ +# MZI_{TP} = +# ie^{i\theta/2} +# \begin{pmatrix} +# e^{i\phi}\sin{\frac{\theta}{2}}&e^{i\phi}\cos{\frac{\theta}{2}}\\ +# \cos{\frac{\theta}{2}}&-\sin{\frac{\theta}{2}}\\ +# \end{pmatrix} +# $$ +# + +# %% +cir = dq.QumodeCircuit(nmode=3, init_state=[1, 0, 1]) +cir.ps(wires=0, inputs=0) # 相位角为0,作用第0个mode上 +cir.ps(1, 0) # 也可省略参数名 +cir.bs_theta([0, 1], torch.pi / 4) # 单参数theta可调分束器,theta为pi/4 作用在第0、1个mode上 +cir.h(wires=[1, 2]) # H型50:50分束器 +cir.bs_rx(wires=[1, 2]) # Rx型分束器 +cir.bs_ry(wires=[0, 1]) # Ry型分束器 +cir.dc(wires=[1, 2]) # 50:50定向耦合器 +cir.mzi(wires=[0, 1]) # mzi干涉仪 + +# %% [markdown] +# 通过调用 ``draw()`` 函数实现线路可视化: + +# %% +cir.draw() + +# %% [markdown] +# 通过调用 ``get_unitary()`` 函数获取线路酉矩阵: + +# %% +cir.get_unitary() + + +# %% [markdown] +# 另外,支持用户添加任意自定义的酉矩阵门,通过 ``any`` 或 ``clements``来实现。``any`` 直接将酉矩阵作用于线路,支持自动微分梯度的运算;而 ``clements`` 会先将酉矩阵分解为Clements架构的线路参数,再把对应的Clements网络作用在线路上,是一种对应的物理实现,暂不支持自动微分。 +# +# 以下展示了初态为 $ \left |1,0,1\right \rangle $ 的光量子线路通过优化``any``门参数得到末态全部为 $ \left |1,1,0\right \rangle $ 的实例。 + + +# %% +from deepquantum.qmath import is_unitary +from torch import nn + + +# 通过旋转矩阵构建 3x3 酉矩阵 +def unitary_3x3(alpha, beta, gamma, delta): + r1 = torch.stack( + [ + torch.cos(alpha), + -torch.sin(alpha), + torch.tensor(0), + torch.sin(alpha), + torch.cos(alpha), + torch.tensor(0), + torch.tensor(0), + torch.tensor(0), + torch.tensor(1), + ] + ).reshape(3, 3) + + r2 = torch.stack( + [ + torch.cos(beta), + torch.tensor(0), + torch.sin(beta), + torch.tensor(0), + torch.tensor(1), + torch.tensor(0), + -torch.sin(beta), + torch.tensor(0), + torch.cos(beta), + ] + ).reshape(3, 3) + + r3 = torch.stack( + [ + torch.tensor(1), + torch.tensor(0), + torch.tensor(0), + torch.tensor(0), + torch.cos(gamma), + -torch.sin(gamma), + torch.tensor(0), + torch.sin(gamma), + torch.cos(gamma), + ] + ).reshape(3, 3) + + u = torch.matmul(torch.matmul(r1, r2), r3) + + # 加入全局相位,any门需要输入复数矩阵 + unitary = u * torch.exp(1j * delta) + return unitary + + +# 生成随机角度作为初始参数,构建酉矩阵。any门支持梯度运算 +para = nn.Parameter(torch.rand(4, requires_grad=True)) +unitary = unitary_3x3(para[0], para[1], para[2], para[3]) + +# 检查是否为酉矩阵 +print(is_unitary(unitary)) + + +def get_prob_110(para): + unitary = unitary_3x3(para[0], para[1], para[2], para[3]) + + cir = dq.QumodeCircuit(nmode=3, init_state=[1, 0, 1]) + cir.any(wires=[0, 1, 2], unitary=unitary) + x = cir(is_prob=True) + + # 得到末态为1,1,0的概率 + return x[dq.FockState([1, 1, 0])] + + +# 使用Adam优化器 +optimizer = torch.optim.Adam([para], lr=0.1) +for _ in range(100): + optimizer.zero_grad() + prob_110 = get_prob_110(para) + loss = torch.abs(1 - prob_110) + if loss < 1e-5: + break + loss.backward() # 反向传播 + optimizer.step() # 更新参数 +print('梯度下降优化后末态为110的概率为:', get_prob_110(para)) + +cir.draw() + +# %% +# clements门不支持梯度 +para = torch.rand(4) +unitary = unitary_3x3(para[0], para[1], para[2], para[3]) + +# 检查是否为酉矩阵 +is_unitary(unitary) + +cir = dq.QumodeCircuit(nmode=3, init_state=[1, 0, 1]) +cir.clements(wires=[0, 1, 2], unitary=unitary) +cir.draw() + +# %% [markdown] +# 给线路添加量子门的逻辑是将实例化的``Gate``类添加至 ``QumodeCircuit.operators``。 它包含了线路所有量子门的操作, 可以对具体的门调用 ``get_unitary`` 查看局部的酉矩阵: + +# %% +# 实例化一个单参数theta可调分束器,theta为pi/4 作用在第0、1个mode上 +from deepquantum.photonic.gate import BeamSplitterTheta + +bs_theta = BeamSplitterTheta(nmode=3, wires=[0, 1], inputs=torch.pi / 4) +print('实例化的单参数theta可调分束器为:', bs_theta) +print('对应的酉矩阵为:', bs_theta.get_unitary()) + +cir = dq.QumodeCircuit(nmode=3, init_state=[1, 0, 1]) +cir.ps(wires=0, inputs=0) # 相位角为0,作用第0个mode上 +cir.ps(1, 0) # 也可省略参数名 +cir.bs_theta([0, 1], torch.pi / 4) # 单参数theta可调分束器,theta为pi/4 作用在第0、1个mode上 +cir.h(wires=[1, 2]) # H型50:50分束器 + +print(cir.operators) + +# 第3个operator等价于刚实例化的bs_theta +print(cir.operators[2]) +print(cir.operators[2].get_unitary()) + +# %% [markdown] +# ### 前向演化 +# +# 调用前向函数对线路进行演化。可以通过改变 ``is_prob`` 的值以控制返回的类型: +# +# ``is_prob=None`` (默认):返回量子线路对应的酉矩阵 +# +# ``is_prob=True`` :返回末态的概率分布 +# +# ``is_prob=False`` :返回末态的振幅分布 +# +# + +# %% +cir = dq.QumodeCircuit(nmode=3, init_state=[1, 0, 1]) +cir.ps(wires=0, inputs=0) # 相位角为0,作用第0个mode上 +cir.ps(1, 0) # 也可省略参数名 +cir.bs_theta([0, 1], torch.pi / 4) # 单参数theta可调分束器,theta为pi/4 作用在第0、1个mode上 +cir.h(wires=[1, 2]) # H型50:50分束器 + +print(cir()) +print(cir(is_prob=True)) +print(cir(is_prob=False)) + +# %% [markdown] +# 支持通过 ``data`` 输入线路中 ``encode=True`` 门的参数,通过 ``state`` 指定初态,对线路进行演化: + +# %% +cir2 = dq.QumodeCircuit(2, init_state=[1, 0, 1]) +cir2.ps(wires=0, encode=True) # 相位角为参数,作用第0个mode上 +cir2.ps(1, encode=True) # 也可省略参数名 +cir2.bs_theta([0, 1], encode=True) # theta为参数,phi为定值pi/2 作用在第0、1个mode上 + +# 构建data作为输入参数 +npara = 3 +data = torch.randn(npara) +print('线路参数为:', data) + +# 完成前向 +cir2(data) +cir2.draw() + +# %% +# 指定初态前向演化 +print(cir2(state=[0, 2, 0])) + +# 同时指定线路参数和初态前向演化,并返回概率分布 +cir2(data, state=[1, 1, 1], is_prob=True) + +# %% [markdown] +# ### 采样测量 +# +# 可以对线路进行测量,返回的结果是字典或者字典的列表,字典的key是Fock态,value是对应测量到的次数,shots默认为1024。 + +# %% +cir = dq.QumodeCircuit(nmode=3, init_state=[1, 0, 1]) +cir.ps(wires=0) # 作用第0个mode上 +cir.ps(1, 0) # 也可省略参数名 +cir.bs_theta([0, 1]) # 单参数theta可调分束器,作用在第0、1个mode上 +cir.h(wires=[1, 2]) # H型50:50分束器 +cir() + +cir.measure() + +# %% [markdown] +# 设定 ``with_prob=True`` 同时返回理论期望值: + +# %% +cir.measure(with_prob=True) + +# %% [markdown] +# 通过设定 ``wires`` 进行部分测量: + +# %% +cir.measure(wires=[0, 1]) + +# %% [markdown] +# 如需要对量子线路进行马尔科夫链蒙特卡洛采样,打开 ``mcmc=True`` 开关即可。 +# +# 通常只在前向返回酉矩阵,即默认 ``is_prob=None`` 的情况下使用MCMC采样,因为其他情况做前向时已经计算了全空间理论的概率分布。 + +# %% +cir() +cir.measure(mcmc=True) + +# %% [markdown] +# 通过 ``get_amplitude()`` 或 ``get_prob()`` 获得指定末态的振幅/概率: + +# %% +print(cir.get_amplitude([1, 1, 0])) +print(cir.get_prob([1, 0, 1])) + +# %% [markdown] +# ## 支持batch输入 + +# %% [markdown] +# ### 初态的batch输入 + +# %% [markdown] +# 无线路参数输入的情况: + +# %% +# 构建batch的初态 +batch_state = torch.randint(0, 3, (4, 2)) +print('初态为:', batch_state) + +cir = dq.QumodeCircuit(nmode=2, init_state=batch_state) +cir.bs_theta([0, 1]) + +unitary = cir() +print('线路对应酉矩阵为:', unitary) + +# 测量并输出结果 +cir.measure() + +# %% [markdown] +# 有线路参数时: + +# %% +# 构建batch的初态 +batch_state = torch.randint(0, 3, (4, 2)) +print('初态为:', batch_state) + +cir = dq.QumodeCircuit(nmode=2, init_state=batch_state) +cir.bs_theta([0, 1], encode=True) +cir.ps(0, encode=True) + +# 构建一组线路参数 +para = torch.randn(1, 2) # 1组数据,2个参数 +unitary = cir(para) +print('线路对应酉矩阵为:', unitary) + +# 测量并输出结果 +cir.measure() + +# %% [markdown] +# ### 线路参数的batch输入 + +# %% +# 构建batch的线路参数 +batch_size = 4 +npara = 1 +batch_para = torch.randn(batch_size, npara) +print('线路参数为:', batch_para) + +cir = dq.QumodeCircuit(nmode=2, init_state=[0, 1]) +cir.bs_theta([0, 1], encode=True) + +unitary = cir(data=batch_para) +print('线路对应酉矩阵为:', unitary) + +# 测量并输出结果 +cir.measure() + +# %% [markdown] +# 注意: 当初态和线路参数都为带batch的输入时,线路会认为每组参数是一对一的,例如初态的第一组参数对应线路的第一组参数,因而batch size需要一致。 + +# %% +# 构建batch初态和线路参数 +batch_size = 4 +nmode = 2 +npara = 1 +batch_state = torch.randint(0, 2, (batch_size, nmode)) +batch_para = torch.randn(batch_size, npara) +print('batch数为:', batch_size) +print('初态shape为:', batch_state.shape) +print('线路参数shape为:', batch_para.shape) + + +cir = dq.QumodeCircuit(nmode=nmode, init_state=batch_state) +cir.bs_theta([0, 1], encode=True) + +unitary = cir(data=batch_para) +print('线路对应酉矩阵为:', unitary) + +# 测量并输出结果 +cir.measure() + +# %% [markdown] +# ## 支持简单的噪声模拟(Gaussian noise) +# +# 噪声模拟使用 ``QumodeCircuit(noise=True)``,这里默认所有参数的噪声都是高斯分布,平均值 ``mu`` 为0,标准差 ``sigma`` 为0.1,支持自定义整条线路的全局噪声。 + +# %% +cir_n = dq.QumodeCircuit(nmode=3, init_state=[1, 0, 1], noise=True, mu=0.3, sigma=0.2) +cir_n.ps(wires=0, encode=True) +cir_n.ps(1, encode=True) +cir_n.bs_theta([0, 1], encode=True) + +# 定义线路参数,并前向演化保存末态 +para = torch.randn(3) +final_state_n = cir_n(para, is_prob=False) + +print('采样结果为:', cir_n.measure(with_prob=True)) + +# %% [markdown] +# 也可以通过量子门的 ``mu`` 和 ``sigma`` 单独指定某些光学器件的噪声,此时会覆盖全局定义的 ``mu`` 和 ``sigma``。 + +# %% +cir_n = dq.QumodeCircuit(nmode=3, init_state=[1, 0, 1], noise=True, mu=0.3, sigma=0.2) +cir_n.ps(wires=0, encode=True, mu=0.2, sigma=0) # 支持自定义mu和sigma的值,默认为0、0.1 +cir_n.ps(1, encode=True) +cir_n.bs_theta([0, 1], encode=True, mu=0, sigma=0.3) + +print(f'第一个ps门mu = {cir_n.operators[0].mu}, sigma = {cir_n.operators[0].sigma}') # 覆盖全局mu、sigma +print(f'第二个ps门mu = {cir_n.operators[1].mu}, sigma = {cir_n.operators[1].sigma}') # 全局mu、sigma + +# 定义线路参数,并前向演化保存末态 +para = torch.randn(3) +final_state_n = cir_n(para, is_prob=False) + +print('采样结果为:', cir_n.measure(with_prob=True)) + +# %% [markdown] +# 与无噪声的情况进行比较,计算保真度: + +# %% +# 设置无噪声线路 +cir = dq.QumodeCircuit(nmode=3, init_state=[1, 0, 1]) +cir.ps(0, encode=True) +cir.ps(1, encode=True) +cir.bs_theta([0, 1], encode=True) + +final_state = cir(para, is_prob=False) + +print('采样结果为:', cir.measure(with_prob=True)) + +# 计算fidelity +f = 0 +for key in final_state_n: + f += final_state_n[key].conj() * final_state[key] +fidelity = torch.abs(f) ** 2 +print('加入噪声后的保真度为:', fidelity) + +# %% [markdown] +# ## 支持GPU计算 +# +# + +# %% [markdown] +# 由于DeepQuantum基于PyTorch框架,可以方便地进行GPU节点上的量子线路模拟。 +# +# 首先检查环境是否支持cuda。如果有NVIDIA的GPU但仍然返回 ``False`` ,常见的原因是未安装或未安装合适版本的 [CUDA](https://anaconda.org/nvidia/cuda) ,或安装的PyTorch为CPU-only版本。 + +# %% +torch.cuda.is_available() + +# %% [markdown] +# PyTorch中,``to('cuda')`` 的机制是把实例化的``QumodeCircuit``模组(即量子态的数据和量子门的参数)放到GPU上。 +# +# 如果对已经在GPU上的线路改变初态或量子门,需要重新 ``to``,或确保加入的数据参数已经在GPU上。 + +# %% +# 如cuda可用, 设定device为cuda +device = torch.device('cpu') +if torch.cuda.is_available(): + device = torch.device('cuda:0') + +cir = dq.QumodeCircuit(nmode=nmode, init_state=[2, 0]) +cir.bs_theta([0, 1], encode=True) + +# 将线路转移到cuda上 +cir.to(device) + +# 因为前向时才加入线路参数,确保线路参数也存在cuda上 +batch_para = batch_para.to(device) +print(batch_para) + +unitary = cir(data=batch_para) +print('线路对应酉矩阵为:', unitary) + +# 测量并输出结果 +cir.measure() + +# %% [markdown] +# ## Clements架构光量子线路 + +# %% [markdown] +# 内置了 ``Clements`` 类可以模拟任意的酉矩阵,通过 ``dq.Clements`` 直接调用。 +# ``nmode`` 表示总的波导数目,``init_state`` 设置初始输入,``cutoff`` 表示对输出的结果做截断,``phi_first`` 表示不同MZI的构造, ``phi_first=True`` 表示PS-BS-PS-BS 结构, ``noise`` 表示是否引入高斯噪声。 +# 下面是使用 ``Clements`` 类构建相同的CNOT光量子线路的例子。 + +# %% [markdown] +# 1. 构建6模Clements线路,这里没有指定参数,所以生成的是随机参数。 + +# %% +clements = dq.Clements(nmode=6, init_state=[1, 0, 1, 0, 0, 0], cutoff=3) +clements.draw() + +# %% [markdown] +# 2. 使用Clements架构实现CNOT门,这里先将CNOT对应的光量子线路酉矩阵做分解映射成Clements参数,然后再加载这些参数。 + +# %% +# 使用Clements架构实现CNOT门 +u6x6 = np.array( + [ + [1, 0, 1, -1, 0, 0], + [0, 1, 0, 0, 0, np.sqrt(2)], + [1, 0, 0, 1, 1, 0], + [-1, 0, 1, 0, 1, 0], + [0, 0, 1, 1, -1, 0], + [0, np.sqrt(2), 0, 0, 0, -1], + ] +) / np.sqrt(3) +# 将酉矩阵分解成clements对应的参数 +ud = dq.UnitaryDecomposer(u6x6) +mzi_info = ud.decomp() +# 构造clements线路实现CNOT门 +data = clements.dict2data(mzi_info[2]) # encoding the 6x6 data +clements(data=data) +# 线路可视化 +clements.draw() + +# %% [markdown] +# 3. 通过 ``Clements.get_unitary()`` 来验证是否正确 + +# %% +abs(clements.get_unitary() - torch.tensor(u6x6)).sum() + +# %% [markdown] +# ## 支持MPS计算 +# +# 当内存成为模拟大规模量子线路瓶颈时,可以采用 MPS(Matrix Product State)功能。MPS模拟基于张量网络,通过局部的矩阵运算而不是全局矩阵的方式减少内存占用,输出末态的近似值。 +# +# 在DeepQuantum光量子模块中,使用MPS模拟需要打开 ``QumodeCircuit(mps=True)`` 和 ``basis=False`` 开关,并自定义 ``chi`` 和 ``cutoff``。``chi`` 是MPS中的bond dimension,表示局部矩阵的大小,``chi`` 越大精度越高;``cutoff`` 表示每模的光子数截断。 +# +# 复用上文的Clements架构中构造的数据进行演示: + +# %% +# 打开mps,chi设为5 +cir = dq.QumodeCircuit(6, init_state=[0, 1, 1, 1, 0, 0], basis=False, mps=True, chi=5, cutoff=4) +clements = dq.Clements(nmode=6, init_state=[0, 1, 1, 1, 0, 0]) + +# 将clements ansatz合并进线路 +cir += clements + +# 得到Fock state tensor形式的末态 +final_state_tensor = cir(data=data) + +# 转换成Fock state vector,方便比较 +final_state_mps = ( + dq.MatrixProductState( + 6, state=final_state_tensor, chi=cir.chi, qudit=cir.cutoff, normalize=cir.init_state.normalize + ) + .full_tensor() + .reshape([4] * 6) +) +print(final_state_mps.shape) + +# %% [markdown] +# 与不打开mps时精确结果进行比较: + +# %% +cir_precise = dq.QumodeCircuit(6, init_state=[0, 1, 1, 1, 0, 0]) +cir_precise += clements + +precise_dict = cir_precise(data=data, is_prob=False) + +# 计算保真度 +f = 0 + +for key in precise_dict: + f += final_state_mps[tuple(key.state)].conj() * precise_dict[key] +fidelity = torch.abs(f) ** 2 +print('保真度为:', fidelity) + +# %% [markdown] +# 若要提高精度,考虑增大``chi``的取值: + +# %% +# 打开mps,chi设为7 +cir = dq.QumodeCircuit(6, init_state=[0, 1, 1, 1, 0, 0], basis=False, mps=True, chi=7, cutoff=4) +cir += clements + +# 得到Fock state tensor形式的末态 +final_state_tensor = cir(data=data) + +# 转换成Fock state vector,方便比较 +final_state_mps = ( + dq.MatrixProductState( + 6, state=final_state_tensor, chi=cir.chi, qudit=cir.cutoff, normalize=cir.init_state.normalize + ) + .full_tensor() + .reshape([4] * 6) +) + +# 计算保真度 +f = 0 +for key in precise_dict: + f += final_state_mps[tuple(key.state)].conj() * precise_dict[key] +fidelity = torch.abs(f) ** 2 +print('保真度为:', fidelity) + +# %% [markdown] +# 对于一个由矩阵乘积态表示的量子系统,由于无法高效地获得其完整的波函数,我们采用一种基于链式法则(chain-rule)的采样方法。该方法通过从左到右逐个对每个mode进行条件采样,最终生成一个完整的样本,从而避免了对整个空间的直接处理。 + +# %% +cir = dq.QumodeCircuit(4, init_state=[1, 1, 1, 1], basis=False, mps=True, chi=5, cutoff=4) +for i in range(4): + cir.ps(i) +cir.bs([0, 1]) +cir.bs([2, 3]) +cir.bs([1, 2]) +cir() +samples = cir.measure(shots=100) +print(samples) + +# %% [markdown] +# # 基于高斯后端构建光量子线路 + +# %% [markdown] +# ## 构建连续变量光量子线路 + +# %% [markdown] +# 基于高斯后端的光量子线路演化的量子态是高斯态,高斯态的概念来源于连续变量光量子计算,它对应的wigner函数为多元高斯分布,比如相干态。\ +# 这里用一对正交分量X、P的协方差矩阵和平均值来刻画连续变量中的量子态,协方差矩阵描述的正交分量X、P的顺序有两种,XXPP和XPXP,这里采用XXPP顺序,通过简单的矩阵行列交换可以实现两者之间的变换。真空态对应的协方差矩阵为单位阵($\mathrm{cov}=\frac{\hbar}{4\kappa^2}I=I$,这里我们取$\hbar=2,\kappa=\frac{\sqrt{2}}{2}$),对应的平均值都为0。\ +# 通过Homodyne测量,设定测量角度为0或$\pi/2$得到的物理量是正交分量X或P的值,它们对应的分布满足边缘分布。 + +# %% [markdown] +# 通过`dqp.set_hbar` 和`dqp.set_kappa`可以设置$\hbar$和$\kappa$的值 + +# %% +dqp.set_hbar(hbar=1) +dqp.set_kappa(kappa=np.sqrt(2) / 2) +print(dqp.hbar, dqp.kappa) +dqp.set_hbar(hbar=2) # 重新设置回原来值 +dqp.set_kappa(kappa=np.sqrt(2) / 2) + +# %% [markdown] +# 1. `GaussianState` 的使用 +# +# `GaussianState` 作为高斯后端的一个类用来描述高斯态,它的`state`参数可以有两种输入,`'vac'`对应着真空高斯态,[cov, mean] 对应自定义的高斯态。 +# `nmode` 参数对应着高斯态的模式数,它应该和输入的`state`维度匹配。`cutoff` 参数对应着截断数。通过`GaussianState.cov`、`GaussianState.mean`可以得到对应的协方差和平均值信息。 + +# %% +gaussian_state_1 = dq.GaussianState(state='vac', nmode=2, cutoff=3) +gaussian_state_2 = dq.GaussianState(state=[torch.eye(4), torch.tensor([0] * 4)], nmode=2, cutoff=3) +print('state_1_cov_mean', gaussian_state_1.cov, gaussian_state_1.mean) +print('state_2_cov_mean', gaussian_state_2.cov, gaussian_state_2.mean) + +# %% [markdown] +# 2. 高斯门的介绍 +# +# 高斯线路中除了可加入之前的PS、BS等基础门,还有一些高斯门加入包括单模压缩门s、位移门d(包括x方向位移门和p方向位移门)、旋转门r、双模压缩门s2,它们对应的酉矩阵以及辛矩阵变换如下。 +# - s:单模压缩门, +# 对应的酉算符表示如下 +# $$S(z) = \exp[\frac{1}{2}(z^* \hat{a}^2-z{\hat{a}^\dagger}^2)]$$ +# $$z=re^{i\theta}$$ +# 作用在正交算符$\hat{x}$和$\hat{p}$上的辛变换如下 +# $$S^{\dagger}(z) +# \begin{pmatrix} +# \hat{x} \\ +# \hat{p} +# \end{pmatrix} +# S(z) = S(r,\theta) +# \begin{pmatrix} +# \hat{x} \\ +# \hat{p} +# \end{pmatrix}$$ +# $$S(r, \theta) = +# \begin{pmatrix} +# \cosh r-\sinh r\cos \theta &-\sinh r\sin \theta\\ +# -\sinh r\sin \theta & +# \cosh r+\sinh r\cos \theta +# \end{pmatrix}$$ +# +# - d:位移门, +# 对应的酉算符表示如下 +# $$D(\alpha) = \exp(\alpha \hat{a}^\dagger-\alpha^*\hat{a})$$ +# $$\alpha=re^{i\theta}$$ +# 作用在正交算符$\hat{x}$和$\hat{p}$上的辛变换如下 +# $$D^\dagger(\alpha) +# \begin{pmatrix} +# \hat{x}\\ +# \hat{p} +# \end{pmatrix} +# D(\alpha) = +# \begin{pmatrix} +# \hat{x}\\ +# \hat{p} +# \end{pmatrix}+ +# \frac{\sqrt{\hbar}}{\kappa} +# \begin{pmatrix} +# r\cos\theta\\ +# r\sin\theta +# \end{pmatrix}$$ +# +# - r:旋转门, +# 对应的酉算符表示如下 +# $$R(\theta) = \exp(i\theta \hat{a}^\dagger \hat{a})$$ +# 作用在正交算符$\hat{x}$和$\hat{p}$上的辛变换如下 +# $$R^\dagger(\theta) +# \begin{pmatrix} +# \hat{x}\\ +# \hat{p} +# \end{pmatrix} +# R(\theta) = S(\theta) +# \begin{pmatrix} +# \hat{x}\\ +# \hat{p} +# \end{pmatrix}$$ +# $$S(\theta) = \begin{pmatrix} +# \cos\theta & -\sin\theta\\ +# \sin\theta & \cos\theta +# \end{pmatrix}$$ +# +# - s2:双模压缩门, +# 对应的酉算符表示如下 +# $$S_2(z) = \exp(z\hat{a}_1^\dagger\hat{a}_2^\dagger-z^*\hat{a}_1\hat{a}_2)$$ +# $$z=re^{i\theta}$$ +# 作用在正交算符$\hat{x}$和$\hat{p}$上的辛变换如下 +# $$ +# S_2^{\dagger}(r, \theta) +# \begin{pmatrix} +# \hat{x}_1 \\ +# \hat{x}_2 \\ +# \hat{p}_1 \\ +# \hat{p}_2 \\ +# \end{pmatrix} +# S_2(r, \theta) +# = +# S_2(r, \theta) +# \begin{pmatrix} +# \hat{x}_1 \\ +# \hat{x}_2 \\ +# \hat{p}_1 \\ +# \hat{p}_2 \\ +# \end{pmatrix} +# $$ +# $$ +# S_2(r, \theta) = +# \begin{pmatrix} +# \cosh r & \cos\theta\sinh r & 0 & \sin\theta\sinh r \\ +# \cos\theta\sinh r & \cosh r & \sin\theta\sinh r & 0 \\ +# 0 & \sin\theta\sinh r & \cosh r & -\cos\theta\sinh r \\ +# \sin\theta\sinh r & 0 & -\cos\theta\sinh r & \cosh r \\ +# \end{pmatrix} +# $$ +# 对于Fock线路中的一般量子门,在高斯线路中作用在正交算符$\hat{x}$和$\hat{p}$上的辛变换如下 +# $$S(\theta, \phi) = \begin{pmatrix} +# \mathrm{Re}(U) & -\mathrm{Im}(U)\\ +# \mathrm{Im}(U) & \mathrm{Re}(U) +# \end{pmatrix}$$ +# 其中$U$表示作用在生成算符$\hat{a}^\dagger$上的等效线性变换 + +# %% [markdown] +# `QumodeCircuit.s`,`QumodeCircuit.s2`中`wires`参数表示作用的波导,`r`参数表示压缩的振幅,`theta`参数表示压缩的角度\ +# `QumodeCircuit.d`中`wires`参数表示作用的波导,`r`参数表示位移的振幅,`theta`参数表示位移的角度\ +# `QumodeCircuit.r`中`wires`参数表示作用的波导,`inputs`参数表示旋转的角度 + +# %% +cir = dq.QumodeCircuit(nmode=2, init_state='vac', cutoff=3, backend='gaussian') +cir.s(wires=0, r=1, theta=0) +cir.s(wires=1, r=1, theta=0) +cir.r(0, inputs=0) +cir.r(1, inputs=0) +cir.s2([0, 1], theta=0) +cir.d(wires=0, r=1, theta=0) +cir.d(wires=1, r=1, theta=0) +cir.bs(wires=[0, 1], inputs=[np.pi / 4, np.pi / 4]) +cir.draw() + +# %% [markdown] +# `cir.get_symplectic()`可以得到线路对应的辛矩阵变换 + +# %% +cir.get_symplectic() + +# %% [markdown] +# `QumodeCircuit.operators` 保存了所有的量子门操作,通过`get_symplectic`和`get_displacement`可以得到对应量子门的辛矩阵变换和位移变换。 + +# %% +cir.operators[0].get_symplectic(), cir.operators[0].get_displacement() + +# %% [markdown] +# 3. 简单线路的构建和演化 +# +# 这里构建2模线路,初始量子态为真空高斯态,经过3种高斯门演化之后得到另一个高斯态。S 表示单模真空压缩门,D表示位移门,BS是可调分束器。通过设置`backend='gaussian'`可以进行高斯后端的演化得到协方差矩阵和平均值。 + +# %% +cir = dq.QumodeCircuit(nmode=2, init_state='vac', cutoff=3, backend='gaussian') +cir.s(wires=0, r=1) +cir.s(wires=1, r=1) +cir.d(wires=0, r=1) +cir.d(wires=1, r=1) +cir.bs(wires=[0, 1], inputs=[np.pi / 4, np.pi / 4]) +state = cir() # 线路演化后得到的高斯态用协方差矩阵和平均值刻画 +print(state) + +# %% [markdown] +# `cir.measure_homodyne` 对指定线路的正交分量进行测量,`shots`对应测量次数,`wires` 表示指定线路 + +# %% +measure_re = cir.measure_homodyne(shots=1024, wires=[0, 1]) # 对正交分量进行测量,这里的每个模对应一组正交分量X、P。 +print(measure_re.shape) + +# %% [markdown] +# 同时还可以计算每个线路的平均光子数和光子数方差 + +# %% +photon_mean, photon_var = cir.photon_number_mean_var() +print(photon_mean, photon_var) + +# %% [markdown] +# 通过设置 `is_prob=True` 参数可以得到对应的Fock态概率分布,`detector='pnrd'` 表示使用粒子数分辨探测器,`detector='threshold'` 表示使用阈值探测器。 + +# %% +state = cir(is_prob=True, detector='threshold') # 线路演化后得到的高斯态用协方差矩阵和平均值刻画 +print(state) + +# %% [markdown] +# 4. 高斯线路的采样 + +# %% [markdown] +# 当计算了末态的概率分布时,使用对应的概率分布进行采样。当只得到末态的协方差和平均值时,通过设置`mcmc=True`使用马尔科夫链蒙特卡洛方法进行采样,设置`mcmc=False` 表示使用链式法则(chain-rule)方法进行采样,默认设置为`False`。`'pnrd'`表示单光子分辨探测器,默认设为`'pnrd'`,`'threshold'`表示阈值探测器。 + +# %% +cir = dq.QumodeCircuit(nmode=2, init_state='vac', cutoff=3, backend='gaussian') +cir.s(wires=0, r=1) +cir.s(wires=1, r=1) +cir.d(wires=0, r=1) +cir.d(wires=1, r=1) +cir.bs(wires=[0, 1], inputs=[np.pi / 4, np.pi / 4]) +state = cir(is_prob=True, detector='pnrd') +samples = cir.measure(shots=100) +print('使用概率分布进行采样', samples) +print('---------') + +cir = dq.QumodeCircuit(nmode=2, init_state='vac', cutoff=3, backend='gaussian') +cir.s(wires=0, r=1) +cir.s(wires=1, r=1) +cir.d(wires=0, r=1) +cir.d(wires=1, r=1) +cir.bs(wires=[0, 1], inputs=[np.pi / 4, np.pi / 4]) +cir() +samples = cir.measure(shots=100, mcmc=True) +print('使用mcmc进行采样', samples) +print('---------') +cir = dq.QumodeCircuit(nmode=2, init_state='vac', cutoff=3, backend='gaussian') +cir.s(wires=0, r=1) +cir.s(wires=1, r=1) +cir.d(wires=0, r=1) +cir.d(wires=1, r=1) +cir.bs(wires=[0, 1], inputs=[np.pi / 4, np.pi / 4]) +cir() +samples = cir.measure(shots=100, mcmc=False) +print('使用chain-rule进行采样', samples) + +# %% [markdown] +# ## 高斯玻色采样(GBS) + +# %% [markdown] +# 基于压缩门和线性光学器件可以构建高斯玻色采样线路,通过`GaussianBosonSampling`可以使用高斯玻色采样模拟,`nmode`表示GBS线路的模数,`squeezing`表示第一列单模压缩门的压缩度,`cutoff`表示每条线路探测到的最大光子数+1,`'pnrd'`表示单光子分辨探测器,默认设为`'pnrd'`,`'threshold'`表示阈值探测器,`noise` 表示是否加入高斯噪声。然后正常运行高斯玻色采样线路采样即可。 + +# %% +gbs = dq.GaussianBosonSampling(nmode=6, squeezing=torch.tensor([1] * 6), unitary=torch.eye(6) + 0j) +gbs() +samples = gbs.measure(shots=100, mcmc=True) + +# %% [markdown] +# `dqp.GBS_Graph`可以构建图问题相关的高斯玻色采样线路,参数 `adj_mat` 表示输入对应的邻接矩阵,`cutoff`表示每条线路探测到的最大光子数+1,`mean_photon_num` 表示每条线路对应的平均光子数之和,默认设为mode数,即每条线路对应的平均光子数为1,`detector` 为 +# `'pnrd'`表示单光子分辨探测器,默认设为`'pnrd'`,`'threshold'`表示阈值探测器,`noise` 表示是否加入高斯分布噪声。 + +# %% +# 采用6个节点的图演示简单的高斯玻色采样 +a = np.array( + [ + [0.0, 1.0, 1.0, 0.0, 0.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 0.0, 1.0], + [1.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0, 1.0, 1.0], + [0.0, 0.0, 0.0, 1.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 1.0, 0.0, 0.0], + ] +) +gbs = dqp.GBS_Graph(adj_mat=a, cutoff=2) +state = gbs() +sample = gbs.measure(shots=5000, mcmc=True) +gbs.draw() + +# %% +print(sample) + +# %% [markdown] +# 下面是GBS解决从11个节点中找到6个节点的稠密子图的一个案例演示。 + +# %% +import networkx as nx + +graph_adj = np.array( + [ + [0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0], + [1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0], + [0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0], + [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0], + ] +) +graph = nx.from_numpy_array(graph_adj) +nx.draw(graph, with_labels=True) + +# %% +gbs = dqp.GraphGBS(adj_mat=graph_adj, cutoff=2) +state = gbs() +sample = gbs.measure(shots=50000, mcmc=True) + +# %% [markdown] +# 下面对结果进行后选择,筛选出六个节点对应的采样结果,计算对应的图密度并从大到小排序 + +# %% +subgraph_sample = gbs.postselect(sample, [6]) +subgraph_density = gbs.graph_density(graph, subgraph_sample[0]) +key = list(subgraph_density) +print(key[0], subgraph_density[key[0]]) + +# %% +print(subgraph_density) + +# %% [markdown] +# ## 部分Homodyne测量 + +# %% [markdown] +# 高斯后端还可以对几个mode做部分测量,那么剩余的量子态会根据相应的测量结果塌缩,先通过`QumodeCircuit.homodyne`添加测量操作,`wires`表示作用单条线路,`phi`对应测量角度,然后通过`QumodeCircuit.measure_homodyne`进行测量操作实现量子态坍缩。 + +# %% +cir = dq.QumodeCircuit(nmode=2, init_state='vac', cutoff=3, backend='gaussian') +cir.s(wires=0, r=1) +cir.s(wires=1, r=1) +cir.d(wires=0, r=1) +cir.d(wires=1, r=1) +cir.bs(wires=[0, 1], inputs=[np.pi / 4, np.pi / 4]) +cir.homodyne(wires=0, phi=0) +# 线路可视化 +cir.draw() + +# %% +state = cir() +sample = cir.measure_homodyne(shots=1) # 运行线路并进行一次采样 +state_measured = cir.state_measured # 得到采样后坍缩的量子态 +print(sample, state_measured) + +# %% [markdown] +# ## 延时线圈组成的时域复用线路 + +# %% [markdown] +# 通过`QumodeCircuit`构造带延时线圈线路,通过`QumodeCircuit.delay`加入可调延时线圈,参数`wires` 表示作用的波导,`ntau`表示延时线圈内延时的个数,`inputs`表示延时线圈中可调参数,`convention` 对应两种,bs和mzi,对应延时线是`DelayBS`和`DelayMZI` 类型,默认是bs,对应一个单参数theta可调分束器和一个移相器。`encode`控制角度自由编码。 + +# %% +r = 5 +nmode = 2 +cir = dq.QumodeCircuit(nmode=nmode, init_state='vac', cutoff=3, backend='gaussian') +cir.s(0) +cir.s(1) +cir.bs_theta([0, 1], [3]) +cir.delay(0, ntau=1, inputs=[3, 0], encode=True) +cir.delay(1, ntau=1, inputs=[3, 0], encode=True) +cir.homodyne(0) +cir.homodyne(1) +cir.draw() + +# %% [markdown] +# `dq.QumodeCircuit` 构造带延时线的线路只能进行单个时间步线路演化,是通过`cir.draw(unroll=True)` 可以得到初次测量的等效的空间线路,然后演化等效空间线路得到最后的结果。 + +# %% +cir.draw(unroll=True) + +# %% +state = cir() +print(state) + +# %% [markdown] +# 若要进行n次测量实验,得到对应的空间等效线路,可以通过`QumodeCircuit.global_circuit` 设置`nstep`得到,当设置`encode=True`的门必定不共享,因为是多个输入数据,`use_deepcopy`只控制变分参数的门是不是共享变分参数。 + +# %% +r = 5 +nmode = 2 +cir = dq.QumodeCircuit(nmode=nmode, init_state='vac', cutoff=3, backend='gaussian') +cir.s(0) +cir.s(1) +cir.bs_theta([0, 1], [3]) +cir.delay(0, ntau=1, inputs=[3, 0], encode=True) +cir.delay(1, ntau=1, inputs=[3, 0], encode=True) +cir.homodyne(0) +cir.homodyne(1) + +# %% +cir_global = cir.global_circuit(nstep=3, use_deepcopy=True) +cir_global.draw(unroll=True) # draw global circuit + +# %% +print(cir_global.ndata) + +# %% [markdown] +# 当设置对应的`encode=True`,就可以自由编码global线路的角度 + +# %% +data = torch.tensor([0] * cir_global.ndata) +state = cir_global(data=data) +cir_global.draw(unroll=True) # draw global circuit + +# %% [markdown] +# 通过`dq.QumodeCircuitTDM`也可以构造带延时线圈线路,它继承自`dq.QumodeCircuit`,不同之处在于可以进行多个时间步的演化,所有空间模式上都要求做Homodyne测量。 + +# %% +r = 5 +nmode = 2 +cir = dq.QumodeCircuitTDM(nmode=nmode, init_state='vac', cutoff=3) +cir.s(0) +cir.s(1) +cir.bs_theta([0, 1], [3]) +cir.delay(0, ntau=1, inputs=[3, 0]) +cir.delay(1, ntau=1, inputs=[3, 0]) +cir.homodyne(0) +cir.homodyne(1) +cir.draw() + +# %% [markdown] +# 通过`cir.samples`查看测量结果 + +# %% +cir(nstep=3) +print(cir.samples) + +# %% [markdown] +# 通过控制`encode=True`可以实现对不同时刻编码不同角度,从而制备不同的纠缠态,下面是EPR态的例子,第一次测量设置延时线圈参数为 +# $[\pi/2, \pi/2]$,第二次测量设置为$[\pi/4, 0]$,然后周期性重复角度测量可以得到纠缠的EPR态。 + +# %% +r = 9 +nmode = 1 +cir = dq.QumodeCircuitTDM(nmode=nmode, init_state='vac', cutoff=3) +cir.s(0, r=r) +cir.delay(0, ntau=1, inputs=[np.pi / 2, np.pi / 2], encode=True) # 参数编码 +cir.homodyne_x(0) +data = torch.tensor([[np.pi / 2, np.pi / 2], [np.pi / 4, 0]]).unsqueeze(0) # 周期性参数组合 +cir(data=data, nstep=13) +print(cir.samples) + +# %% [markdown] +# # 基于Bosonic后端构建光量子线路 + +# %% [markdown] +# ## Bosonic态 + +# %% [markdown] +# 基于Bosonic后端的光量子线路演化的量子态不再是高斯态,而是非高斯态,它对应的Wigner函数可以写成多个高斯函数的线性叠加。 +# $$ W(\xi) = \sum c_m G_{\mu_m, \Sigma_m}(\xi)$$ +# 其中 +# $$G_{\boldsymbol{\mu},\boldsymbol{\Sigma}}(\boldsymbol{\xi})\equiv\frac{\exp\left[-\frac{1}{2}(\boldsymbol{\xi}-\boldsymbol{\mu})^T\boldsymbol{\Sigma}^{-1}(\boldsymbol{\xi}-\boldsymbol{\mu})\right]}{\sqrt{\det(2\pi\boldsymbol{\Sigma})}}$$ +# $c_m$ 满足归一化条件,$\sum c_m = 1$。 +# +# 我们用一组协方差矩阵、位移矢量和权重来描述这样的非高斯态,这里我们称这种非高斯态为Bosonic态。 +# +# 和高斯态类似,通过Homodyne测量可以得到正交分量,设定测量角度为0或 $\pi/2$ 得到的物理量是正交分量X或P的值,它们对应的分布满足边缘分布。 + +# %% [markdown] +# 1. `BosonicState` +# +# `BosonicState`作为Bosonic后端的一个类用来描述Bosonic量子态,它的`state`参数可以有两种输入,`'vac'`对应着真空态,[cov, mean, weight] 对应自定义的Bosonic态。 +# `nmode` 参数对应着这个态的模式数,它应该和输入的`state`维度匹配。`cutoff` 参数对应着光子截断数。 +# +# 通过`BosonicState.cov`、`BosonicState.mean`、`BosonicState.weight`可以得到对应的协方差矩阵、位移矢量和权重。 +# +# 同时这个类也提供了计算Wigner函数和边缘分布的功能,通过`BosonicState.wigner`和`BosonicState.marginal`函数调用,`BosonicState.wigner`函数中设置`wire`会指定线路,`qrange`和`prange`分别表示两个正交方向的取值范围,当`qrange`或`prange`为整型时,表示对称的离散,`npoints`表示离散的点数,当`npoints`为整型时,表示两个正交方向离散的点数相同,设置`plot=True`可以方便地实现可视化。 +# +# `BosonicState.marginal`函数中设置`wire`会指定线路,`phi`表示计算边缘分布的正交分量为 $\hat{q}\cos(\phi) + \hat{p}\sin(\phi)$,`qrange`表示正交方向的取值范围,`npoints`表示离散的点数,设置`plot=True`可以方便地实现可视化。 + +# %% +bosonic_state_1 = dq.BosonicState(state='vac', nmode=2) +bosonic_state_2 = dq.BosonicState( + state=[torch.eye(4).expand(2, 4, 4), torch.zeros(4).expand(2, 4) + 0j, torch.ones(2) / 2 + 0j], nmode=2 +) +print('state_1_cov_mean', bosonic_state_1.cov, bosonic_state_1.mean, bosonic_state_1.weight) +print('state_2_cov_mean', bosonic_state_2.cov, bosonic_state_2.mean, bosonic_state_2.weight) + +# %% +qrange = 5 +prange = 5 +npoints = 100 +wigner = bosonic_state_1.wigner(wire=0, qrange=qrange, prange=prange, npoints=npoints) # Wigner函数可视化 +marginal = bosonic_state_1.marginal(wire=0, qrange=qrange) # 边缘分布p(x)可视化 + +# %% [markdown] +# ### 猫态和GKP态 + +# %% [markdown] +# 猫态和GKP态作为常见的Bosonic态,可以通过`CatState`和`GKPState`来实现。 +# +# 猫态定义为两个相干态的线性叠加: +# $$|k^{\alpha}\rangle_{\mathrm{cat}}=\sqrt{\mathcal{N}}\left(|\alpha\rangle+e^{i\pi k}|-{\alpha}\rangle\right)$$ +# 这里$|\alpha\rangle$表示相干态,$k$ 是相位因子,$\mathcal{N}$ 是归一化因子。 +# $$\mathcal{N}=\frac{1}{2\left(1+e^{-|\alpha|^{2}}\cos(\pi k)\right)}$$ +# 猫态的参数说明: +# `r`表示相干态的对应的位移大小,`theta`表示相干态的对应的位移角度,`p`决定叠加系数是+1(`p=0`)还是-1(`p=1`),`cutoff`表示Fock态空间的截断数。 +# +# 方形晶格形状的GKP态由一组狄拉克 $\delta$ 函数决定,用坐标本征态的表示如下 +# $$\left|k\right\rangle_{\mathrm{gkp}}=\sum_{s=-\infty}^\infty\left|\sqrt{\pi\hbar}(2s+k)\right\rangle_q,k=\{0,1\}$$ +# 采用GKP态编码量子比特,一般的量子态可以表示成 +# $$|\psi\rangle=\cos\frac{\theta}{2}|0\rangle_{\mathrm{gkp}}+e^{-i\phi}\sin\frac{\theta}{2}|1\rangle_{\mathrm{gkp}}$$ +# 但是在实际模拟中一般会考虑有限能量的GKP态,即作用一个Fock阻尼算符 $\hat{E}(\epsilon)$, +# $$\hat{E}(\epsilon) = e^{-\epsilon \hat{n}}$$ +# 此时的GKP态的狄拉克 $\delta$ 函数变为一系列高斯波包,对应的包络为高斯函数。阻尼算符的作用下线性叠加的每一个高斯态的平均值和协方差也会相应更新。 +# $$\mu_m(\epsilon)=\frac{2e^{-\epsilon}}{1+e^{-2\epsilon}}\mu_m, \ \Sigma_m(\epsilon)=\frac{\hbar}{4\kappa^2}\frac{1-e^{-2\epsilon}}{1+e^{-2\epsilon}}\mathbb{1}$$ +# GKP态的参数说明: +# `theta`表示Bloch球中的角度 $\theta$,`phi`表示表示Bloch球中的角度 $\phi$,`amp_cutoff`表示高斯包络的振幅截断,只保留截断范围以内的高斯波包,`epsilon`表示阻尼算符的强度,`cutoff`表示Fock态空间的截断数。 + +# %% +# 猫态实例化 +cat = dq.CatState(r=1, theta=0) +wigner = cat.wigner(wire=0, qrange=5, prange=5, npoints=100) # Wigner函数可视化 +marginal = cat.marginal(wire=0, qrange=5) # 边缘分布p(x)可视化 + +# %% +# GKP态实例化 +gkp_0 = dq.GKPState(theta=0.0, phi=0.0) +wigner = gkp_0.wigner(wire=0, qrange=10, prange=10, npoints=500) # Wigner函数可视化 +marginal = gkp_0.marginal(wire=0, qrange=10) # 边缘分布p(x)可视化 + +# %% [markdown] +# ### 单模Fock态的Bosonic表示 + +# %% [markdown] +# `FockStateBosonic`可以实现单模Fock态的Bosonic表示,即通过一组协方差矩阵、位移矢量和权重表示。输入参数`n`表示粒子数,`r`用来表示近似程度,`cutoff`表示Fock态空间的截断数。下面是两光子Fock态的例子。 + +# %% +bosonic_fock2 = dq.FockStateBosonic(n=2, r=0.1) +print(bosonic_fock2.cov, bosonic_fock2.mean, bosonic_fock2.weight) + +# %% +marginal = bosonic_fock2.marginal(wire=0, qrange=5, npoints=500) +wigner = bosonic_fock2.wigner(wire=0, qrange=5, prange=5, npoints=500) + +# %% [markdown] +# ## Bosonic后端构建光量子线路 + +# %% [markdown] +# Bosonic后端构建光量子线路时要设置`backend`为`'bosonic'`,初态`init_state`设置可以为真空态`'vac'`,可以是自定义的[cov, mean, weight],也可以通过`QumodeCircuit.cat`和`QumodeCircuit.gkp`自定义猫态和GKP态作为初态。 +# +# Bosonic后端的量子门和高斯后端是通用的,线路中的协方差矩阵、位移矢量都遵循相同的辛变换规则,但是权重保持不变。 +# Bosonic后端的输出为包含协方差矩阵、位移矢量以及权重的列表。 + +# %% +cir = dq.QumodeCircuit(nmode=2, init_state='vac', backend='bosonic') +cir.cat(wires=0, r=1) # 设置猫态 +cir.gkp(wires=1, theta=0.0, phi=0.0) # 设置GKP态 +cir.s(wires=0, r=1) +cir.s(wires=1, r=1) +cir.d(wires=0, r=1) +cir.d(wires=1, r=1) +cir.bs(wires=[0, 1], inputs=[np.pi / 4, np.pi / 4]) +state = cir() +for i in state: + print(i.shape) + +# %% [markdown] +# ## Bosonic后端的Homodyne探测 + +# %% [markdown] +# `QumodeCircuit.measure_homodyne` 对指定线路的正交分量进行测量,`shots`对应测量次数,`wires` 表示指定线路。此时测量指定线路得到的是一对正交分量X、P的测量值。 + +# %% +cir = dq.QumodeCircuit(nmode=2, init_state='vac', backend='bosonic') +cir.cat(wires=0, r=1) +state = cir() +samples = cir.measure_homodyne(wires=0, shots=100) # 对正交分量进行测量,这里的每一条线路对应一组正交分量X、P。 +print(samples.shape) + +# %% [markdown] +# Bosonic后端还可以指定几个线路做部分测量,此时测量指定线路得到的是一个正交分量测量值。剩余的量子态会根据相应的测量结果塌缩,先通过`QumodeCircuit.homodyne`添加测量操作,`wires`表示作用单条线路,`phi`对应测量角度,然后通过`QumodeCircuit.measure_homodyne`进行测量操作实现量子态坍缩,通过`QumodeCircuit.state_measured`可以得到测量坍缩后的量子态。 + +# %% +cir = dq.QumodeCircuit(nmode=2, init_state='vac', backend='bosonic') +cir.cat(wires=0, r=1) +cir.homodyne(wires=0, phi=0) +state = cir() +samples = cir.measure_homodyne(shots=100) # 对正交分量进行测量,这里的每一条线路对应一个正交分量。 +print(samples.shape) + +# %% +state_measured = cir.state_measured +print(state_measured[0].shape) + +# %% [markdown] +# 类似高斯后端,通过设置 `is_prob=True` 参数也可以得到对应的Fock态概率分布,`detector='pnrd'` 表示使用粒子数分辨探测器,`detector='threshold'` 表示使用阈值探测器。但是测量得到概率分布之后不能继续做Homodyne测量,因为此时的末态不是Bosonic态的列表表示。 + +# %% +cir = dq.QumodeCircuit(nmode=2, init_state='vac', cutoff=3, backend='bosonic') +cir.cat(wires=0, r=1) +cir.gkp(wires=1) +state = cir(is_prob=True, detector='pnrd') +print(state) + +# %% [markdown] +# ## Bosonic态的粒子数分辨探测 + +# %% [markdown] +# 基于单模Fock态的Bosonic表示可以实现对Bosonic态的指定线路做Fock态测量,并得到测量坍缩后的结果,这个功能通过`PhotonNumberResolvingBosonic`实现。 +# 输入参数`n`表示要测量投影的Fock态的光子数,`r`表示近似程度,`nmode`表示模式数, `wires`表示要指定测量的线路。`cutoff`表示Fock态空间的截断数。在前向计算中输入Bosonic态可以实现测量坍缩。 + +# %% [markdown] +# 这里先构建一个两模的Bosonic态 + +# %% +cir = dq.QumodeCircuit(nmode=2, init_state='vac', backend='bosonic') +cir.s(1, 1) # squeezing vac state +cir.s2([0, 1], r=0.1) # SPDC operator +state = cir() +cir.draw() + +# %% [markdown] +# 对第一模做粒子数分辨探测然后指定测量结果为1,可以得到测量坍缩的结果 + +# %% +prnd = dqp.PhotonNumberResolvingBosonic(n=1, r=0.05, nmode=2, wires=0) +bs_lst = dqp.qmath.align_shape(*state) # 维度对齐 +bs_lst2 = prnd(bs_lst) # 量子态测量坍缩 +bs = dq.BosonicState(bs_lst2, nmode=2) + +# %% +# 可视化验证 +marginal_q = bs.marginal(wire=1, qrange=5, npoints=500) +marginal_p = bs.marginal(wire=1, phi=np.pi / 2, qrange=10, npoints=500) diff --git a/tutorials/ruff.toml b/tutorials/ruff.toml index 95963ec0..d45bd144 100644 --- a/tutorials/ruff.toml +++ b/tutorials/ruff.toml @@ -1,3 +1,8 @@ +extend = '../pyproject.toml' + +[lint] +extend-ignore = ['E402', 'E501'] + [lint.isort] known-first-party = [] known-third-party = ['deepquantum']