diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000000000000000000000000000000000000..2a59a88d83a41f1c5705c8215b3af4dc6ef63fca --- /dev/null +++ b/.clang-format @@ -0,0 +1,76 @@ +BasedOnStyle: Google +AccessModifierOffset: -3 +AlignConsecutiveMacros: true +AlignEscapedNewlines: Right +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakTemplateDeclarations: Yes +BraceWrapping: + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterExternBlock: false + AfterFunction: false + AfterNamespace: false + AfterStruct: false + AfterUnion: false + BeforeCatch: false + BeforeElse: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: false +BreakBeforeBinaryOperators: All +BreakBeforeBraces: Custom +BreakBeforeConceptDeclarations: true +BreakBeforeInheritanceComma: true +BreakConstructorInitializers: BeforeComma +ColumnLimit: 120 +CompactNamespaces: false +FixNamespaceComments: true +IncludeBlocks: Regroup +IncludeCategories: + - Regex: '<(assert|complex|ctype|errno|fenv|float|inttypes|iso646|limits|locale|math|setjmp|signal|stdalign|stdarg|stdatomic|stdbool|stddef|stdint|stdio|stdlib|stdnoreturn|string|tdmath|threads|time|uchar|wchar|wctype)\.h>' + Priority: 1 + - Regex: '<[[:alnum:]_\.]+.h>' + Priority: 2 + - Regex: '<[[:alnum:]_\.]+>' + Priority: 3 + - Regex: '<[[:alnum:]_\.]+' + Priority: 4 + - Regex: '[<"]tweedledum/([A-Za-z0-9.\/\-_])+[>"]' + Priority: 5 + - Regex: '<(catch2|boost|eigen)\/' + Priority: 6 + - Regex: '"]' + Priority: 8 + - Regex: '[<"](driver_types.h)[>"]' + Priority: 8 + - Regex: '"[[:alpha:]\/]*config\.hpp"' + Priority: 10 + - Regex: '"([A-Za-z0-9.\/\-_])+"' + Priority: 20 +IndentPPDirectives: AfterHash +IndentRequires: true +IndentWidth: 4 +KeepEmptyLinesAtTheStartOfBlocks: false +NamespaceIndentation: None +PenaltyBreakAssignment: 40 +ReflowComments: true +SortIncludes: true +SortUsingDeclarations: true +SpaceAfterCStyleCast: true +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyParentheses: false +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +Standard: c++17 +TabWidth: 4 +UseTab: Never +WhitespaceSensitiveMacros: ['CLANG_DIAG_ON', 'CLANG_DIAG_OFF', 'GCC_DIAG_ON', 'GCC_DIAG_OFF', 'MSVC_DIAG_ON', 'MSVC_DIAG_OFF'] diff --git a/.cmake-format.yaml b/.cmake-format.yaml new file mode 100644 index 0000000000000000000000000000000000000000..5d305883547545387fcbef5db115573da97e3390 --- /dev/null +++ b/.cmake-format.yaml @@ -0,0 +1,48 @@ +markup: + first_comment_is_literal: true +format: + disable: false + line_width: 120 + tab_size: 2 + use_tabchars: false + max_subgroups_hwrap: 2 + max_pargs_hwrap: 6 + max_rows_cmdline: 2 + separate_ctrl_name_with_space: false + separate_fn_name_with_space: false + dangle_parens: false + dangle_align: prefix + min_prefix_chars: 4 + max_prefix_chars: 10 + max_lines_hwrap: 2 + line_ending: unix + command_case: canonical + keyword_case: unchanged + enable_sort: true + autosort: false + require_valid_layout: false +parse: + additional_commands: + test_compile_option: + pargs: + nargs: 1+ + flags: + - AUTO_ADD_CO + kwargs: + LANGS: + + FLAGS: + + GENEX: 1 + test_link_option: + pargs: + nargs: 1+ + flags: + - AUTO_ADD_LO + - VERBATIM + kwargs: + LANGS: + + FLAGS: + + GENEX: 1 +lint: + argument_var_pattern: _?[a-z][a-z0-9_]+ + local_var_pattern: _?([a-z][a-z0-9_]+|[A-Z][A-Z0-9_]+) + macro_pattern: '[0-9a-z_]+' diff --git a/.codespell.allow b/.codespell.allow new file mode 100644 index 0000000000000000000000000000000000000000..ce7bda948ee0569d73289f65b019ea8e30079ebd --- /dev/null +++ b/.codespell.allow @@ -0,0 +1,16 @@ +nd +te +ans +tbe +TBE +dout +claus +crate +imBED +rouge +noone +outputOf +followings +functionAble +Aline +ket diff --git a/.cppcheck.suppressions b/.cppcheck.suppressions new file mode 100644 index 0000000000000000000000000000000000000000..4e5147a240e108d1e877218c89bf06b3bf0bc1b0 --- /dev/null +++ b/.cppcheck.suppressions @@ -0,0 +1,12 @@ +unusedFunction +missingInclude +missingReturn + +unusedStructMember:mindquantum/src/core/type.h:79 +unusedStructMember:mindquantum/src/core/type.h:80 +unusedStructMember:mindquantum/src/core/type.h:81 +unusedStructMember:mindquantum/src/core/type.h:82 +unusedStructMember:mindquantum/src/core/type.h:83 +unusedStructMember:mindquantum/src/core/type.h:84 + +objectIndex:mindquantum/src/core/utils.h:87 diff --git a/.gitignore b/.gitignore index 727a73a69d596c9dd893f039ab394b0e44e2e8b0..a2dfbcd5b223d5cac2532724d3bb891f118b9eac 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,9 @@ .vscode __pycache__/ *.pyc +*.pyd *.so +*.dylib *.so.* *.o *.out @@ -18,3 +20,6 @@ output .ipynb_checkpoints somas_meta analyze_fail.dat +VERSION.txt +requirements.txt +.eggs diff --git a/.jenkins/check/config/filter_cmakelint.txt b/.jenkins/check/config/filter_cmakelint.txt new file mode 100644 index 0000000000000000000000000000000000000000..c7bebf8e20c1a2780a8eeaa13c1c342a0155e1d2 --- /dev/null +++ b/.jenkins/check/config/filter_cmakelint.txt @@ -0,0 +1,4 @@ +# MindQuantum +"mindquantum/cmake" "whitespace/indent" +"mindquantum/cmake/Modules" "whitespace/indent" +"mindquantum/cmake/Modules/apple" "whitespace/indent" diff --git a/.jenkins/check/config/filter_cppcheck.txt b/.jenkins/check/config/filter_cppcheck.txt new file mode 100644 index 0000000000000000000000000000000000000000..3dbcf575e336917a58af37e032ca41f1e407cfda --- /dev/null +++ b/.jenkins/check/config/filter_cppcheck.txt @@ -0,0 +1,3 @@ +# MindQuantum + +"mindquantum/mindquantum/src/binding.cc" "syntaxError" diff --git a/.jenkins/check/config/filter_cpplint.txt b/.jenkins/check/config/filter_cpplint.txt new file mode 100644 index 0000000000000000000000000000000000000000..fb0ffebad75f0315d5fb82242842afeefeaab2fd --- /dev/null +++ b/.jenkins/check/config/filter_cpplint.txt @@ -0,0 +1,14 @@ +# MindQuantum +"mindquantum/tests/cmake-ldtest" "whitespace/braces" + +"mindquantum/mindquantum/src" "whitespace/comments" + +"mindquantum/mindquantum/src" "build/include_subdir" +"mindquantum/mindquantum/src/backends/quest" "build/include_subdir" +"mindquantum/mindquantum/src/backends/projectq" "build/include_subdir" +"mindquantum/mindquantum/src/gate" "build/include_subdir" +"mindquantum/mindquantum/src/hamiltonian" "build/include_subdir" +"mindquantum/mindquantum/src/matrix" "build/include_subdir" +"mindquantum/mindquantum/src/pr" "build/include_subdir" +"mindquantum/mindquantum/src/projector" "build/include_subdir" +"mindquantum/mindquantum/src/sparse" "build/include_subdir" diff --git a/.jenkins/check/config/filter_pylint.txt b/.jenkins/check/config/filter_pylint.txt new file mode 100644 index 0000000000000000000000000000000000000000..723a06e3b71254b3455ab4ccc004c915b92a6cf2 --- /dev/null +++ b/.jenkins/check/config/filter_pylint.txt @@ -0,0 +1,43 @@ +# MindQuantum +"mindquantum" "ungrouped-imports" +"mindquantum" "bad-whitespace" +"mindquantum/mindquantum/framework" "unused-argument" +"mindquantum/mindquantum/framework" "arguments-differ" +"mindquantum/setup.py" "invalid-name" +"mindquantum/setup.py" "missing-docstring" +"mindquantum/setup.py" "wrong-import-order" +"mindquantum/mindquantum/framework/_check_qnn_input.py" "duplicate-string-formatting-argument" +"mindquantum/mindquantum/core/ops/_base_operator.py" "bare-except" +"mindquantum/mindquantum/algorithm/nisq" "arguments-differ" +"mindquantum/mindquantum/core/circuit/circuit.py" "redefined-outer-name" +"mindquantum/mindquantum/core/circuit/circuit.py" "invalid-name" + +# Tests +"mindquantum/tests/st" "missing-docstring" +"mindquantum/tests/st" "unused-variable" +"mindquantum/tests/st" "len-as-condition" +"mindquantum/tests/ut" "missing-docstring" +"mindquantum/tests/st" "wrong-import-position" +"mindquantum/tests/st" "protected-access" + +# Benchmarks +"mindquantum/tutorials/benchmarks" "wrong-import-position" +"mindquantum/tutorials/benchmarks" "redefined-outer-name" +"mindquantum/tutorials/benchmarks" "protected-access" +"mindquantum/tutorials/benchmarks" "missing-docstring" +"mindquantum/tutorials/benchmarks" "unused-argument" + +# Tutorials +"mindquantum/tutorials" "missing-docstring" +"mindquantum/tutorials" "wrong-import-position" +"mindquantum/tutorials" "unused-argument" +"mindquantum/tutorials" "redefined-outer-name" +"mindquantum/tutorials" "pointless-statement" +"mindquantum/tutorials" "reimported" +"mindquantum/tutorials" "expression-not-assigned" +"mindquantum/tutorials" "function-redefined" +"mindquantum/tutorials" "unused-variable" +"mindquantum/tutorials" "len-as-condition" +"mindquantum/tutorials" "superfluous-parens" +"mindquantum/tutorials" "invalid-name" +"mindquantum/tutorials" "unused-import" diff --git a/.jenkins/check/config/whitelizard.txt b/.jenkins/check/config/whitelizard.txt new file mode 100644 index 0000000000000000000000000000000000000000..d748aac7a896719cc08809b5609e082d7931c2c5 --- /dev/null +++ b/.jenkins/check/config/whitelizard.txt @@ -0,0 +1,8 @@ +# Scene1: +# function_name1, function_name2 +# Scene2: +# file_path:function_name1, function_name2 +# +mindquantum/mindquantum/src/binding.cc:mindquantum::PYBIND11_MODULE +mindquantum/mindquantum/io/qasm/hiqasm.py:trans_v01 +mindquantum/mindquantum/io/qasm/hiqasm.py:to_string diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000000000000000000000000000000000..6f3bf9ad4a9c8dace7fc48f07679b777c215a158 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,153 @@ +# To use: +# +# pre-commit run -a +# +# Or: +# +# pre-commit install # (runs every time you commit in git) +# +# To update this file: +# +# pre-commit autoupdate +# +# See https://github.com/pre-commit/pre-commit + +--- + +ci: + skip: [check-manifest] + +exclude: >- + (?x)^( + .*/kernel[0-9]+\.hpp| + third_party/patch/.* + )$ + +repos: + - repo: meta + hooks: + - id: check-useless-excludes + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.0.1 + hooks: + - id: check-added-large-files + - id: check-case-conflict + - id: check-merge-conflict + - id: check-symlinks + - id: check-yaml + - id: check-toml + - id: debug-statements + - id: end-of-file-fixer + - id: mixed-line-ending + - id: trailing-whitespace + - id: fix-encoding-pragma + + # Changes tabs to spaces + - repo: https://github.com/Lucas-C/pre-commit-hooks + rev: v1.1.10 + hooks: + - id: remove-tabs + + - repo: https://github.com/codespell-project/codespell + rev: v2.1.0 + hooks: + - id: codespell + files: (.*mindquantum/.*) + args: [-q, '7', -S, '.git,third_party', -I, .codespell.allow] + + - repo: https://github.com/cheshirekow/cmake-format-precommit + rev: v0.6.13 + hooks: + - id: cmake-format + additional_dependencies: [pyyaml] + - id: cmake-lint + exclude: ^(cmake/Modules/.*)$ + additional_dependencies: [pyyaml] + + - repo: https://github.com/PyCQA/isort + rev: 5.9.3 + hooks: + - id: isort + name: isort (python) + + - repo: https://github.com/psf/black + rev: 21.9b0 + hooks: + - id: black + language_version: python3 + # This is a slow hook, so only run this if --hook-stage manual is passed + stages: [manual] + + - repo: https://gitlab.com/PyCQA/flake8 + rev: 3.9.2 + hooks: + - id: flake8 + name: flake8-strict + exclude: (test_.*\.py)$ + additional_dependencies: [flake8-comprehensions, flake8-breakpoint, + flake8-eradicate, flake8-mutable, pep8-naming, + flake8-docstrings, flake8-secure-coding-standard] + - id: flake8 + name: flake8-test-files + files: (.test_*\.py)$ + additional_dependencies: [flake8-comprehensions, flake8-breakpoint, + flake8-eradicate, flake8-mutable] + + - repo: https://github.com/pre-commit/mirrors-pylint + rev: v3.0.0a4 + hooks: + - id: pylint + name: pylint-strict + exclude: (test_.*\.py)$ + args: [--score=n, --load-plugins=pylint_secure_coding_standard] + # This is a slow hook, so only run this if --hook-stage manual is passed + stages: [manual] + additional_dependencies: [pybind11>=2.6, numpy, scipy, projectq, openfermion, sympy, matplotlib, rich, + pylint-secure-coding-standard] + - id: pylint + name: pylint-test-files + files: ^(test_.*\.py)$ + args: ['--score=n'] + # This is a slow hook, so only run this if --hook-stage manual is passed + stages: [manual] + additional_dependencies: [pybind11>=2.6, numpy, scipy, projectq, openfermion, sympy, matplotlib, rich] + + - repo: https://github.com/mgedmin/check-manifest + rev: '0.47' + hooks: + - id: check-manifest + additional_dependencies: ['setuptools-scm[toml]', 'pybind11>=2.6'] + + - repo: https://github.com/Takishima/cmake-pre-commit-hooks/ + rev: v1.4.0 + hooks: + - id: clang-format + args: [-i] + - id: clang-tidy + stages: [manual] + args: [-Bbuild, -B.pre-commit-build] + exclude: >- + (?x)^( + .*/kernel[0-9]+\.hpp| + .*third_party/.*| + .*\.cu + )$ + + - repo: https://github.com/pocc/pre-commit-hooks + rev: v1.3.4 + hooks: + - id: cppcheck + args: [--force, + --inline-suppr, + --std=c++17, + --language=c++, + --suppressions-list=.cppcheck.suppressions] + - id: cpplint + args: [--root=src, + '--extensions=cxx,cu,hh,cpp,hxx,cuh,h++,cc,c,hpp,c++,h,tpp,txx,cl', + '--filter=-build/header_guard,-build/c++11', + --quiet, + --repository=mindquantum, + --linelength=120, + --recursive] diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..e02654cbc6d25cf628f191ab4ec037becfe8d2dc --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,126 @@ +# ============================================================================== +# +# Copyright 2021 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ============================================================================== + +cmake_minimum_required(VERSION 3.15) + +set(_policy_list + CMP0012 + CMP0015 + CMP0022 + CMP0023 + CMP0028 + CMP0042 + CMP0048 + CMP0051 + CMP0054 + CMP0056 + CMP0057 + CMP0066 + CMP0067 + CMP0068 + CMP0074 + CMP0076 + CMP0077 + CMP0079 + CMP0094 + CMP0104) +foreach(_policy ${_policy_list}) + if(POLICY ${_policy}) + cmake_policy(SET ${_policy} NEW) + endif() + # cmake-format: off + # CMP0012: if() recognizes numbers and booleans + # CMP0015: paths relative to source dir for link_directories + # CMP0028: :: in target names + # CMP0042: MACOS_RPATH + # CMP0048: allow VERSION in project() + # CMP0051: list TARGET_OBJECTS in SOURCES property + # CMP0054: no more de-referencing of "expr" in if() statements + # CMP0056: try_compile(): link flags + # CMP0057: if IN_LIST + # CMP0066: try_compile(): use per-config flags, like CMAKE_CXX_FLAGS_RELEASE + # CMP0067: try_compile(): honor language standard variables (like C++11) + # CMP0068: RPATH on Mac OS does not affect install_name + # CMP0074: XXX_ROOT variables for find_package(XXX) + # CMP0076: target_sources relative paths + # CMP0077: option() honors normal variables + # CMP0079: target_link_libraries allows use with targets in other directories + # (CMake 3.13 minimum) + # CMP0094: FindPython* use LOCATION strategy (stop at first valid version) + # CMP0104: Empty CUDA_ARCHITECTURES target property is an error + # cmake-format: on +endforeach() + +list(PREPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}/cmake) +list(PREPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}/cmake/Modules) + +# ============================================================================== +# Macro definitions + +include(${CMAKE_CURRENT_LIST_DIR}/cmake/macros.cmake) + +# ============================================================================== +# Create the MindQuantum project +project(MindQuantum LANGUAGES C CXX) + +# Set a default build type if none was specified +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + message(STATUS "Setting build type to 'Release' as none was specified.") + set(CMAKE_BUILD_TYPE + Release + CACHE STRING "Choose the type of build." FORCE) + # Set the possible values of build type for cmake-gui + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "RelWithDebInfo") +endif() + +if(NOT CMAKE_LIBRARY_OUTPUT_DIRECTORY) + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/mindquantum) +endif() + +# ============================================================================== +# OS-detection + +include(${CMAKE_CURRENT_LIST_DIR}/cmake/os_detection.cmake) + +# ============================================================================== +# Options + +include(${CMAKE_CURRENT_LIST_DIR}/cmake/options.cmake) + +# ============================================================================== +# Package dependencies + +include(${CMAKE_CURRENT_LIST_DIR}/cmake/packages.cmake) + +# ============================================================================== +# Setup compiler flags + +include(${CMAKE_CURRENT_LIST_DIR}/cmake/compiler_flags.cmake) +include(${CMAKE_CURRENT_LIST_DIR}/cmake/linker_flags.cmake) + +# ============================================================================== +# Add submodule dependencies (now rather than later so that the relevant macros/variables are defined) + +add_subdirectory(mindquantum/src) + +# ============================================================================== +# Some more macro definitions + +include(${CMAKE_CURRENT_LIST_DIR}/cmake/macros_more.cmake) + +# ============================================================================== diff --git a/NOTICE b/NOTICE index 58d2ef0619b7017c882a0b7f05742e2996c2ecc4..fd00f5349d94ff758250f4a295a1f853212e82d4 100644 --- a/NOTICE +++ b/NOTICE @@ -1,2 +1,2 @@ MindSpore MindQuantum -Copyright 2019-2021 Huawei Technologies Co., Ltd \ No newline at end of file +Copyright 2019-2021 Huawei Technologies Co., Ltd diff --git a/README.md b/README.md index 4fc5722b0ceb6462f49400adc2a596727b823759..e5595cdf797ab97f06779dbf41b9e38f7717a1e0 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,10 @@ - [What is MindQuantum](#what-is-mindquantum) +- [First experience](#first-experience) + - [Build parameterized quantum circuit](#build-parameterized-quantum-circuit) + - [Train quantum neural network](#train-quantum-neural-network) +- [API](#api) - [Installation](#installation) - [Confirming System Environment Information](#confirming-system-environment-information) - [Install by Source Code](#install-by-source-code) @@ -25,16 +29,74 @@ ## What is MindQuantum -MindQuantum is a quantum machine learning framework developed by [MindSpore](https://www.mindspore.cn/en) and [HiQ](https://hiq.huaweicloud.com/), that can be used to build and train different quantum neural networks. Thanks to the powerful algorithm of quantum software group of Huawei and High-performance automatic differentiation ability of MindSpore, MindQuantum can efficiently handle problems such as quantum chemical simulation and quantum approximation optimization with [TOP1](https://gitee.com/mindspore/mindquantum/tree/master/tutorials/benchmarks) performance, which provides an efficient platform for researchers, teachers and students to quickly design and verify quantum machine learning algorithms. - +MindQuantum is a general quantum computing framework developed by [MindSpore](https://www.mindspore.cn/en) and [HiQ](https://hiq.huaweicloud.com/), that can be used to build and train different quantum neural networks. Thanks to the powerful algorithm of quantum software group of Huawei and High-performance automatic differentiation ability of MindSpore, MindQuantum can efficiently handle problems such as quantum machine learning, quantum chemistry simulation, and quantum optimization, which provides an efficient platform for researchers, teachers and students to quickly design and verify quantum machine learning algorithms. MindQuantum Architecture +## First experience + +### Build parameterized quantum circuit + +The below example shows how to build a parameterized quantum circuit. + +```python +from mindquantum import * +import numpy as np +encoder = Circuit().h(0).rx({'a0': 2}, 0).ry('a1', 1) +print(encoder) +print(encoder.get_qs(pr={'a0': np.pi/2, 'a1': np.pi/2}, ket=True)) +``` + +Then you will get, + +```bash +q0: ────H───────RX(2*a0)── + +q1: ──RY(a1)────────────── + +-1/2j¦00⟩ +-1/2j¦01⟩ +-1/2j¦10⟩ +-1/2j¦11⟩ +``` + +### Train quantum neural network + +```python +ansatz = CPN(encoder.hermitian(), {'a0': 'b0', 'a1': 'b1'}) +sim = Simulator('projectq', 2) +ham = Hamiltonian(-QubitOperator('Z0 Z1')) +grad_ops = sim.get_expectation_with_grad(ham, + encoder + ansatz, + encoder_params_name=encoder.params_name, + ansatz_params_name=ansatz.params_name) + +import mindspore as ms +ms.context.set_context(mode=ms.context.PYNATIVE_MODE, device_target="CPU") +net = MQLayer(grad_ops) +encoder_data = ms.Tensor(np.array([[np.pi/2, np.pi/2]])) +opti = ms.nn.Adam(net.trainable_params(), learning_rate=0.1) +train_net = ms.nn.TrainOneStepCell(net, opti) +for i in range(100): + train_net(encoder_data) +print(dict(zip(ansatz.params_name, net.trainable_params()[0].asnumpy()))) +``` + +The trained parameters are, + +```bash +{'b1': 1.5720831, 'b0': 0.006396801} +``` + +## API + +For more API, please refers to [MindQuantum API](https://www.mindspore.cn/mindquantum/api/zh-CN/master/index.html) + ## Installation ### Confirming System Environment Information -- The hardware platform should be Linux CPU with avx supported. -- Refer to [MindQuantum Installation Guide](https://www.mindspore.cn/install/en), install MindSpore, version 1.2.0 or later is required. +- The hardware platform should be Linux CPU with avx2 supported. +- Refer to [MindQuantum Installation Guide](https://www.mindspore.cn/install/en), install MindSpore, version 1.3.0 or later is required. - See [setup.py](https://gitee.com/mindspore/mindquantum/blob/master/setup.py) for the remaining dependencies. ### Install by Source Code @@ -50,7 +112,9 @@ git clone https://gitee.com/mindspore/mindquantum.git ```bash cd ~/mindquantum -python setup.py install --user +bash build.sh +cd output +pip install mindquantum-*.whl ``` ### Install by pip @@ -63,10 +127,19 @@ pip install https://hiq.huaweicloud.com/download/mindspore/cpu/x86_64/mindspore- #### Install MindQuantum +- Linux + +```bash +pip install https://hiq.huaweicloud.com/download/mindquantum/newest/linux/mindquantum-master-cp37-cp37m-linux_x86_64.whl -i https://pypi.tuna.tsinghua.edu.cn/simple +``` + +- Windows + ```bash -pip install https://hiq.huaweicloud.com/download/mindquantum/any/mindquantum-0.2.0-py3-none-any.whl -i https://pypi.tuna.tsinghua.edu.cn/simple +pip install https://hiq.huaweicloud.com/download/mindquantum/newest/windows/mindquantum-master-cp37-cp37m-win_amd64.whl -i https://pypi.tuna.tsinghua.edu.cn/simple ``` +> - Change `cp37-cp37m` to `cp38-cp38` or `cp39-cp39` according to your python version. > - When the network is connected, dependency items are automatically downloaded during .whl package installation. (For details about other dependency items, see [setup.py](https://gitee.com/mindspore/mindquantum/blob/master/setup.py)). In other cases, you need to manually install dependency items. ## Verifying Successful Installation diff --git a/README_CN.md b/README_CN.md index 50d9ce093080294acc7b1c05ea92b1d280f79cd6..7894b8cc4a95b7035d81c024625cff87c846adbe 100644 --- a/README_CN.md +++ b/README_CN.md @@ -5,16 +5,19 @@ - [MindQuantum介绍](#mindquantum介绍) +- [初体验](#初体验) + - [搭建参数化量子线路](#搭建参数化量子线路) + - [训练量子神经网络](#训练量子神经网络) +- [API](#api) - [安装教程](#安装教程) - [确认系统环境信息](#确认系统环境信息) - [pip安装](#pip安装) - [安装MindSpore](#安装mindspore) - [安装MindQuantum](#安装mindquantum) - [源码安装](#源码安装) -- [API](#api) - [验证是否成功安装](#验证是否成功安装) - [Docker安装](#docker安装) -- [注意事项](#注意事项) +- [注意事项FAQ](#注意事项faq) - [快速入门](#快速入门) - [文档](#文档) - [社区](#社区) @@ -26,34 +29,101 @@ ## MindQuantum介绍 -MindQuantum是基于华为开源自研AI框架MindSpore开发的高性能量子-经典混合计算框架,能高效的生成多种变分量子线路,支持量子模拟、量子组合优化、量子机器学习等NISQ算法,性能达到业界[领先水平](https://gitee.com/mindspore/mindquantum/tree/master/tutorials/benchmarks)。结合HiQ量子计算云平台,MindQuantum可以作为广大的科研人员、老师和学生快速设计和体验量子计算的高效解决方案。 +MindQuantum是基于昇思MindSpore开源深度学习框架和HiQ量子计算云平台开发的通用量子计算框架,支持多种量子神经网络的训练和推理。得益于华为HiQ团队的量子计算模拟器和昇思MindSpore高性能自动微分能力,MindQuantum能够高效处理量子机器学习、量子化学模拟和量子优化等问题,为广大的科研人员、老师和学生提供快速设计和验证量子机器学习算法的高效平台。 MindQuantum Architecture +## 初体验 + +### 搭建参数化量子线路 + +通过如下示例可便捷搭建参数化量子线路 + +```python +from mindquantum import * +import numpy as np +encoder = Circuit().h(0).rx({'a0': 2}, 0).ry('a1', 1) +print(encoder) +print(encoder.get_qs(pr={'a0': np.pi/2, 'a1': np.pi/2}, ket=True)) +``` + +你将得到 + +```bash +q0: ────H───────RX(2*a0)── + +q1: ──RY(a1)────────────── + +-1/2j¦00⟩ +-1/2j¦01⟩ +-1/2j¦10⟩ +-1/2j¦11⟩ +``` + +### 训练量子神经网络 + +```python +ansatz = CPN(encoder.hermitian(), {'a0': 'b0', 'a1': 'b1'}) +sim = Simulator('projectq', 2) +ham = Hamiltonian(-QubitOperator('Z0 Z1')) +grad_ops = sim.get_expectation_with_grad(ham, + encoder + ansatz, + encoder_params_name=encoder.params_name, + ansatz_params_name=ansatz.params_name) + +import mindspore as ms +ms.context.set_context(mode=ms.context.PYNATIVE_MODE, device_target="CPU") +net = MQLayer(grad_ops) +encoder_data = ms.Tensor(np.array([[np.pi/2, np.pi/2]])) +opti = ms.nn.Adam(net.trainable_params(), learning_rate=0.1) +train_net = ms.nn.TrainOneStepCell(net, opti) +for i in range(100): + train_net(encoder_data) +print(dict(zip(ansatz.params_name, net.trainable_params()[0].asnumpy()))) +``` + +训练得到参数为 + +```bash +{'b1': 1.5720831, 'b0': 0.006396801} +``` + +## API + +对于上述示例所涉及API和其他更多用法,请查看MindQuantum API文档[文档链接](https://www.mindspore.cn/mindquantum/api/zh-CN/master/index.html) + ## 安装教程 ### 确认系统环境信息 -- 硬件平台确认为Linux系统下的CPU,并支持avx指令集。 -- 参考[MindSpore安装指南](https://www.mindspore.cn/install),完成MindSpore的安装,要求至少1.2.0版本。 +- 硬件平台确认为Linux系统下的CPU,并支持avx2指令集。 +- 参考[MindSpore安装指南](https://www.mindspore.cn/install),完成MindSpore的安装,要求至少1.3.0版本。 - 其余依赖请参见[setup.py](https://gitee.com/mindspore/mindquantum/blob/master/setup.py) ### pip安装 #### 安装MindSpore +请根据MindSpore官网[安装指南](https://www.mindspore.cn/install),安装1.3.0及以上版本的MindSpore。 + +#### 安装MindQuantum + +最新版MindQuantum请通过如下指令安装。 + +- Linux系统: + ```bash -pip install https://hiq.huaweicloud.com/download/mindspore/cpu/x86_64/mindspore-1.3.0-cp38-cp38-linux_x86_64.whl -i https://pypi.tuna.tsinghua.edu.cn/simple +pip install https://hiq.huaweicloud.com/download/mindquantum/newest/linux/mindquantum-master-cp37-cp37m-linux_x86_64.whl -i https://pypi.tuna.tsinghua.edu.cn/simple ``` -> - 请根据本机的python版本选择合适的安装包,如本机为python 3.7,则可将上面命令中的`cp38-cp38`修改为`cp37-cp37m`。 - -#### 安装MindQuantum +- Windows系统: ```bash -pip install https://hiq.huaweicloud.com/download/mindquantum/any/mindquantum-0.2.0-py3-none-any.whl -i https://pypi.tuna.tsinghua.edu.cn/simple +pip install https://hiq.huaweicloud.com/download/mindquantum/newest/windows/mindquantum-master-cp37-cp37m-win_amd64.whl -i https://pypi.tuna.tsinghua.edu.cn/simple ``` +> - 如需安装其他python版本的MindQuantum,请将上述地址中的`cp37-cp37m`更换为`cp38-cp38`或者`cp39-cp39`。 +> - 如需安装其他版本的MindQuantum,请浏览[所有安装包](https://hiq.huaweicloud.com/download/mindquantum/)。 > - 在联网状态下,安装whl包时会自动下载MindQuantum安装包的依赖项(依赖项详情参见[setup.py](https://gitee.com/mindspore/mindquantum/blob/master/setup.py)),其余情况需自行安装。 ### 源码安装 @@ -65,16 +135,25 @@ pip install https://hiq.huaweicloud.com/download/mindquantum/any/mindquantum-0.2 git clone https://gitee.com/mindspore/mindquantum.git ``` -2. 编译安装MindQuantum +2. 编译MindQuantum + + Linux系统下请确保安装好CMake >= 3.18.3,然后运行如下命令: ```bash cd ~/mindquantum - python setup.py install --user + bash build.sh ``` -## API + Windows系统下请确保安装好MinGW-W64和CMake >= 3.18.3,然后运行如下命令: -MindQuantum API文档请查看[文档链接](https://www.mindspore.cn/mindquantum/api/zh-CN/master/index.html) + ```bash + cd mindquantum + ./build.bat -G "MinGW Makefiles" + ``` + +3. 安装编译好的whl包 + + 进入output目录,通过`pip`命令安装编译好的mindquantum的whl包。 ## 验证是否成功安装 @@ -88,7 +167,7 @@ python -c 'import mindquantum' 通过Docker也可以在Mac系统或者Windows系统中使用Mindquantum。具体参考[Docker安装指南](./install_with_docker.md). -## 注意事项 +## 注意事项FAQ 运行代码前请设置量子模拟器运行时并行内核数,例如设置并行内核数为4,可运行如下代码: @@ -98,6 +177,8 @@ export OMP_NUM_THREADS=4 对于大型服务器,请根据模型规模合理设置并行内核数以达到最优效果。 +更多注意事项请查看[FAQ页面](https://gitee.com/mindspore/mindquantum/blob/master/tutorials/0.frequently_asked_questions.ipynb)。 + ## 快速入门 关于如何快速搭建参数化量子线路和量子神经网络,并进行训练,请点击查看[MindQuantum使用教程](https://www.mindspore.cn/mindquantum/docs/zh-CN/master/index.html) diff --git a/RELEASE.md b/RELEASE.md index d775ae72b09c6ab4b5cd8b564d87ad3e069bf361..9fef17f840ec3d1dba6f30cc363c9adee31686fa 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,15 +1,418 @@ -# MindQuantum 0.1.0 Release Notes +# MindQuantum 0.5.0 + +## MindQuantum 0.5.0 Release Notes + +### Major Features and Improvements + +### API Change + +#### Backwards Incompatible Change + +We unified the abbreviations of some nouns in MindQuantum. + +- `isparameter` property of gate changes to `parameterized` + + + + + + + + + +
0.3.1 0.5.0
+ +```python +>>> from mindquantum import RX +>>> gate = RX('a').on(0) +>>> gate.isparameter +True +``` + + + +```python +>>> from mindquantum import RX +>>> gate = RX('a').on(0) +>>> gate.parameterized +True +``` + +
+ +- `para_name` of a quantum circuit changes to `params_name` + + + + + + + + + +
0.3.1 0.5.0
+ +```python +>>> from mindquantum import Circuit +>>> circ = Circuit().rx('a', 0) +>>> circ.para_name +['a'] +``` + + + +```python +>>> from mindquantum import Circuit +>>> circ = Circuit().rx('a', 0) +>>> circ.params_name +['a'] +``` + +
+ +The quantum neural network API was redesigned in this version. From now on, we can easily build a hybrid quantum neural network with the help of `Simulator` in `PYNATIVE_MODE`. + +The following API was removed. + +1. `generate_pqc_operator` +2. `PQC` +3. `MindQuantumLayer` +4. `generate_evolution_operator` +5. `Evolution` +6. `MindQuantumAnsatzOnlyLayer` +7. `MindQuantumAnsatzOnlyOperator` + +The new API was shown as below. + +1. `MQOps` +2. `MQN2Ops` +3. `MQAnsatzOnlyOps` +4. `MQN2AnsatzOnlyOps` +5. `MQEncoderOnlyOps` +6. `MQN2EncoderOnlyOps` +7. `MQLayer` +8. `MQN2Layer` +9. `MQAnsatzOnlyLayer` +10. `MQN2AnsatzOnlyLayer` + +The above modules are placed in `mindquantum.framework`. + +#### Removed + +Due to the duplication of functions, we deleted some APIs. + +- `mindquantum.circuit.StateEvolution` + +The following APIs have been remoted. + +- `mindquantum.core.operators.Hamiltonian.mindspore_data` +- `mindquantum.core.operators.Projector.mindspore_data` +- `mindquantum.core.circuit.Circuit.mindspore_data` +- `mindquantum.core.parameterresolver.ParameterResolver.mindspore_data` + +#### New feature + +New gates are shown as below. + +- `mindquantum.core.SGate` +- `mindquantum.core.TGate` + +Measurement on certain qubits are now supported. The related APIs are shown as below. + +- `mindquantum.core.Measure` +- `mindquantum.core.MeasureResult` + +Displaying a circuit in text format is now supported. + +- `mindquantum.io.brick_model` +- `mindquantum.io.measure_text_drawer` + +QASM is now supported. + +- `mindquantum.io.OpenQASM` +- `mindquantum.io.random_hiqasm` +- `mindquantum.io.HiQASM` + +Simulator is now separated from MindSpore backend. Now you can easily to use a simulator. + +- `mindquantum.simulator.Simulator` + +### Refactoring + +For improving MindQuantum's package structure, we did some refactoring on MindQuantum. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
old new
+ +`mindquantum.gate.Hamiltonian` + + +`mindquantum.core.operators.Hamiltonian` +
+ +`mindquantum.gate.Projector` + + +`mindquantum.core.operators.Projector` +
+ +`mindquantum.circuit.qft` + + +`mindquantum.algorithm.library.qft` +
+ +`mindquantum.circuit.generate_uccsd` + + +`mindquantum.algorithm.nisq.chem.generate_uccsd` +
+ +`mindquantum.circuit.TimeEvolution` + + +`mindquantum.core.operators.TimeEvolution` +
+ +`mindquantum.utils.count_qubits` + + +`mindquantum.core.operators.count_qubits` +
+ +`mindquantum.utils.commutator` + + +`mindquantum.core.operators.commutator` +
+ +`mindquantum.utils.normal_ordered` + + +`mindquantum.core.operators.normal_ordered` +
+ +`mindquantum.utils.get_fermion_operator` + + +`mindquantum.core.operators.get_fermion_operator` +
+ +`mindquantum.utils.number_operator` + + +`mindquantum.core.operators.number_operator` +
+ +`mindquantum.utils.hermitian_conjugated` + + +`mindquantum.core.operators.hermitian_conjugated` +
+ +`mindquantum.utils.up_index` + + +`mindquantum.core.operators.up_index` +
+ +`mindquantum.utils.down_index` + + +`mindquantum.core.operators.down_index` +
+ +`mindquantum.utils.sz_operator` + + +`mindquantum.core.operators.sz_operator` +
+ +`mindquantum.ansatz.Ansatz` + +`mindquantum.algorithm.nisq.Ansatz` +
+ +`mindquantum.ansatz.MaxCutAnsatz` + + +`mindquantum.algorithm.nisq.qaoa.MaxCutAnsatz` +
+ +`mindquantum.ansatz.Max2SATAnsatz` + + +`mindquantum.algorithm.nisq.qaoa.Max2SATAnsatz` +
+ +`mindquantum.ansatz.HardwareEfficientAnsatz` + + +`mindquantum.algorithm.nisq.chem.HardwareEfficientAnsatz` +
+ +`mindquantum.ansatz.QubitUCCAnsatz` + + +`mindquantum.algorithm.nisq.chem.QubitUCCAnsatz` +
+ +`mindquantum.ansatz.UCCAnsatz` + + +`mindquantum.algorithm.nisq.chem.UCCAnsatz` +
+ +`mindquantum.hiqfermion.Transform` + + +`mindquantum.algorithm.nisq.chem.Transform` +
+ +`mindquantum.hiqfermion.get_qubit_hamiltonian` + + +`mindquantum.algorithm.nisq.chem.get_qubit_hamiltonian` +
+ +`mindquantum.hiqfermion.uccsd_singlet_generator` + + +`mindquantum.algorithm.nisq.chem.uccsd_singlet_generator` +
+ +`mindquantum.hiqfermion.uccsd_singlet_get_packed_amplitudes` + + +`mindquantum.algorithm.nisq.chem.uccsd_singlet_get_packed_amplitudes` +
+ +`mindquantum.hiqfermion.uccsd0_singlet_generator` + + +`mindquantum.algorithm.nisq.chem.uccsd0_singlet_generator` +
+ +`mindquantum.hiqfermion.quccsd_generator` + + +`mindquantum.algorithm.nisq.chem.quccsd_generator` +
+ +`mindquantum.utils.bprint` + + +`mindquantum.io.bprint` +
+ +`mindquantum.circuit` + + +`mindquantum.core.circuit` +
+ +`mindquantum.gate` + + +`mindquantum.core.gates` +
+ +`mindquantum.ops` + + +`mindquantum.core.operators` +
+ +`mindquantum.parameterresolver` + + +`mindquantum.core.parameterresolver` +
+ +### Contributors + +Thanks goes to these wonderful people: + +yufan, wengwenkang, xuxusheng, Damien Ngyuen, zhouxu, wangzidong, yankang, lujiale, zhangzhenghai, fanyi, zhangwengang, wangkaisheng, zhoufeng, wangsiyuan, gongxiaoqing, chengxianbin, sunxiyin, wenwenkang, lvdingshun, cuijiangyu, chendiqing, zhangkai, Zotov Yuriy, liqin, zengjinglin, cuixiaopeng. + +Contributions of any kind are welcome! + +## MindQuantum 0.3.1 Release Notes + +### Major Features and Improvements + +- Three tutorials have been rewritten to make them easier to read +- Circuit information such as qubit number, parameters will update immediately after you add gate +- The UN operator now support parameterized gate +- New ansatz that solving max 2 sat problem now are supported + +### Contributors + +Thanks goes to these wonderful people: + +yufan, wengwenkang, xuxusheng, wangzidong, yangkang, lujiale, fanyi, zhangwengang, wangkaisheng, zhoufeng, wangsiyuan, gongxiaoqing, chengxianbin, sunxiyin, wenwenkang, lvdingshun, cuijiangyu, chendiqing, zhangkai, Damien Ngyuen, Zotov Yuriy, liqin, zengjinglin, cuixiaopeng. + +Contributions of any kind are welcome! + +## MindQuantum 0.2.0 Release Notes + +### Major Features and Improvements + +1. Parameterized FermionOperator and QubitOperator for quantum chemistry +2. Different kinds of transformation between FermionOperator and QubitOperator +3. UCCSD, QAOA and hardware efficient ansatz supported +4. MindQuantumAnsatzOnlyLayer for simulating circuit with ansatz only circuit +5. TimeEvolution with first order Trotter decomposition +6. High level operations for modifying quantum circuit + +### Contributors + +Thanks goes to these wonderful people: + +yufan, wengwenkang, xuxusheng, wanzidong, yankang, lujiale, fanyi, zhangwengang, wangkaisheng, zhoufeng, wangsiyuan, gongxiaoqing, chengxianbin, sunxiyin, wenwenkang, lvdingshun, cuijiangyu, chendiqing, zhangkai, Damien Ngyuen, Zotov Yuriy, liqin, zengjinglin, cuixiaopeng. + +Contributions of any kind are welcome! + +## MindQuantum 0.1.0 Release Notes Initial release of MindQuantum. ### Major Features and Improvements -* Easily build parameterized quantum circuit. -* Effectively simulate quantum circuit. -* Calculating the gradient of parameters of quantum circuit. -* PQC (parameterized quantum circuit) operator that naturally compatible with other operators in mindspore framework. -* Evolution operator that evaluate a quantum circuit and return the quantum state. -* Data parallelization for PQC operator. +1. Easily build parameterized quantum circuit. +2. Effectively simulate quantum circuit. +3. Calculating the gradient of parameters of quantum circuit. +4. PQC (parameterized quantum circuit) operator that naturally compatible with other operators in mindspore framework. +5. Evolution operator that evaluate a quantum circuit and return the quantum state. +6. Data parallelization for PQC operator. ### Contributors @@ -17,4 +420,4 @@ Thanks goes to these wonderful people: yufan, wengwenkang, xuxusheng, wanzidong, yankang, lujiale, wangkaisheng, zhoufeng, wangsiyuan, gongxiaoqing, chengxianbin, sunxiyin, wenwenkang, lvdingshun, cuijiangyu, chendiqing, zhangkai, Damien Ngyuen, Zotov Yuriy, liqin, zengjinglin, cuixiaopeng. -Contributions of any kind are welcome! \ No newline at end of file +Contributions of any kind are welcome! diff --git a/build.bat b/build.bat new file mode 100644 index 0000000000000000000000000000000000000000..bac67c1cc08a5c81425ccf50f436b313b0b7573b --- /dev/null +++ b/build.bat @@ -0,0 +1,27 @@ +@rem Copyright 2020 Huawei Technologies Co., Ltd +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem http://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem ============================================================================ +@echo off +@title mindquantum_build + +SET BASE_PATH=%CD% +SET BUILD_PATH=%BASE_PATH%/build +SET OUTPUT=%BASE_PATH%/output + +IF NOT EXIST "%BUILD_PATH%" ( + md "build" +) + +cd %BASE_PATH% +python %BASE_PATH%/setup.py bdist_wheel -d %OUTPUT% --set ENABLE_PROJECTQ --unset ENABLE_QUEST %* diff --git a/build.sh b/build.sh old mode 100644 new mode 100755 index 4e55df72b402e7b95c1792a38f3db623836b7f0f..6855ef0eebe4c7271c8ce8e26f9b854547be06ac --- a/build.sh +++ b/build.sh @@ -13,11 +13,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -set -e - BASEPATH=$(cd "$(dirname $0)"; pwd) OUTPUT_PATH="${BASEPATH}/output" -PYTHON=$(which python3) +if command -v python3 >/dev/null 2>&1; then + PYTHON=python3 +elif command -v python >/dev/null 2>&1; then + PYTHON=python3 +else + echo 'Unable to locate python or python3!' 1>&2 + exit 1 +fi + +# ============================================================================== mk_new_dir() { local create_dir="$1" # the target to make @@ -29,22 +36,21 @@ mk_new_dir() { mkdir -pv "${create_dir}" } -write_checksum() { - cd "$OUTPUT_PATH" || exit - PACKAGE_LIST=$(ls mindquantum-*.whl) || exit - for PACKAGE_NAME in $PACKAGE_LIST; do - echo $PACKAGE_NAME - sha256sum -b "$PACKAGE_NAME" >"$PACKAGE_NAME.sha256" - done -} +# ============================================================================== + +set -e + +cd ${BASEPATH} mk_new_dir "${OUTPUT_PATH}" -${PYTHON} ${BASEPATH}/setup.py bdist_wheel +args=(--set ENABLE_PROJECTQ --unset ENABLE_QUEST) -mv ${BASEPATH}/dist/*whl ${OUTPUT_PATH} +if [[ $1 = "gpu" ]]; then + args+=(--set ENABLE_CUDA --unset MULTITHREADED --set VERBOSE_CMAKE) +fi -write_checksum +${PYTHON} ${BASEPATH}/setup.py bdist_wheel -d ${OUTPUT_PATH} "${args[@]}" echo "------Successfully created mindquantum package------" diff --git a/cmake/Modules/CheckLinkerFlag.cmake b/cmake/Modules/CheckLinkerFlag.cmake new file mode 100644 index 0000000000000000000000000000000000000000..da5ec50fab5e3c0a0907ca0a2148dc2dc97c49d5 --- /dev/null +++ b/cmake/Modules/CheckLinkerFlag.cmake @@ -0,0 +1,86 @@ +# Distributed under the OSI-approved BSD 3-Clause License. See accompanying +# file Copyright.txt or https://cmake.org/licensing for details. + +#[=======================================================================[.rst: +CheckLinkerFlag +--------------- + +.. versionadded:: 3.18 + +Check whether the compiler supports a given link flag. + +.. command:: check_linker_flag + + .. code-block:: cmake + + check_linker_flag( ) + +Check that the link ```` is accepted by the ```` compiler without +a diagnostic. Stores the result in an internal cache entry named ````. + +This command temporarily sets the ``CMAKE_REQUIRED_LINK_OPTIONS`` variable +and calls the :command:`check_source_compiles` command from the +:module:`CheckSourceCompiles` module. See that module's documentation +for a listing of variables that can otherwise modify the build. + +The underlying implementation relies on the :prop_tgt:`LINK_OPTIONS` property +to check the specified flag. The ``LINKER:`` prefix, as described in the +:command:`target_link_options` command, can be used as well. + +A positive result from this check indicates only that the compiler did not +issue a diagnostic message when given the link flag. Whether the flag has any +effect or even a specific one is beyond the scope of this module. + +.. note:: + Since the :command:`try_compile` command forwards flags from variables + like :variable:`CMAKE__FLAGS`, unknown flags in such variables may + cause a false negative for this check. +#]=======================================================================] + +include_guard(GLOBAL) + +include(CMakeCheckCompilerFlagCommonPatterns) + +function(CHECK_LINKER_FLAG _lang _flag _var) + get_property(_supported_languages GLOBAL PROPERTY ENABLED_LANGUAGES) + if(NOT _lang IN_LIST _supported_languages) + message(SEND_ERROR "check_linker_flag: ${_lang}: unknown language.") + return() + endif() + + include(CheckSourceCompiles) + + set(CMAKE_REQUIRED_LINK_OPTIONS "${_flag}") + + # Normalize locale during test compilation. + set(_locale_vars LC_ALL LC_MESSAGES LANG) + foreach(v IN LISTS _locale_vars) + set(_locale_vars_saved_${v} "$ENV{${v}}") + set(ENV{${v}} C) + endforeach() + + if(_lang MATCHES "^(C|CXX)$") + set(_source "int main() { return 0; }") + elseif(_lang STREQUAL "Fortran") + set(_source " program test\n stop\n end program") + elseif(_lang MATCHES "CUDA") + set(_source "__host__ int main() { return 0; }") + elseif(_lang MATCHES "HIP") + set(_source "__host__ int main() { return 0; }") + elseif(_lang MATCHES "^(OBJC|OBJCXX)$") + set(_source "#ifndef __OBJC__\n# error \"Not an Objective-C++ compiler\"\n#endif\nint main(void) { return 0; }") + else() + message(SEND_ERROR "check_linker_flag: ${_lang}: unsupported language.") + return() + endif() + check_compiler_flag_common_patterns(_common_patterns) + + check_source_compiles(${_lang} "${_source}" ${_var} ${_common_patterns}) + + foreach(v IN LISTS _locale_vars) + set(ENV{${v}} ${_locale_vars_saved_${v}}) + endforeach() + set(${_var} + "${${_var}}" + PARENT_SCOPE) +endfunction() diff --git a/cmake/Modules/CheckSourceCompiles.cmake b/cmake/Modules/CheckSourceCompiles.cmake new file mode 100644 index 0000000000000000000000000000000000000000..2a196315a5a93f6ca6eec4ba5bcb4c2ab933f63f --- /dev/null +++ b/cmake/Modules/CheckSourceCompiles.cmake @@ -0,0 +1,82 @@ +# Distributed under the OSI-approved BSD 3-Clause License. See accompanying +# file Copyright.txt or https://cmake.org/licensing for details. + +# lint_cmake: -whitespace/indent + +#[=======================================================================[.rst: +CheckSourceCompiles +---------------------- + +.. versionadded:: 3.19 + +Check if given source compiles and links into an executable. + +.. command:: check_source_compiles + + .. code-block:: cmake + + check_source_compiles( + [FAIL_REGEX [...]] + [SRC_EXT ]) + + Check that the source supplied in ```` can be compiled as a source + file for the requested language and linked as an executable (so it must + contain at least a ``main()`` function). The result will be stored in the + internal cache variable specified by ````, with a boolean true + value for success and boolean false for failure. If ``FAIL_REGEX`` is + provided, then failure is determined by checking if anything in the output + matches any of the specified regular expressions. + + By default, the test source file will be given a file extension that matches + the requested language. The ``SRC_EXT`` option can be used to override this + with ``.`` instead. + + The underlying check is performed by the :command:`try_compile` command. The + compile and link commands can be influenced by setting any of the following + variables prior to calling ``check_source_compiles()``: + + ``CMAKE_REQUIRED_FLAGS`` + Additional flags to pass to the compiler. Note that the contents of + :variable:`CMAKE__FLAGS _FLAGS>` and its associated + configuration-specific variable are automatically added to the compiler + command before the contents of ``CMAKE_REQUIRED_FLAGS``. + + ``CMAKE_REQUIRED_DEFINITIONS`` + A :ref:`;-list ` of compiler definitions of the form + ``-DFOO`` or ``-DFOO=bar``. A definition for the name specified by + ```` will also be added automatically. + + ``CMAKE_REQUIRED_INCLUDES`` + A :ref:`;-list ` of header search paths to pass to + the compiler. These will be the only header search paths used by + ``try_compile()``, i.e. the contents of the :prop_dir:`INCLUDE_DIRECTORIES` + directory property will be ignored. + + ``CMAKE_REQUIRED_LINK_OPTIONS`` + A :ref:`;-list ` of options to add to the link + command(see :command:`try_compile` for further details). + + ``CMAKE_REQUIRED_LIBRARIES`` + A :ref:`;-list ` of libraries to add to the link + command. These can be the name of system libraries or they can be + :ref:`Imported Targets ` (see :command:`try_compile` for + further details). + + ``CMAKE_REQUIRED_QUIET`` + If this variable evaluates to a boolean true value, all status messages + associated with the check will be suppressed. + + The check is only performed once, with the result cached in the variable + named by ````. Every subsequent CMake run will re-use this cached + value rather than performing the check again, even if the ```` changes. + In order to force the check to be re-evaluated, the variable named by + ```` must be manually removed from the cache. + +#]=======================================================================] + +include_guard(GLOBAL) +include(CheckSourceCompilesLocal) + +function(CHECK_SOURCE_COMPILES _lang _source _var) + cmake_check_source_compiles(${_lang} "${_source}" ${_var} ${ARGN}) +endfunction() diff --git a/cmake/Modules/CheckSourceCompilesLocal.cmake b/cmake/Modules/CheckSourceCompilesLocal.cmake new file mode 100644 index 0000000000000000000000000000000000000000..f4d86616b3a5a1e430a28d29d593a5f4e35a06a2 --- /dev/null +++ b/cmake/Modules/CheckSourceCompilesLocal.cmake @@ -0,0 +1,125 @@ +# Distributed under the OSI-approved BSD 3-Clause License. See accompanying +# file Copyright.txt or https://cmake.org/licensing for details. + +# lint_cmake: -whitespace/indent + +include_guard(GLOBAL) + +cmake_policy(PUSH) +cmake_policy(SET CMP0054 NEW) # if() quoted variables not dereferenced +cmake_policy(SET CMP0057 NEW) # if() supports IN_LIST + +function(CMAKE_CHECK_SOURCE_COMPILES _lang _source _var) + if(NOT DEFINED "${_var}") + + if(_lang STREQUAL "C") + set(_lang_textual "C") + set(_lang_ext "c") + elseif(_lang STREQUAL "CXX") + set(_lang_textual "C++") + set(_lang_ext "cxx") + elseif(_lang STREQUAL "CUDA") + set(_lang_textual "CUDA") + set(_lang_ext "cu") + elseif(_lang STREQUAL "Fortran") + set(_lang_textual "Fortran") + set(_lang_ext "F90") + elseif(_lang STREQUAL "ISPC") + set(_lang_textual "ISPC") + set(_lang_ext "ispc") + elseif(_lang STREQUAL "OBJC") + set(_lang_textual "Objective-C") + set(_lang_ext "m") + elseif(_lang STREQUAL "OBJCXX") + set(_lang_textual "Objective-C++") + set(_lang_ext "mm") + else() + message(SEND_ERROR "check_source_compiles: ${_lang}: unknown language.") + return() + endif() + + get_property(_supported_languages GLOBAL PROPERTY ENABLED_LANGUAGES) + if(NOT _lang IN_LIST _supported_languages) + message(SEND_ERROR "check_source_compiles: ${_lang}: needs to be enabled before use.") + return() + endif() + + set(_FAIL_REGEX) + set(_SRC_EXT) + set(_key) + foreach(arg ${ARGN}) + if("${arg}" MATCHES "^(FAIL_REGEX|SRC_EXT)$") + set(_key "${arg}") + elseif(_key STREQUAL "FAIL_REGEX") + list(APPEND _FAIL_REGEX "${arg}") + elseif(_key STREQUAL "SRC_EXT") + set(_SRC_EXT "${arg}") + set(_key "") + else() + message(FATAL_ERROR "Unknown argument:\n ${arg}\n") + endif() + endforeach() + + if(NOT _SRC_EXT) + set(_SRC_EXT ${_lang_ext}) + endif() + + if(CMAKE_REQUIRED_LINK_OPTIONS) + set(CHECK_${LANG}_SOURCE_COMPILES_ADD_LINK_OPTIONS LINK_OPTIONS ${CMAKE_REQUIRED_LINK_OPTIONS}) + else() + set(CHECK_${LANG}_SOURCE_COMPILES_ADD_LINK_OPTIONS) + endif() + if(CMAKE_REQUIRED_LIBRARIES) + set(CHECK_${LANG}_SOURCE_COMPILES_ADD_LIBRARIES LINK_LIBRARIES ${CMAKE_REQUIRED_LIBRARIES}) + else() + set(CHECK_${LANG}_SOURCE_COMPILES_ADD_LIBRARIES) + endif() + if(CMAKE_REQUIRED_INCLUDES) + set(CHECK_${LANG}_SOURCE_COMPILES_ADD_INCLUDES "-DINCLUDE_DIRECTORIES:STRING=${CMAKE_REQUIRED_INCLUDES}") + else() + set(CHECK_${LANG}_SOURCE_COMPILES_ADD_INCLUDES) + endif() + file(WRITE "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeTmp/src.${_SRC_EXT}" "${_source}\n") + + if(NOT CMAKE_REQUIRED_QUIET) + message(CHECK_START "Performing Test ${_var}") + endif() + try_compile( + ${_var} ${CMAKE_BINARY_DIR} + ${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeTmp/src.${_SRC_EXT} + COMPILE_DEFINITIONS -D${_var} ${CMAKE_REQUIRED_DEFINITIONS} ${CHECK_${LANG}_SOURCE_COMPILES_ADD_LINK_OPTIONS} + ${CHECK_${LANG}_SOURCE_COMPILES_ADD_LIBRARIES} + CMAKE_FLAGS -DCOMPILE_DEFINITIONS:STRING=${CMAKE_REQUIRED_FLAGS} "${CHECK_${LANG}_SOURCE_COMPILES_ADD_INCLUDES}" + OUTPUT_VARIABLE OUTPUT) + + foreach(_regex ${_FAIL_REGEX}) + if("${OUTPUT}" MATCHES "${_regex}") + set(${_var} 0) + endif() + endforeach() + + if(${_var}) + set(${_var} + 1 + CACHE INTERNAL "Test ${_var}") + if(NOT CMAKE_REQUIRED_QUIET) + message(CHECK_PASS "Success") + endif() + file(APPEND ${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeOutput.log + "Performing ${_lang_textual} SOURCE FILE Test ${_var} succeeded with the following output:\n" "${OUTPUT}\n" + "Source file was:\n${_source}\n") + else() + if(NOT CMAKE_REQUIRED_QUIET) + message(CHECK_FAIL "Failed") + endif() + set(${_var} + "" + CACHE INTERNAL "Test ${_var}") + file(APPEND ${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeError.log + "Performing ${_lang_textual} SOURCE FILE Test ${_var} failed with the following output:\n" "${OUTPUT}\n" + "Source file was:\n${_source}\n") + endif() + endif() +endfunction() + +cmake_policy(POP) diff --git a/cmake/Modules/apple/FindOpenMP.cmake b/cmake/Modules/apple/FindOpenMP.cmake new file mode 100644 index 0000000000000000000000000000000000000000..1d85478756f846cb187db9a86279af1599908ff3 --- /dev/null +++ b/cmake/Modules/apple/FindOpenMP.cmake @@ -0,0 +1,725 @@ +# Distributed under the OSI-approved BSD 3-Clause License. See accompanying file Copyright.txt or +# https://cmake.org/licensing for details. + +# lint_cmake: -convention/filename,-whitespace/indent,-package/stdargs + +# cmake-lint: disable=C0103,C0111,R0912,R0915,E1121 + +#[=======================================================================[.rst: +FindOpenMP +---------- + +Finds Open Multi-Processing (OpenMP) support. + +This module can be used to detect OpenMP support in a compiler. If +the compiler supports OpenMP, the flags required to compile with +OpenMP support are returned in variables for the different languages. +The variables may be empty if the compiler does not need a special +flag to support OpenMP. + +.. versionadded:: 3.5 + Clang support. + +Variables +^^^^^^^^^ + +.. versionadded:: 3.10 + The module exposes the components ``C``, ``CXX``, and ``Fortran``. + Each of these controls the various languages to search OpenMP support for. + +Depending on the enabled components the following variables will be set: + +``OpenMP_FOUND`` + Variable indicating that OpenMP flags for all requested languages have been found. + If no components are specified, this is true if OpenMP settings for all enabled languages + were detected. +``OpenMP_VERSION`` + Minimal version of the OpenMP standard detected among the requested languages, + or all enabled languages if no components were specified. + +This module will set the following variables per language in your +project, where ```` is one of C, CXX, or Fortran: + +``OpenMP__FOUND`` + Variable indicating if OpenMP support for ```` was detected. +``OpenMP__FLAGS`` + OpenMP compiler flags for ````, separated by spaces. +``OpenMP__INCLUDE_DIRS`` + Directories that must be added to the header search path for ```` + when using OpenMP. + +For linking with OpenMP code written in ````, the following +variables are provided: + +``OpenMP__LIB_NAMES`` + :ref:`;-list ` of libraries for OpenMP programs for ````. +``OpenMP__LIBRARY`` + Location of the individual libraries needed for OpenMP support in ````. +``OpenMP__LIBRARIES`` + A list of libraries needed to link with OpenMP code written in ````. + +Additionally, the module provides :prop_tgt:`IMPORTED` targets: + +``OpenMP::OpenMP_`` + Target for using OpenMP from ````. + +Specifically for Fortran, the module sets the following variables: + +``OpenMP_Fortran_HAVE_OMPLIB_HEADER`` + Boolean indicating if OpenMP is accessible through ``omp_lib.h``. +``OpenMP_Fortran_HAVE_OMPLIB_MODULE`` + Boolean indicating if OpenMP is accessible through the ``omp_lib`` Fortran module. + +The module will also try to provide the OpenMP version variables: + +``OpenMP__SPEC_DATE`` + .. versionadded:: 3.7 + + Date of the OpenMP specification implemented by the ```` compiler. +``OpenMP__VERSION_MAJOR`` + Major version of OpenMP implemented by the ```` compiler. +``OpenMP__VERSION_MINOR`` + Minor version of OpenMP implemented by the ```` compiler. +``OpenMP__VERSION`` + OpenMP version implemented by the ```` compiler. + +The specification date is formatted as given in the OpenMP standard: +``yyyymm`` where ``yyyy`` and ``mm`` represents the year and month of +the OpenMP specification implemented by the ```` compiler. + +For some compilers, it may be necessary to add a header search path to find +the relevant OpenMP headers. This location may be language-specific. Where +this is needed, the module may attempt to find the location, but it can be +provided directly by setting the ``OpenMP__INCLUDE_DIR`` cache variable. +Note that this variable is an _input_ control to the module. Project code +should use the ``OpenMP__INCLUDE_DIRS`` _output_ variable if it needs +to know what include directories are needed. +#]=======================================================================] + +cmake_policy(PUSH) +cmake_policy(SET CMP0012 NEW) # if() recognizes numbers and booleans +cmake_policy(SET CMP0054 NEW) # if() quoted variables not dereferenced +cmake_policy(SET CMP0057 NEW) # if IN_LIST + +function(_OPENMP_FLAG_CANDIDATES LANG) + if(NOT OpenMP_${LANG}_FLAG) + unset(OpenMP_FLAG_CANDIDATES) + + set(OMP_FLAG_GNU "-fopenmp") + # set(OMP_FLAG_Clang "-fopenmp=libomp" "-fopenmp=libiomp5" "-fopenmp" "-Xclang -fopenmp") + set(OMP_FLAG_Clang "-fopenmp") + set(OMP_FLAG_AppleClang "-Xclang -fopenmp") + set(OMP_FLAG_HP "+Oopenmp") + if(WIN32) + set(OMP_FLAG_Intel "-Qopenmp") + elseif(CMAKE_${LANG}_COMPILER_ID STREQUAL "Intel" AND "${CMAKE_${LANG}_COMPILER_VERSION}" VERSION_LESS + "15.0.0.20140528") + set(OMP_FLAG_Intel "-openmp") + else() + set(OMP_FLAG_Intel "-qopenmp") + endif() + if(CMAKE_${LANG}_COMPILER_ID STREQUAL "IntelLLVM" AND "x${CMAKE_${LANG}_COMPILER_FRONTEND_VARIANT}" STREQUAL + "xMSVC") + set(OMP_FLAG_IntelLLVM "-Qiopenmp") + else() + set(OMP_FLAG_IntelLLVM "-fiopenmp") + endif() + set(OMP_FLAG_MSVC "-openmp") + set(OMP_FLAG_PathScale "-openmp") + set(OMP_FLAG_NAG "-openmp") + set(OMP_FLAG_Absoft "-openmp") + set(OMP_FLAG_NVHPC "-mp") + set(OMP_FLAG_PGI "-mp") + set(OMP_FLAG_Flang "-fopenmp") + set(OMP_FLAG_SunPro "-xopenmp") + set(OMP_FLAG_XL "-qsmp=omp") + # Cray compiler activate OpenMP with -h omp, which is enabled by default. + set(OMP_FLAG_Cray " " "-h omp") + + # If we know the correct flags, use those + if(DEFINED OMP_FLAG_${CMAKE_${LANG}_COMPILER_ID}) + set(OpenMP_FLAG_CANDIDATES "${OMP_FLAG_${CMAKE_${LANG}_COMPILER_ID}}") + # Fall back to reasonable default tries otherwise + else() + set(OpenMP_FLAG_CANDIDATES "-openmp" "-fopenmp" "-mp" " ") + endif() + set(OpenMP_${LANG}_FLAG_CANDIDATES + "${OpenMP_FLAG_CANDIDATES}" + PARENT_SCOPE) + else() + set(OpenMP_${LANG}_FLAG_CANDIDATES + "${OpenMP_${LANG}_FLAG}" + PARENT_SCOPE) + endif() +endfunction() + +# sample openmp source code to test +set(OpenMP_C_CXX_TEST_SOURCE + " +#include +int main(void) { +#ifdef _OPENMP + omp_get_max_threads(); + return 0; +#elif defined(__HIP_DEVICE_COMPILE__) + return 0; +#else + breaks_on_purpose +#endif +} +") + +# in Fortran, an implementation may provide an omp_lib.h header or omp_lib module, or both (OpenMP standard, section +# 3.1) Furthmore !$ is the Fortran equivalent of #ifdef _OPENMP (OpenMP standard, 2.2.2) Without the conditional +# compilation, some compilers (e.g. PGI) might compile OpenMP code while not actually enabling OpenMP, building code +# sequentially +set(OpenMP_Fortran_TEST_SOURCE + " + program test + @OpenMP_Fortran_INCLUDE_LINE@ + !$ integer :: n + n = omp_get_num_threads() + end program test + ") + +function(_OPENMP_WRITE_SOURCE_FILE LANG SRC_FILE_CONTENT_VAR SRC_FILE_NAME SRC_FILE_FULLPATH) + set(WORK_DIR ${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/FindOpenMP) + if("${LANG}" STREQUAL "C") + set(SRC_FILE "${WORK_DIR}/${SRC_FILE_NAME}.c") + file(WRITE "${SRC_FILE}" "${OpenMP_C_CXX_${SRC_FILE_CONTENT_VAR}}") + elseif("${LANG}" STREQUAL "CXX") + set(SRC_FILE "${WORK_DIR}/${SRC_FILE_NAME}.cpp") + file(WRITE "${SRC_FILE}" "${OpenMP_C_CXX_${SRC_FILE_CONTENT_VAR}}") + elseif("${LANG}" STREQUAL "Fortran") + set(SRC_FILE "${WORK_DIR}/${SRC_FILE_NAME}.f90") + file(WRITE "${SRC_FILE}_in" "${OpenMP_Fortran_${SRC_FILE_CONTENT_VAR}}") + configure_file("${SRC_FILE}_in" "${SRC_FILE}" @ONLY) + endif() + set(${SRC_FILE_FULLPATH} + "${SRC_FILE}" + PARENT_SCOPE) +endfunction() + +include(CMakeParseImplicitLinkInfo) + +function(_OPENMP_GET_FLAGS LANG FLAG_MODE OPENMP_FLAG_VAR OPENMP_LIB_NAMES_VAR) + _openmp_flag_candidates("${LANG}") + _openmp_write_source_file("${LANG}" "TEST_SOURCE" OpenMPTryFlag _OPENMP_TEST_SRC) + + unset(OpenMP_VERBOSE_COMPILE_OPTIONS) + separate_arguments(OpenMP_VERBOSE_OPTIONS NATIVE_COMMAND "${CMAKE_${LANG}_VERBOSE_FLAG}") + foreach(_VERBOSE_OPTION IN LISTS OpenMP_VERBOSE_OPTIONS) + if(NOT _VERBOSE_OPTION MATCHES "^-Wl,") + list(APPEND OpenMP_VERBOSE_COMPILE_OPTIONS ${_VERBOSE_OPTION}) + endif() + endforeach() + + foreach(OPENMP_FLAG IN LISTS OpenMP_${LANG}_FLAG_CANDIDATES) + set(OPENMP_FLAGS_TEST "${OPENMP_FLAG}") + if(OpenMP_VERBOSE_COMPILE_OPTIONS) + string(APPEND OPENMP_FLAGS_TEST " ${OpenMP_VERBOSE_COMPILE_OPTIONS}") + endif() + string(REGEX REPLACE "[-/=+]" "" OPENMP_PLAIN_FLAG "${OPENMP_FLAG}") + try_compile( + OpenMP_COMPILE_RESULT_${FLAG_MODE}_${OPENMP_PLAIN_FLAG} ${CMAKE_BINARY_DIR} + ${_OPENMP_TEST_SRC} + CMAKE_FLAGS "-DCOMPILE_DEFINITIONS:STRING=${OPENMP_FLAGS_TEST}" + LINK_LIBRARIES ${CMAKE_${LANG}_VERBOSE_FLAG} + OUTPUT_VARIABLE OpenMP_TRY_COMPILE_OUTPUT) + + if(OpenMP_COMPILE_RESULT_${FLAG_MODE}_${OPENMP_PLAIN_FLAG}) + set("${OPENMP_FLAG_VAR}" + "${OPENMP_FLAG}" + PARENT_SCOPE) + + if(CMAKE_${LANG}_VERBOSE_FLAG) + unset(OpenMP_${LANG}_IMPLICIT_LIBRARIES) + unset(OpenMP_${LANG}_IMPLICIT_LINK_DIRS) + unset(OpenMP_${LANG}_IMPLICIT_FWK_DIRS) + unset(OpenMP_${LANG}_LOG_VAR) + + file(APPEND ${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeOutput.log + "Detecting ${LANG} OpenMP compiler ABI info compiled with the following output:\ +\n${OpenMP_TRY_COMPILE_OUTPUT}\n\n") + + cmake_parse_implicit_link_info( + "${OpenMP_TRY_COMPILE_OUTPUT}" OpenMP_${LANG}_IMPLICIT_LIBRARIES OpenMP_${LANG}_IMPLICIT_LINK_DIRS + OpenMP_${LANG}_IMPLICIT_FWK_DIRS OpenMP_${LANG}_LOG_VAR "${CMAKE_${LANG}_IMPLICIT_OBJECT_REGEX}") + + file(APPEND ${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeOutput.log + "Parsed ${LANG} OpenMP implicit link information from above output:\n${OpenMP_${LANG}_LOG_VAR}\n\n") + + unset(_OPENMP_LIB_NAMES) + foreach(_OPENMP_IMPLICIT_LIB IN LISTS OpenMP_${LANG}_IMPLICIT_LIBRARIES) + get_filename_component(_OPENMP_IMPLICIT_LIB_DIR "${_OPENMP_IMPLICIT_LIB}" DIRECTORY) + get_filename_component(_OPENMP_IMPLICIT_LIB_NAME "${_OPENMP_IMPLICIT_LIB}" NAME) + get_filename_component(_OPENMP_IMPLICIT_LIB_PLAIN "${_OPENMP_IMPLICIT_LIB}" NAME_WE) + string(REGEX REPLACE "([][+.*?()^$])" "\\\\\\1" _OPENMP_IMPLICIT_LIB_PLAIN_ESC + "${_OPENMP_IMPLICIT_LIB_PLAIN}") + string(REGEX REPLACE "([][+.*?()^$])" "\\\\\\1" _OPENMP_IMPLICIT_LIB_PATH_ESC "${_OPENMP_IMPLICIT_LIB}") + if(NOT + ("${_OPENMP_IMPLICIT_LIB}" IN_LIST CMAKE_${LANG}_IMPLICIT_LINK_LIBRARIES + OR "${CMAKE_${LANG}_STANDARD_LIBRARIES}" MATCHES + "(^| )(-Wl,)?(-l)?(${_OPENMP_IMPLICIT_LIB_PLAIN_ESC}|${_OPENMP_IMPLICIT_LIB_PATH_ESC})( |$)" + OR "${CMAKE_${LANG}_LINK_EXECUTABLE}" MATCHES + "(^| )(-Wl,)?(-l)?(${_OPENMP_IMPLICIT_LIB_PLAIN_ESC}|${_OPENMP_IMPLICIT_LIB_PATH_ESC})( |$)")) + if(_OPENMP_IMPLICIT_LIB_DIR) + set(OpenMP_${_OPENMP_IMPLICIT_LIB_PLAIN}_LIBRARY + "${_OPENMP_IMPLICIT_LIB}" + CACHE FILEPATH "Path to the ${_OPENMP_IMPLICIT_LIB_PLAIN} library for OpenMP") + else() + find_library( + OpenMP_${_OPENMP_IMPLICIT_LIB_PLAIN}_LIBRARY + NAMES "${_OPENMP_IMPLICIT_LIB_NAME}" + DOC "Path to the ${_OPENMP_IMPLICIT_LIB_PLAIN} library for OpenMP" + HINTS ${OpenMP_${LANG}_IMPLICIT_LINK_DIRS} + CMAKE_FIND_ROOT_PATH_BOTH NO_DEFAULT_PATH) + endif() + mark_as_advanced(OpenMP_${_OPENMP_IMPLICIT_LIB_PLAIN}_LIBRARY) + list(APPEND _OPENMP_LIB_NAMES ${_OPENMP_IMPLICIT_LIB_PLAIN}) + endif() + endforeach() + set("${OPENMP_LIB_NAMES_VAR}" + "${_OPENMP_LIB_NAMES}" + PARENT_SCOPE) + else() + # We do not know how to extract implicit OpenMP libraries for this compiler. Assume that it handles them + # automatically, e.g. the Intel Compiler on Windows should put the dependency in its object files. + set("${OPENMP_LIB_NAMES_VAR}" + "" + PARENT_SCOPE) + endif() + break() + elseif((CMAKE_${LANG}_COMPILER_ID STREQUAL "AppleClang" AND CMAKE_${LANG}_COMPILER_VERSION VERSION_GREATER_EQUAL + "7.0") OR (CMAKE_${LANG}_COMPILER_ID STREQUAL "Clang" + AND APPLE)) + # Check for separate OpenMP library on AppleClang 7+ + find_library( + OpenMP_libomp_LIBRARY + NAMES omp gomp iomp5 + HINTS ${CMAKE_${LANG}_IMPLICIT_LINK_DIRECTORIES}) + mark_as_advanced(OpenMP_libomp_LIBRARY) + + if(OpenMP_libomp_LIBRARY) + # Try without specifying include directory first. We only want to explicitly add a search path if the header + # can't be found on the default header search path already. + try_compile( + OpenMP_COMPILE_RESULT_${FLAG_MODE}_${OPENMP_PLAIN_FLAG} ${CMAKE_BINARY_DIR} + ${_OPENMP_TEST_SRC} + CMAKE_FLAGS "-DCOMPILE_DEFINITIONS:STRING=${OPENMP_FLAGS_TEST}" + LINK_LIBRARIES ${CMAKE_${LANG}_VERBOSE_FLAG} ${OpenMP_libomp_LIBRARY} + OUTPUT_VARIABLE OpenMP_TRY_COMPILE_OUTPUT) + + if(NOT OpenMP_COMPILE_RESULT_${FLAG_MODE}_${OPENMP_PLAIN_FLAG}) + # Retry with the LINK_DIRECTORIES path set + + get_filename_component(_omp_dir ${OpenMP_libomp_LIBRARY} DIRECTORY) + set(OpenMP_libomp_LIBRARY_DIR + ${_omp_dir} + PARENT_SCOPE) + set(_linkDirFlags "-DLINK_DIRECTORIES:STRING=${_omp_dir} ") + + try_compile( + OpenMP_COMPILE_RESULT_${FLAG_MODE}_${OPENMP_PLAIN_FLAG} ${CMAKE_BINARY_DIR} + ${_OPENMP_TEST_SRC} + CMAKE_FLAGS "-DCOMPILE_DEFINITIONS:STRING=${OPENMP_FLAGS_TEST}" ${_linkDirFlags} + LINK_LIBRARIES ${CMAKE_${LANG}_VERBOSE_FLAG} ${OpenMP_libomp_LIBRARY} + OUTPUT_VARIABLE OpenMP_TRY_COMPILE_OUTPUT) + endif() + + if(NOT OpenMP_COMPILE_RESULT_${FLAG_MODE}_${OPENMP_PLAIN_FLAG}) + find_path(OpenMP_${LANG}_INCLUDE_DIR omp.h) + mark_as_advanced(OpenMP_${LANG}_INCLUDE_DIR) + set(OpenMP_${LANG}_INCLUDE_DIR + "${OpenMP_${LANG}_INCLUDE_DIR}" + PARENT_SCOPE) + if(OpenMP_${LANG}_INCLUDE_DIR) + try_compile( + OpenMP_COMPILE_RESULT_${FLAG_MODE}_${OPENMP_PLAIN_FLAG} ${CMAKE_BINARY_DIR} + ${_OPENMP_TEST_SRC} + CMAKE_FLAGS "-DCOMPILE_DEFINITIONS:STRING=${OPENMP_FLAGS_TEST}" ${_linkDirFlags} + "-DINCLUDE_DIRECTORIES:STRING=${OpenMP_${LANG}_INCLUDE_DIR}" + LINK_LIBRARIES ${CMAKE_${LANG}_VERBOSE_FLAG} ${OpenMP_libomp_LIBRARY} + OUTPUT_VARIABLE OpenMP_TRY_COMPILE_OUTPUT) + endif() + else() + set(_tmp) + endif() + if(OpenMP_COMPILE_RESULT_${FLAG_MODE}_${OPENMP_PLAIN_FLAG}) + set("${OPENMP_FLAG_VAR}" + "${OPENMP_FLAG}" + PARENT_SCOPE) + set("${OPENMP_LIB_NAMES_VAR}" + "libomp" + PARENT_SCOPE) + if(OpenMP_libomp_LIBRARY_DIR) + set(OpenMP_libomp_LIBRARY_DIR + "${OpenMP_libomp_LIBRARY_DIR}" + PARENT_SCOPE) + endif() + break() + endif() + endif() + elseif(CMAKE_${LANG}_COMPILER_ID STREQUAL "Clang" AND WIN32) + # Check for separate OpenMP library for Clang on Windows + find_library( + OpenMP_libomp_LIBRARY + NAMES libomp libgomp libiomp5 + HINTS ${CMAKE_${LANG}_IMPLICIT_LINK_DIRECTORIES}) + mark_as_advanced(OpenMP_libomp_LIBRARY) + if(OpenMP_libomp_LIBRARY) + try_compile( + OpenMP_COMPILE_RESULT_${FLAG_MODE}_${OPENMP_PLAIN_FLAG} ${CMAKE_BINARY_DIR} + ${_OPENMP_TEST_SRC} + CMAKE_FLAGS "-DCOMPILE_DEFINITIONS:STRING=${OPENMP_FLAGS_TEST}" + LINK_LIBRARIES ${CMAKE_${LANG}_VERBOSE_FLAG} ${OpenMP_libomp_LIBRARY} + OUTPUT_VARIABLE OpenMP_TRY_COMPILE_OUTPUT) + if(OpenMP_COMPILE_RESULT_${FLAG_MODE}_${OPENMP_PLAIN_FLAG}) + set("${OPENMP_FLAG_VAR}" + "${OPENMP_FLAG}" + PARENT_SCOPE) + set("${OPENMP_LIB_NAMES_VAR}" + "libomp" + PARENT_SCOPE) + break() + endif() + endif() + else() + file(APPEND ${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeError.log + "Detecting ${LANG} OpenMP failed with the following output:\n${OpenMP_TRY_COMPILE_OUTPUT}\n\n") + endif() + set("${OPENMP_LIB_NAMES_VAR}" + "NOTFOUND" + PARENT_SCOPE) + set("${OPENMP_FLAG_VAR}" + "NOTFOUND" + PARENT_SCOPE) + endforeach() + + unset(OpenMP_VERBOSE_COMPILE_OPTIONS) +endfunction() + +set(OpenMP_C_CXX_CHECK_VERSION_SOURCE + " +#include +#include +const char ompver_str[] = { 'I', 'N', 'F', 'O', ':', 'O', 'p', 'e', 'n', 'M', + 'P', '-', 'd', 'a', 't', 'e', '[', + ('0' + ((_OPENMP/100000)%10)), + ('0' + ((_OPENMP/10000)%10)), + ('0' + ((_OPENMP/1000)%10)), + ('0' + ((_OPENMP/100)%10)), + ('0' + ((_OPENMP/10)%10)), + ('0' + ((_OPENMP/1)%10)), + ']', '\\0' }; +int main(void) +{ + puts(ompver_str); + return 0; +} +") + +set(OpenMP_Fortran_CHECK_VERSION_SOURCE + " + program omp_ver + @OpenMP_Fortran_INCLUDE_LINE@ + integer, parameter :: zero = ichar('0') + integer, parameter :: ompv = openmp_version + character, dimension(24), parameter :: ompver_str =& + (/ 'I', 'N', 'F', 'O', ':', 'O', 'p', 'e', 'n', 'M', 'P', '-',& + 'd', 'a', 't', 'e', '[',& + char(zero + mod(ompv/100000, 10)),& + char(zero + mod(ompv/10000, 10)),& + char(zero + mod(ompv/1000, 10)),& + char(zero + mod(ompv/100, 10)),& + char(zero + mod(ompv/10, 10)),& + char(zero + mod(ompv/1, 10)), ']' /) + print *, ompver_str + end program omp_ver +") + +function(_OPENMP_GET_SPEC_DATE LANG SPEC_DATE) + _openmp_write_source_file("${LANG}" "CHECK_VERSION_SOURCE" OpenMPCheckVersion _OPENMP_TEST_SRC) + + unset(_includeDirFlags) + if(OpenMP_${LANG}_INCLUDE_DIR) + set(_includeDirFlags "-DINCLUDE_DIRECTORIES:STRING=${OpenMP_${LANG}_INCLUDE_DIR}") + endif() + + unset(_linkDirFlags) + if(OpenMP_libomp_LIBRARY_DIR) + set(_linkDirFlags "-DLINK_DIRECTORIES:STRING=${OpenMP_libomp_LIBRARY_DIR}") + endif() + + set(BIN_FILE "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/FindOpenMP/ompver_${LANG}.bin") + string(REGEX REPLACE "[-/=+]" "" OPENMP_PLAIN_FLAG "${OPENMP_FLAG}") + try_compile( + OpenMP_SPECTEST_${LANG}_${OPENMP_PLAIN_FLAG} "${CMAKE_BINARY_DIR}" + "${_OPENMP_TEST_SRC}" + CMAKE_FLAGS "-DCOMPILE_DEFINITIONS:STRING=${OpenMP_${LANG}_FLAGS}" ${_linkDirFlags} ${_includeDirFlags} + COPY_FILE ${BIN_FILE} + OUTPUT_VARIABLE OpenMP_TRY_COMPILE_OUTPUT) + + if(${OpenMP_SPECTEST_${LANG}_${OPENMP_PLAIN_FLAG}}) + file( + STRINGS ${BIN_FILE} specstr + LIMIT_COUNT 1 + REGEX "INFO:OpenMP-date") + set(regex_spec_date ".*INFO:OpenMP-date\\[0*([^]]*)\\].*") + if("${specstr}" MATCHES "${regex_spec_date}") + set(${SPEC_DATE} + "${CMAKE_MATCH_1}" + PARENT_SCOPE) + endif() + else() + file(APPEND ${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeError.log + "Detecting ${LANG} OpenMP version failed with the following output:\n${OpenMP_TRY_COMPILE_OUTPUT}\n\n") + endif() +endfunction() + +macro(_OPENMP_SET_VERSION_BY_SPEC_DATE LANG) + set(OpenMP_SPEC_DATE_MAP + # Preview versions + "201611=5.0" # OpenMP 5.0 preview 1 + # Combined versions, 2.5 onwards + "201811=5.0" + "201511=4.5" + "201307=4.0" + "201107=3.1" + "200805=3.0" + "200505=2.5" + # C/C++ version 2.0 + "200203=2.0" + # Fortran version 2.0 + "200011=2.0" + # Fortran version 1.1 + "199911=1.1" + # C/C++ version 1.0 (there's no 1.1 for C/C++) + "199810=1.0" + # Fortran version 1.0 + "199710=1.0") + if(MSVC) + list(APPEND OpenMP_SPEC_DATE_MAP "2019=2.0") + endif() + + if(OpenMP_${LANG}_SPEC_DATE) + string(REGEX MATCHALL "${OpenMP_${LANG}_SPEC_DATE}=([0-9]+)\\.([0-9]+)" _version_match "${OpenMP_SPEC_DATE_MAP}") + else() + set(_version_match "") + endif() + if(NOT _version_match STREQUAL "") + set(OpenMP_${LANG}_VERSION_MAJOR ${CMAKE_MATCH_1}) + set(OpenMP_${LANG}_VERSION_MINOR ${CMAKE_MATCH_2}) + set(OpenMP_${LANG}_VERSION "${OpenMP_${LANG}_VERSION_MAJOR}.${OpenMP_${LANG}_VERSION_MINOR}") + else() + unset(OpenMP_${LANG}_VERSION_MAJOR) + unset(OpenMP_${LANG}_VERSION_MINOR) + unset(OpenMP_${LANG}_VERSION) + endif() + unset(_version_match) + unset(OpenMP_SPEC_DATE_MAP) +endmacro() + +foreach(LANG IN ITEMS C CXX) + if(CMAKE_${LANG}_COMPILER_LOADED) + if(NOT DEFINED OpenMP_${LANG}_FLAGS + OR "${OpenMP_${LANG}_FLAGS}" STREQUAL "NOTFOUND" + OR NOT DEFINED OpenMP_${LANG}_LIB_NAMES + OR "${OpenMP_${LANG}_LIB_NAMES}" STREQUAL "NOTFOUND") + _openmp_get_flags("${LANG}" "${LANG}" OpenMP_${LANG}_FLAGS_WORK OpenMP_${LANG}_LIB_NAMES_WORK) + set(OpenMP_${LANG}_FLAGS + "${OpenMP_${LANG}_FLAGS_WORK}" + CACHE STRING "${LANG} compiler flags for OpenMP parallelization" FORCE) + set(OpenMP_${LANG}_LIB_NAMES + "${OpenMP_${LANG}_LIB_NAMES_WORK}" + CACHE STRING "${LANG} compiler libraries for OpenMP parallelization" FORCE) + if(OpenMP_libomp_LIBRARY_DIR) + set(OpenMP_libomp_LIBRARY_DIR + "${OpenMP_libomp_LIBRARY_DIR}" + CACHE STRING "Directory to OpenMP library directory" FORCE) + mark_as_advanced(OpenMP_libomp_LIBRARY_DIR) + endif() + mark_as_advanced(OpenMP_${LANG}_FLAGS OpenMP_${LANG}_LIB_NAMES) + endif() + endif() +endforeach() + +if(CMAKE_Fortran_COMPILER_LOADED) + if(NOT DEFINED OpenMP_Fortran_FLAGS + OR "${OpenMP_Fortran_FLAGS}" STREQUAL "NOTFOUND" + OR NOT DEFINED OpenMP_Fortran_LIB_NAMES + OR "${OpenMP_Fortran_LIB_NAMES}" STREQUAL "NOTFOUND" + OR NOT DEFINED OpenMP_Fortran_HAVE_OMPLIB_MODULE) + set(OpenMP_Fortran_INCLUDE_LINE "use omp_lib\n implicit none") + _openmp_get_flags("Fortran" "FortranHeader" OpenMP_Fortran_FLAGS_WORK OpenMP_Fortran_LIB_NAMES_WORK) + if(OpenMP_Fortran_FLAGS_WORK) + set(OpenMP_Fortran_HAVE_OMPLIB_MODULE + TRUE + CACHE BOOL INTERNAL "") + endif() + + set(OpenMP_Fortran_FLAGS + "${OpenMP_Fortran_FLAGS_WORK}" + CACHE STRING "Fortran compiler flags for OpenMP parallelization") + set(OpenMP_Fortran_LIB_NAMES + "${OpenMP_Fortran_LIB_NAMES_WORK}" + CACHE STRING "Fortran compiler libraries for OpenMP parallelization") + mark_as_advanced(OpenMP_Fortran_FLAGS OpenMP_Fortran_LIB_NAMES) + endif() + + if(NOT DEFINED OpenMP_Fortran_FLAGS + OR "${OpenMP_Fortran_FLAGS}" STREQUAL "NOTFOUND" + OR NOT DEFINED OpenMP_Fortran_LIB_NAMES + OR "${OpenMP_Fortran_LIB_NAMES}" STREQUAL "NOTFOUND" + OR NOT DEFINED OpenMP_Fortran_HAVE_OMPLIB_HEADER) + set(OpenMP_Fortran_INCLUDE_LINE "implicit none\n include 'omp_lib.h'") + _openmp_get_flags("Fortran" "FortranModule" OpenMP_Fortran_FLAGS_WORK OpenMP_Fortran_LIB_NAMES_WORK) + if(OpenMP_Fortran_FLAGS_WORK) + set(OpenMP_Fortran_HAVE_OMPLIB_HEADER + TRUE + CACHE BOOL INTERNAL "") + endif() + + set(OpenMP_Fortran_FLAGS + "${OpenMP_Fortran_FLAGS_WORK}" + CACHE STRING "Fortran compiler flags for OpenMP parallelization") + + set(OpenMP_Fortran_LIB_NAMES + "${OpenMP_Fortran_LIB_NAMES}" + CACHE STRING "Fortran compiler libraries for OpenMP parallelization") + endif() + + if(OpenMP_Fortran_HAVE_OMPLIB_MODULE) + set(OpenMP_Fortran_INCLUDE_LINE "use omp_lib\n implicit none") + else() + set(OpenMP_Fortran_INCLUDE_LINE "implicit none\n include 'omp_lib.h'") + endif() +endif() + +if(NOT OpenMP_FIND_COMPONENTS) + set(OpenMP_FINDLIST C CXX Fortran) +else() + set(OpenMP_FINDLIST ${OpenMP_FIND_COMPONENTS}) +endif() + +unset(_OpenMP_MIN_VERSION) + +include(FindPackageHandleStandardArgs) + +foreach(LANG IN LISTS OpenMP_FINDLIST) + if(CMAKE_${LANG}_COMPILER_LOADED) + if(NOT OpenMP_${LANG}_SPEC_DATE AND OpenMP_${LANG}_FLAGS) + _openmp_get_spec_date("${LANG}" OpenMP_${LANG}_SPEC_DATE_INTERNAL) + set(OpenMP_${LANG}_SPEC_DATE + "${OpenMP_${LANG}_SPEC_DATE_INTERNAL}" + CACHE INTERNAL "${LANG} compiler's OpenMP specification date") + endif() + + _openmp_set_version_by_spec_date("${LANG}") + + set(OpenMP_${LANG}_FIND_QUIETLY ${OpenMP_FIND_QUIETLY}) + set(OpenMP_${LANG}_FIND_REQUIRED ${OpenMP_FIND_REQUIRED}) + set(OpenMP_${LANG}_FIND_VERSION ${OpenMP_FIND_VERSION}) + set(OpenMP_${LANG}_FIND_VERSION_EXACT ${OpenMP_FIND_VERSION_EXACT}) + + set(_OPENMP_${LANG}_REQUIRED_VARS OpenMP_${LANG}_FLAGS) + if("${OpenMP_${LANG}_LIB_NAMES}" STREQUAL "NOTFOUND") + set(_OPENMP_${LANG}_REQUIRED_LIB_VARS OpenMP_${LANG}_LIB_NAMES) + else() + foreach(_OPENMP_IMPLICIT_LIB IN LISTS OpenMP_${LANG}_LIB_NAMES) + list(APPEND _OPENMP_${LANG}_REQUIRED_LIB_VARS OpenMP_${_OPENMP_IMPLICIT_LIB}_LIBRARY) + endforeach() + endif() + + set(_names_mismatch) + if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.17) + set(_names_mismatch NAME_MISMATCHED) + endif() + + find_package_handle_standard_args( + OpenMP_${LANG} ${_names_mismatch} + REQUIRED_VARS OpenMP_${LANG}_FLAGS ${_OPENMP_${LANG}_REQUIRED_LIB_VARS} + VERSION_VAR OpenMP_${LANG}_VERSION) + + if(OpenMP_${LANG}_FOUND) + if(DEFINED OpenMP_${LANG}_VERSION) + if(NOT _OpenMP_MIN_VERSION OR _OpenMP_MIN_VERSION VERSION_GREATER OpenMP_${LANG}_VERSION) + set(_OpenMP_MIN_VERSION OpenMP_${LANG}_VERSION) + endif() + endif() + set(OpenMP_${LANG}_LIBRARIES "") + foreach(_OPENMP_IMPLICIT_LIB IN LISTS OpenMP_${LANG}_LIB_NAMES) + list(APPEND OpenMP_${LANG}_LIBRARIES "${OpenMP_${_OPENMP_IMPLICIT_LIB}_LIBRARY}") + endforeach() + if(OpenMP_${LANG}_INCLUDE_DIR) + set(OpenMP_${LANG}_INCLUDE_DIRS ${OpenMP_${LANG}_INCLUDE_DIR}) + else() + set(OpenMP_${LANG}_INCLUDE_DIRS "") + endif() + + if(NOT TARGET OpenMP::OpenMP_${LANG}) + add_library(OpenMP::OpenMP_${LANG} INTERFACE IMPORTED) + endif() + if(OpenMP_${LANG}_FLAGS) + separate_arguments(_OpenMP_${LANG}_OPTIONS NATIVE_COMMAND "${OpenMP_${LANG}_FLAGS}") + set_property(TARGET OpenMP::OpenMP_${LANG} PROPERTY INTERFACE_COMPILE_OPTIONS + "$<$:${_OpenMP_${LANG}_OPTIONS}>") + unset(_OpenMP_${LANG}_OPTIONS) + endif() + if(OpenMP_${LANG}_INCLUDE_DIRS) + set_property(TARGET OpenMP::OpenMP_${LANG} PROPERTY INTERFACE_INCLUDE_DIRECTORIES + "$") + endif() + if(OpenMP_libomp_LIBRARY_DIR) + # NB: As long as this file is for MacOS related workaround, we should be ok with -LXXX ... + set_property(TARGET OpenMP::OpenMP_${LANG} PROPERTY INTERFACE_LINK_OPTIONS "-L${OpenMP_libomp_LIBRARY_DIR}") + endif() + if(OpenMP_${LANG}_LIBRARIES) + set_property(TARGET OpenMP::OpenMP_${LANG} PROPERTY INTERFACE_LINK_LIBRARIES "${OpenMP_${LANG}_LIBRARIES}") + endif() + endif() + endif() +endforeach() + +unset(_OpenMP_REQ_VARS) +foreach(LANG IN ITEMS C CXX Fortran) + if((NOT OpenMP_FIND_COMPONENTS AND CMAKE_${LANG}_COMPILER_LOADED) OR LANG IN_LIST OpenMP_FIND_COMPONENTS) + list(APPEND _OpenMP_REQ_VARS "OpenMP_${LANG}_FOUND") + endif() +endforeach() + +find_package_handle_standard_args( + OpenMP + REQUIRED_VARS ${_OpenMP_REQ_VARS} + VERSION_VAR ${_OpenMP_MIN_VERSION} + HANDLE_COMPONENTS) + +set(OPENMP_FOUND ${OpenMP_FOUND}) + +if(CMAKE_Fortran_COMPILER_LOADED AND OpenMP_Fortran_FOUND) + if(NOT DEFINED OpenMP_Fortran_HAVE_OMPLIB_MODULE) + set(OpenMP_Fortran_HAVE_OMPLIB_MODULE + FALSE + CACHE BOOL INTERNAL "") + endif() + if(NOT DEFINED OpenMP_Fortran_HAVE_OMPLIB_HEADER) + set(OpenMP_Fortran_HAVE_OMPLIB_HEADER + FALSE + CACHE BOOL INTERNAL "") + endif() +endif() + +if(NOT + (CMAKE_C_COMPILER_LOADED + OR CMAKE_CXX_COMPILER_LOADED + OR CMAKE_Fortran_COMPILER_LOADED)) + message(SEND_ERROR "FindOpenMP requires the C, CXX or Fortran languages to be enabled") +endif() + +unset(OpenMP_C_CXX_TEST_SOURCE) +unset(OpenMP_Fortran_TEST_SOURCE) +unset(OpenMP_C_CXX_CHECK_VERSION_SOURCE) +unset(OpenMP_Fortran_CHECK_VERSION_SOURCE) +unset(OpenMP_Fortran_INCLUDE_LINE) + +cmake_policy(POP) diff --git a/cmake/binscope.cmake b/cmake/binscope.cmake new file mode 100644 index 0000000000000000000000000000000000000000..fa9d9e7dce3814d24deb4aea2a5ca0eca25f3d94 --- /dev/null +++ b/cmake/binscope.cmake @@ -0,0 +1,45 @@ +# ============================================================================== +# +# Copyright 2020 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ============================================================================== + +# ~~~ +# Generate a custom ` binscope` target to call binscope on a list of targets. +# +# gen_binscope_target( [target ...]) +# ~~~ +function(gen_binscope_target) + if(BINSCOPE_OUTPUT) + set(_binscope_output "${BINSCOPE_OUTPUT}") + else() + set(_binscope_output "${PROJECT_BINARY_DIR}/binscope_output.xls") + endif() + + set(_binscope_args) + list(APPEND _binscope_args "-a") + list(APPEND _binscope_args "-o") + list(APPEND _binscope_args "${_binscope_output}") + + foreach(tgt ${ARGN}) + list(APPEND _binscope_args "-f") + list(APPEND _binscope_args "$") + endforeach() + + add_custom_target( + binscope + COMMAND ${binscope_exec} ${_binscope_args} + COMMENT "Running binscope") +endfunction() diff --git a/cmake/compiler_flags.cmake b/cmake/compiler_flags.cmake new file mode 100644 index 0000000000000000000000000000000000000000..feb603d93ba3df292a76eec1d9aff75f61a23cf3 --- /dev/null +++ b/cmake/compiler_flags.cmake @@ -0,0 +1,109 @@ +# ============================================================================== +# +# Copyright 2020 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ============================================================================== + +# C++ standard flags +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED OFF) +set(CMAKE_CXX_EXTENSIONS OFF) + +# Always generate position independent code +set(CMAKE_POSITION_INDEPENDENT_CODE ON) +# set(CMAKE_CXX_VISIBILITY_PRESET hidden) + +# RPATH settings... Funadamentally, we do not want to use RPATH but RUNPATH. In order to achieve this, we use a +# combination of these CMake options, some target properties (namely INSTALL_RPATH; see *_set_rpath macros in +# macros.cmake) and some linker flags (see linker_flags.cmake) +# +# All of this should achieve the desired effect on all platforms and compilers + +set(CMAKE_BUILD_SKIP_RPATH TRUE) +set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE) +set(CMAKE_INSTALL_RPATH_USE_LINK_PATH FALSE) + +# CMake usually does not add /usr/local/include to any compiler commands. This can lead to some issues on Mac OS when +# using the -isysroot option so we allow for explicit -I/usr/local/include on the command line. +if(APPLE) + list(REMOVE_ITEM CMAKE_C_IMPLICIT_INCLUDE_DIRECTORIES /usr/local/include) + list(REMOVE_ITEM CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES /usr/local/include) + list(REMOVE_ITEM CMAKE_CUDA_IMPLICIT_INCLUDE_DIRECTORIES /usr/local/include) + list(REMOVE_ITEM CMAKE_C_IMPLICIT_LINK_DIRECTORIES /usr/local/lib) + list(REMOVE_ITEM CMAKE_CXX_IMPLICIT_LINK_DIRECTORIES /usr/local/lib) +endif() + +# ------------------------------------------------------------------------------ + +test_compile_option( + _compile_flags_release + LANGS CXX DPCXX + FLAGS "-ffast-math /fp:fast -fast" "-O3 /Ox" + AUTO_ADD_CO + GENEX "$,$>,$>") + +# -------------------------------------- + +test_compile_option( + _dpcpp_flags + LANGS DPCXX + FLAGS "-fsycl" + AUTO_ADD_CO) + +# -------------------------------------- + +if(ENABLE_PROFILING) + test_compile_option( + _profiling_flags + LANGS CXX DPCXX + FLAGS "-pg -prof-gen /Qprof-gen" "-fprofile-instr-generate" + AUTO_ADD_CO) +endif() + +# -------------------------------------- + +if(ENABLE_STACK_PROTECTION) + test_compile_option( + _stack_protection + LANGS CXX DPCXX + FLAGS "-fstack-protector-all" + AUTO_ADD_CO) +endif() + +# ------------------------------------------------------------------------------ + +if(NOT VERSION_INFO) + execute_process( + COMMAND ${Python_EXECUTABLE} setup.py --version + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE _version_info + OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) + set(VERSION_INFO "\"${_version_info}\"") +endif() + +# -------------------------------------- + +add_compile_definitions( + "$<$:ENABLE_OPENMP>" "$<$:VERSION_INFO=${VERSION_INFO}>" + "$<$,$>:_FORTIFY_SOURCE=2>") + +# ============================================================================== +# Platform specific flags + +if(WIN32) + add_compile_definitions(_USE_MATH_DEFINES _CRT_SECURE_NO_WARNINGS WIN32_LEAN_AND_MEAN) +endif() + +# ============================================================================== diff --git a/cmake/linker_flags.cmake b/cmake/linker_flags.cmake new file mode 100644 index 0000000000000000000000000000000000000000..67607b81db24f3b4d958d65f3b0fc55a66198eec --- /dev/null +++ b/cmake/linker_flags.cmake @@ -0,0 +1,263 @@ +# ============================================================================== +# +# Copyright 2020 +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. +# +# ============================================================================== + +# lint_cmake: -whitespace/indent + +# NB: no -Wl, here, CMake automatically adds the correct prefix for the linker +if(LINKER_STRIP_ALL) + test_link_option( + _linker_flags + LANGS CXX DPCXX CUDA NVCXX + FLAGS "--strip-all -s" + AUTO_ADD_LO + GENEX "$,$>,$>" + ) +endif() + +test_link_option( + _linker_flags + LANGS CXX DPCXX CUDA NVCXX + FLAGS "-z,now" + AUTO_ADD_LO +) + +# ------------------------------------------------------------------------------ + +if(LINKER_NOEXECSTACK) + test_link_option( + _link_no_execstack + LANGS CXX DPCXX CUDA NVCXX + FLAGS "-z,noexecstack" + AUTO_ADD_LO + ) +endif() + +# ------------------------------------------------------------------------------ + +if(LINKER_RELRO) + test_link_option( + _link_relro + LANGS CXX DPCXX CUDA NVCXX + FLAGS "-z,relro" + AUTO_ADD_LO + ) +endif() + +# ------------------------------------------------------------------------------ + +if(ENABLE_STACK_PROTECTION) + test_link_option( + _stack_protection + LANGS CXX DPCXX + FLAGS "-fstack-protector-all" + AUTO_ADD_LO VERBATIM) +endif() + +# ------------------------------------------------------------------------------ + +if(ENABLE_RUNPATH) + if(LINKER_DTAGS) + test_link_option( + _linker_dtags + LANGS CXX DPCXX CUDA NVCXX + FLAGS "--enable-new-dtags" + AUTO_ADD_LO + ) + endif() +else() + if(LINKER_DTAGS) + test_link_option( + _linker_dtags + LANGS CXX DPCXX CUDA NVCXX + FLAGS "--disable-new-dtags" + AUTO_ADD_LO + ) + endif() + + if(CUDA_STATIC) + test_link_option( + _nvhpc_static_flags + LANGS NVCXX + FLAGS "-static-nvidia" "-Mnorpath" + AUTO_ADD_LO VERBATIM + ) + endif() +endif() + +if(UNIX AND NOT APPLE) + if(NOT DEFINED _cmake_rpath_check) + set(_cmake_rpath_check FALSE) + find_program(_readelf readelf) + if(_readelf) + set(_cmake_rpath_check TRUE) + else() + message(STATUS "Readelf program not found -> skipping RPATH/RUNPATH check") + endif() + # cmake-lint: disable=C0103 + set( + _cmake_rpath_check + ${_cmake_rpath_check} + CACHE BOOL "Do an extended CMake test to make sure no RPATH are set?" + ) + + mark_as_advanced(_readelf _cmake_rpath_check) + endif() +endif() + +# ============================================================================== + +if(_cmake_rpath_check) + foreach(_lang CXX CUDA NVCXX DPCXX) + is_language_enabled(${_lang} _enabled) + if(_enabled) + message(CHECK_START "Performing extended CMake RPATH test for ${_lang}") + list(APPEND CMAKE_MESSAGE_INDENT " ") + set(LANG ${_lang}) + set(LANGS ${_lang}) + + if(_lang STREQUAL CUDA) + set(LANGS "${LANGS} CXX") + elseif(_lang STREQUAL NVCXX) + set(LANGS "${LANGS} CXX") + get_property(_flags GLOBAL PROPERTY _nvcxx_try_compile_extra_flags) + if(_flags) + string(APPEND CMAKE_REQUIRED_FLAGS " ${_flags}") + list(APPEND CMAKE_REQUIRED_LINK_OPTIONS ${_flags}) + endif() + set(CMAKE_EXTRA_CONTENT "set(CMAKE_NVCXX_FLAGS_INIT \"${CMAKE_NVCXX_FLAGS_INIT} -v\")\n +set(CMAKE_NVCXX_LDFLAGS_INIT \"${CMAKE_NVCXX_LDFLAGS_INIT} -v\")") + endif() + + file(REMOVE ${CMAKE_SOURCE_DIR}/tests/cmake-ldtest/CMakeLists.txt) + configure_file( + ${CMAKE_SOURCE_DIR}/tests/cmake-ldtest/CMakeLists.txt.in + ${CMAKE_SOURCE_DIR}/tests/cmake-ldtest/CMakeLists.txt @ONLY + ) + + # ------------------------------------ + + message(CHECK_START "Compiling test library (${_lang})") + set(_binary_dir ${CMAKE_BINARY_DIR}/cmake-ldtest-${_lang}) + try_compile( + _create_shared_lib_${lang} ${_binary_dir} + ${CMAKE_SOURCE_DIR}/tests/cmake-ldtest cmake-ldtest + CMAKE_FLAGS -DCMAKE_VERBOSE_MAKEFILE=ON -DLINKER_FLAGS=${_linker_dtags_CXX} + OUTPUT_VARIABLE _compile_output + ) + if(_create_shared_lib_${lang}) + message(CHECK_PASS "succeeded") + else() + message(CHECK_FAIL "failed") + file( + APPEND ${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeError.log + "Failed to compile CMake RPATH extended ${_lang} test project.\nOutput of build:\n${_compile_output}\n" + ) + endif() + + # ------------------------------------ + + if(_create_shared_lib_${lang}) + if(ENABLE_RUNPATH) + set(_name "RPATH") + else() + set(_name "RUNPATH") + endif() + + message(CHECK_START "Looking for absence of ${_name} (${_lang})") + + find_library( + _shared_lib_${_lang} + NAMES shared_lib_${_lang} libshared_lib_${_lang} + PATHS ${_binary_dir} REQUIRED + NO_DEFAULT_PATH + ) + mark_as_advanced(_shared_lib_${_lang}) + + execute_process( + COMMAND ${_readelf} -Wd ${_shared_lib_${_lang}} + OUTPUT_VARIABLE _dyn_symbols + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + + # Local helper macro to add RPATH to the log file + macro(_rpath_add_to_log name success msg) + if(${success}) + set(_file "CMakeOutput.log") + set(_state_msg "succeeded") + else() + set(_file "CMakeError.log") + set(_state_msg "failed") + endif() + file( + APPEND ${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/${_file} + "\n\nLooking for absence of ${name} in ${_shared_lib_${_lang}} ${_state_msg}.\n" + "Output of build for ${_shared_lib_${_lang}}:\n${_compile_output}\nOutput of readelf -Wd:" + "\n${_dyn_symbols}\n\n${msg}\n\n" + ) + endmacro() + + set(_test_result FALSE) + if(ENABLE_RUNPATH AND ${_dyn_symbols} MATCHES ".*\\(RPATH\\)[ ]+([^\n\r\t]*)") + # Most not have RPATH but found one -> not good + _rpath_add_to_log(${_name} FALSE "RPATH detected: ${CMAKE_MATCH_1}") + elseif(ENABLE_RUNPATH AND ${_dyn_symbols} MATCHES ".*\\(RUNPATH\\)[ ]+([^\n\r\t]*)") + set(_test_result TRUE) + _rpath_add_to_log(${_name} TRUE "Found RUNPATH: ${CMAKE_MATCH_1} and no RPATH") + # -------------------------------- + elseif(NOT ENABLE_RUNPATH AND ${_dyn_symbols} MATCHES ".*\\(RUNPATH\\)[ ]+([^\n\r\t]*)") + # Most not have RUNPATH but found one -> not good + _rpath_add_to_log(${_name} FALSE "RUNPATH detected: ${CMAKE_MATCH_1}") + elseif(NOT ENABLE_RUNPATH AND ${_dyn_symbols} MATCHES ".*\\(RPATH\\)[ ]+([^\n\r\t]*)") + set(_test_result TRUE) + _rpath_add_to_log(${_name} TRUE "Found RPATH: ${CMAKE_MATCH_1} and no RUNPATH") + # -------------------------------- + else() + _rpath_add_to_log(${_name} FALSE "No RPATH or RUNPATH found.") + message(CHECK_FAIL "failed") + message(FATAL_ERROR "No RPATH or RUNPATH found in ${_shared_lib}") + endif() + + if(_test_result) + message(CHECK_PASS "succeeded") + else() + message(CHECK_FAIL "failed") + endif() + endif() + + # ------------------------------------ + + list(POP_BACK CMAKE_MESSAGE_INDENT) + if(_test_result) + message(CHECK_PASS "succeeded") + # cmake-lint: disable=C0103 + + # Only perform the RPATH/RUNPATH check once + set( + _cmake_rpath_check + FALSE + CACHE INTERNAL "" + ) + else() + message(CHECK_FAIL "failed") + message(FATAL_ERROR "Failed extended RPATH test: cannot continue!") + endif() + endif() + endforeach() +endif() + +# ============================================================================== diff --git a/cmake/macros.cmake b/cmake/macros.cmake new file mode 100644 index 0000000000000000000000000000000000000000..f71f5f1e4e8b6def0438555a6e815ce1bd454c8e --- /dev/null +++ b/cmake/macros.cmake @@ -0,0 +1,527 @@ +# ============================================================================== +# +# Copyright 2020 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ============================================================================== + +# lint_cmake: -whitespace/indent + +include(CheckCompilerFlag OPTIONAL RESULT_VARIABLE _check_compiler_flag) +if(NOT _check_compiler_flag) + include(Internal/CMakeCheckCompilerFlag) +endif() + +# Check if a language has been enabled without attempting to enable it +# +# is_language_enabled( ) +# +# If the language has already been enabled, is set to TRUE. Otherwise it is set to FALSE. +function(is_language_enabled _lang _var) + get_property(_supported_languages GLOBAL PROPERTY ENABLED_LANGUAGES) + if(NOT _lang IN_LIST _supported_languages) + set(${_var} + FALSE + PARENT_SCOPE) + else() + set(${_var} + TRUE + PARENT_SCOPE) + endif() +endfunction() + +# ============================================================================== + +# ~~~ +# Convenience function to test for the existence of some compiler flags for a a particular language +# +# check_compiler_flag( [...]) +# +# Check whether a compiler option is valid for the compiler. For each set of compiler options provided in the +# lists , it will test whether one of the element can be used by the corresponding compiler. If a flag is valid, +# it will be added to the GLOBAL property named _ as well as to a variable with the same name. If the +# property already exists, any valid flag is appended to the current value. +# +# Each call to this function also sets the _added_count variable to the number of flags added automatically (if any). +# ~~~ +function(check_compiler_flags lang var_prefix) + # cmake-lint: disable=C0103,E1120 + set(_${lang}_opts) + + foreach(_flag_list ${ARGN}) + separate_arguments(_flag_list) + + foreach(_flag ${_flag_list}) + # Drop the first character (most likely either '-' or '/') + string(SUBSTRING ${_flag} 1 -1 _flag_name) + string(REGEX REPLACE "^-+" "" _flag_name ${_flag_name}) + string(REGEX REPLACE "[-:/,=]" "_" _flag_name ${_flag_name}) + + cmake_check_compiler_flag(${lang} ${_flag} ${lang}_compiler_has_${_flag_name}) + if(${lang}_compiler_has_${_flag_name}) + list(APPEND _${lang}_opts ${_flag}) + break() + endif() + endforeach() + endforeach() + + # Is there a property that already corresponds to this? + get_property(_opts GLOBAL PROPERTY ${var_prefix}_${lang}) + if(_opts) + list(APPEND _opts ${_${lang}_opts}) + else() + define_property( + GLOBAL + PROPERTY ${var_prefix}_${lang} + BRIEF_DOCS "Compiler flags for ${var_prefix}" + FULL_DOCS "Compiler flags for ${var_prefix}") + set(_opts ${_${lang}_opts}) + endif() + + # Set GLOBAL property so that other parts of the code can have access to it + set_property(GLOBAL PROPERTY ${var_prefix}_${lang} ${_opts}) + + list(LENGTH _${lang}_opts _added_count) + set(_added_count + ${_added_count} + PARENT_SCOPE) + set(_added_flags + ${_${lang}_opts} + PARENT_SCOPE) + + # Also set a variable for convenience + set(${var_prefix}_${lang} + ${_opts} + PARENT_SCOPE) +endfunction() + +# ~~~ +# Convenience function to test for the existence of some compiler flags for a set of languages. +# +# test_compile_option( +# LANGS [...] +# FLAGS [...] +# [AUTO_ADD_CO] +# [GENEX ]) +# +# Check that a compiler option can be applied to each of the specified languages . For each set of compiler +# options provided in the lists , it will test whether one of the element can be used by the corresponding +# compiler. If a flag is valid, it will be added to the GLOBAL property named _ as well as to a variable +# with the same name. +# If AUTO_ADD_CO is specified, the compiler option will be automatically added globally using +# add_compile_option(...). By default, the generator expression used in that function call restricts the compile option +# to the current language (ie. $<$:${_flag}>). This can be changed by using the +# argument (which defaults to "$"). +# +# NB: This function calls check_compiler_flags() internally. +# +# ~~~ +function(test_compile_option prefix) + cmake_parse_arguments(PARSE_ARGV 1 TEST_CO "AUTO_ADD_CO" "GENEX" "LANGS;FLAGS") + + if(NOT TEST_CO_LANGS) + message(FATAL_ERROR "Missing LANGS argument") + endif() + if(NOT TEST_CO_FLAGS) + message(FATAL_ERROR "Missing FLAGS argument") + endif() + + if(NOT TEST_CO_GENEX) + set(TEST_CO_GENEX "$") + endif() + + # cmake-lint: disable=C0103 + foreach(lang ${TEST_CO_LANGS}) + is_language_enabled(${lang} _enabled) + if(_enabled) + check_compiler_flags(${lang} ${prefix} ${TEST_CO_FLAGS}) + + set(${prefix}_${lang} + ${${prefix}_${lang}} + PARENT_SCOPE) + + if(TEST_CO_AUTO_ADD_CO AND _added_flags) + string(CONFIGURE "${TEST_CO_GENEX}" _genex @ONLY) + list(LENGTH ${prefix}_${lang} _L) + math(EXPR _start_idx "${_L} - ${_added_count}") + list(SUBLIST ${prefix}_${lang} ${_start_idx} -1 _added_flags) + foreach(_flag ${_added_flags}) + add_compile_options("$<${_genex}:${_flag}>") + endforeach() + endif() + else() + set(${prefix}_${lang} PARENT_SCOPE) + endif() + endforeach() +endfunction() + +# ============================================================================== + +# ~~~ +# Convenience function to test for the existence of some compiler flags for a a particular language +# +# check_link_flag( [VERBATIM] [...]) +# +# Check whether a linker option is valid for the linker. For each set of linker options provided in the lists +# , it will test whether one of the element can be used by the corresponding compiler. If a flag is valid, it +# will be added to the GLOBAL property named _ as well as to a variable with the same name. If the +# property already exists, any valid flag is appended to the current value. +# +# If VERBATIM is passed as argument, the flag is passed onto the linker without prepending the 'LINKER:' prefix. +# +# Each call to this function also sets the _added_count variable to the number of flags added automatically (if any). +# ~~~ +function(check_link_flags lang var_prefix) + # cmake-lint: disable=R0915,C0103,E1120 + + cmake_parse_arguments(PARSE_ARGV 2 CHECK_LF "VERBATIM" "" "") + set(_${lang}_link_opts) + # This is a CMake 3.18 addition + include(CheckLinkerFlag OPTIONAL RESULT_VARIABLE _check_linker_flags) + + set(_wrapper_flag ${CMAKE_${lang}_LINKER_WRAPPER_FLAG}) + list(GET _wrapper_flag -1 _last) + set(_separate_options FALSE) + if(_last STREQUAL " ") + set(_separate_options TRUE) + list(REMOVE_AT _wrapper_flag -1) + endif() + string(REPLACE ";" " " _wrapper_flag ${_wrapper_flag}) + + foreach(_flag_list ${CHECK_LF_UNPARSED_ARGUMENTS}) + separate_arguments(_flag_list) + + foreach(_flag ${_flag_list}) + # Drop the first character (most likely either '-' or '/') + string(SUBSTRING ${_flag} 1 -1 _flag_name) + string(REGEX REPLACE "^-+" "" _flag_name ${_flag_name}) + string(REGEX REPLACE "[-:/,=]" "_" _flag_name ${_flag_name}) + if(CHECK_LF_VERBATIM) + set(_prefix) + else() + set(_prefix "LINKER:") + endif() + + if(_check_linker_flags) + check_linker_flag(${lang} "${_prefix}${_flag}" ${lang}_linker_has_${_flag_name}) + else() + if(NOT CHECK_LF_VERBATIM) + if(_separate_options) + string(REPLACE "," ";" _flags ${_flag}) + set(_expanded_flag) + foreach(subflag ${_flags}) + set(_expanded_flag "${_expanded_flag} ${_wrapper_flag} ${subflag}") + endforeach() + else() + set(_expanded_flag "${_wrapper_flag}${_flag}") + endif() + else() + set(_expanded_flag "${_flag}") + endif() + + set(CMAKE_REQUIRED_LINK_OPTIONS ${_expanded_flag}) + check_compiler_flag(${lang} "" ${lang}_linker_has_${_flag_name}) + endif() + + if(${lang}_linker_has_${_flag_name}) + list(APPEND _${lang}_link_opts ${_flag}) + break() + endif() + endforeach() + endforeach() + + # Is there a property that already corresponds to this? + get_property(_opts GLOBAL PROPERTY ${var_prefix}_${lang}) + if(_opts) + list(APPEND _opts ${_${lang}_link_opts}) + else() + define_property( + GLOBAL + PROPERTY ${var_prefix}_${lang} + BRIEF_DOCS "Linker flags ${var_prefix}" + FULL_DOCS "Linker flags ${var_prefix}") + set(_opts ${_${lang}_link_opts}) + endif() + + # Set GLOBAL property so that other parts of the code can have access to it + set_property(GLOBAL PROPERTY ${var_prefix}_${lang} ${_opts}) + + list(LENGTH _${lang}_link_opts _added_count) + set(_added_count + ${_added_count} + PARENT_SCOPE) + set(_added_flags + ${_${lang}_link_opts} + PARENT_SCOPE) + + # Also set a variable for convenience + set(${var_prefix}_${lang} + ${_opts} + PARENT_SCOPE) +endfunction() + +# ~~~ +# Convenience function to test for the existence of some linker flags for a set of languages. +# +# test_link_option( +# LANGS [...] +# FLAGS [...] +# [AUTO_ADD_LO] +# [VERBATIM] +# [GENEX ]) +# +# Check that a linker option can be applied to each of the specified languages . For each set of linker +# options provided in the lists , it will test whether one of the element can be used by the corresponding +# linker. If a flag is valid, it will be added to the GLOBAL property named _ as well as to a variable +# with the same name. +# If VERBATIM is passed as argument, the flag is passed onto the linker without prepending the 'LINKER:' prefix. +# If AUTO_ADD_LO is specified, the linker option will be automatically added globally using +# add_compile_option(...). By default, the generator expression used in that function call restricts the compile option +# to the current language (ie. $<$:LINKER:${_flag}>). This can be changed by using the +# argument (which defaults to "$"). +# +# NB: This function calls check_link_flags() internally. +# ~~~ +function(test_link_option prefix) + cmake_parse_arguments(PARSE_ARGV 1 TEST_LO "AUTO_ADD_LO;VERBATIM" "GENEX" "LANGS;FLAGS") + + if(NOT TEST_LO_LANGS) + message(FATAL_ERROR "Missing LANGS argument") + endif() + if(NOT TEST_LO_FLAGS) + message(FATAL_ERROR "Missing FLAGS argument") + endif() + + if(NOT TEST_LO_GENEX) + if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.18) + set(TEST_LO_GENEX "$") + else() + set(TEST_LO_GENEX "1") + endif() + endif() + + # cmake-lint: disable=C0103 + foreach(lang ${TEST_LO_LANGS}) + is_language_enabled(${lang} _enabled) + if(_enabled) + set(_args ${TEST_LO_FLAGS}) + if(TEST_LO_VERBATIM) + set(_args "VERBATIM;${_args}") + endif() + check_link_flags(${lang} ${prefix} ${_args}) + + set(${prefix}_${lang} + ${${prefix}_${lang}} + PARENT_SCOPE) + + if(TEST_LO_AUTO_ADD_LO AND _added_flags) + string(CONFIGURE "${TEST_LO_GENEX}" _genex @ONLY) + list(LENGTH ${prefix}_${lang} _L) + math(EXPR _start_idx "${_L} - ${_added_count}") + list(SUBLIST ${prefix}_${lang} ${_start_idx} -1 _added_flags) + foreach(_flag ${_added_flags}) + if(TEST_LO_VERBATIM) + add_link_options("$<${_genex}:${_flag}>") + else() + add_link_options("$<${_genex}:LINKER:${_flag}>") + endif() + endforeach() + endif() + else() + set(${prefix}_${lang} PARENT_SCOPE) + endif() + endforeach() +endfunction() + +# ============================================================================== + +# ~~~ +# Append a value to a property (creating the latter if necessary) +# +# append_to_property( +# ] | +# TARGET | +# SOURCE | +# [DIRECTORY | TARGET_DIRECTORY ] | +# INSTALL | +# TEST | +# CACHE | +# VARIABLE > +# ) +# ~~~ +macro(append_to_property name scope value) + get_property(_prop ${scope} PROPERTY ${name}) + if(_prop) + list(APPEND _prop ${value}) + else() + define_property( + ${scope} + PROPERTY ${name} + BRIEF_DOCS "${scope} property for ${name}" + FULL_DOCS "${scope} property for ${name}") + set(_prop ${value}) + endif() + + set_property(${scope} PROPERTY ${name} ${_prop}) +endmacro() + +# ============================================================================== + +# Automatically set the output directory for a particular target with a potential hint +# +# set_output_directory_auto( ) +# +# Automatically set the output directory for . must be an existing path in the current CMake project +# directory. It is only used if the CMake variable IN_PLACE_BUILD is set to a truthful value. Otherwise, the macro will +# look at ${target}_OUTPUT_DIR CMake variable (if it exists) to set the output directory (it will create the directory +# if it does not already exist on the filesystem). +# +macro(set_output_directory_auto target hint) + string(TOUPPER ${target} _TARGET) + + if(IN_PLACE_BUILD) + # Automatically calculate the output directory + set(${_TARGET}_OUTPUT_DIR ${CMAKE_SOURCE_DIR}/${hint}) + endif() + + # Normalize variable name + if(${target}_OUTPUT_DIR) + set(${_TARGET}_OUTPUT_DIR ${${target}_OUTPUT_DIR}) + endif() + + # Create output directory if it does not exist already + if(${_TARGET}_OUTPUT_DIR AND NOT EXISTS ${${_TARGET}_OUTPUT_DIR}) + file(MAKE_DIRECTORY ${${_TARGET}_OUTPUT_DIR}) + endif() + + # Properly set output directory for a target so that during an installation using either 'pip install' or 'python3 + # setup.py install' the libraries get built in the proper directory + if(${_TARGET}_OUTPUT_DIR) + set_target_properties( + ${target} + PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${${_TARGET}_OUTPUT_DIR} + LIBRARY_OUTPUT_DIRECTORY_DEBUG ${${_TARGET}_OUTPUT_DIR} + LIBRARY_OUTPUT_DIRECTORY_RELEASE ${${_TARGET}_OUTPUT_DIR} + LIBRARY_OUTPUT_DIRECTORY_RELWITHDEBINFO ${${_TARGET}_OUTPUT_DIR} + LIBRARY_OUTPUT_DIRECTORY_MINSIZEREL ${${_TARGET}_OUTPUT_DIR}) + elseif(IS_PYTHON_BUILD) + message( + WARNING "IS_PYTHON_BUILD=ON but ${_TARGET}_OUTPUT_DIR " + "was not defined! The shared library for target ${target} " + "will probably not be copied to the correct directory. " + "Did you forget to add a CMakeExtension in setup.py?") + elseif(CMAKE_LIBRARY_OUTPUT_DIRECTORY) + set_target_properties( + ${target} + PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} + LIBRARY_OUTPUT_DIRECTORY_DEBUG ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} + LIBRARY_OUTPUT_DIRECTORY_RELEASE ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} + LIBRARY_OUTPUT_DIRECTORY_RELWITHDEBINFO ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} + LIBRARY_OUTPUT_DIRECTORY_MINSIZEREL ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}) + endif() +endmacro() + +# ============================================================================== + +# Set RPATH of target only if building for Python (ie. IS_PYTHON_BUILD=ON) or if building in-place (IN_PLACE_BUILD=ON) +macro(python_install_set_rpath target path) + if((IS_PYTHON_BUILD OR IN_PLACE_BUILD) AND LINKER_RPATH) + if(APPLE) + set_target_properties(${target} PROPERTIES INSTALL_RPATH "@loader_path/${path}") + elseif(UNIX) + set_target_properties(${target} PROPERTIES INSTALL_RPATH "$ORIGIN/${path}") + endif() + endif() +endmacro() + +# ============================================================================== + +include(FindPackageHandleStandardArgs) +# Find a Python module in the current (potential virtual) environment +# +# find_python_module( [REQUIRED|EXACT|QUIET] [VERSION ]) +# +# Usage is similar to the builtin find_package(...) +function(find_python_module module) + # cmake-lint: disable=C0103 + cmake_parse_arguments(PARSE_ARGV 1 PYMOD "REQUIRED;EXACT;QUIET" "VERSION" "") + + string(REPLACE "-" "_" module_name ${module}) + string(TOUPPER ${module_name} MODULE) + if(NOT PYMOD_${MODULE}) + if(PYMOD_REQUIRED) + set(PYMOD_${module}_FIND_REQUIRED TRUE) + set(PYMOD_${MODULE}_FIND_REQUIRED TRUE) + endif() + if(PYMOD_QUIET) + set(PYMOD_${module}_FIND_QUIETLY TRUE) + set(PYMOD_${MODULE}_FIND_QUIETLY TRUE) + endif() + if(PYMOD_EXACT) + set(PYMOD_${module}_FIND_VERSION_EXACT TRUE) + set(PYMOD_${MODULE}_FIND_VERSION_EXACT TRUE) + endif() + if(PYMOD_VERSION) + set(PYMOD_${module}_FIND_VERSION ${PYMOD_VERSION}) + set(PYMOD_${MODULE}_FIND_VERSION ${PYMOD_VERSION}) + endif() + + execute_process( + COMMAND "${Python_EXECUTABLE}" "-c" "import os, ${module_name}; print(os.path.dirname(${module_name}.__file__))" + RESULT_VARIABLE _${MODULE}_status + OUTPUT_VARIABLE _${MODULE}_location + ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) + + if(NOT _${MODULE}_status) + set(PYMOD_${MODULE}_PATH + ${_${MODULE}_location} + CACHE STRING "Location of Python module ${module}") + + if(PYMOD_VERSION) + execute_process( + COMMAND "${Python_EXECUTABLE}" "-c" "import ${module_name}; print(${module_name}.__version__)" + RESULT_VARIABLE _${MODULE}_status + OUTPUT_VARIABLE _${MODULE}_version + ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) + + if(NOT _${MODULE}_status) + set(PYMOD_${MODULE}_VERSION + ${_${MODULE}_version} + CACHE STRING "Version of Python module ${module}") + set(PYMOD_${module}_VERSION + ${PYMOD_${MODULE}_VERSION} + CACHE STRING "Version of Python module ${module}") + endif() + endif() + endif() + endif() + + if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.19 AND CMAKE_VERSION VERSION_LESS 3.20) + set(CMAKE_FIND_PACKAGE_NAME PYMOD_${module}) + endif() + + find_package_handle_standard_args( + PYMOD_${module_name} + REQUIRED_VARS PYMOD_${MODULE}_PATH + VERSION_VAR PYMOD_${MODULE}_VERSION) + + set(PYMOD_${MODULE}_FOUND + ${PYMOD_${MODULE}_FOUND} + CACHE INTERNAL "") + + mark_as_advanced(PYMOD_${MODULE}_FOUND PYMOD_${MODULE}_PATH PYMOD_${MODULE}_VERSION) +endfunction() + +# ============================================================================== diff --git a/cmake/macros_more.cmake b/cmake/macros_more.cmake new file mode 100644 index 0000000000000000000000000000000000000000..b75445a93fe01af04cd08b81f22375f4a9eafb36 --- /dev/null +++ b/cmake/macros_more.cmake @@ -0,0 +1,57 @@ +# ============================================================================== +# +# Copyright 2020 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ============================================================================== + +# Add a Python library (overload of the original python_add_library()) +# +# python_add_library() +# +# Override the original python_add_library() to keep track of all python libraries and properly set some target +# properties depending on the current CMake version. +macro(python_add_library target) + set(_args ${ARGN}) + if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.17) + # Position 0 is the library type + list(GET _args 0 _lib_type) + if(_lib_type STREQUAL "MODULE") + list(INSERT _args 1 WITH_SOABI) + endif() + endif() + + _python_add_library(${target} ${_args}) + append_to_property(_doc_targets GLOBAL ${target}) + append_to_property(_python_targets GLOBAL ${target}) + + if(_python_so_extension) + set_target_properties(${target} PROPERTIES SUFFIX "${_python_so_extension}") + endif() +endmacro() + +# Add a Pybind11 library (overload of the original pybind11_add_module()) +# +# python_add_library() +# +# Override the original python_add_module() to keep track of all python libraries and properly set some target +# properties depending on the current CMake version. +function(pybind11_add_module target) + _pybind11_add_module(${target} ${ARGN}) + + append_to_property(_doc_targets GLOBAL ${target}) + append_to_property(_python_targets GLOBAL ${target}) +endfunction() + +# ============================================================================== diff --git a/cmake/options.cmake b/cmake/options.cmake new file mode 100644 index 0000000000000000000000000000000000000000..6b4a9cb78bacb1d0c9ff474cbcb82390f3d97da8 --- /dev/null +++ b/cmake/options.cmake @@ -0,0 +1,164 @@ +# ============================================================================== +# +# Copyright 2020 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ============================================================================== + +include(CMakeDependentOption) + +# ============================================================================== +# Python related options + +if(APPLE) + option(PYTHON_VIRTUALENV_COMPAT "(Mac OS X) Make CMake search for Python Framework *after* any available\ + unix-style package. Can be useful in case of virtual environments." ON) +else() + option(PYTHON_VIRTUALENV_COMPAT "(Mac OS X) Make CMake search for Python Framework *after* any available\ + unix-style package. Can be useful in case of virtual environments." OFF) +endif() + +option(IS_PYTHON_BUILD "Is CMake called from setup.py? (e.g. python3 setup.py install?)" OFF) +option(IN_PLACE_BUILD "Are we building in-place for testing/development?" ON) + +# ============================================================================== +# CUDA related options + +if(DEFINED ENABLE_CUDA) + set(_enable_cuda_init ${ENABLE_CUDA}) +elseif(DEFINED GPUACCELERATED) + set(_enable_cuda_init ${GPUACCELERATED}) +else() + set(_enable_cuda_init OFF) +endif() + +option(ENABLE_CUDA "Enable building of CUDA libraries" _enable_cuda_init) +option(CUDA_ALLOW_UNSUPPORTED_COMPILER "Allow the use of an unsupported compiler version" OFF) +option(CUDA_STATIC "Use static version of Nvidia CUDA libraries during linking (also applies to nvc++)" OFF) + +# ============================================================================== +# Compilation options + +option(ENABLE_OPENMP "Use OpenMP for multi-threading" ON) + +option(ENABLE_PROJECTQ "Enable ProjectQ support" ON) +option(ENABLE_QUEST "Enable QuEST support" ON) + +# ------------------------------------------------------------------------------ + +option(ENABLE_PROFILING "Enable compilation with profiling flags." OFF) +option(ENABLE_STACK_PROTECTION "Enable the use of -fstack-protector during compilation" ON) + +# ============================================================================== +# Linking options + +option(ENABLE_RUNPATH "Prefer RUNPATH over RPATH when linking" ON) + +option(LINKER_DTAGS "Use --enable-new-dtags or --disable-new-dtags during linking" ON) +option(LINKER_NOEXECSTACK "Use -z,noexecstack during linking" ON) +option(LINKER_RELRO "Use -z,relro during linking for certain targets" ON) +option(LINKER_RPATH "Enable the use of RPATH/RUNPATH related flags during linking" ON) +option(LINKER_STRIP_ALL "Use --strip-all during linking" ON) + +# ============================================================================== +# Package related options + +# ============================================================================== +# Other CMake related options + +option(BUILD_TESTING "Build the test suite?" OFF) + +# NB: most if not all of our libraries have the type explicitly specified. +option(BUILD_SHARED_LIBS "Build shared libs" OFF) + +option(USE_VERBOSE_MAKEFILE "Use verbose Makefiles" ON) + +# ============================================================================== +# ============================================================================== +# Python related options + +if(PYTHON_VIRTUALENV_COMPAT) + set(CMAKE_FIND_FRAMEWORK LAST) +endif() + +# ------------------------------------------------------------------------------ + +if(IS_PYTHON_BUILD AND IN_PLACE_BUILD) + message(FATAL_ERROR "Cannot specify both IS_PYTHON_BUILD=ON and IN_PLACE_BUILD=ON!") +endif() + +# ============================================================================== +# CUDA related options + +if(CUDA_ALLOW_UNSUPPORTED_COMPILER) + set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} -allow-unsupported-compiler") +endif() + +if(ENABLE_CUDA) + enable_language(CUDA) + + if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.17) + find_package(CUDAToolkit REQUIRED) + else() + find_package(CUDA REQUIRED) + + if(CUDA_LIBRARIES) + if(NOT TARGET CUDA::cudart) + add_library(CUDA::cudart IMPORTED INTERFACE) + target_include_directories(CUDA::cudart SYSTEM INTERFACE "${CUDA_INCLUDE_DIRS}") + target_link_libraries(CUDA::cudart INTERFACE "${CUDA_LIBRARIES}") + endif() + endif() + + if(CUDA_cudart_static_LIBRARY) + if(NOT TARGET CUDA::cudart_static) + add_library(CUDA::cudart_static IMPORTED INTERFACE) + target_include_directories(CUDA::cudart_static SYSTEM INTERFACE "${CUDA_INCLUDE_DIRS}") + target_link_libraries(CUDA::cudart_static INTERFACE "${CUDA_cudart_static_LIBRARY}" Threads::Threads) + endif() + endif() + + find_library( + CUDA_driver_LIBRARY + NAMES cuda_driver cuda + HINTS ${CUDA_TOOLKIT_ROOT_DIR} ENV CUDA_PATH + PATH_SUFFIXES nvidia/current lib64 lib/x64 lib) + if(NOT CUDA_driver_LIBRARY) + # Don't try any stub directories until we have exhausted all other search locations. + find_library( + CUDA_driver_LIBRARY + NAMES cuda_driver cuda + HINTS ${CUDA_TOOLKIT_ROOT_DIR} ENV CUDA_PATH + PATH_SUFFIXES lib64/stubs lib/x64/stubs lib/stubs stubs) + endif() + mark_as_advanced(CUDA_driver_LIBRARY) + if(CUDA_driver_LIBRARY) + add_library(CUDA::cuda_driver IMPORTED INTERFACE) + target_include_directories(CUDA::cuda_driver SYSTEM INTERFACE "${CUDA_INCLUDE_DIRS}") + target_link_libraries(CUDA::cuda_driver INTERFACE "${CUDA_driver_LIBRARY}") + endif() + endif() +endif() + +# ============================================================================== +# Compilation options + +# ============================================================================== +# Other CMake related options + +if(USE_VERBOSE_MAKEFILE) + set(CMAKE_VERBOSE_MAKEFILE ON) +endif() + +# ============================================================================== diff --git a/cmake/os_detection.cmake b/cmake/os_detection.cmake new file mode 100644 index 0000000000000000000000000000000000000000..c412f76d9cbdba7d8b985285fa8c8f82db73c0a4 --- /dev/null +++ b/cmake/os_detection.cmake @@ -0,0 +1,88 @@ +# ============================================================================== +# +# Copyright 2020 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ============================================================================== + +if(UNIX) + if(APPLE) + set(OS_NAME + "OSX" + CACHE STRING "Operating system name" FORCE) + else() + # Tested with: + # + # * ArchLinux (x86_64 & aarch64) + # * CentOS + # * Debian + # * Fedora + # * OpenSUSE (leap) + + set(_fname /etc/os-release) + if(NOT EXISTS ${_fname}) + set(_fname /usr/lib/os-release) + if(NOT EXISTS ${_fname}) + set(_fname) + endif() + endif() + + if(_fname) + set(_regex "^ID=\"?([a-zA-Z]+)\"?") + file(STRINGS ${_fname} OS_NAME_RAW REGEX ${_regex}) + string(REGEX MATCH ${_regex} OS_NAME_RAW ${OS_NAME_RAW}) + set(OS_NAME ${CMAKE_MATCH_1}) + + set(OS_RELEASE 0) + set(_regex "^VERSION_ID=\"?([0-9\\.]+)\"?") + file(STRINGS ${_fname} OS_RELEASE_RAW REGEX ${_regex}) + if(OS_RELEASE_RAW) + string(REGEX MATCH ${_regex} OS_RELEASE_RAW ${OS_RELEASE_RAW}) + set(OS_RELEASE ${CMAKE_MATCH_1}) + endif() + elseif(NOT $ENV{NIX_STORE} STREQUAL "") # CMake 3.14: if(DEFINED ENV{NIX_STORE}) + set(OS_NAME "nix") + set(OS_RELEASE 0) + # Try to find out Nix version if possible + find_program(_nix nix) + if(_nix) + execute_process( + COMMAND ${_nix} --version + OUTPUT_VARIABLE _nix_version + OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) + set(OS_RELEASE ${_nix_version}) + endif() + else() + set(OS_NAME "unknown") + set(OS_RELEASE "0") + endif() + endif() # APPLE +endif() # UNIX + +# ============================================================================== + +message(STATUS "Detected processor: ${CMAKE_SYSTEM_PROCESSOR}") +if(MQ_SKIP_SYSTEM_PROCESSOR_DETECTION) + # custom setup: required variables are passed through cache / CMake's command-line +elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "amd64.*|x86_64.*|AMD64.*") + set(X86_64 1) +elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "i686.*|i386.*|x86.*") + set(X86 1) +elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64.*|AARCH64.*|arm64.*|ARM64.*)") + set(AARCH64 1) +else() + message(WARNING "MindQuantum: unrecognized target processor configuration") +endif() + +# ============================================================================== diff --git a/cmake/packages.cmake b/cmake/packages.cmake new file mode 100644 index 0000000000000000000000000000000000000000..d6aa0d62a0e84a5beb0951ecc51395be66a0865a --- /dev/null +++ b/cmake/packages.cmake @@ -0,0 +1,200 @@ +# ============================================================================== +# +# Copyright 2020 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ============================================================================== + +# lint_cmake: -whitespace/indent + +# OpenMP + +set(PARALLEL_LIBS) +if(ENABLE_OPENMP) + if(APPLE) + find_program(BREW_CMD brew PATHS /usr/local/bin) + if(BREW_CMD) + # Homebrew installs libomp in ${LLVM_PREFIX}/lib and the headers in /usr/local/include + execute_process(COMMAND ${BREW_CMD} --prefix llvm OUTPUT_VARIABLE LLVM_PREFIX) + string(STRIP ${LLVM_PREFIX} LLVM_PREFIX) + list(APPEND CMAKE_LIBRARY_PATH ${LLVM_PREFIX}/lib) + else() + # MacPorts install libomp in /opt/local/lib/libomp and the headers in /opt/local/include/libomp + find_library( + LIBOMP_LIB omp gomp libomp + HINTS /opt/local/lib + PATH_SUFFIXES libomp + NO_DEFAULT_PATH) + fin_path( + LIBOMP_INC + omp.h + HINTS + /opt/local/include + PATH_SUFFIXES + libomp + NO_DEFAULT_PATH) + if(LIBOMP_LIB AND LIBOMP_INC) + get_filename_component(LIBOMP_DIR ${LIBOMP_LIB} DIRECTORY) + list(APPEND CMAKE_LIBRARY_PATH ${LIBOMP_DIR}) + list(APPEND CMAKE_INCLUDE_PATH ${LIBOMP_INC}) + endif() + endif() + endif() + + # ---------------------------------------------------------------------------- + + if(APPLE) + list(PREPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}/Modules/apple) + endif() + find_package(OpenMP) + if(OpenMP_FOUND) + list(APPEND PARALLEL_LIBS OpenMP::OpenMP_CXX) + endif() +endif() + +# ============================================================================== + +find_package(Threads REQUIRED) +list(APPEND PARALLEL_LIBS Threads::Threads) + +find_package(Patch REQUIRED) +include(FetchContent) +set(FETCHCONTENT_QUIET OFF) +set(BUILD_DIR ${CMAKE_BINARY_DIR}) +set(DEP_DIR ${BUILD_DIR}/_deps) +set(PATCH_DIR ${CMAKE_SOURCE_DIR}/third_party/patch) + +# ============================================================================== + +# Only helps set the Python executable for CMake >= 3.16 +if(DEFINED PYTHON_EXECUTABLE) + set(Python_EXECUTABLE ${PYTHON_EXECUTABLE}) # cmake-lint: disable=C0103 +endif() + +find_package(Python 3.5 COMPONENTS Interpreter Development) + +# NB: This should be removed for CMake >= 3.16 +if(NOT Python_FOUND) + # Use PYTHON_EXECUTABLE if it is defined, otherwise default to python + if(PYTHON_EXECUTABLE) + set(Python_EXECUTABLE ${PYTHON_EXECUTABLE}) # cmake-lint: disable=C0103 + elseif(NOT Python_EXECUTABLE) + find_program(Python_EXECUTABLE NAMES python3 python) + if(NOT Python_EXECUTABLE) + message(FATAL_ERROR "Unable to locate Python!") + endif() + endif() + + execute_process( + COMMAND "${Python_EXECUTABLE}" --version + RESULT_VARIABLE result + OUTPUT_VARIABLE _python_version) + string(STRIP "${_python_version}" Python_VERSION) + + if(Python_VERSION VERSION_LESS 3.6) + message(FATAL_ERROR "Cannot use Python ${Python_VERSION} (${Python_EXECUTABLE}): version too old!") + endif() + + execute_process( + COMMAND "${Python_EXECUTABLE}" -c "from distutils.sysconfig import get_python_inc; print(get_python_inc())" + RESULT_VARIABLE result + OUTPUT_VARIABLE _python_inc) + string(STRIP "${_python_inc}" _python_inc) + + execute_process( + COMMAND "${Python_EXECUTABLE}" -c "import distutils.sysconfig as sysconfig; import os; \ + print(os.path.join(sysconfig.get_config_var('LIBDIR'), sysconfig.get_config_var('LDLIBRARY')))" + RESULT_VARIABLE result + OUTPUT_VARIABLE _python_lib) + string(STRIP "${_python_lib}" _python_lib) + + # Define an imported library for Python + macro(_python_import_library lib_name) + if(lib MATCHES "${CMAKE_SHARED_LIBRARY_SUFFIX}$") + set(_type SHARED) + else() + set(_type STATIC) + endif() + + add_library(${lib_name} ${_type} IMPORTED) + target_include_directories(${lib_name} INTERFACE "${_python_inc}") + # cmake-lint: disable=C0307 + set_target_properties(${lib_name} PROPERTIES IMPORTED_LINK_INTERFACE_LANGUAGES "C" IMPORTED_LOCATION + "${_python_lib}") + endmacro() + + _python_import_library(Python::Python) + if(WIN32 + OR CYGWIN + OR MSYS) + # On Windows/Cygwin/MSYS Python::Module is an alias for Python::Python. See CMake code for FindPython. + _python_import_library(Python::Module) + else() + if(NOT TARGET Python::Module) + add_library(Python::Module INTERFACE IMPORTED) + endif() + target_include_directories(Python::Module INTERFACE "${_python_inc}") + target_link_options(Python::Module INTERFACE $<$:LINKER:-undefined,dynamic_lookup> + $<$:LINKER:-z,nodefs> $<$:LINKER:-b,erok>) + endif() +endif() + +if(CMAKE_VERSION VERSION_LESS 3.17) + message(CHECK_START "Looking for python SOABI") + + execute_process( + COMMAND "${Python_EXECUTABLE}" "-c" "from sysconfig import get_config_var; \ +print(get_config_var ('EXT_SUFFIX') or s.get_config_var ('SO'))" + RESULT_VARIABLE _soabi_success + OUTPUT_VARIABLE _python_so_extension + ERROR_VARIABLE _soabi_error_value + OUTPUT_STRIP_TRAILING_WHITESPACE) + + if(NOT _soabi_success MATCHES 0) + message(CHECK_FAIL "failed") + message(FATAL_ERROR "Failed to extract Python SOABI extension:\n${_soabi_error_value}") + else() + message(CHECK_PASS "done") + endif() +endif() + +# ------------------------------------------------------------------------------ + +include(${CMAKE_CURRENT_LIST_DIR}/pybind11.cmake) + +# ------------------------------------------------------------------------------ + +if(ENABLE_PROJECTQ) + include(${CMAKE_CURRENT_LIST_DIR}/projectq.cmake) +endif() + +if(ENABLE_QUEST) + include(${CMAKE_CURRENT_LIST_DIR}/quest.cmake) +endif() + +# ============================================================================== +# For Huawei internal security assessment + +if(BINSCOPE) + get_filename_component(_binscope_path ${BINSCOPE} DIRECTORY) + get_filename_component(_binscope_name ${BINSCOPE} NAME) +endif() + +find_program( + binscope_exec + NAMES binscope ${_binscope_name} + HINTS ${_binscope_path}) +include(${CMAKE_CURRENT_LIST_DIR}/binscope.cmake) + +# ============================================================================== diff --git a/cmake/projectq.cmake b/cmake/projectq.cmake new file mode 100644 index 0000000000000000000000000000000000000000..b96d22d5f86a850173666e6fa9c60e77468edb9f --- /dev/null +++ b/cmake/projectq.cmake @@ -0,0 +1,63 @@ +# ============================================================================== +# +# Copyright 2021 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ============================================================================== + +set(PKG_NAME projectq) +set(PKG_ROOT ${DEP_DIR}/${PKG_NAME}-src) + +set(URL "https://gitee.com/mirrors/ProjectQ/repository/archive/v0.5.1.tar.gz") + +file(GLOB _patch_files "${PATCH_DIR}/projectq/*.patch*") + +# ============================================================================== +# Fetch the source code + +FetchContent_Declare(${PKG_NAME} URL ${URL}) +FetchContent_Populate(${PKG_NAME}) + +# ============================================================================== +# Patch the source code + +# cmake-lint: disable=C0103 +set(${PKG_NAME}_PATCHED + FALSE + CACHE STRING INTERNAL) + +if(NOT ${PKG_NAME}_PATCHED) + message("Patching for ${PKG_NAME}") + foreach(_file ${_patch_files}) + execute_process( + COMMAND ${Patch_EXECUTABLE} -p1 + INPUT_FILE ${_file} + WORKING_DIRECTORY ${PKG_ROOT} + RESULT_VARIABLE _result) + + if(NOT _result EQUAL 0) + message(FATAL_ERROR "Failed patch: ${_file}") + endif() + endforeach() + + set(${PKG_NAME}_PATCHED + TRUE + CACHE STRING INTERNAL FORCE) +endif() + +# ============================================================================== + +add_library(projectq INTERFACE) +target_compile_features(projectq INTERFACE cxx_std_14) +target_include_directories(projectq INTERFACE ${PKG_ROOT}) diff --git a/cmake/pybind11.cmake b/cmake/pybind11.cmake new file mode 100644 index 0000000000000000000000000000000000000000..1062fe0c168c9c9c330b6004647717fc3622278a --- /dev/null +++ b/cmake/pybind11.cmake @@ -0,0 +1,274 @@ +# ============================================================================== +# +# Copyright 2020 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +# +# Looking for pybind11 can be somewhat complicated now that there are two +# possible packages: +# - pybind11 +# - pybind11-global +# +# Here we try our best to look for pybind11 in all possible locations and in a +# meaningful way. +# In practice, this means that we look for a valid package in the +# following order: +# 1. First we look in a virtualenv (if one is active) for +# a) pybind11-global +# b) pybind11 >= 2.6.0 +# c) pybind11 < 2.6.0 && pybind11-cmake +# 2. pybind11-gobal in user site (if not in a virtualenv) +# 3. pybind11-gobal in global site package (also done if within a virtualenv) +# 4. pybind11 >= 2.6.0 in user and global sites +# NB: if the version of pybind11 is more recent than pybind11-global, we +# choose pybind11 +# 5. pybind11 < 2.6.0 && pybind11-cmake in user and global sites +# +# ============================================================================== + +# ~~~ +# Make sure that pybind11 finds the correct python version +# - pybind11 < 2.6.0 uses find_package(PythonInterp ...) +# - pybind11 >= 2.6.0 looks for the `python` command or PYTHON_EXECUTABLE +# => specify PYTHON_EXECUTABLE manually to guarantee we find the same +# interpreter and libraries as for other python packages +# ~~~ +set(PYTHON_EXECUTABLE ${Python_EXECUTABLE}) +set(PYBIND11_PYTHON_VERSION ${Python_VERSION}) # maybe not strictly required + +message(CHECK_START "Looking for pybind11") +list(APPEND CMAKE_MESSAGE_INDENT " ") + +# ============================================================================== +# First detect whether we are in a virtualenv or not + +execute_process( + COMMAND ${Python_EXECUTABLE} -c "import sys; print(int(sys.prefix != sys.base_prefix))" + OUTPUT_VARIABLE _is_virtualenv + ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) + +if(_is_virtualenv) + # ~~~ + # Try to find in order within the virtualenv: + # - pybind11-global + # - pybind11 >= 2.6.0 + # - pybind11 < 2.6.0 with pybind11-cmake + # - if all of that fails, revert to user and global sites (as far as is possible) + # ~~~ + + message(CHECK_START "Looking for pybind11-global in virtualenv") + + # Look for pybind11-global + execute_process( + COMMAND ${Python_EXECUTABLE} -c "import sys, os; print(os.path.join(sys.prefix, 'share', 'cmake', 'pybind11'))" + OUTPUT_VARIABLE pybind11_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE) + + if(NOT EXISTS pybind11_DIR) + message(CHECK_FAIL "Not-found") + + # Could not find pybind11-global, try pybind11 >= 2.6.0 + message(CHECK_START "Looking for pybind11 >= 2.6.0 in virtualenv") + + find_python_module(pybind11 VERSION 2.6.0) + if(PYMOD_PYBIND11_FOUND) + message(CHECK_PASS "Found") + execute_process( + COMMAND ${Python_EXECUTABLE} -m pybind11 --cmakedir + OUTPUT_VARIABLE pybind11_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE) + else() + message(CHECK_FAIL "Not-found") + # Now try pybind11 < 2.6.0 && pybind11-cmake + find_python_module(pybind11-cmake) + if(PYMOD_PYBIND11_CMAKE_FOUND) + execute_process( + COMMAND ${Python_EXECUTABLE} -c "import pybind11_cmake; print(pybind11_cmake.__path__[0])" + OUTPUT_VARIABLE pybind11_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE) + endif() + endif() + else() + message(CHECK_PASS "Found") + endif() + + # Try to find some valid pybind11 config within the virtualenv + find_package(pybind11 2.6.0 CONFIG QUIET NO_DEFAULT_PATH) +endif() + +# ------------------------------------------------------------------------------ + +if(NOT pybind11_FOUND) + message(CHECK_START "Looking for pybind11-global in global and user sites") + + if(NOT _is_virtualenv) + # Try pybind11-global in user site + execute_process( + COMMAND ${Python_EXECUTABLE} -m site --user-base + RESULT_VARIABLE _status + OUTPUT_VARIABLE _user_base + OUTPUT_STRIP_TRAILING_WHITESPACE) + + # NB: _status == 0 means user site enabled. Anything else and we really should not consider it + if(_status EQUAL 0 AND EXISTS "${_user_base}/share/cmake/pybind11/") + # cmake-lint: disable=C0103 + set(pybind11_DIR "${_user_base}/share/cmake/pybind11/") + endif() + endif() + + # Try to find pybind11-global either in user-site or global-site (even in the case of a virtualenv since we did not + # find anything useful in it previously) + find_package(pybind11 2.6.0 CONFIG QUIET) + + if(pybind11_FOUND) + message(CHECK_PASS "Found") + else() + message(CHECK_PASS "Not-found") + endif() + + if(NOT _is_virtualenv) + message(CHECK_START "Looking for pybind11 Python module") + + # Try to find pybind11 either in user-site or global-site + find_python_module(pybind11 VERSION 2.6.0) + + if(PYMOD_PYBIND11_FOUND) + message(CHECK_PASS "Found") + else() + message(CHECK_PASS "Not-found") + endif() + + if(PYMOD_PYBIND11_FOUND AND PYMOD_PYBIND11_VERSION VERSION_GREATER pybind11_VERSION) + # We prefer pybind11 over pybind11 global if its version is more recent. This could typically be the case if a + # user installs a more recent version of pybind11 in its user site. + execute_process( + COMMAND ${Python_EXECUTABLE} -m pybind11 --cmakedir + OUTPUT_VARIABLE pybind11_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE) + endif() + + message(CHECK_START "Looking for pybind11 (final)") + + # Look for pybind11 again in case we have pybind11 more recent than pybind11-global + find_package(pybind11 2.6.0 CONFIG QUIET NO_DEFAULT_PATH) + + # If we are not in a virtualenv, we should still try to look for pybind11 < 2.6.0 && pybind11-cmake + if(NOT pybind11_FOUND) + message(CHECK_PASS "Not-found") + + message(CHECK_START "Looking for pybind11-cmake") + # If everything else fails, rely on the pybind11_cmake package + find_python_module(pybind11-cmake) + if(PYMOD_PYBIND11_CMAKE_FOUND) + message(CHECK_PASS "Done") + execute_process( + COMMAND ${Python_EXECUTABLE} -c "import pybind11_cmake; print(pybind11_cmake.__path__[0])" + OUTPUT_VARIABLE pybind11_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE) + else() + message(CHECK_FAIL "Failed") + endif() + else() + message(CHECK_PASS "Found") + endif() + endif() +endif() + +# ------------------------------------------------------------------------------ +# Now look for pybind11 using the CONFIG method one last time. This would typically only be useful in the case of +# pybind11 < 2.6.0 located in either user or global sites. + +list(POP_BACK CMAKE_MESSAGE_INDENT) +if(pybind11_DIR) + message(CHECK_PASS "Done") +else() + message(CHECK_FAIL "Failed") +endif() +find_package(pybind11 2.6.0 CONFIG NO_DEFAULT_PATH) + +# ============================================================================== + +# With pybind11-cmake 1.0.0 we might need to fix the include path +if(PYMOD_PYBIND11_CMAKE_FOUND) + include(CheckCXXSourceCompiles) + check_cxx_source_compiles("#include +int main() {return 0;}" pybind11_compiles) + + if(NOT pybind11_compiles) + # This could happen with pybind11_cmake == 1.0.0 because there is a typo in the pybind11Config.cmake + execute_process( + COMMAND ${Python_EXECUTABLE} -c "import pybind11 +print(pybind11.get_include(False) + ';' + pybind11.get_include(True))" + OUTPUT_VARIABLE _pybind11_inc_dir + OUTPUT_STRIP_TRAILING_WHITESPACE) + get_directory_property(_inc_dirs INCLUDE_DIRECTORIES) + list(FILTER _inc_dirs EXCLUDE REGEX .*pybind11_INCLUDE_DIR$) + list(APPEND _inc_dirs ${_pybind11_inc_dir}) + set_directory_properties(PROPERTIES INCLUDE_DIRECTORIES "${_inc_dirs}") + endif() +endif() + +# ============================================================================== + +if(pybind11_FOUND) + if(NOT TARGET pybind11::headers) + message(SEND_ERROR "Target pybind11::headers was not defined! Perhaps try updating pybind11 on your system?") + endif() + if(NOT TARGET pybind11::pybind11) + message(SEND_ERROR "Target pybind11::pybind11 was not defined! Perhaps try updating pybind11 on your system?") + endif() + if(NOT TARGET pybind11::module) + message(SEND_ERROR "Target pybind11::module was not defined! Perhaps try updating pybind11 on your system?") + endif() + + # NB: workardound for NVHPC compiler that requires -isystem (e.g. for /usr/include) + get_target_property(_include_dir pybind11::pybind11_headers INTERFACE_INCLUDE_DIRECTORIES) + + if(_include_dir) + target_include_directories(pybind11::pybind11_headers SYSTEM INTERFACE ${_include_dir}) + endif() +else() + message(STATUS "pybind11 was not found on your system or it is an incompatible version") + message(STATUS " -> will be fetching pybind11 from an external Git repository") + + set(PKG_NAME pybind11) + set(PKG_ROOT ${DEP_DIR}/${PKG_NAME}-src) + + set(URL "https://gitee.com/mirrors/pybind11/repository/archive/v2.7.1.tar.gz") + + FetchContent_Declare(${PKG_NAME} URL ${URL}) + FetchContent_GetProperties(${PKG_NAME}) + if(NOT ${PKG_NAME}_POPULATED) + FetchContent_Populate(${PKG_NAME}) + endif() + + include_directories(${PKG_ROOT}/include) + add_subdirectory(${PKG_ROOT} pybind11-build) +endif() + +# ------------------------------------------------------------------------------ + +# For debugging +if(pybind11_FOUND) + message(STATUS "Found pybind11 using the CONFIG method in ${pybind11_DIR}") + message(STATUS "Found pybind11 and defined the pybind11::pybind11 imported target:") + message(STATUS " - include: ${pybind11_INCLUDE_DIR}") + message(STATUS " - version: ${pybind11_VERSION}") +endif() + +# ============================================================================== + +mark_as_advanced(pybind11_DIR) + +# ============================================================================== diff --git a/cmake/quest.cmake b/cmake/quest.cmake new file mode 100644 index 0000000000000000000000000000000000000000..639421ad8759d5b754fc8a3b25bd232747099dca --- /dev/null +++ b/cmake/quest.cmake @@ -0,0 +1,74 @@ +# ============================================================================== +# +# Copyright 2021 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ============================================================================== + +set(PKG_NAME quest) +set(PKG_ROOT ${DEP_DIR}/${PKG_NAME}-src) + +set(URL "https://gitee.com/donghufeng/QuEST/repository/archive/v3.2.1.tar.gz") + +file(GLOB _patch_files "${PATCH_DIR}/quest/*.patch*") + +# ============================================================================== +# Fetch the source code + +if(NOT DEFINED CMAKE_CUDA_ARCHITECTURES) + set(CMAKE_CUDA_ARCHITECTURES 60) +endif() + +FetchContent_Declare(${PKG_NAME} URL ${URL}) +FetchContent_Populate(${PKG_NAME}) + +# ============================================================================== +# Patch the source code + +# cmake-lint: disable=C0103 +set(${PKG_NAME}_PATCHED + FALSE + CACHE STRING INTERNAL) + +if(NOT ${PKG_NAME}_PATCHED) + message("Patching for ${PKG_NAME}") + foreach(_file ${_patch_files}) + execute_process( + COMMAND ${Patch_EXECUTABLE} -p1 + INPUT_FILE ${_file} + WORKING_DIRECTORY ${PKG_ROOT} + RESULT_VARIABLE _result) + + if(NOT _result EQUAL 0) + message(FATAL_ERROR "Failed patch: ${_file}") + endif() + endforeach() + + set(${PKG_NAME}_PATCHED + TRUE + CACHE STRING INTERNAL FORCE) +endif() + +# ============================================================================== + +# Add QuEST as a sub-directory (NB: only used to create a new scope for temporary variables) +function(add_quest_subdirectory) + set(GPUACCELERATED ${ENABLE_CUDA}) + set(TESTING OFF) + set(GPU_COMPUTE_CAPABILITY ${CMAKE_CUDA_ARCHITECTURES}) + add_subdirectory(${PKG_ROOT} ${CMAKE_BINARY_DIR}/_deps/${PKG_NAME}-build EXCLUDE_FROM_ALL) +endfunction() + +add_quest_subdirectory() +set_output_directory_auto(QuEST mindquantum) diff --git a/install_with_docker.md b/install_with_docker.md index 85fbd31192a400e8d18248a4f1fe369ea3fcbf96..66d4e4896704ca3f03ca388156a5f729e5d8b010 100644 --- a/install_with_docker.md +++ b/install_with_docker.md @@ -44,7 +44,7 @@ docker run -it swr.cn-south-1.myhuaweicloud.com/mindspore/mindspore-cpu:{tag} /b ```python import numpy as np import mindspore.context as context -import mindspore.ops as ops +import mindspore.operators as ops from mindspore import Tensor context.set_context(mode=context.PYNATIVE_MODE, device_target="CPU") diff --git a/install_with_docker_en.md b/install_with_docker_en.md index 9b771eb6cae90f07773ae27f2689bc9ff5d3651e..0c822a88569f4871d648f196e24943107422e1ba 100644 --- a/install_with_docker_en.md +++ b/install_with_docker_en.md @@ -44,7 +44,7 @@ After entering the MindSpore container according to the above steps, to test whe ```python import numpy as np import mindspore.context as context -import mindspore.ops as ops +import mindspore.operators as ops from mindspore import Tensor context.set_context(mode=context.PYNATIVE_MODE, device_target="CPU") @@ -102,4 +102,4 @@ At this point, you have successfully installed the MindSpore CPU version by Dock ```python python -c 'import mindquantum' -``` \ No newline at end of file +``` diff --git a/mindquantum/__init__.py b/mindquantum/__init__.py index 213ac46deb88cfce0e40d67f63ab3d7c785aba1e..739befa1119e040a2a498eeee45aa5d143f86c02 100644 --- a/mindquantum/__init__.py +++ b/mindquantum/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,34 +17,42 @@ import os import warnings +import sys -from . import ops -from . import circuit +from . import core +from .core import gates +from .core import operators from . import engine -from . import gate -from . import nn -from . import parameterresolver +from . import framework from . import utils -from . import hiqfermion -from . import ansatz -from .circuit import * -from .gate import * -from .parameterresolver import * -from .version import __version__ +from . import algorithm +from . import simulator +from . import io +from .core import * +from .algorithm import * +from .utils import * +from .simulator import * +from .framework import * +from .io import * -__version_info__ = tuple(__version__.split('.')) -__all__ = ['__version__', '__version_info__'] -__all__.extend(circuit.__all__) -__all__.extend(gate.__all__) -__all__.extend(parameterresolver.__all__) -__all__.sort() +if sys.version_info < (3, 8): # pragma: no cover + from importlib_metadata import version, PackageNotFoundError +else: # pragma: no cover + from importlib.metadata import version, PackageNotFoundError + +try: + __version__ = version("mindquantum") + __version_info__ = tuple(__version__.split('.')) + __all__ = ['__version__', '__version_info__'] +except PackageNotFoundError: + __all__ = [] -total_num_core = os.cpu_count() -omp_num_threads = os.environ.get('OMP_NUM_THREADS') -if omp_num_threads is None: - omp_num_threads = total_num_core -warnings.warn( - "[NOTE] Current simulator thread is {}. If your simulation is slow, \ -set OMP_NUM_THREADS to a appropriate number according to your model.".format( - omp_num_threads)) + +__all__.extend(core.__all__) +__all__.extend(algorithm.__all__) +__all__.extend(utils.__all__) +__all__.extend(simulator.__all__) +__all__.extend(framework.__all__) +__all__.extend(io.__all__) +__all__.sort() diff --git a/mindquantum/algorithm/__init__.py b/mindquantum/algorithm/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c8bc18fceb9a1a0fbea511592c9cb6f9c103c8d5 --- /dev/null +++ b/mindquantum/algorithm/__init__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""Quantum algorithms""" + +from . import library +from . import nisq +from .library import * +from .nisq import * + +__all__ = [] +__all__.extend(library.__all__) +__all__.extend(nisq.__all__) +__all__.sort() diff --git a/mindquantum/hiqfermion/transforms/__init__.py b/mindquantum/algorithm/library/__init__.py similarity index 87% rename from mindquantum/hiqfermion/transforms/__init__.py rename to mindquantum/algorithm/library/__init__.py index 2ecd9c4b04a45eca009d4b8de4bccbde3bcd540c..2987cef3ea6b8a8d199b60017ddb807eea27bf33 100644 --- a/mindquantum/hiqfermion/transforms/__init__.py +++ b/mindquantum/algorithm/library/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,9 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================ -"""transform""" -from .transform import Transform +"""Circuit library""" -__all__ = ['Transform'] +from .quantum_fourier import qft + +__all__ = ['qft'] __all__.sort() diff --git a/mindquantum/circuit/quantum_fourier.py b/mindquantum/algorithm/library/quantum_fourier.py similarity index 81% rename from mindquantum/circuit/quantum_fourier.py rename to mindquantum/algorithm/library/quantum_fourier.py index 5f7fd613fd506e24555484f8b4f626f1e1b6c3b8..577df318af5330aefc1e3d4ce9fbe045b37e9d61 100644 --- a/mindquantum/circuit/quantum_fourier.py +++ b/mindquantum/algorithm/library/quantum_fourier.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,8 +16,8 @@ """Quantum fourier transform.""" import numpy as np -from mindquantum.gate import H, PhaseShift -from mindquantum.circuit import SwapParts, Circuit +from mindquantum.core.gates import H, PhaseShift +from mindquantum.core.circuit import SwapParts, Circuit def _rn(k): @@ -41,13 +42,12 @@ def qft(qubits): qubits (list[int]): Qubits you want to apply quantum fourier transform. Examples: - >>> from mindquantum.circuit import qft - >>> from mindquantum.circuit import StateEvolution - >>> print(StateEvolution(qft([0, 1])).final_state(ket=True)) - 0.5¦00⟩ - 0.5¦01⟩ - 0.5¦10⟩ - 0.5¦11⟩ + >>> from mindquantum.algorithm.library import qft + >>> print(qft([0, 1]).get_qs(ket=True)) + 1/2¦00⟩ + 1/2¦01⟩ + 1/2¦10⟩ + 1/2¦11⟩ """ c = Circuit() n_qubits = len(qubits) diff --git a/mindquantum/algorithm/nisq/__init__.py b/mindquantum/algorithm/nisq/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b57765ef830abfff3d4ce808c9b93333117ebf13 --- /dev/null +++ b/mindquantum/algorithm/nisq/__init__.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""NISQ algorithms""" + +from ._ansatz import Ansatz +from . import chem +from . import qaoa +from .chem import * +from .qaoa import * + +__all__ = ['Ansatz'] +__all__.extend(chem.__all__) +__all__.extend(qaoa.__all__) +__all__.sort() diff --git a/mindquantum/ansatz/_ansatz.py b/mindquantum/algorithm/nisq/_ansatz.py similarity index 95% rename from mindquantum/ansatz/_ansatz.py rename to mindquantum/algorithm/nisq/_ansatz.py index f491a0308fdc9401b3fca105b6c7ffe187442a66..b9c4360fdf20fd727de59c9e482c3b7b3f2247cc 100644 --- a/mindquantum/ansatz/_ansatz.py +++ b/mindquantum/algorithm/nisq/_ansatz.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,7 +16,7 @@ """Basic class of ansatz.""" from abc import abstractmethod -from mindquantum.circuit import Circuit +from mindquantum.core.circuit import Circuit class Ansatz: diff --git a/mindquantum/hiqfermion/ucc/__init__.py b/mindquantum/algorithm/nisq/chem/__init__.py similarity index 65% rename from mindquantum/hiqfermion/ucc/__init__.py rename to mindquantum/algorithm/nisq/chem/__init__.py index 0a14e2bde90a9d020a4c4964fe2f2e6aad97eba6..94ac71c96cd4b5e8a1baab2f43aa609c57a9a2b8 100644 --- a/mindquantum/hiqfermion/ucc/__init__.py +++ b/mindquantum/algorithm/nisq/chem/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,17 +13,24 @@ # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================ -"""Quantum unitary coupled cluster.""" +"""Algorithm for quantum chemistry""" from mindquantum.third_party.unitary_cc import uccsd_singlet_generator from mindquantum.third_party.unitary_cc import uccsd_singlet_get_packed_amplitudes +from .transform import Transform from .qubit_hamiltonian import get_qubit_hamiltonian from .uccsd0 import uccsd0_singlet_generator from .quccsd import quccsd_generator +from .hardware_efficient_ansatz import HardwareEfficientAnsatz +from .qubit_ucc_ansatz import QubitUCCAnsatz +from .uccsd import generate_uccsd +from .unitary_cc import UCCAnsatz __all__ = [ - 'uccsd_singlet_generator', 'uccsd_singlet_get_packed_amplitudes', - 'get_qubit_hamiltonian', 'uccsd0_singlet_generator', 'quccsd_generator' + 'Transform', 'get_qubit_hamiltonian', 'uccsd_singlet_generator', + 'uccsd_singlet_get_packed_amplitudes', 'uccsd0_singlet_generator', + 'quccsd_generator', 'HardwareEfficientAnsatz', 'QubitUCCAnsatz', + 'generate_uccsd', 'UCCAnsatz' ] __all__.sort() diff --git a/mindquantum/ansatz/hardware_efficient.py b/mindquantum/algorithm/nisq/chem/hardware_efficient_ansatz.py similarity index 88% rename from mindquantum/ansatz/hardware_efficient.py rename to mindquantum/algorithm/nisq/chem/hardware_efficient_ansatz.py index e266a9ae5c6f152f781d781e4ac979bdf69f06a6..e23dca717e0853c8aedbfe27052b3d2b5f45c663 100644 --- a/mindquantum/ansatz/hardware_efficient.py +++ b/mindquantum/algorithm/nisq/chem/hardware_efficient_ansatz.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,9 +17,10 @@ import itertools import numpy as np -from mindquantum.gate import BasicGate, X -from mindquantum.circuit import Circuit, AP, A -from ._ansatz import Ansatz +from mindquantum.core.gates import BasicGate, X +from mindquantum.core.circuit import Circuit +from mindquantum.core.circuit.utils import AP, A +from .._ansatz import Ansatz def _check_single_rot_gate_seq(single_rot_gate_seq): @@ -62,24 +64,16 @@ class HardwareEfficientAnsatz(Ansatz): depth (int): The depth of ansatz. Default: 1. Examples: - >>> from mindquantum.ansatz import HardwareEfficientAnsatz + >>> from mindquantum.algorithm.nisq.chem import HardwareEfficientAnsatz >>> from mindquantum import RY, RZ, Z >>> hea = HardwareEfficientAnsatz(3, [RY, RZ], Z, [(0, 1), (0, 2)]) >>> hea.circuit - RY(d0_n0_0|0) - RZ(d0_n0_1|0) - RY(d0_n1_0|1) - RZ(d0_n1_1|1) - RY(d0_n2_0|2) - RZ(d0_n2_1|2) - Z(1 <-: 0) - Z(2 <-: 0) - RY(d1_n0_0|0) - RZ(d1_n0_1|0) - RY(d1_n1_0|1) - RZ(d1_n1_1|1) - RY(d1_n2_0|2) - RZ(d1_n2_1|2) + q0: ──RY(d0_n0_0)────RZ(d0_n0_1)────●────●────RY(d1_n0_0)────RZ(d1_n0_1)── + │ │ + q1: ──RY(d0_n1_0)────RZ(d0_n1_1)────Z────┼────RY(d1_n1_0)────RZ(d1_n1_1)── + │ + q2: ──RY(d0_n2_0)────RZ(d0_n2_1)─────────Z────RY(d1_n2_0)────RZ(d1_n2_1)── + """ def __init__(self, n_qubits, @@ -91,7 +85,7 @@ class HardwareEfficientAnsatz(Ansatz): if not isinstance(depth, int) or depth <= 0: raise ValueError(f"depth requires a positive int, but get {depth}") if not isinstance(entangle_gate, - BasicGate) or entangle_gate.isparameter: + BasicGate) or entangle_gate.parameterized: raise ValueError( f"entangle gate requires a non parameterized gate, but get {entangle_gate}" ) diff --git a/mindquantum/hiqfermion/ucc/qubit_hamiltonian.py b/mindquantum/algorithm/nisq/chem/qubit_hamiltonian.py similarity index 86% rename from mindquantum/hiqfermion/ucc/qubit_hamiltonian.py rename to mindquantum/algorithm/nisq/chem/qubit_hamiltonian.py index b8d7216c9d227845c84f090ddbedff5cd780b192..a72cb5d11e566799a0016efc18f36e6a009fd07b 100644 --- a/mindquantum/hiqfermion/ucc/qubit_hamiltonian.py +++ b/mindquantum/algorithm/nisq/chem/qubit_hamiltonian.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,9 +15,9 @@ # ============================================================================ """Get qubit hamiltonian""" -from mindquantum.ops import InteractionOperator -from mindquantum.utils import get_fermion_operator -from mindquantum.hiqfermion.transforms import Transform +from mindquantum.core.operators import InteractionOperator +from mindquantum.core.operators.utils import get_fermion_operator +from .transform import Transform def get_qubit_hamiltonian(mol): diff --git a/mindquantum/ansatz/qubit_ucc.py b/mindquantum/algorithm/nisq/chem/qubit_ucc_ansatz.py similarity index 88% rename from mindquantum/ansatz/qubit_ucc.py rename to mindquantum/algorithm/nisq/chem/qubit_ucc_ansatz.py index effdb8adccadfb6a55c73c57d2e976e6262285eb..6bbc2b8f0a53a14c5235aa027e7cb13614601b2a 100644 --- a/mindquantum/ansatz/qubit_ucc.py +++ b/mindquantum/algorithm/nisq/chem/qubit_ucc_ansatz.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,12 +19,12 @@ import warnings import itertools import numpy -from mindquantum.gate import CNOT, X, RY -from mindquantum.circuit import Circuit -from mindquantum.parameterresolver import ParameterResolver as PR -from mindquantum.ops import QubitExcitationOperator -from mindquantum.utils import hermitian_conjugated -from ._ansatz import Ansatz +from mindquantum.core.gates import CNOT, X, RY +from mindquantum.core.circuit import Circuit +from mindquantum.core.parameterresolver import ParameterResolver as PR +from mindquantum.core.operators import QubitExcitationOperator +from mindquantum.core.operators.utils import hermitian_conjugated +from .._ansatz import Ansatz def _check_int_list(input_list, name): @@ -68,21 +69,18 @@ class QubitUCCAnsatz(Ansatz): good accuracy. Default: 1. Examples: - >>> from mindquantum.ansatz import QubitUCCAnsatz + >>> from mindquantum.algorithm.nisq.chem import QubitUCCAnsatz >>> QubitUCCAnsatz().n_qubits 0 >>> qucc = QubitUCCAnsatz(4, 2, trotter_step=2) >>> qucc.circuit[:10] - CNOT(0 <-: 2) - RY(t_0_q_s_0|2 <-: 0) - CNOT(0 <-: 2) - CNOT(1 <-: 2) - RY(t_0_q_s_1|2 <-: 1) - CNOT(1 <-: 2) - CNOT(0 <-: 3) - RY(t_0_q_s_2|3 <-: 0) - CNOT(0 <-: 3) - CNOT(1 <-: 3) + q0: ──X──────────●──────────X───────────────────────────────X──────────●──────────X─────── + │ │ │ │ │ │ + q1: ──┼──────────┼──────────┼────X──────────●──────────X────┼──────────┼──────────┼────X── + │ │ │ │ │ │ │ │ │ │ + q2: ──●────RY(t_0_q_s_0)────●────●────RY(t_0_q_s_1)────●────┼──────────┼──────────┼────┼── + │ │ │ │ + q3: ────────────────────────────────────────────────────────●────RY(t_0_q_s_2)────●────●── >>> qucc.n_qubits 4 >>> qucc_2 = QubitUCCAnsatz(occ_orb=[0, 1], vir_orb=[2]) diff --git a/mindquantum/hiqfermion/ucc/quccsd.py b/mindquantum/algorithm/nisq/chem/quccsd.py similarity index 96% rename from mindquantum/hiqfermion/ucc/quccsd.py rename to mindquantum/algorithm/nisq/chem/quccsd.py index 81a961d4b85db41f0d4c4d82019695aa639bc9d4..55ef0d278128a18a919a723abc9eb65b2b9e9ddc 100644 --- a/mindquantum/hiqfermion/ucc/quccsd.py +++ b/mindquantum/algorithm/nisq/chem/quccsd.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,9 +19,9 @@ import itertools import warnings import numpy -from mindquantum.ops import QubitExcitationOperator -from mindquantum.parameterresolver import ParameterResolver as PR -from mindquantum.utils import hermitian_conjugated +from mindquantum.core.operators import QubitExcitationOperator +from mindquantum.core.operators.utils import hermitian_conjugated +from mindquantum.core.parameterresolver import ParameterResolver as PR def _check_int_list(input_list, name): @@ -63,7 +64,7 @@ def quccsd_generator(n_qubits=None, QubitExcitationOperator: Generator of the qUCCSD operators. Examples: - >>> from mindquantum.hiqfermion.ucc import quccsd_generator + >>> from mindquantum.algorithm.nisq.chem import quccsd_generator >>> quccsd_generator() 0 >>> quccsd_generator(4, 2) diff --git a/mindquantum/hiqfermion/transforms/transform.py b/mindquantum/algorithm/nisq/chem/transform.py similarity index 97% rename from mindquantum/hiqfermion/transforms/transform.py rename to mindquantum/algorithm/nisq/chem/transform.py index 6bf5afe001b81cb3e5c4099dc7cae9e07853f108..137659c5f6e4348e9d920f2e4b07750f23abf400 100644 --- a/mindquantum/hiqfermion/transforms/transform.py +++ b/mindquantum/algorithm/nisq/chem/transform.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (c) 2020 Huawei Technologies Co.,ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,9 +19,9 @@ thus can be simulated in quantum computer from math import floor, log import numpy as np -from mindquantum.ops.fermion_operator import FermionOperator -from mindquantum.ops.qubit_operator import QubitOperator -from mindquantum.utils import count_qubits, number_operator, normal_ordered +from mindquantum.core.operators.fermion_operator import FermionOperator +from mindquantum.core.operators.qubit_operator import QubitOperator +from mindquantum.core.operators.utils import count_qubits, number_operator, normal_ordered class Transform: @@ -40,11 +41,11 @@ class Transform: n_qubits (int): The total qubits of this operator. Default: None Examples: - >>> from mindquantum.ops import FermionOperator + >>> from mindquantum.core.operators import FermionOperator >>> op1 = FermionOperator('1^') >>> op1 1.0 [1^] - >>> from mindquantum.hiqfermion.transforms.transform import Transform + >>> from mindquantum.algorithm.nisq.chem import Transform >>> op_transform = Transform(op1) >>> op_transform.jordan_wigner() 0.5 [Z0 X1] + @@ -457,9 +458,9 @@ class Transform: # Handle Pauli X and Y. else: raising_term = FermionOperator( - ((pauli_operator[0], 1),)) + ((pauli_operator[0], 1), )) lowering_term = FermionOperator( - ((pauli_operator[0], 0),)) + ((pauli_operator[0], 0), )) if pauli_operator[1] == 'Y': raising_term *= 1.j lowering_term *= -1.j @@ -468,7 +469,7 @@ class Transform: # Account for the phase terms. for j in reversed(range(pauli_operator[0])): - z_term = QubitOperator(((j, 'Z'),)) + z_term = QubitOperator(((j, 'Z'), )) working_term = z_term * working_term term_key = list(working_term.terms)[0] transformed_pauli *= working_term.terms[term_key] diff --git a/mindquantum/circuit/uccsd.py b/mindquantum/algorithm/nisq/chem/uccsd.py similarity index 75% rename from mindquantum/circuit/uccsd.py rename to mindquantum/algorithm/nisq/chem/uccsd.py index 5af6565b9e57fc7a5757de8b1c6b16b46c962a1e..c7346bd40bc47665a1e2aaf9997b38d9533583b3 100644 --- a/mindquantum/circuit/uccsd.py +++ b/mindquantum/algorithm/nisq/chem/uccsd.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,19 +19,12 @@ from collections import OrderedDict as ordict import itertools import numpy as np from openfermion.chem import MolecularData -from mindquantum.ops import FermionOperator -from mindquantum.utils import down_index -from mindquantum.utils import up_index -from mindquantum.utils import get_fermion_operator -from mindquantum.hiqfermion.transforms import Transform +from mindquantum.core.operators import FermionOperator +from mindquantum.core.operators.utils import down_index, up_index, get_fermion_operator +from mindquantum.algorithm.nisq.chem.transform import Transform from mindquantum.third_party.interaction_operator import InteractionOperator -from mindquantum.gate import RX -from mindquantum.gate import H -from mindquantum.gate import X -from mindquantum.gate import RZ -from mindquantum.gate import RY -from mindquantum.parameterresolver import ParameterResolver -from .circuit import Circuit +from mindquantum.core.circuit.utils import decompose_single_term_time_evolution +from mindquantum.core.circuit import Circuit def _para_uccsd_singlet_generator(mol, th=0): @@ -182,78 +176,6 @@ def _pauli2circuit(pauli_ansatz): return circuit -def decompose_single_term_time_evolution(term, para): - """ - Decompose a time evolution gate into basic quantum gates. - - This function only work for the hamiltonian with only single pauli word. - For example, exp(-i * t * ham), ham can only be a single pauli word, such - as ham = X0 x Y1 x Z2, and at this time, term will be - ((0, 'X'), (1, 'Y'), (2, 'Z')). When the evolution time is expressd as - t = a*x + b*y, para would be {'x':a, 'y':b}. - - Args: - term (tuple, QubitOperator): the hamiltonian term of just the - evolution qubit operator. - para (Union[dict, numbers.Number]): the parameters of evolution operator. - - Returns: - Circuit, a quantum circuit. - - Example: - >>> from mindquantum.ops import QubitOperator - >>> ham = QubitOperator('X0 Y1') - >>> circuit = decompose_single_term_time_evolution(ham, {'a':1}) - >>> print(circuit) - H(0) - RX(1.571,1) - X(1 <-: 0) - RZ(a|1) - X(1 <-: 0) - RX(10.996,1) - H(0) - """ - if not isinstance(term, tuple): - try: - if len(term.terms) != 1: - raise ValueError("Only work for single term time \ - evolution operator, but get {}".format(len(term))) - term = list(term.terms.keys())[0] - except TypeError: - raise Exception("Not supported type:{}".format(type(term))) - - out = [] - term = sorted(term) - rxs = [] - if len(term) == 1: # single pauli operator - if term[0][1] == 'X': - out.append(RX(para).on(term[0][0])) - elif term[0][1] == 'Y': - out.append(RY(para).on(term[0][0])) - else: - out.append(RZ(para).on(term[0][0])) - else: - for index, action in term: - if action == 'X': - out.append(H.on(index)) - elif action == 'Y': - rxs.append(len(out)) - out.append(RX(np.pi / 2).on(index)) - - for i in range(len(term) - 1): - out.append(X.on(term[i + 1][0], term[i][0])) - if isinstance(para, (dict, ParameterResolver)): - out.append(RZ({i: 2 * j for i, j in para.items()}).on(term[-1][0])) - else: - out.append(RZ(2 * para).on(term[-1][0])) - for i in range(len(out) - 1)[::-1]: - if i in rxs: - out.append(RX(np.pi * 3.5).on(out[i].obj_qubits)) - else: - out.append(out[i]) - return Circuit(out) - - def generate_uccsd(molecular, th=0): """ Generate a uccsd quantum circuit based on a molecular data generated by diff --git a/mindquantum/hiqfermion/ucc/uccsd0.py b/mindquantum/algorithm/nisq/chem/uccsd0.py similarity index 96% rename from mindquantum/hiqfermion/ucc/uccsd0.py rename to mindquantum/algorithm/nisq/chem/uccsd0.py index d1a6b10ace271b4669c04a6ef719948be5737b6c..d788c453e667138b344aaef8dffdd02c02b82db7 100644 --- a/mindquantum/hiqfermion/ucc/uccsd0.py +++ b/mindquantum/algorithm/nisq/chem/uccsd0.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,9 +19,9 @@ import itertools import warnings import numpy -from mindquantum.ops import FermionOperator -from mindquantum.parameterresolver import ParameterResolver as PR -from mindquantum.utils import hermitian_conjugated, normal_ordered +from mindquantum.core.operators import FermionOperator +from mindquantum.core.operators.utils import hermitian_conjugated, normal_ordered +from mindquantum.core.parameterresolver import ParameterResolver as PR def _check_int_list(input_list, name): @@ -142,7 +143,7 @@ def spin_adapted_t1(i, j): tpq_list (list): Spin-adapted single excitation operators. Examples: - >>> from mindquantum.hiqfermion.ucc.uccsd0 import spin_adapted_t1 + >>> from mindquantum.algorithm.nisq.chem.uccsd0 import spin_adapted_t1 >>> spin_adapted_t1(2, 3) [1.0 [4^ 6] + 1.0 [5^ 7] ] @@ -182,7 +183,7 @@ def spin_adapted_t2(creation_list, annihilation_list): tpqrs_list(list): Spin-adapted double excitation operators. Examples: - >>> from mindquantum.hiqfermion.ucc.uccsd0 import spin_adapted_t2 + >>> from mindquantum.algorithm.nisq.chem.uccsd0 import spin_adapted_t2 >>> spin_adapted_t2([0, 1], [2, 3]) [0.5000000000000001 [1^ 2^ 4 7] + 0.5000000000000001 [1^ 2^ 6 5] + @@ -256,7 +257,7 @@ def uccsd0_singlet_generator(n_qubits=None, FermionOperator, Generator of the UCCSD operators that uses CCD0 ansatz. Examples: - >>> from mindquantum.hiqfermion.ucc.uccsd0 import uccsd0_singlet_generator + >>> from mindquantum.algorithm.nisq.chem.uccsd0 import uccsd0_singlet_generator >>> uccsd0_singlet_generator(4, 2) -1.0*d0_s_0 [0^ 2] + 2.0*d0_d_0 [1^ 0^ 3 2] + diff --git a/mindquantum/ansatz/unitary_cc.py b/mindquantum/algorithm/nisq/chem/unitary_cc.py similarity index 71% rename from mindquantum/ansatz/unitary_cc.py rename to mindquantum/algorithm/nisq/chem/unitary_cc.py index 29b220af4be77050d51a64bd22e9f1e4af2b6fd0..5ff1bf7d79fc97cd142a93dd5901042309f97deb 100644 --- a/mindquantum/ansatz/unitary_cc.py +++ b/mindquantum/algorithm/nisq/chem/unitary_cc.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,10 +15,12 @@ # ============================================================================ """Unitary coupled-cluster ansatz.""" -from mindquantum.circuit import Circuit, TimeEvolution, add_prefix -from mindquantum.hiqfermion.transforms import Transform -from mindquantum.hiqfermion.ucc import uccsd0_singlet_generator -from ._ansatz import Ansatz +from mindquantum.core.circuit import Circuit +from mindquantum.core.operators import TimeEvolution +from mindquantum.core.circuit.utils import add_prefix +from .uccsd0 import uccsd0_singlet_generator +from .transform import Transform +from .._ansatz import Ansatz def _check_int_list(input_list, name): @@ -57,38 +60,23 @@ class UCCAnsatz(Ansatz): trotter_step(int): The order of Trotterization step. Default: 1. Examples: - >>> from mindquantum.ansatz import UCCAnsatz + >>> from mindquantum.algorithm.nisq.chem import UCCAnsatz >>> ucc = UCCAnsatz(12, 4, occ_orb=[1], ... vir_orb=[2, 3], ... generalized=True, ... trotter_step=2) - >>> circuit_list = list(ucc.circuit) - >>> len(circuit_list) + >>> circuit = ucc.circuit.remove_barrier() + >>> len(circuit) 3624 - >>> params_list = ucc.circuit.para_name + >>> params_list = ucc.circuit.params_name >>> len(params_list) - 40 - >>> for i in range(10): - ... print(circuit_list[i]) - ... - RX(1.571|2) - H(4) - X(3 <-: 2) - X(4 <-: 3) - RZ(-1.0*t_0_d0_s_0|4) - X(4 <-: 3) - X(3 <-: 2) - H(4) - RX(10.996|2) - H(2) + 48 + >>> circuit[-10:] + q5: ──●────RX(7π/2)───────H───────●────────────────────────────●───────H────── + │ │ │ + q7: ──X───────H────────RX(π/2)────X────RZ(-0.5*t_1_d0_d_17)────X────RX(7π/2)── """ - def __init__(self, - n_qubits=None, - n_electrons=None, - occ_orb=None, - vir_orb=None, - generalized=False, - trotter_step=1): + def __init__(self, n_qubits=None, n_electrons=None, occ_orb=None, vir_orb=None, generalized=False, trotter_step=1): if n_qubits is not None and not isinstance(n_qubits, int): raise ValueError("The number of qubits should be integer, \ but get {}.".format(type(n_qubits))) @@ -108,26 +96,16 @@ but get {}.".format(type(generalized))) if not isinstance(trotter_step, int) or trotter_step < 1: raise ValueError("Trotter step must be a positive integer!") - super().__init__("Unitary CC", n_qubits, n_qubits, n_electrons, - occ_orb, vir_orb, generalized, trotter_step) + super().__init__("Unitary CC", n_qubits, n_qubits, n_electrons, occ_orb, vir_orb, generalized, trotter_step) - def _implement(self, - n_qubits, - n_electrons, - occ_orb=None, - vir_orb=None, - generalized=False, - trotter_step=1): + def _implement(self, n_qubits, n_electrons, occ_orb=None, vir_orb=None, generalized=False, trotter_step=1): """Implement the UCC ansatz using uccsd0""" ansatz_circuit = Circuit() for trotter_idx in range(trotter_step): - uccsd0_fermion_op = uccsd0_singlet_generator( - n_qubits, n_electrons, True, occ_orb, vir_orb, generalized) - uccsd0_circuit = TimeEvolution( - Transform(uccsd0_fermion_op).jordan_wigner().imag, 1).circuit + uccsd0_fermion_op = uccsd0_singlet_generator(n_qubits, n_electrons, True, occ_orb, vir_orb, generalized) + uccsd0_circuit = TimeEvolution(Transform(uccsd0_fermion_op).jordan_wigner().imag, 1).circuit # Modify parameter names - uccsd0_circuit_modified = add_prefix(uccsd0_circuit, - "t_" + str(trotter_idx)) + uccsd0_circuit_modified = add_prefix(uccsd0_circuit, "t_" + str(trotter_idx)) ansatz_circuit += uccsd0_circuit_modified n_qubits_circuit = 0 if list(ansatz_circuit): @@ -136,8 +114,6 @@ but get {}.".format(type(generalized))) if self.n_qubits is None: self.n_qubits = n_qubits_circuit if self.n_qubits < n_qubits_circuit: - raise ValueError( - "The number of qubits in the ansatz circuit {} is larger than \ -the input n_qubits {}! Please check input parameters such as occ_orb, etc.". - format(n_qubits_circuit, n_qubits)) + raise ValueError("The number of qubits in the ansatz circuit {} is larger than \ +the input n_qubits {}! Please check input parameters such as occ_orb, etc.".format(n_qubits_circuit, n_qubits)) self._circuit = ansatz_circuit diff --git a/tests/st/test_ansatz/__init__.py b/mindquantum/algorithm/nisq/qaoa/__init__.py similarity index 75% rename from tests/st/test_ansatz/__init__.py rename to mindquantum/algorithm/nisq/qaoa/__init__.py index 6228b7132697d24157a4052193061e9913f031c4..fc510396da5c094e6f9043b503f68b20b4f6374d 100644 --- a/tests/st/test_ansatz/__init__.py +++ b/mindquantum/algorithm/nisq/qaoa/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,3 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================ +"""Algorithm for quantum approximation optimization algorithm""" +from .max_2_sat_ansatz import Max2SATAnsatz +from .max_cut_ansatz import MaxCutAnsatz + +__all__ = ['Max2SATAnsatz', 'MaxCutAnsatz'] diff --git a/mindquantum/ansatz/max_2_sat.py b/mindquantum/algorithm/nisq/qaoa/max_2_sat_ansatz.py similarity index 63% rename from mindquantum/ansatz/max_2_sat.py rename to mindquantum/algorithm/nisq/qaoa/max_2_sat_ansatz.py index a4b7a477f89da5896160ab55654e7f660aaa2386..8f8a39eab9c55736975813f3c520137067bbec8c 100644 --- a/mindquantum/ansatz/max_2_sat.py +++ b/mindquantum/algorithm/nisq/qaoa/max_2_sat_ansatz.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,10 +16,11 @@ """Max-2-SAT ansatz.""" from math import copysign as sign -from mindquantum.ansatz import Ansatz -from mindquantum.circuit import Circuit, CPN, UN, TimeEvolution -from mindquantum.gate import H, RX -from mindquantum.ops import QubitOperator +from mindquantum.core.circuit import Circuit, UN +from mindquantum.core.circuit.utils import CPN +from mindquantum.core.gates import H, RX +from mindquantum.core.operators import TimeEvolution, QubitOperator +from .._ansatz import Ansatz def _get_clause_act_qubits_num(clauses): @@ -45,9 +47,12 @@ def _check_clause(clauses): if not isinstance(clause, tuple): raise TypeError(f"clause requires a tuple, but get {type(clause)}") if len(clause) != 2: - raise ValueError(f"each clause must contain two integers, but get {len(clause)}") + raise ValueError( + f"each clause must contain two integers, but get {len(clause)}" + ) if 0 in clause: - raise ValueError("clause must contain non-zero integers, but get 0") + raise ValueError( + "clause must contain non-zero integers, but get 0") class Max2SATAnsatz(Ansatz): @@ -77,43 +82,17 @@ class Max2SATAnsatz(Ansatz): depth (int): The depth of Max-2-SAT ansatz. Default: 1. Examples: - >>> from mindquantum.ansatz import Max2SATAnsatz - >>> clauses = [(1, 2), (2, -3)] - >>> max2sat = Max2SATAnsatz(clauses, 2) + >>> from mindquantum.algorithm.nisq.qaoa import Max2SATAnsatz + >>> clauses = [(2, -3)] + >>> max2sat = Max2SATAnsatz(clauses, 1) >>> max2sat.circuit - H(0) - H(1) - H(2) - RZ(0.25*beta_0|0) - RZ(0.5*beta_0|1) - X(1 <-: 0) - RZ(0.5*beta_0|1) - X(1 <-: 0) - RZ(-0.25*beta_0|2) - X(2 <-: 1) - RZ(-0.5*beta_0|2) - X(2 <-: 1) - RX(alpha_0|0) - RX(alpha_0|1) - RX(alpha_0|2) - RZ(0.25*beta_1|0) - RZ(0.5*beta_1|1) - X(1 <-: 0) - RZ(0.5*beta_1|1) - X(1 <-: 0) - RZ(-0.25*beta_1|2) - X(2 <-: 1) - RZ(-0.5*beta_1|2) - X(2 <-: 1) - RX(alpha_1|0) - RX(alpha_1|1) - RX(alpha_1|2) + q1: ──H─────RZ(0.5*beta_0)────●───────────────────────●────RX(alpha_0)── + │ │ + q2: ──H────RZ(-0.5*beta_0)────X────RZ(-0.5*beta_0)────X────RX(alpha_0)── >>> max2sat.hamiltonian - 0.5 [] + - 0.25 [Z0] + - 0.25 [Z0 Z1] + - 0.5 [Z1] + + 0.25 [] + + 0.25 [Z1] + -0.25 [Z1 Z2] + -0.25 [Z2] """ @@ -123,9 +102,9 @@ class Max2SATAnsatz(Ansatz): if depth <= 0: raise ValueError(f"depth must be greater than 0, but get {depth}.") _check_clause(clauses) - super(Max2SATAnsatz, self).__init__('Max2SAT', - _get_clause_act_qubits_num(clauses), - clauses, depth) + super(Max2SATAnsatz, + self).__init__('Max2SAT', _get_clause_act_qubits_num(clauses), + clauses, depth) self.clauses = clauses self.depth = depth @@ -133,10 +112,11 @@ class Max2SATAnsatz(Ansatz): """Build hc circuit.""" ham = QubitOperator() for clause in clauses: - ham += (sign(1, clause[0]) * QubitOperator(f'Z{abs(clause[0]) - 1}', 'beta') + - sign(1, clause[1]) * QubitOperator(f'Z{abs(clause[1]) - 1}', 'beta') + - sign(1, clause[0]) * sign(1, clause[1]) * - QubitOperator(f'Z{abs(clause[0]) - 1} Z{abs(clause[1]) - 1}', 'beta') + ham += (sign(1, clause[0]) * QubitOperator( + f'Z{abs(clause[0]) - 1}', 'beta') + sign(1, clause[1]) * + QubitOperator(f'Z{abs(clause[1]) - 1}', 'beta') + + sign(1, clause[0]) * sign(1, clause[1]) * QubitOperator( + f'Z{abs(clause[0]) - 1} Z{abs(clause[1]) - 1}', 'beta') ) / 4 return TimeEvolution(ham).circuit @@ -156,18 +136,20 @@ class Max2SATAnsatz(Ansatz): """ qo = QubitOperator() for clause in self.clauses: - qo += (QubitOperator('') + - sign(1, clause[0]) * QubitOperator(f'Z{abs(clause[0]) - 1}') + - sign(1, clause[1]) * QubitOperator(f'Z{abs(clause[1]) - 1}') + - sign(1, clause[0]) * sign(1, clause[1]) * - QubitOperator(f'Z{abs(clause[0]) - 1} Z{abs(clause[1]) - 1}') - ) / 4 + qo += ( + QubitOperator('') + + sign(1, clause[0]) * QubitOperator(f'Z{abs(clause[0]) - 1}') + + sign(1, clause[1]) * QubitOperator(f'Z{abs(clause[1]) - 1}') + + sign(1, clause[0]) * sign(1, clause[1]) * + QubitOperator(f'Z{abs(clause[0]) - 1} Z{abs(clause[1]) - 1}') + ) / 4 return qo def _implement(self, clauses, depth): """Implement of max 2 sat ansatz.""" self._circuit = UN(H, _get_clause_act_qubits(clauses)) for d in range(depth): - self._circuit += CPN(self._build_hc(clauses), {'beta': f'beta_{d}'}) + self._circuit += CPN(self._build_hc(clauses), + {'beta': f'beta_{d}'}) self._circuit += CPN(self._build_hb(clauses), {'alpha': f'alpha_{d}'}) diff --git a/mindquantum/ansatz/max_cut.py b/mindquantum/algorithm/nisq/qaoa/max_cut_ansatz.py similarity index 82% rename from mindquantum/ansatz/max_cut.py rename to mindquantum/algorithm/nisq/qaoa/max_cut_ansatz.py index b4c32ef11e0c64a71af629f7cdeffd1423e3d0c1..37606ac918998f76b3aa3b75cee7542780a40870 100644 --- a/mindquantum/ansatz/max_cut.py +++ b/mindquantum/algorithm/nisq/qaoa/max_cut_ansatz.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,10 +15,11 @@ # ============================================================================ """MaxCut ansatz.""" -from mindquantum.gate import H, RX, ZZ -from mindquantum.circuit import Circuit, CPN, UN -from mindquantum.ops import QubitOperator -from ._ansatz import Ansatz +from mindquantum.core.gates import H, RX, ZZ +from mindquantum.core.circuit import Circuit, UN +from mindquantum.core.operators import QubitOperator +from mindquantum.core.circuit.utils import CPN +from .._ansatz import Ansatz def _get_graph_act_qubits_num(graph): @@ -74,25 +76,15 @@ class MaxCutAnsatz(Ansatz): depth (int): The depth of max cut ansatz. Default: 1. Examples: - >>> from mindquantum.ansatz import MaxCutAnsatz + >>> from mindquantum.algorithm.nisq.qaoa import MaxCutAnsatz >>> graph = [(0, 1), (1, 2), (0, 2)] - >>> maxcut = MaxCutAnsatz(graph, 2) + >>> maxcut = MaxCutAnsatz(graph, 1) >>> maxcut.circuit - H(0) - H(1) - H(2) - ZZ(beta_0|0 1) - ZZ(beta_0|1 2) - ZZ(beta_0|0 2) - RX(alpha_0|0) - RX(alpha_0|1) - RX(alpha_0|2) - ZZ(beta_1|0 1) - ZZ(beta_1|1 2) - ZZ(beta_1|0 2) - RX(alpha_1|0) - RX(alpha_1|1) - RX(alpha_1|2) + q0: ──H────ZZ(beta_0)──────────────────ZZ(beta_0)────RX(alpha_0)── + │ │ + q1: ──H────ZZ(beta_0)────ZZ(beta_0)────────┼─────────RX(alpha_0)── + │ │ + q2: ──H──────────────────ZZ(beta_0)────ZZ(beta_0)────RX(alpha_0)── >>> maxcut.hamiltonian 1.5 [] + diff --git a/mindquantum/circuit/state_evolution.py b/mindquantum/circuit/state_evolution.py deleted file mode 100644 index 158cee073dc92364e463cb08b53c8d7b3ff1cde7..0000000000000000000000000000000000000000 --- a/mindquantum/circuit/state_evolution.py +++ /dev/null @@ -1,130 +0,0 @@ -# Copyright 2021 Huawei Technologies Co., Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ============================================================================ -"""Evaluate a quantum circuit.""" - -from collections import Counter -import numpy as np -import matplotlib.pyplot as plt - -from mindspore import Tensor -from mindquantum.parameterresolver import ParameterResolver as PR -from mindquantum.nn import generate_evolution_operator -from mindquantum.utils import normalize -from mindquantum.utils import ket_string -from mindquantum.circuit import Circuit - - -def _generate_n_qubits_index(n_qubits): - out = [] - for i in range(1 << n_qubits): - out.append(bin(i)[2:].zfill(n_qubits)) - return out - - -class StateEvolution: - """ - Calculate the final state of a parameterized or non parameterized quantum circuit. - - Args: - circuit (Circuit): The circuit that you want to do evolution. - - Examples: - >>> from mindquantum.circuit import StateEvolution - >>> from mindquantum.circuit import qft - >>> print(StateEvolution(qft([0, 1])).final_state(ket=True)) - 0.5¦00⟩ - 0.5¦01⟩ - 0.5¦10⟩ - 0.5¦11⟩ - """ - def __init__(self, circuit): - if not isinstance(circuit, Circuit): - raise TypeError( - f'Input circuit should be a quantum circuit, but get {type(circuit)}' - ) - self.circuit = circuit - self.evol = generate_evolution_operator(self.circuit) - self.index = _generate_n_qubits_index(self.circuit.n_qubits) - - def final_state(self, param=None, ket=False): - """ - Get the final state of the input quantum circuit. - - Args: - param (Union[Tensor, numpy.ndarray, ParameterResolver, dict]): The - parameter for the parameterized quantum circuit. If None, the - quantum circuit should be a non parameterized quantum circuit. - Default: None. - ket (bool): Whether to print the final state in ket format. Default: False. - - Returns: - numpy.ndarray, the final state in numpy array format. - """ - if param is None: - if self.circuit.para_name: - raise ValueError( - "Require a non parameterized quantum circuit, since not parameters specified." - ) - return self.evol() if not ket else '\n'.join( - ket_string(self.evol())) - if isinstance(param, np.ndarray): - return self.evol(Tensor(param)) if not ket else '\n'.join( - ket_string(self.evol(Tensor(param)))) - if isinstance(param, Tensor): - return self.evol(param) if not ket else '\n'.join( - ket_string(self.evol(param))) - if isinstance(param, (PR, dict)): - data = [param[i] for i in self.circuit.para_name] - data = Tensor(np.array(data).astype(np.float32)) - return self.evol(data) if not ket else '\n'.join( - ket_string(self.evol(data))) - raise TypeError( - f"parameter requires a numpy array or a ParameterResolver or a dict, ut get {type(param)}" - ) - - def sampling(self, shots=1, param=None, show=False): - """ - Sampling the bit string based on the final state. - - Args: - shots (int): How many samples you want to get. Default: 1. - param (Union[Tensor, numpy.ndarray, ParameterResolver, dict]): The - parameter for the parameterized quantum circuit. If None, the - quantum circuit should be a non parameterized quantum circuit. - Default: None. - show (bool): Whether to show the sampling result in bar plot. Default: False. - - Returns: - dict, a dict with key as bit string and value as number of samples. - - Examples: - >>> from mindquantum.circuit import StateEvolution - >>> from mindquantum.circuit import qft - >>> import numpy as np - >>> np.random.seed(42) - >>> StateEvolution(qft([0, 1])).sampling(100) - {'00': 29, '01': 24, '10': 23, '11': 24} - """ - final_state = self.final_state(param) - amps = normalize(np.abs(final_state)**2)**2 - sampling = Counter(np.random.choice(self.index, p=amps, size=shots)) - result = dict(zip(self.index, [0] * len(self.index))) - result.update(sampling) - if show: - plt.bar(result.keys(), result.values()) - if self.circuit.n_qubits > 2: - plt.xticks(rotation=45) - plt.show() - return result diff --git a/mindquantum/ops/__init__.py b/mindquantum/core/__init__.py similarity index 54% rename from mindquantum/ops/__init__.py rename to mindquantum/core/__init__.py index 6bc9fa34d6db09601a4285bd5a8f18e85574bdb0..7934cb02b633e5b23262fb2e57ecb3735ae42e77 100644 --- a/mindquantum/ops/__init__.py +++ b/mindquantum/core/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,17 +13,26 @@ # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================ -"""Fermion operator and qubit operator.""" +"""MindQuantum core features (eDSL)""" -from mindquantum.third_party.interaction_operator import InteractionOperator -from .fermion_operator import FermionOperator -from .qubit_operator import QubitOperator -from .polynomial_tensor import PolynomialTensor -from .qubit_excitation_operator import QubitExcitationOperator +from . import circuit +from . import gates +from . import operators +from . import parameterresolver +from . import third_party +from .circuit import * +from .gates import * +from .operators import * +from .parameterresolver import * +from .third_party import * -__all__ = [ - 'FermionOperator', 'QubitOperator', 'PolynomialTensor', - 'InteractionOperator', 'QubitExcitationOperator' -] +# Provide alias for convenience +from . import operators as ops +__all__ = [] +__all__.extend(circuit.__all__) +__all__.extend(gates.__all__) +__all__.extend(operators.__all__) +__all__.extend(parameterresolver.__all__) +__all__.extend(third_party.__all__) __all__.sort() diff --git a/mindquantum/core/circuit/__init__.py b/mindquantum/core/circuit/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c057604c99e45897ccbfdeecb2b0e0485f189409 --- /dev/null +++ b/mindquantum/core/circuit/__init__.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +""" +Circuit. + +Quantum circuit module. +""" + +from .circuit import Circuit +from .module_circuit import UN, SwapParts, U3 +from .utils import decompose_single_term_time_evolution +from .utils import pauli_word_to_circuits +from .utils import controlled +from .utils import dagger +from .utils import apply +from .utils import add_prefix +from .utils import change_param_name +from .utils import C +from .utils import A +from .utils import D +from .utils import AP +from .utils import CPN + +__all__ = [ + 'Circuit', 'U3', 'UN', 'SwapParts', 'C', 'A', 'D', 'AP', 'CPN', + 'decompose_single_term_time_evolution', 'pauli_word_to_circuits', + 'controlled', 'dagger', 'apply', 'add_prefix', 'change_param_name' +] +__all__.sort() diff --git a/mindquantum/circuit/circuit.py b/mindquantum/core/circuit/circuit.py similarity index 46% rename from mindquantum/circuit/circuit.py rename to mindquantum/core/circuit/circuit.py index fcbfc346900381204eebb0733a619b8f0379dcdb..6d1b5321f2c6514e5d6579b3257d212e3892a6a4 100644 --- a/mindquantum/circuit/circuit.py +++ b/mindquantum/core/circuit/circuit.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,21 +19,16 @@ from collections.abc import Iterable from typing import List import copy import numpy as np -from projectq.ops import QubitOperator as pq_operator -from openfermion.ops import QubitOperator as of_operator -from mindquantum.ops import QubitOperator as hiq_operator -from mindquantum.gate import BasicGate -from mindquantum.gate import I -from mindquantum.gate import X -from mindquantum.gate import Y -from mindquantum.gate import Z -from mindquantum.gate import Hamiltonian -import mindquantum.gate as G -from mindquantum.gate.basic import _check_gate_type -from mindquantum.utils import bprint -from mindquantum.parameterresolver import ParameterResolver as PR - -GateSeq = List[BasicGate] +from rich.console import Console +import mindquantum.core.gates as G +from mindquantum.core.gates.basic import _check_gate_type +from mindquantum.core.parameterresolver import ParameterResolver as PR +from mindquantum.io import bprint +from mindquantum.io.display import brick_model +from mindquantum.utils.type_value_check import _check_input_type +from .utils import apply + +GateSeq = List[G.BasicGate] def _two_dim_array_to_list(data): @@ -40,9 +36,7 @@ def _two_dim_array_to_list(data): Convert a two dimension array to a list of string. """ if len(data.shape) != 2: - raise ValueError( - "data need two dimensions, but get {} dimensions".format( - len(data.shape))) + raise ValueError("data need two dimensions, but get {} dimensions".format(len(data.shape))) out_real = [] out_imag = [] for i in data: @@ -75,6 +69,15 @@ class CollectionMap: else: self.map[k] += 1 + def collect_only_one(self, keys, raise_msg): + """collect item only single time, otherwise raise error""" + if not isinstance(keys, list): + keys = [keys] + for k in keys: + if k in self.map: + raise ValueError(raise_msg) + self.map[k] = 1 + def delete(self, keys): """delete items""" if not isinstance(keys, list): @@ -112,6 +115,13 @@ class CollectionMap: else: self.map[k] = v + def merge_only_one(self, other, raise_msg): + """merge with other collection container""" + for k, _ in other.map.items(): + if k in self.map: + raise ValueError(raise_msg) + self.map[k] = 1 + def unmerge(self, other): """delete with other collection container""" for k, v in other.map.items(): @@ -149,12 +159,12 @@ class Circuit(list): Examples: + >>> from mindquantum import Circuit, RX, X >>> circuit1 = Circuit() >>> circuit1 += RX('a').on(0) >>> circuit1 *= 2 - >>> print(circuit1) - RX(a|0) - RX(a|0) + >>> circuit1 + q0: ──RX(a)────RX(a)── >>> circuit2 = Circuit([X.on(0,1)]) >>> circuit3= circuit1 + circuit2 >>> assert len(circuit3) == 3 @@ -165,40 +175,61 @@ class Circuit(list): |with 1 parameters are : a.| |Number qubit of circuit: 2 | ============================= + >>> circuit3 + q0: ──RX(a)────RX(a)────X── + │ + q1: ────────────────────●── """ def __init__(self, gates=None): list.__init__([]) self.all_qubits = CollectionMap() self.all_paras = CollectionMap() + self.all_measures = CollectionMap() if gates is not None: if isinstance(gates, Iterable): self.extend(gates) else: self.append(gates) + self.has_cpp_obj = False def append(self, gate): - """Append a gate.""" + """ + Append a gate. + + Args: + gate (BasicGate): The gate you want to append. + """ _check_gate_type(gate) - super().append(gate) + if isinstance(gate, G.Measure): + self.all_measures.collect_only_one(gate, f'measure key {gate.key} already exist.') self.all_qubits.collect(gate.obj_qubits) self.all_qubits.collect(gate.ctrl_qubits) - if gate.isparameter: + if gate.parameterized: self.all_paras.collect(list(gate.coeff.keys())) + super().append(gate) + self.has_cpp_obj = False def extend(self, gates): - """Extend a circuit.""" + """ + Extend a circuit. + + Args: + gates (Union[Circuit, list[BasicGate]]), A `Circuit` or a list of `BasicGate` you want to extend. + """ if isinstance(gates, Circuit): - super().extend(gates) + self.all_measures.merge_only_one(gates.all_measures, "Measure already exist.") self.all_qubits.merge(gates.all_qubits) self.all_paras.merge(gates.all_paras) + super().extend(gates) else: for gate in gates: self.append(gate) + self.has_cpp_obj = False def __add__(self, gates): out = Circuit() out.extend(self) - if isinstance(gates, BasicGate): + if isinstance(gates, G.BasicGate): out.append(gates) else: out.extend(gates) @@ -208,20 +239,17 @@ class Circuit(list): return Circuit(gates) + self def __iadd__(self, gates): - if isinstance(gates, BasicGate): + if isinstance(gates, G.BasicGate): self.append(gates) elif isinstance(gates, Circuit): self.extend(gates) else: - raise TypeError( - "Require a quantum gate or a quantum circuit, but get {}.". - format(type(gates))) + raise TypeError("Require a quantum gate or a quantum circuit, but get {}.".format(type(gates))) return self def __mul__(self, num): if not isinstance(num, int): - raise TypeError( - f'{type(num)} object cannot be interpreted as an integer') + raise TypeError(f'{type(num)} object cannot be interpreted as an integer') out = Circuit() for _ in range(num): out += copy.deepcopy(self) @@ -247,19 +275,32 @@ class Circuit(list): old_v = self[k] self.all_qubits.delete(old_v.obj_qubits) self.all_qubits.delete(old_v.ctrl_qubits) - if old_v.isparameter: + if old_v.parameterized: self.all_paras.delete(list(old_v.coeff.keys())) + if isinstance(old_v, G.Measure): + self.all_measures.delete(old_v) super().__setitem__(k, v) self.all_qubits.collect(v.obj_qubits) self.all_qubits.collect(v.ctrl_qubits) - if v.isparameter: + if v.parameterized: self.all_paras.collect(list(v.coeff.keys())) + if isinstance(v, G.Measure): + self.all_measures.collect_only_one(v, f'measure key {v.key} already exist.') + self.has_cpp_obj = False def __getitem__(self, sliced): if isinstance(sliced, int): return super().__getitem__(sliced) return Circuit(super().__getitem__(sliced)) + @property + def has_measure_gate(self): + return self.all_measures.size != 0 + + @property + def parameterized(self): + return self.all_paras.size != 0 + def insert(self, index, gates): """ Insert a quantum gate or quantum circuit in index. @@ -268,23 +309,27 @@ class Circuit(list): index (int): Index to set gate. gates (Union[BasicGate, list[BasicGate]]): Gates you need to insert. """ - if isinstance(gates, BasicGate): + if isinstance(gates, G.BasicGate): super().insert(index, gates) self.all_qubits.collect(gates.obj_qubits) self.all_qubits.collect(gates.ctrl_qubits) - if gates.isparameter: + if gates.parameterized: self.all_paras.collect(list(gates.coeff.keys())) + if isinstance(gates, G.Measure): + self.all_measures.collect_only_one(gates, f'measure key {gates.key} already exist.') elif isinstance(gates, Iterable): for gate in gates[::-1]: _check_gate_type(gate) self.insert(index, gate) self.all_qubits.collect(gate.obj_qubits) self.all_qubits.collect(gate.ctrl_qubits) - if gate.isparameter: + if gate.parameterized: self.all_paras.collect(list(gate.coeff.keys())) + if isinstance(gate, G.Measure): + self.all_measures.collect_only_one(gate, f'measure key {gate.key} already exist.') else: - raise TypeError("Unsupported type for quantum gate: {}".format( - type(gates))) + raise TypeError("Unsupported type for quantum gate: {}".format(type(gates))) + self.has_cpp_obj = False def no_grad(self): """ @@ -292,6 +337,7 @@ class Circuit(list): """ for gate in self: gate.no_grad() + self.has_cpp_obj = False return self def requires_grad(self): @@ -300,17 +346,66 @@ class Circuit(list): """ for gate in self: gate.requires_grad() + self.has_cpp_obj = False return self + def compress(self): + r""" + Remove all unused qubits, and map qubits to `range(n_qubits)`. + + Examples: + >>> from mindquantum import qft + >>> qft([0, 2, 4]) + q0: ──H────PS(π/2)────PS(π/4)─────────────────────────@── + │ │ │ + q2: ──────────●──────────┼───────H────PS(π/2)─────────┼── + │ │ │ + q4: ─────────────────────●───────────────●───────H────@── + >>> qft([0, 2, 4]).compress() + q0: ──H────PS(π/2)────PS(π/4)─────────────────────────@── + │ │ │ + q1: ──────────●──────────┼───────H────PS(π/2)─────────┼── + │ │ │ + q2: ─────────────────────●───────────────●───────H────@── + """ + circ = apply(self, list(range(len(self.all_qubits)))) + return circ + def __str__(self): - return '\n'.join(repr(i) for i in self) + return self.__repr__() def __repr__(self): - return self.__str__() + from mindquantum.io.display._config import _CIRCUIT_STYLE + circ = self.compress() + s = brick_model(circ, sorted(self.all_qubits.map)) + s = '\n'.join(s) + console = Console(record=True) + if not console.is_jupyter: + console.width = len(s) + with console.capture() as capture: + console.print(s, style=_CIRCUIT_STYLE['style'], width=len(s)) + s = capture.get() + return s + + def _repr_html_(self): + """repr for jupyter nontebook""" + from mindquantum.io.display._config import CIRCUIT_HTML_FORMAT + from mindquantum.io.display._config import _CIRCUIT_STYLE + console = Console(record=True) + circ = self.compress() + s = brick_model(circ, sorted(self.all_qubits.map)) + s = '\n'.join(s) + console.width = len(s) + with console.capture() as _: + console.print(s, style=_CIRCUIT_STYLE['style'], width=len(s)) + s = console.export_html(code_format=CIRCUIT_HTML_FORMAT, inline_styles=True) + return '\n'.join(s.split('\n')[1:]) @property def n_qubits(self): - return max(self.all_qubits.keys()) + 1 + if self.all_qubits: + return max(self.all_qubits.keys()) + 1 + return 0 def summary(self, show=True): """ @@ -336,32 +431,28 @@ class Circuit(list): self.num_non_para_gate = 0 self.num_para_gate = 0 for gate in self: - if gate.isparameter: + if gate.parameterized: self.num_para_gate += 1 else: self.num_non_para_gate += 1 if show: info = bprint([ - 'Total number of gates: {}.'.format(self.num_para_gate + - self.num_non_para_gate), - 'Parameter gates: {}.'.format(self.num_para_gate), - 'with {} parameters are: {}{}'.format( + 'Total number of gates: {}.'.format(self.num_para_gate + self.num_non_para_gate), + 'Parameter gates: {}.'.format(self.num_para_gate), 'with {} parameters are: {}{}'.format( len(self.all_paras), ', '.join(self.all_paras.keys()[:10]), - ('.' if len(self.all_paras) <= 10 else '...')), - 'Number qubit of circuit: {}'.format(self.n_qubits) + ('.' if len(self.all_paras) <= 10 else '...')), 'Number qubit of circuit: {}'.format(self.n_qubits) ], title='Circuit Summary') for i in info: print(i) - @property def hermitian(self): """ Get the hermitian of this quantum circuit. Examples: >>> circ = Circuit(RX({'a': 0.2}).on(0)) - >>> herm_circ = circ.hermitian + >>> herm_circ = circ.hermitian() >>> herm_circ[0].coeff {'a': -0.2} """ @@ -386,7 +477,7 @@ class Circuit(list): return pr @property - def para_name(self): + def params_name(self): """ Get the parameter name of this circuit. @@ -394,10 +485,10 @@ class Circuit(list): list, a list that contains the parameter name. Examples: - >>> from mindquantum.gate import RX - >>> from mindquantum.circuit import Circuit + >>> from mindquantum.core.gates import RX + >>> from mindquantum.core.circuit import Circuit >>> circuit = Circuit(RX({'a': 1, 'b': 2}).on(0)) - >>> circuit.para_name + >>> circuit.params_name ['a', 'b'] """ return list(self.all_paras.keys()) @@ -413,159 +504,305 @@ class Circuit(list): Circuit, a non parameterized circuit. Examples: - >>> from mindquantum.gate import X, RX - >>> from mindquantum.circuit import Circuit + >>> from mindquantum.core.gates import X, RX + >>> from mindquantum.core.circuit import Circuit >>> circuit = Circuit() >>> circuit += X.on(0) >>> circuit += RX({'a': 2}).on(0) >>> circuit = circuit.apply_value({'a': 1.5}) >>> circuit - X(0) - RX(3.0|0) + q0: ──X────RX(3)── """ circuit = Circuit() for gate in self: - if not gate.isparameter: + if not gate.parameterized: circuit += gate else: - if set(gate.coeff.para_name).issubset(pr): + if set(gate.coeff.params_name).issubset(pr): coeff = gate.coeff.combination(pr) else: coeff = 1 * gate.coeff - circuit += gate.__class__(coeff).on(gate.obj_qubits, - gate.ctrl_qubits) + circuit += gate.__class__(coeff).on(gate.obj_qubits, gate.ctrl_qubits) return circuit - def mindspore_data(self): + def remove_barrier(self): + """Remove all barrier gates""" + circ = Circuit() + for g in self: + if not isinstance(g, G.BarrierGate): + circ += g + return circ + + def remove_measure(self): + """Remove all measure gate.""" + circ = Circuit() + for g in self: + if not isinstance(g, G.Measure): + circ += g + return circ + + def remove_measure_on_qubits(self, qubits): """ - Serialize the circuit. The result can be used by QNN operators. + Remove all measure gate on some certain qubits. + + Args: + qubit (Union[int, list[int]]): The qubits you want to remove measure. + + Examples: + >>> from mindquantum import UN, H, Measure + >>> circ = UN(H, 3).x(0, 1).x(1, 2).measure_all() + >>> circ += H.on(0) + >>> circ += Measure('q0_1').on(0) + >>> circ.remove_measure_on_qubits(0) + q0: ──H────X────H─────────── + │ + q1: ──H────●────X────M(q1)── + │ + q2: ──H─────────●────M(q2)── """ - m_data = { - 'gate_names': [], - 'gate_matrix': [], - 'gate_obj_qubits': [], - 'gate_ctrl_qubits': [], - 'gate_params_names': [], - 'gate_coeff': [], - 'gate_requires_grad': [] - } + if not isinstance(qubits, list): + qubits = [qubits] + circ = Circuit() for gate in self: - if gate.isparameter: - m_data['gate_names'].append(gate.name) - m_data['gate_matrix'].append([[["0.0", "0.0"], ["0.0", "0.0"]], - [["0.0", "0.0"], ["0.0", - "0.0"]]]) - pr_data = gate.coeff.mindspore_data() - else: - m_data['gate_names'].append("npg") - m_data['gate_matrix'].append( - _two_dim_array_to_list(gate.matrix())) - pr_data = PR().mindspore_data() - m_data['gate_params_names'].append(pr_data['gate_params_names']) - m_data['gate_coeff'].append(pr_data['gate_coeff']) - m_data['gate_requires_grad'].append(pr_data['gate_requires_grad']) - m_data['gate_obj_qubits'].append(gate.obj_qubits) - m_data['gate_ctrl_qubits'].append(gate.ctrl_qubits) - return m_data + if isinstance(gate, G.Measure) and gate.obj_qubits[0] in qubits: + continue + circ += gate + return circ + + def get_cpp_obj(self, hermitian=False): + """ + Get cpp obj of circuit. + + Args: + hermitian (bool): Whether to get cpp object of this circuit in hermitian version. Default: False. + """ + if not self.has_cpp_obj: + self.has_cpp_obj = True + self.cpp_obj = [i.get_cpp_obj() for i in self if not isinstance(i, G.BarrierGate)] + self.herm_cpp_obj = [i.get_cpp_obj() for i in self.hermitian() if not isinstance(i, G.BarrierGate)] + + if hasattr(self, 'cpp_obj') and hasattr(self, 'herm_cpp_obj'): + if hermitian: + return self.herm_cpp_obj + return self.cpp_obj + raise ValueError("Circuit does not generate cpp obj yet.") def h(self, obj_qubits, ctrl_qubits=None): - """Add a hadamard gate.""" + """ + Add a hadamard gate. + + Args: + obj_qubits (Union[int, list[int]]): The object qubits of `H` gate. + ctrl_qubits (Union[int, list[int]]): the control qubits of `H` gate. Default: None. + """ self.append(G.H.on(obj_qubits, ctrl_qubits)) return self def x(self, obj_qubits, ctrl_qubits=None): - """Add a X gate.""" + """ + Add a X gate. + + Args: + obj_qubits (Union[int, list[int]]): The object qubits of `X` gate. + ctrl_qubits (Union[int, list[int]]): the control qubits of `X` gate. Default: None. + """ self.append(G.X.on(obj_qubits, ctrl_qubits)) return self def y(self, obj_qubits, ctrl_qubits=None): - """Add a Y gate.""" + """ + Add a Y gate. + + Args: + obj_qubits (Union[int, list[int]]): The object qubits of `Y` gate. + ctrl_qubits (Union[int, list[int]]): the control qubits of `Y` gate. Default: None. + """ self.append(G.Y.on(obj_qubits, ctrl_qubits)) return self def z(self, obj_qubits, ctrl_qubits=None): - """Add a Z gate.""" + """ + Add a Z gate. + + Args: + obj_qubits (Union[int, list[int]]): The object qubits of `Z` gate. + ctrl_qubits (Union[int, list[int]]): the control qubits of `Z` gate. Default: None. + """ self.append(G.Z.on(obj_qubits, ctrl_qubits)) return self def s(self, obj_qubits, ctrl_qubits=None): - """Add a S gate.""" + """ + Add a S gate. + + Args: + obj_qubits (Union[int, list[int]]): The object qubits of `S` gate. + ctrl_qubits (Union[int, list[int]]): the control qubits of `S` gate. Default: None. + """ self.append(G.S.on(obj_qubits, ctrl_qubits)) return self def swap(self, obj_qubits, ctrl_qubits=None): - """Add a SWAP gate.""" + """ + Add a SWAP gate. + + Args: + obj_qubits (Union[int, list[int]]): The object qubits of `SWAP` gate. + ctrl_qubits (Union[int, list[int]]): the control qubits of `SWAP` gate. Default: None. + """ self.append(G.SWAP.on(obj_qubits, ctrl_qubits)) return self def rx(self, para, obj_qubits, ctrl_qubits=None): - """Add a RX gate.""" + """ + Add a RX gate. + + Args: + para (Union[dict, ParameterResolver]): The parameter for `RX` gate. + obj_qubits (Union[int, list[int]]): The object qubits of `RX` gate. + ctrl_qubits (Union[int, list[int]]): the control qubits of `RX` gate. Default: None. + """ self.append(G.RX(para).on(obj_qubits, ctrl_qubits)) return self def ry(self, para, obj_qubits, ctrl_qubits=None): - """Add a RY gate.""" + """ + Add a RY gate. + + Args: + para (Union[dict, ParameterResolver]): The parameter for `RY` gate. + obj_qubits (Union[int, list[int]]): The object qubits of `RY` gate. + ctrl_qubits (Union[int, list[int]]): the control qubits of `RY` gate. Default: None. + """ self.append(G.RY(para).on(obj_qubits, ctrl_qubits)) return self def rz(self, para, obj_qubits, ctrl_qubits=None): - """Add a RZ gate.""" + """ + Add a RZ gate. + + Args: + para (Union[dict, ParameterResolver]): The parameter for `RZ` gate. + obj_qubits (Union[int, list[int]]): The object qubits of `RZ` gate. + ctrl_qubits (Union[int, list[int]]): the control qubits of `RZ` gate. Default: None. + """ self.append(G.RZ(para).on(obj_qubits, ctrl_qubits)) return self def phase_shift(self, para, obj_qubits, ctrl_qubits=None): - """Add a Phase Shift gate.""" + """ + Add a Phase Shift gate. + + Args: + para (Union[dict, ParameterResolver]): The parameter for `PhaseShift` gate. + obj_qubits (Union[int, list[int]]): The object qubits of `PhaseShift` gate. + ctrl_qubits (Union[int, list[int]]): the control qubits of `PhaseShift` gate. Default: None.""" self.append(G.PhaseShift(para).on(obj_qubits, ctrl_qubits)) return self def xx(self, para, obj_qubits, ctrl_qubits=None): - """Add a XX gate.""" + """ + Add a XX gate. + + Args: + para (Union[dict, ParameterResolver]): The parameter for `XX` gate. + obj_qubits (Union[int, list[int]]): The object qubits of `XX` gate. + ctrl_qubits (Union[int, list[int]]): the control qubits of `XX` gate. Default: None. + """ self.append(G.XX(para).on(obj_qubits, ctrl_qubits)) return self def yy(self, para, obj_qubits, ctrl_qubits=None): - """Add a YY gate.""" + """ + Add a YY gate. + + Args: + para (Union[dict, ParameterResolver]): The parameter for `YY` gate. + obj_qubits (Union[int, list[int]]): The object qubits of `YY` gate. + ctrl_qubits (Union[int, list[int]]): the control qubits of `YY` gate. Default: None. + """ self.append(G.YY(para).on(obj_qubits, ctrl_qubits)) return self def zz(self, para, obj_qubits, ctrl_qubits=None): - """Add a ZZ gate.""" + """ + Add a ZZ gate. + + Args: + para (Union[dict, ParameterResolver]): The parameter for `ZZ` gate. + obj_qubits (Union[int, list[int]]): The object qubits of `ZZ` gate. + ctrl_qubits (Union[int, list[int]]): the control qubits of `ZZ` gate. Default: None. + """ self.append(G.ZZ(para).on(obj_qubits, ctrl_qubits)) return self + def measure(self, key, obj_qubit=None): + """ + Add a measure gate. -def pauli_word_to_circuits(qubitops): - """ - Convert a single pauli word qubit operator to a quantum circuit. + Args: + key (Union[int, str]): If `obj_qubit` is None, then `key` should be a int and means which qubit to measure, + otherwise, `key` should be a str and means the name of this measure gate. + obj_qubit (int): Which qubit to measure. Default: None. + """ + if obj_qubit is None: + self.append(G.Measure().on(key)) + else: + self.append(G.Measure(key).on(obj_qubit)) + return self - Args: - qubitops (QubitOperator, Hamiltonian): The single pauli word qubit operator. + def measure_all(self, subfix=None): + """ + Measure all qubits + + Args: + subfix (str): The subfix string you want to add to the name of measure gate. + """ + for i in range(self.n_qubits): + s = f"q{i}" if subfix is None else f"q{i}_{subfix}" + self += G.Measure(s).on(i) + return self - Returns: - Circuit, a quantum circuit. + def barrier(self, show=True): + """ + Add a barrier. - Examples: - >>> from mindquantum.ops import QubitOperator - >>> qubitops = QubitOperator('X0 Y1') - >>> pauli_word_to_circuits(qubitops) - X(0) - Y(1) - """ - if not isinstance(qubitops, - (pq_operator, of_operator, hiq_operator, Hamiltonian)): - raise TypeError( - "Require a QubitOperator or a Hamiltonian, but get {}!".format( - type(qubitops))) - if isinstance(qubitops, Hamiltonian): - qubitops = qubitops.hamiltonian - if len(qubitops.terms) > 1: - raise Exception("Onle work for QubitOperator with single pauliword!") - gate_map = {'X': X, 'Y': Y, 'Z': Z} - for ops in qubitops.terms.keys(): - circ = Circuit() - if ops: - for ind, single_op in ops: - circ += gate_map[single_op].on(ind) - else: - circ += I.on(0) - return circ + Args: + show (bool): Whether show barrier or not. Default: True. + """ + _check_input_type('show', bool, show) + self.append(G.BarrierGate(show)) + return self + + def un(self, gate, maps_obj, maps_ctrl=None): + """ + Map a quantum gate to different objective qubits and control qubits. + Please refers to UN. + + Args: + gate (BasicBage): The BasicGate you want to map. + map_obj (Union[int, list[int]]): object qubits. + maps_ctrl (Union[int, list[int]]): control qubits. Default: None. + """ + from mindquantum import UN + self += UN(gate, maps_obj, maps_ctrl) + return self + + def get_qs(self, backend='projectq', pr=None, ket=False, seed=42): + """ + Get the final quantum state of this circuit. + + Args: + backend (str): Which backend you want to use. Default: 'projectq'. + pr (Union[numbers.Number, ParameterResolver, dict, numpy.ndarray]): The parameter of this circuit, + if this circuit is parameterized. Default: None. + ket (str): Whether to return the quantum state in ket format. Default: False. + seed (int): The random seed of simulator. + """ + from mindquantum import Simulator + sim = Simulator(backend, self.n_qubits, seed) + sim.apply_circuit(self, pr) + return sim.get_qs(ket) + + +__all__ = ['Circuit'] diff --git a/mindquantum/circuit/module_circuit.py b/mindquantum/core/circuit/module_circuit.py similarity index 73% rename from mindquantum/circuit/module_circuit.py rename to mindquantum/core/circuit/module_circuit.py index 51e3050802b8aae4baf99e2a94433293bd0c196d..2beb1ff939429e74c9a2e1005cb2ef72756e7b72 100644 --- a/mindquantum/circuit/module_circuit.py +++ b/mindquantum/core/circuit/module_circuit.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,9 +17,9 @@ from collections.abc import Iterable import numpy as np -from mindquantum.gate import BasicGate -from mindquantum.gate import SWAP -from mindquantum.gate.basic import _check_gate_type +from mindquantum.core.gates import BasicGate +from mindquantum.core.gates import SWAP +from mindquantum.core.gates.basic import _check_gate_type from .circuit import Circuit @@ -35,15 +36,26 @@ class UN(Circuit): Circuit, Return a quantum circuit. Examples: - >>> from mindquantum import UN + >>> from mindquantum.core import UN, X >>> circuit1 = UN(X, maps_obj = [0, 1], maps_ctrl = [2, 3]) >>> print(circuit1) - X(0 <-: 2) - X(1 <-: 3) - >>> circuit2 = UN(SWAP, maps_obj =[[0, 1], [2, 3]]) + q0: ──X─────── + │ + q1: ──┼────X── + │ │ + q2: ──●────┼── + │ + q3: ───────●── + >>> from mindquantum.core import SWAP + >>> circuit2 = UN(SWAP, maps_obj =[[0, 1], [2, 3]]).x(2, 1) >>> print(circuit2) - SWAP(0 1) - SWAP(2 3) + q0: ──@─────── + │ + q1: ──@────●── + │ + q2: ──@────X── + │ + q3: ──@─────── """ def __init__(self, gate: BasicGate, maps_obj, maps_ctrl=None): _check_gate_type(gate) @@ -52,10 +64,7 @@ class UN(Circuit): gates = [gate.on(i) for i in maps_obj] else: if isinstance(maps_ctrl, Iterable): - gates = [ - gate.on(maps_obj[i], maps_ctrl[i]) - for i in range(len(maps_obj)) - ] + gates = [gate.on(maps_obj[i], maps_ctrl[i]) for i in range(len(maps_obj))] else: gates = [gate.on(i, maps_ctrl) for i in maps_obj] else: @@ -83,8 +92,15 @@ class SwapParts(Circuit): Examples: >>> from mindquantum import SwapParts >>> SwapParts([1, 2], [3, 4], 0) - SWAP(1 3 <-: 0) - SWAP(2 4 <-: 0) + q0: ──●────●── + │ │ + q1: ──@────┼── + │ │ + q2: ──┼────@── + │ │ + q3: ──@────┼── + │ + q4: ───────@── """ def __init__(self, a: Iterable, b: Iterable, maps_ctrl=None): if not isinstance(a, Iterable) or not isinstance(b, Iterable): @@ -104,13 +120,9 @@ class U3(Circuit): obj_qubit (int): Which qubit the U3 circuit will act on. Default: None. Examples: - >>> from mindquantum import U3 + >>> from mindquantum.core import U3 >>> U3('a','b','c') - RZ(a|0) - RX(-1.571|0) - RZ(b|0) - RX(1.571|0) - RZ(c|0) + q0: ──RZ(a)────RX(-π/2)────RZ(b)────RX(π/2)────RZ(c)── """ def __init__(self, a, b, c, obj_qubit=None): if obj_qubit is None: diff --git a/mindquantum/circuit/high_level_ops.py b/mindquantum/core/circuit/utils.py similarity index 43% rename from mindquantum/circuit/high_level_ops.py rename to mindquantum/core/circuit/utils.py index 3d97ed9dadee2fdc270498c010dfa6b8c74febf7..0ccdb76f6c3a5fede2d441169b05c8328b55b1c3 100644 --- a/mindquantum/circuit/high_level_ops.py +++ b/mindquantum/core/circuit/utils.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,41 +13,170 @@ # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================ -"""High level circuit operators.""" - +"""Tools for MindQuantum eDSL""" from types import FunctionType, MethodType import copy +import numpy as np +from projectq.ops import QubitOperator as pq_operator +from openfermion.ops import QubitOperator as of_operator + +from mindquantum.core.parameterresolver.parameterresolver import ParameterResolver + + +def decompose_single_term_time_evolution(term, para): + """ + Decompose a time evolution gate into basic quantum gates. + + This function only work for the hamiltonian with only single pauli word. + For example, exp(-i * t * ham), ham can only be a single pauli word, such + as ham = X0 x Y1 x Z2, and at this time, term will be + ((0, 'X'), (1, 'Y'), (2, 'Z')). When the evolution time is expressd as + t = a*x + b*y, para would be {'x':a, 'y':b}. + + Args: + term (tuple, QubitOperator): the hamiltonian term of just the + evolution qubit operator. + para (Union[dict, numbers.Number]): the parameters of evolution operator. + + Returns: + Circuit, a quantum circuit. + + Raises: + ValueError: If term has more than one pauli string. + TypeError: If term is not map. + + Example: + >>> from mindquantum.core.operators import QubitOperator + >>> from mindquantum.core.circuit import decompose_single_term_time_evolution + >>> ham = QubitOperator('X0 Y1') + >>> circuit = decompose_single_term_time_evolution(ham, {'a':1}) + >>> print(circuit) + q0: ─────H───────●───────────────●───────H────── + │ │ + q1: ──RX(π/2)────X────RZ(2*a)────X────RX(7π/2)── + """ + from mindquantum import gates as G + from mindquantum.core.circuit import Circuit + from mindquantum.core.parameterresolver import ParameterResolver as PR + from mindquantum.utils.type_value_check import _num_type + if not isinstance(term, tuple): + try: + if len(term.terms) != 1: + raise ValueError("Only work for single term time \ + evolution operator, but get {}".format(len(term))) + term = list(term.terms.keys())[0] + except TypeError: + raise TypeError("Not supported type:{}".format(type(term))) + if not isinstance(para, _num_type): + if not isinstance(para, (dict, ParameterResolver)): + raise TypeError(f'para requiers a number or a dict or a ParameterResolver, but get {type(para)}') + para = ParameterResolver(para) + + out = [] + term = sorted(term) + rxs = [] + if len(term) == 1: # single pauli operator + if term[0][1] == 'X': + out.append(G.RX(para * 2).on(term[0][0])) + elif term[0][1] == 'Y': + out.append(G.RY(para * 2).on(term[0][0])) + else: + out.append(G.RZ(para * 2).on(term[0][0])) + else: + for index, action in term: + if action == 'X': + out.append(G.H.on(index)) + elif action == 'Y': + rxs.append(len(out)) + out.append(G.RX(np.pi / 2).on(index)) + + out.append(G.BarrierGate(False)) + for i in range(len(term) - 1): + out.append(G.X.on(term[i + 1][0], term[i][0])) + out.append(G.BarrierGate(False)) + if isinstance(para, (dict, PR)): + out.append(G.RZ({i: 2 * j for i, j in para.items()}).on(term[-1][0])) + else: + out.append(G.RZ(2 * para).on(term[-1][0])) + for i in range(len(out) - 1)[::-1]: + if i in rxs: + out.append(G.RX(np.pi * 3.5).on(out[i].obj_qubits)) + else: + out.append(out[i]) + return Circuit(out) + + +def pauli_word_to_circuits(qubitops): + """ + Convert a single pauli word qubit operator to a quantum circuit. + + Args: + qubitops (QubitOperator, Hamiltonian): The single pauli word qubit operator. + + Returns: + Circuit, a quantum circuit. + + Raises: + TypeError: If qubitops is not a QubitOperator or a Hamiltonian. + ValueError: If qubitops is Hamiltonian but not in origin mode. + ValueError: If qubitops has more than one pauliwords. + + Examples: + >>> from mindquantum.core.operators import QubitOperator + >>> from mindquantum.core.circuit import pauli_word_to_circuits + >>> qubitops = QubitOperator('X0 Y1') + >>> pauli_word_to_circuits(qubitops) + q0: ──X── -from mindquantum.circuit import Circuit -from mindquantum.parameterresolver import ParameterResolver as PR + q1: ──Y── + """ + from mindquantum import gates as G + from mindquantum import operators as ops + from mindquantum.core import Circuit + allow_ops = (pq_operator, of_operator, ops.QubitOperator, ops.Hamiltonian) + if not isinstance(qubitops, allow_ops): + raise TypeError("qubitops require a QubitOperator or a Hamiltonian, but get {}!".format(type(qubitops))) + if isinstance(qubitops, ops.Hamiltonian): + if qubitops.how_to != 0: + raise ValueError("Hamiltonian should be in origin mode.") + qubitops = qubitops.hamiltonian + if len(qubitops.terms) > 1: + raise ValueError("Onle work for QubitOperator with single pauliword!") + gate_map = {'X': G.X, 'Y': G.Y, 'Z': G.Z} + for ops in qubitops.terms.keys(): + circ = Circuit() + if ops: + for ind, single_op in ops: + circ += gate_map[single_op].on(ind) + else: + circ += G.I.on(0) + return circ def _add_ctrl_qubits(circ, ctrl_qubits): """Add control qubits on a circuit.""" + from mindquantum.core import Circuit + from mindquantum import gates as G if not isinstance(ctrl_qubits, (int, list)): - raise TypeError( - "Require a int or a list of int for ctrl_qubits, but get {}!". - format(type(ctrl_qubits))) + raise TypeError("Require a int or a list of int for ctrl_qubits, but get {}!".format(type(ctrl_qubits))) if isinstance(ctrl_qubits, int): ctrl_qubits = [ctrl_qubits] for q in ctrl_qubits: if q < 0: - raise ValueError( - "ctrl_qubits should not be negative value, but get {}!".format( - q)) + raise ValueError("ctrl_qubits should not be negative value, but get {}!".format(q)) circ_out = Circuit() ctrl_qubits = set(ctrl_qubits) for gate in circ: intersection = ctrl_qubits.intersection(set(gate.obj_qubits)) if intersection: raise ValueError( - f"Qubit {intersection} in ctrl_qubits {ctrl_qubits} already used n obj_qubits of gate {gate}" - ) + f"Qubit {intersection} in ctrl_qubits {ctrl_qubits} already used n obj_qubits of gate {gate}") curr_ctrl = set(gate.ctrl_qubits) curr_ctrl = list(curr_ctrl.union(ctrl_qubits)) curr_ctrl.sort() new_gate = copy.deepcopy(gate) - new_gate.ctrl_qubits = curr_ctrl + if not isinstance(gate, (G.Measure, G.BarrierGate)): + new_gate.ctrl_qubits = curr_ctrl new_gate.generate_description() circ_out += new_gate return circ_out @@ -61,23 +191,33 @@ def controlled(circuit_fn): circuit_fn (Union[Circuit, FunctionType, MethodType]): A quantum circuit, or a function that can generate a quantum circuit. + Raises: + TypeError: circuit_fn is not a Circuit or can not return a Circuit. + + Returns: + function that can generate a Circuit. + Examples: - >>> from mindquantum.circuit import qft, controlled + >>> from mindquantum.algorithm.library import qft + >>> from mindquantum.core.circuit import controlled >>> u1 = qft([0, 1]) >>> u2 = controlled(u1) >>> u3 = controlled(qft) >>> u3 = u3(2, [0, 1]) >>> u2(2) - H(0 <-: 2) - PS(1.571|0 <-: 1 2) - H(1 <-: 2) - SWAP(0 1 <-: 2) + q0: ──H────PS(π/2)─────────@── + │ │ │ + q1: ──┼───────●───────H────@── + │ │ │ │ + q2: ──●───────●───────●────●── >>> u3 - H(0 <-: 2) - PS(1.571|0 <-: 1 2) - H(1 <-: 2) - SWAP(0 1 <-: 2) + q0: ──H────PS(π/2)─────────@── + │ │ │ + q1: ──┼───────●───────H────@── + │ │ │ │ + q2: ──●───────●───────●────●── """ + from mindquantum.core import Circuit if isinstance(circuit_fn, (FunctionType, MethodType)): def wrapper(ctrl_qubits, *arg, **keywords): @@ -89,8 +229,7 @@ def controlled(circuit_fn): return wrapper if isinstance(circuit_fn, Circuit): return lambda ctrl_qubits: _add_ctrl_qubits(circuit_fn, ctrl_qubits) - raise TypeError( - "Input need a circuit or a function that can generate a circuit.") + raise TypeError("Input need a circuit or a function that can generate a circuit.") def dagger(circuit_fn): @@ -102,40 +241,46 @@ def dagger(circuit_fn): circuit_fn (Union[Circuit, FunctionType, MethodType]): A quantum circuit, or a function that can generate a quantum circuit. + Raises: + TypeError: If circuit_fn is not a Circuit or can not return a Circuit. + + Returns: + Circuit or a function that can generate Circuit. + Examples: - >>> from mindquantum.circuit import qft, dagger + >>> from mindquantum.algorithm.library import qft + >>> from mindquantum.core.circuit import dagger >>> u1 = qft([0, 1]) >>> u2 = dagger(u1) >>> u3 = dagger(qft) >>> u3 = u3([0, 1]) >>> u2 - SWAP(0 1) - H(1) - PS(-1.571|0 <-: 1) - H(0) + q0: ──@─────────PS(-π/2)────H── + │ │ + q1: ──@────H───────●─────────── >>> u3 - SWAP(0 1) - H(1) - PS(-1.571|0 <-: 1) - H(0) + q0: ──@─────────PS(-π/2)────H── + │ │ + q1: ──@────H───────●─────────── """ + from mindquantum.core import Circuit if isinstance(circuit_fn, (FunctionType, MethodType)): def wrapper(*arg, **keywords): circ = circuit_fn(*arg, **keywords) if not isinstance(circ, Circuit): return dagger(circ) - return circ.hermitian + return circ.hermitian() return wrapper if isinstance(circuit_fn, Circuit): - return circuit_fn.hermitian - raise TypeError( - "Input need a circuit or a function that can generate a circuit.") + return circuit_fn.hermitian() + raise TypeError("circuit_fn need a circuit or a function that can generate a circuit.") def _apply_circuit(circ, qubits): """Apply a circuit to other different qubits.""" + from mindquantum.core import Circuit old_qubits = set([]) for g in circ: old_qubits.update(g.obj_qubits) @@ -143,9 +288,7 @@ def _apply_circuit(circ, qubits): old_qubits = list(old_qubits) old_qubits.sort() if len(old_qubits) != len(qubits): - raise ValueError( - f"Can not apply a {len(old_qubits)} qubits unit to {len(qubits)} qubits circuit." - ) + raise ValueError(f"Can not apply a {len(old_qubits)} qubits unit to {len(qubits)} qubits circuit.") qubits_map = dict(zip(old_qubits, qubits)) out = Circuit() for g in circ: @@ -167,31 +310,37 @@ def apply(circuit_fn, qubits): or a function that can generate a quantum circuit. qubits (list[int]): The new qubits that you want to apply. + Raises: + TypeError: If qubits is not a list. + ValueError: If any element of qubits is negative. + TypeError: If circuit_fn is not Circuit or can not return a Circuit. + + Returns: + Circuit or a function that can generate a Circuit. + Examples: - >>> from mindquantum.circuit import qft, apply + >>> from mindquantum.algorithm.library import qft + >>> from mindquantum.core.circuit import apply >>> u1 = qft([0, 1]) - >>> u2 = apply(u1, [1, 2]) - >>> u3 = apply(qft, [1, 2]) + >>> u2 = apply(u1, [1, 0]) + >>> u3 = apply(qft, [1, 0]) >>> u3 = u3([0, 1]) >>> u2 - H(1) - PS(1.571|1 <-: 2) - H(2) - SWAP(1 2) + q0: ──────────●───────H────@── + │ │ + q1: ──H────PS(π/2)─────────@── >>> u3 - H(1) - PS(1.571|1 <-: 2) - H(2) - SWAP(1 2) + q0: ──────────●───────H────@── + │ │ + q1: ──H────PS(π/2)─────────@── """ + from mindquantum.core import Circuit if not isinstance(qubits, list): - raise TypeError(f"New qubits need a list, but get {type(qubits)}!") + raise TypeError(f"qubits need a list, but get {type(qubits)}!") if len(qubits) > 1: for index, q in enumerate(qubits[1:]): if q < 0 or qubits[index] < 0: raise ValueError(f"Qubit index can not negative!") - if q <= qubits[index]: - raise ValueError(f"Qubits should be in ascending order!") if isinstance(circuit_fn, (FunctionType, MethodType)): def wrapper(*arg, **keywords): @@ -203,16 +352,17 @@ def apply(circuit_fn, qubits): return wrapper if isinstance(circuit_fn, Circuit): return _apply_circuit(circuit_fn, qubits) - raise TypeError( - "Input need a circuit or a function that can generate a circuit.") + raise TypeError("circuit_fn need a circuit or a function that can generate a circuit.") def _add_prefix(circ, prefix): """Add prefix to every parameters in circuit.""" + from mindquantum.core import Circuit + from mindquantum.core import ParameterResolver as PR out = Circuit() for g in circ: g = copy.deepcopy(g) - if g.isparameter: + if g.parameterized: pr = PR() for k, v in g.coeff.items(): pr[f'{prefix}_{k}'] = v @@ -232,8 +382,16 @@ def add_prefix(circuit_fn, prefix): or a function that can generate a quantum circuit. prefix (str): The prefix you want to add to every parameters. + Raises: + TypeError: If prefix is not a string. + TypeError: circuit_fn is not a Circuit or can not return a Circuit. + + Returns: + Circuit or a function that can generate a Circuit. + Examples: - >>> from mindquantum.circuit import qft, add_prefix + >>> from mindquantum.algorithm.library import qft + >>> from mindquantum.core.circuit import add_prefix >>> from mindquantum import RX, H, Circuit >>> u = lambda qubit: Circuit([H.on(0), RX('a').on(qubit)]) >>> u1 = u(0) @@ -241,12 +399,11 @@ def add_prefix(circuit_fn, prefix): >>> u3 = add_prefix(u, 'ansatz') >>> u3 = u3(0) >>> u2 - H(0) - RX(ansatz_a|0) + q0: ──H────RX(ansatz_a)── >>> u3 - H(0) - RX(ansatz_a|0) + q0: ──H────RX(ansatz_a)── """ + from mindquantum.core import Circuit if not isinstance(prefix, str): raise TypeError(f"prefix need string, but get {type(prefix)}") if isinstance(circuit_fn, (FunctionType, MethodType)): @@ -260,16 +417,17 @@ def add_prefix(circuit_fn, prefix): return wrapper if isinstance(circuit_fn, Circuit): return _add_prefix(circuit_fn, prefix) - raise TypeError( - "Input need a circuit or a function that can generate a circuit.") + raise TypeError("circuit_fn need a circuit or a function that can generate a circuit.") def _change_param_name(circ, name_map): """Change the parameter of circuit according to the name map.""" + from mindquantum.core import Circuit + from mindquantum.core import ParameterResolver as PR out = Circuit() for g in circ: g = copy.deepcopy(g) - if g.isparameter: + if g.parameterized: pr = PR() for k, v in g.coeff.items(): if k not in name_map: @@ -291,8 +449,18 @@ def change_param_name(circuit_fn, name_map): or a function that can generate a quantum circuit. name_map (dict): The parameter name mapping dict. + Raises: + TypeError: If name_map is not a map. + TypeError: If key of name_map is not string. + TypeError: If value of name_map is not string. + TypeError: If circuit_fn is not a Circuit or can not return a Circuit. + + Returns: + Circuit or a function that can generate a Circuit. + Examples: - >>> from mindquantum.circuit import qft, change_param_name + >>> from mindquantum.algorithm.library import qft + >>> from mindquantum.core.circuit import change_param_name >>> from mindquantum import RX, H, Circuit >>> u = lambda qubit: Circuit([H.on(0), RX('a').on(qubit)]) >>> u1 = u(0) @@ -300,22 +468,18 @@ def change_param_name(circuit_fn, name_map): >>> u3 = change_param_name(u, {'a': 'b'}) >>> u3 = u3(0) >>> u2 - H(0) - RX(b|0) + q0: ──H────RX(b)── >>> u3 - H(0) - RX(b|0) + q0: ──H────RX(b)── """ + from mindquantum.core import Circuit if not isinstance(name_map, dict): - raise TypeError( - f"Parameters name map need map, but get {type(name_map)}") + raise TypeError(f"name_map need map, but get {type(name_map)}") for k, v in name_map.items(): if not isinstance(k, str): - raise ValueError( - f"Parameter need a string, but get {k}, which is {type(k)}") + raise TypeError(f"key of name_map need a string, but get {k}, which is {type(k)}") if not isinstance(v, str): - raise ValueError( - f"Parameter need a string, but get {v}, which is {type(v)}") + raise TypeError(f"value of name_map need a string, but get {v}, which is {type(v)}") if isinstance(circuit_fn, (FunctionType, MethodType)): def wrapper(*arg, **keywords): @@ -327,8 +491,7 @@ def change_param_name(circuit_fn, name_map): return wrapper if isinstance(circuit_fn, Circuit): return _change_param_name(circuit_fn, name_map) - raise TypeError( - "Input need a circuit or a function that can generate a circuit.") + raise TypeError("circuit_fn need a circuit or a function that can generate a circuit.") C = controlled diff --git a/mindquantum/gate/__init__.py b/mindquantum/core/gates/__init__.py similarity index 64% rename from mindquantum/gate/__init__.py rename to mindquantum/core/gates/__init__.py index 35c66331cc0aecef68aeea553162948c346eb3d6..a8b5ac5c39208eecc272f4b3a9aaa35cae97a8fe 100644 --- a/mindquantum/gate/__init__.py +++ b/mindquantum/core/gates/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,41 +20,52 @@ Gate provides different quantum gate. """ from .basic import BasicGate -from .basic import IntrinsicOneParaGate from .basic import NoneParameterGate from .basic import ParameterGate +from .basic import IntrinsicOneParaGate +from .basic import HERMITIAN_PROPERTIES +from .basicgate import BarrierGate +from .basicgate import CNOTGate +from .basicgate import HGate from .basicgate import IGate from .basicgate import XGate from .basicgate import YGate from .basicgate import ZGate -from .basicgate import HGate -from .basicgate import SWAPGate -from .basicgate import CNOTGate -from .basicgate import H -from .basicgate import CNOT -from .basicgate import X -from .basicgate import Y -from .basicgate import Z -from .basicgate import I -from .basicgate import S -from .basicgate import Power -from .basicgate import SWAP +from .basicgate import gene_univ_parameterized_gate from .basicgate import UnivMathGate +from .basicgate import SWAPGate +from .basicgate import ISWAPGate from .basicgate import RX from .basicgate import RY from .basicgate import RZ from .basicgate import PhaseShift +from .basicgate import SGate +from .basicgate import TGate from .basicgate import XX from .basicgate import YY from .basicgate import ZZ -from .hamiltonian import Hamiltonian -from .projector import Projector - +from .basicgate import Power +from .basicgate import I +from .basicgate import X +from .basicgate import Y +from .basicgate import Z +from .basicgate import H +from .basicgate import S +from .basicgate import T +from .basicgate import SWAP +from .basicgate import ISWAP +from .basicgate import CNOT +from .basicgate import BARRIER +from .measurement import Measure +from .measurement import MeasureResult __all__ = [ - 'BasicGate', 'IntrinsicOneParaGate', 'NoneParameterGate', 'ParameterGate', - 'H', 'CNOT', 'X', 'Y', 'Z', 'I', 'S', 'Power', 'SWAP', 'UnivMathGate', - 'RX', 'RY', 'RZ', 'PhaseShift', 'XX', 'YY', 'ZZ', 'IGate', 'XGate', - 'YGate', 'ZGate', 'HGate', 'SWAPGate', 'CNOTGate', 'Hamiltonian', - 'Projector' + "BasicGate", "NoneParameterGate", "ParameterGate", "IntrinsicOneParaGate", + "HERMITIAN_PROPERTIES", "BarrierGate", "CNOTGate", "HGate", "IGate", + "XGate", "YGate", "ZGate", "gene_univ_parameterized_gate", "UnivMathGate", + "SWAPGate", "ISWAPGate", "RX", "RY", "RZ", "PhaseShift", "SGate", "TGate", + "XX", "YY", "ZZ", "Power", "I", "X", "Y", "Z", "H", "S", "T", "SWAP", + "ISWAP", "CNOT", "BARRIER", "Measure", "MeasureResult" ] + +__all__.sort() diff --git a/mindquantum/gate/basic.py b/mindquantum/core/gates/basic.py similarity index 73% rename from mindquantum/gate/basic.py rename to mindquantum/core/gates/basic.py index 354aa85f0f6105b98037baed8e0fe4284b130482..963b676174678777003b1efcf65c1fc05eb820f7 100644 --- a/mindquantum/gate/basic.py +++ b/mindquantum/core/gates/basic.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,7 +20,15 @@ from copy import deepcopy from abc import abstractmethod from collections.abc import Iterable import numpy as np -from mindquantum.parameterresolver import ParameterResolver as PR +from mindquantum.core.parameterresolver import ParameterResolver as PR +from mindquantum.utils.f import _common_exp +from mindquantum import mqbackend as mb + +HERMITIAN_PROPERTIES = { + 'self_hermitian': 0, # the hermitian of this gate is its self + 'do_hermitian': 1, # just do hermitian when you need hermitian + 'params_opposite': 2 # use the negative parameters for hermitian +} class BasicGate(): @@ -28,16 +37,19 @@ class BasicGate(): Args: name (str): the name of this gate. - isparameter (bool): whether this is a parameterized gate. Default: False. + parameterized (bool): whether this is a parameterized gate. Default: False. """ - def __init__(self, name, isparameter=False): + def __init__(self, name, parameterized=False): if not isinstance(name, str): - raise TypeError("Excepted string for gate name, get {}".format( - type(name))) + raise TypeError("Excepted string for gate name, get {}".format(type(name))) self.name = name - self.isparameter = isparameter + self.parameterized = parameterized self.str = self.name self.projectq_gate = None + self.obj_qubits = [] + self.ctrl_qubits = [] + self.hermitian_property = HERMITIAN_PROPERTIES['self_hermitian'] + self.daggered = False @abstractmethod def matrix(self, *args): @@ -53,13 +65,17 @@ class BasicGate(): def generate_description(self): """Description generator.""" - if hasattr(self, 'ctrl_qubits') and self.ctrl_qubits: + name = self.name + if self.hermitian_property == HERMITIAN_PROPERTIES['do_hermitian'] and self.daggered: + name += '†' + if self.ctrl_qubits: obj_str = ' '.join([str(i) for i in self.obj_qubits]) ctrl_str = ' '.join([str(i) for i in self.ctrl_qubits]) - self.str = "{}({} <-: {})".format(self.name, obj_str, ctrl_str) - elif hasattr(self, 'obj_qubits'): - self.str = "{}({})".format( - self.name, ' '.join([str(i) for i in self.obj_qubits])) + self.str = "{}({} <-: {})".format(name, obj_str, ctrl_str) + elif self.obj_qubits: + self.str = "{}({})".format(name, ' '.join([str(i) for i in self.obj_qubits])) + else: + self.str = name def on(self, obj_qubits, ctrl_qubits=None): """ @@ -78,7 +94,7 @@ class BasicGate(): Gate, Return a new gate. Examples: - >>> from mindquantum.gate import X + >>> from mindquantum.core.gates import X >>> x = X.on(1) >>> x.obj_qubits [1] @@ -138,7 +154,7 @@ class BasicGate(): def __eq__(self, other): _check_gate_type(other) if self.name != other.name or \ - self.isparameter != other.isparameter or \ + self.parameterized != other.parameterized or \ self.obj_qubits != other.obj_qubits or \ self.ctrl_qubits != other.ctrl_qubits: return False @@ -146,7 +162,7 @@ class BasicGate(): def __or__(self, qubits): if not isinstance(qubits, tuple): - qubits = (qubits,) + qubits = (qubits, ) qubits = list(qubits) @@ -175,6 +191,19 @@ class NoneParameterGate(BasicGate): self.coeff = None self.matrix_value = None + def get_cpp_obj(self): + """Get the cpp obj of this gate.""" + cpp_gate = mb.get_gate_by_name(self.name) + cpp_gate.obj_qubits = self.obj_qubits + cpp_gate.ctrl_qubits = self.ctrl_qubits + if self.daggered: + cpp_gate.daggered = True + if self.hermitian_property == HERMITIAN_PROPERTIES["do_hermitian"]: + cpp_gate.base_matrix = mb.dim2matrix(self.matrix()) + if self.hermitian_property == HERMITIAN_PROPERTIES["params_opposite"]: + raise ValueError(f"Hermitian properties of None parameterized gate {self} can not be params_opposite") + return cpp_gate + def matrix(self, *args): """ Get the matrix of this none parameterized gate. @@ -186,7 +215,10 @@ class NoneParameterGate(BasicGate): Get hermitian gate of this none parameterized gate. """ hermitian_gate = deepcopy(self) - hermitian_gate.matrix_value = np.conj(self.matrix_value.T) + hermitian_gate.daggered = not hermitian_gate.daggered + if self.hermitian_property == HERMITIAN_PROPERTIES["do_hermitian"]: + hermitian_gate.matrix_value = np.conj(self.matrix_value.T) + hermitian_gate.generate_description() return hermitian_gate def define_projectq_gate(self): @@ -199,10 +231,9 @@ class NoneParameterGate(BasicGate): """Check obj qubit number""" n_qubits_exp = np.log2(len(self.matrix_value)).astype(int) n_qubits = len(self.obj_qubits) + self.n_qubits = n_qubits_exp if n_qubits_exp != n_qubits: - raise ValueError( - f"obj_qubits of {self.name} requires {n_qubits_exp} qubits, but get {n_qubits}" - ) + raise ValueError(f"obj_qubits of {self.name} requires {n_qubits_exp} qubits, but get {n_qubits}") class ParameterGate(NoneParameterGate, BasicGate): @@ -218,7 +249,7 @@ class ParameterGate(NoneParameterGate, BasicGate): if isinstance(coeff, (int, float, complex)): NoneParameterGate.__init__(self, name) self.coeff = coeff - self.str = self.str + "({})".format(round(self.coeff, 3)) + self.str = self.str + "({})".format(_common_exp(self.coeff, 3)) else: BasicGate.__init__(self, name, True) if coeff is None: @@ -240,21 +271,24 @@ but get {}".format(type(coeff))) self.str = self.str + "({})".format(self.coeff.expression()) def generate_description(self): + name = self.name + if self.hermitian_property == HERMITIAN_PROPERTIES['do_hermitian'] and self.daggered: + name += '†' BasicGate.generate_description(self) - if not hasattr(self, 'obj_qubits'): - if self.isparameter: - self.str = f'{self.name}({self.coeff.expression()})' + if not hasattr(self, 'obj_qubits') or not self.obj_qubits: + if self.parameterized: + self.str = f'{name}({self.coeff.expression()})' else: - self.str = f'{self.name}({round(self.coeff, 3)})' + self.str = f'{name}({_common_exp(self.coeff, 3)})' else: - if self.isparameter: + if self.parameterized: self.str = self.str[:len( - self.name) + 1] + str(self.coeff.expression())\ - + '|' + self.str[len(self.name) + 1:] + name) + 1] + str(self.coeff.expression())\ + + '|' + self.str[len(name) + 1:] else: self.str = self.str[:len( - self.name) + 1] + str(round(self.coeff, 3))\ - + '|' + self.str[len(self.name) + 1:] + name) + 1] + str(_common_exp(self.coeff, 3))\ + + '|' + self.str[len(name) + 1:] @abstractmethod def matrix(self, *paras_out): @@ -276,16 +310,13 @@ but get {}".format(type(coeff))) Returns: float, Multiply the values of the common keys of these two dicts. """ - if not isinstance(coeff_in, - (dict, PR)) or not isinstance(paras_out, (dict, PR)): - raise TypeError( - "Require a dict or ParameterResolver for parameters, but get {} and {}!" - .format(type(coeff_in), type(paras_out))) + if not isinstance(coeff_in, (dict, PR)) or not isinstance(paras_out, (dict, PR)): + raise TypeError("Require a dict or ParameterResolver for parameters, but get {} and {}!".format( + type(coeff_in), type(paras_out))) params = 0 for key, value in coeff_in.items(): if key not in paras_out: - raise KeyError( - "parameter {} not in parameters you send in!".format(key)) + raise KeyError("parameter {} not in parameters you send in!".format(key)) params += value * paras_out[key] return params @@ -300,10 +331,10 @@ but get {}".format(type(coeff))) All parameters requires grad. Inplace operation. Returns: - BaseGate, a parameterized gate with all parameters need to + BasicGate, a parameterized gate with all parameters need to update gradient. """ - if self.isparameter: + if self.parameterized: self.coeff.requires_grad() return self @@ -312,10 +343,10 @@ but get {}".format(type(coeff))) All parameters do not need grad. Inplace operation. Returns: - BaseGate, a parameterized gate with all parameters not need to + BasicGate, a parameterized gate with all parameters not need to update gradient. """ - if self.isparameter: + if self.parameterized: self.coeff.no_grad() return self @@ -327,7 +358,7 @@ but get {}".format(type(coeff))) names (tuple[str]): Parameters that requires grad. Returns: - BaseGate, with some part of parameters need to update gradient. + BasicGate, with some part of parameters need to update gradient. """ self.coeff.requires_grad_part(names) return self @@ -340,7 +371,7 @@ but get {}".format(type(coeff))) names (tuple[str]): Parameters that not requires grad. Returns: - BaseGate, with some part of parameters not need to update gradient. + BasicGate, with some part of parameters not need to update gradient. """ self.coeff.no_grad_part(names) return self @@ -360,10 +391,10 @@ class IntrinsicOneParaGate(ParameterGate): coeff (Union[dict, ParameterResolver]): the parameter of this gate. Default: Nnoe. Examples: - >>> from mindquantum.gate import RX + >>> from mindquantum.core.gates import RX >>> rx1 = RX(1.2) >>> rx1 - RX(1.2) + RX(6/5) >>> rx2 = RX({'a' : 0.5}) >>> rx2.coeff {'a': 0.5} @@ -372,6 +403,19 @@ class IntrinsicOneParaGate(ParameterGate): """ def __init__(self, name, coeff=None): ParameterGate.__init__(self, name, coeff) + self.hermitian_property = HERMITIAN_PROPERTIES['params_opposite'] + + def get_cpp_obj(self): + """Get cpp obj of this gate.""" + cpp_gate = mb.get_gate_by_name(self.name) + cpp_gate.obj_qubits = self.obj_qubits + cpp_gate.ctrl_qubits = self.ctrl_qubits + cpp_gate.daggered = self.daggered + if not self.parameterized: + cpp_gate.apply_value(self.coeff) + else: + cpp_gate.params = self.coeff.get_cpp_obj() + return cpp_gate def hermitian(self): """ @@ -387,6 +431,7 @@ class IntrinsicOneParaGate(ParameterGate): RX(a*(-1.0 - 2.0*I)) """ hermitian_gate = deepcopy(self) + hermitian_gate.daggered = not hermitian_gate.daggered hermitian_gate.coeff = 1 * self.coeff if isinstance(self.coeff, PR): hermitian_gate.coeff *= -1 @@ -418,7 +463,7 @@ class IntrinsicOneParaGate(ParameterGate): numpy.ndarray, Return the numpy array of the matrix. Examples: - >>> from mindquantum.gate import RX + >>> from mindquantum.core.gates import RX >>> rx1 = RX(0) >>> rx1.matrix() array([[1.+0.j, 0.-0.j], @@ -428,7 +473,7 @@ class IntrinsicOneParaGate(ParameterGate): array([[0.36+0.j , 0. -0.93j], [0. -0.93j, 0.36+0.j ]]) """ - if self.isparameter: + if self.parameterized: theta = 0 if isinstance(paras_out[0], dict): theta = self.linearcombination(self.coeff, paras_out[0]) @@ -459,7 +504,7 @@ class IntrinsicOneParaGate(ParameterGate): array([[-0.42+0.j , 0. -0.27j], [ 0. -0.27j, -0.42+0.j ]]) """ - if self.isparameter: + if self.parameterized: theta = 0 if isinstance(paras_out[0], dict): theta = self.linearcombination(self.coeff, paras_out[0]) @@ -470,8 +515,7 @@ class IntrinsicOneParaGate(ParameterGate): theta = paras_out[0] * list(self.coeff.values())[0] if about_what is None: if len(self.coeff) != 1: - raise Exception( - "Please specific the diff is about which parameter.") + raise Exception("Please specific the diff is about which parameter.") about_what = list(self.coeff.keys())[0] return self.coeff[about_what] * self._diff_matrix(theta) raise Exception("Not a parameterized gate!") @@ -480,26 +524,21 @@ class IntrinsicOneParaGate(ParameterGate): """Check obj qubit number""" n_qubits = len(self.obj_qubits) n_qubits_exp = np.log2(len(self._matrix(0))).astype(int) + self.n_qubits = n_qubits_exp if n_qubits_exp != n_qubits: - raise ValueError( - f"obj_qubits of {self.name} requires {n_qubits_exp} qubits, but get {n_qubits}" - ) + raise ValueError(f"obj_qubits of {self.name} requires {n_qubits_exp} qubits, but get {n_qubits}") def _check_gate_type(gate): if not isinstance(gate, BasicGate): - raise TypeError("Require a quantum gate, but get {}".format( - type(gate))) + raise TypeError("Require a quantum gate, but get {}".format(type(gate))) def _check_qubit_id(qubit_id): if not isinstance(qubit_id, int): - raise TypeError( - "Qubit should be a non negative int, but get {}!".format( - type(qubit_id))) + raise TypeError("Qubit should be a non negative int, but get {}!".format(type(qubit_id))) if qubit_id < 0: - raise ValueError( - "Qubit should be non negative int, but get {}!".format(qubit_id)) + raise ValueError("Qubit should be non negative int, but get {}!".format(qubit_id)) def _check_obj_and_ctrl_qubits(obj_qubits, ctrl_qubits): diff --git a/mindquantum/gate/basicgate.py b/mindquantum/core/gates/basicgate.py similarity index 57% rename from mindquantum/gate/basicgate.py rename to mindquantum/core/gates/basicgate.py index db70d3cb879e9af14bfb00f2b7f9ab92e6c3599f..7ab80ae0b23aa94acc5e36a4586e671a3fab649c 100644 --- a/mindquantum/gate/basicgate.py +++ b/mindquantum/core/gates/basicgate.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,19 +15,49 @@ # ============================================================================ """Basic quantum gate.""" +from types import FunctionType, MethodType +from copy import deepcopy import numpy as np import projectq.ops as pjops from scipy.linalg import fractional_matrix_power -from mindquantum.parameterresolver import ParameterResolver as PR +from mindquantum.core.parameterresolver import ParameterResolver as PR +from mindquantum import mqbackend as mb +from mindquantum.utils.type_value_check import _check_input_type +from .basic import HERMITIAN_PROPERTIES from .basic import IntrinsicOneParaGate from .basic import NoneParameterGate +class BarrierGate(NoneParameterGate): + """ + BARRIER gate do nothing but set a barrier for drawing circuit. + + Args: + show (bool): whether show this barrier gate. Default: True. + + Raises: + TypeError: if `show` is not bool. + """ + def __init__(self, show=True): + _check_input_type('show', bool, show) + NoneParameterGate.__init__(self, 'BARRIER') + self.show = show + + def get_cpp_obj(self): + return None + + def hermitian(self): + return BarrierGate(self.show) + + def on(self, obj_qubits, ctrl_qubits=None): + raise NotImplementedError + + class CNOTGate(NoneParameterGate): r""" Control-X gate. - More usage, please see :class:`mindquantum.gate.XGate`. + More usage, please see :class:`mindquantum.core.gates.XGate`. """ def __init__(self): NoneParameterGate.__init__(self, 'CNOT') @@ -36,6 +67,14 @@ class CNOTGate(NoneParameterGate): """Define the corresponded projectq gate.""" self.projectq_gate = pjops.CNOT + def on(self, obj_qubits, ctrl_qubits=None): + out = super(CNOTGate, self).on(obj_qubits, ctrl_qubits) + if ctrl_qubits is None: + raise ValueError("A control qubit is needed for CNOT gate!") + out.ctrl_qubits = [] + out.obj_qubits = [obj_qubits, ctrl_qubits] + return out + class HGate(NoneParameterGate): r""" @@ -45,7 +84,7 @@ class HGate(NoneParameterGate): {\rm H}=\frac{1}{\sqrt{2}}\begin{pmatrix}1&1\\1&-1\end{pmatrix} - More usage, please see :class:`mindquantum.gate.XGate`. + More usage, please see :class:`mindquantum.core.gates.XGate`. """ def __init__(self): NoneParameterGate.__init__(self, 'H') @@ -64,7 +103,7 @@ class IGate(NoneParameterGate): {\rm I}=\begin{pmatrix}1&0\\0&1\end{pmatrix} - More usage, please see :class:`mindquantum.gate.XGate`. + More usage, please see :class:`mindquantum.core.gates.XGate`. """ def __init__(self): NoneParameterGate.__init__(self, 'I') @@ -92,7 +131,7 @@ class XGate(NoneParameterGate): X^\theta = RX(\theta\pi) Examples: - >>> from mindquantum.gate import X + >>> from mindquantum.core.gates import X >>> x1 = X.on(0) >>> cnot = X.on(0, 1) >>> print(x1) @@ -103,7 +142,7 @@ class XGate(NoneParameterGate): array([[0, 1], [1, 0]]) >>> x1**2 - RX(6.283) + RX(2π) >>> (x1**'a').coeff {'a': 3.141592653589793} >>> (x1**{'a' : 2}).coeff @@ -138,7 +177,7 @@ class YGate(NoneParameterGate): {\rm Y}=\begin{pmatrix}0&-i\\i&0\end{pmatrix} - More usage, please see :class:`mindquantum.gate.XGate`. + More usage, please see :class:`mindquantum.core.gates.XGate`. """ def __init__(self): NoneParameterGate.__init__(self, 'Y') @@ -169,7 +208,7 @@ class ZGate(NoneParameterGate): {\rm Z}=\begin{pmatrix}1&0\\0&-1\end{pmatrix} - More usage, please see :class:`mindquantum.gate.XGate`. + More usage, please see :class:`mindquantum.core.gates.XGate`. """ def __init__(self): NoneParameterGate.__init__(self, 'Z') @@ -192,18 +231,100 @@ class ZGate(NoneParameterGate): self.projectq_gate = pjops.Z +def gene_univ_parameterized_gate(name, matrix_generator, + diff_matrix_generator): + """ + Generate a customer parameterized gate based on the single parameter defined + unitary matrix. + + Args: + name (str): The name of this gate. + matrix_generator (Union[FunctionType, MethodType]): A function or a method that + take exactly one argument to generate a unitary matrix. + diff_matrix_generator (Union[FunctionType, MethodType]): A function or a method + that take exactly one argument to generate the derivative of this unitary matrix. + + Returns: + _UnivParameterizedGate, a customer parameterized gate. + + Examples: + >>> import numpy as np + >>> from mindquantum import gene_univ_parameterized_gate + >>> from mindquantum import Simulator, Circuit + >>> def matrix(theta): + ... return np.array([[np.exp(1j * theta), 0], + ... [0, np.exp(-1j * theta)]]) + >>> def diff_matrix(theta): + ... return 1j*np.array([[np.exp(1j * theta), 0], + ... [0, -np.exp(-1j * theta)]]) + >>> TestGate = gene_univ_parameterized_gate('Test', matrix, diff_matrix) + >>> circ = Circuit().h(0) + >>> circ += TestGate('a').on(0) + >>> circ + q0: ──H────Test(a)── + >>> circ.get_qs(pr={'a': 1.2}) + array([0.25622563+0.65905116j, 0.25622563-0.65905116j]) + """ + if not isinstance(matrix_generator, (FunctionType, MethodType)): + raise ValueError('matrix_generator requires a function or a method.') + if not isinstance(diff_matrix_generator, (FunctionType, MethodType)): + raise ValueError('matrix_generator requires a function or a method.') + + class _UnivParameterizedGate(IntrinsicOneParaGate): + """The customer parameterized gate.""" + def __init__(self, coeff): + IntrinsicOneParaGate.__init__(self, name, coeff) + self.matrix_generator = matrix_generator + self.diff_matrix_generator = diff_matrix_generator + self.hermitian_property = HERMITIAN_PROPERTIES['do_hermitian'] + + def _matrix(self, theta): + if self.daggered: + return np.conj(self.matrix_generator(theta)).T + return self.matrix_generator(theta) + + def _diff_matrix(self, theta): + if self.daggered: + return np.conj(self.matrix_generator(theta)).T + return self.diff_matrix_generator(theta) + + def hermitian(self): + hermitian_gate = deepcopy(self) + hermitian_gate.daggered = not hermitian_gate.daggered + hermitian_gate.coeff = 1 * self.coeff + hermitian_gate.generate_description() + return hermitian_gate + + def get_cpp_obj(self): + cpp_gate = mb.basic_gate(self.name, self.hermitian_property, + self._matrix, self._diff_matrix) + cpp_gate.daggered = self.daggered + cpp_gate.obj_qubits = self.obj_qubits + cpp_gate.ctrl_qubits = self.ctrl_qubits + if not self.parameterized: + cpp_gate.apply_value(self.coeff) + else: + cpp_gate.params = self.coeff.get_cpp_obj() + return cpp_gate + + def define_projectq_gate(self): + raise NotImplementedError + + return _UnivParameterizedGate + + class UnivMathGate(NoneParameterGate): r""" Universal math gate. - More usage, please see :class:`mindquantum.gate.XGate`. + More usage, please see :class:`mindquantum.core.gates.XGate`. Args: name (str): the name of this gate. mat (np.ndarray): the matrix value of this gate. Examples: - >>> from mindquantum.gate import UnivMathGate + >>> from mindquantum.core.gates import UnivMathGate >>> x_mat=np.array([[0,1],[1,0]]) >>> X_gate=UnivMathGate('X',x_mat) >>> x1=X_gate.on(0,1) @@ -213,17 +334,27 @@ class UnivMathGate(NoneParameterGate): def __init__(self, name, mat): NoneParameterGate.__init__(self, name) self.matrix_value = mat + self.hermitian_property = HERMITIAN_PROPERTIES['do_hermitian'] def define_projectq_gate(self): """Define the corresponded projectq gate.""" self.projectq_gate = None + def get_cpp_obj(self): + mat = mb.dim2matrix(self.matrix()) + cpp_gate = mb.basic_gate(False, self.name, self.hermitian_property, + mat) + cpp_gate.daggered = self.daggered + cpp_gate.obj_qubits = self.obj_qubits + cpp_gate.ctrl_qubits = self.ctrl_qubits + return cpp_gate + class SWAPGate(NoneParameterGate): """ SWAP gate that swap two different qubits. - More usage, please see :class:`mindquantum.gate.XGate`. + More usage, please see :class:`mindquantum.core.gates.XGate`. """ def __init__(self): NoneParameterGate.__init__(self, 'SWAP') @@ -235,6 +366,25 @@ class SWAPGate(NoneParameterGate): self.projectq_gate = pjops.Swap +class ISWAPGate(NoneParameterGate): + r""" + ISWAP gate that swap two different qubits and phase the + :math:`\left|01\right>` and :math:`\left|10\right>` amplitudes by + :math:`i`. + + More usage, please see :class:`mindquantum.core.gates.XGate`. + """ + def __init__(self): + NoneParameterGate.__init__(self, 'ISWAP') + self.hermitian_property = HERMITIAN_PROPERTIES['do_hermitian'] + self.matrix_value = np.array([[1, 0, 0, 0], [0, 0, 1j, 0], + [0, 1j, 0, 0], [0, 0, 0, 1]]) + + def define_projectq_gate(self): + """Define the corresponded projectq gate.""" + self.projectq_gate = pjops.Swap + + class RX(IntrinsicOneParaGate): r""" Rotation gate around x-axis. @@ -263,11 +413,11 @@ class RX(IntrinsicOneParaGate): RX(a+2b) Args: - coeff (Union[int, float, str, dict]): the parameters of + coeff (Union[int, float, str, dict, ParameterResolver]): the parameters of parameterized gate, see above for detail explanation. Default: None. Examples: - >>> from mindquantum.gate import RX + >>> from mindquantum.core.gates import RX >>> import numpy as np >>> rx1 = RX(0.5) >>> np.round(rx1.matrix(), 2) @@ -279,7 +429,7 @@ class RX(IntrinsicOneParaGate): [0. -0.05j, 0.999+0.j ]]) >>> rx3 = RX({'a' : 0.2, 'b': 0.5}).on(0, 2) >>> print(rx3) - RX(a b|0 <-: 2) + RX(0.2*a + 0.5*b|0 <-: 2) >>> np.round(rx3.matrix({'a' : 1, 'b' : 2}), 2) array([[0.83+0.j , 0. -0.56j], [0. -0.56j, 0.83+0.j ]]) @@ -308,14 +458,16 @@ class RX(IntrinsicOneParaGate): class RZ(IntrinsicOneParaGate): r""" - Rotation gate around z-axis. + Rotation gate around z-axis. More usage, please see :class:`mindquantum.core.gates.RX`. .. math:: {\rm RZ}=\begin{pmatrix}\exp(-i\theta/2)&0\\ 0&\exp(i\theta/2)\end{pmatrix} - More usage, please see :class:`mindquantum.gate.RX`. + Args: + coeff (Union[int, float, str, dict, ParameterResolver]): the parameters of + parameterized gate, see above for detail explanation. Default: None. """ def __init__(self, coeff=None): IntrinsicOneParaGate.__init__(self, 'RZ', coeff) @@ -335,14 +487,16 @@ class RZ(IntrinsicOneParaGate): class RY(IntrinsicOneParaGate): r""" - Rotation gate around z-axis. + Rotation gate around y-axis. More usage, please see :class:`mindquantum.core.gates.RX`. .. math:: {\rm RY}=\begin{pmatrix}\cos(\theta/2)&-\sin(\theta/2)\\ \sin(\theta/2)&\cos(\theta/2)\end{pmatrix} - More usage, please see :class:`mindquantum.gate.RX`. + Args: + coeff (Union[int, float, str, dict, ParameterResolver]): the parameters of + parameterized gate, see above for detail explanation. Default: None. """ def __init__(self, coeff=None): IntrinsicOneParaGate.__init__(self, 'RY', coeff) @@ -363,14 +517,16 @@ class RY(IntrinsicOneParaGate): class PhaseShift(IntrinsicOneParaGate): r""" - Phase shift gate. + Phase shift gate. More usage, please see :class:`mindquantum.core.gates.RX`. .. math:: {\rm PhaseShift}=\begin{pmatrix}1&0\\ 0&\exp(i\theta)\end{pmatrix} - More usage, please see :class:`mindquantum.gate.RX`. + Args: + coeff (Union[int, float, str, dict, ParameterResolver]): the parameters of + parameterized gate, see above for detail explanation. Default: None. """ def __init__(self, coeff=None): IntrinsicOneParaGate.__init__(self, 'PS', coeff) @@ -386,15 +542,79 @@ class PhaseShift(IntrinsicOneParaGate): self.projectq_gate = pjops.R +class SGate(PhaseShift): + r""" + S gate with matrix as : + + .. math:: + {\rm S}=\begin{pmatrix}1&0\\0&i\end{pmatrix} + + More usage, please see :class:`mindquantum.core.gates.XGate`. + """ + def __init__(self): + PhaseShift.__init__(self, np.pi / 2) + self.name = 'S' + self.str = self.name + self.hermitian_property = HERMITIAN_PROPERTIES['do_hermitian'] + + def generate_description(self): + PhaseShift.generate_description(self) + idx = self.str.find('|') + if idx != -1: + self.str = self.name + ('†(' if self.daggered else + '(') + self.str[idx + 1:] + else: + self.str = self.name + ('†' if self.daggered else '') + + def get_cpp_obj(self): + f = -1 if self.daggered else 1 + f = f * np.pi / 2 + return PhaseShift(f).on(self.obj_qubits, + self.ctrl_qubits).get_cpp_obj() + + +class TGate(PhaseShift): + r""" + T gate with matrix as : + + .. math:: + {\rm T}=\begin{pmatrix}1&0\\0&(1+i)/\sqrt(2)\end{pmatrix} + + More usage, please see :class:`mindquantum.core.gates.XGate`. + """ + def __init__(self): + PhaseShift.__init__(self, np.pi / 4) + self.name = 'T' + self.str = self.name + self.hermitian_property = HERMITIAN_PROPERTIES['do_hermitian'] + + def generate_description(self): + PhaseShift.generate_description(self) + idx = self.str.find('|') + if idx != -1: + self.str = self.name + ('†(' if self.daggered else + '(') + self.str[idx + 1:] + else: + self.str = self.name + ('†' if self.daggered else '') + + def get_cpp_obj(self): + f = -1 if self.daggered else 1 + f = f * np.pi / 4 + return PhaseShift(f).on(self.obj_qubits, + self.ctrl_qubits).get_cpp_obj() + + class XX(IntrinsicOneParaGate): r""" - Ising XX gate. + Ising XX gate. More usage, please see :class:`mindquantum.core.gates.RX`. .. math:: {\rm XX_\theta}=\cos(\theta)I\otimes I-i\sin(\theta)\sigma_x\otimes\sigma_x - More usage, please see :class:`mindquantum.gate.RX`. + Args: + coeff (Union[int, float, str, dict, ParameterResolver]): the parameters of + parameterized gate, see above for detail explanation. Default: None. """ def __init__(self, coeff=None): IntrinsicOneParaGate.__init__(self, 'XX', coeff) @@ -420,13 +640,15 @@ class XX(IntrinsicOneParaGate): class YY(IntrinsicOneParaGate): r""" - Ising YY gate. + Ising YY gate. More usage, please see :class:`mindquantum.core.gates.RX`. .. math:: {\rm YY_\theta}=\cos(\theta)I\otimes I-i\sin(\theta)\sigma_y\otimes\sigma_y - More usage, please see :class:`mindquantum.gate.RX`. + Args: + coeff (Union[int, float, str, dict, ParameterResolver]): the parameters of + parameterized gate, see above for detail explanation. Default: None. """ def __init__(self, coeff=None): IntrinsicOneParaGate.__init__(self, 'YY', coeff) @@ -452,13 +674,15 @@ class YY(IntrinsicOneParaGate): class ZZ(IntrinsicOneParaGate): r""" - Ising ZZ gate. + Ising ZZ gate. More usage, please see :class:`mindquantum.core.gates.RX`. .. math:: {\rm ZZ_\theta}=\cos(\theta)I\otimes I-i\sin(\theta)\sigma_Z\otimes\sigma_Z - More usage, please see :class:`mindquantum.gate.RX`. + Args: + coeff (Union[int, float, str, dict, ParameterResolver]): the parameters of + parameterized gate, see above for detail explanation. Default: None. """ def __init__(self, coeff=None): IntrinsicOneParaGate.__init__(self, 'ZZ', coeff) @@ -485,7 +709,7 @@ class Power(NoneParameterGate): Power operator on a non parameterized gate. Args: - gates (:class:`mindquantum.gate.NoneParameterGate`): The basic gate you need to apply power operator. + gates (:class:`mindquantum.core.gates.NoneParameterGate`): The basic gate you need to apply power operator. t (int, float): The exponenet. Default: 0.5. Examples: @@ -499,17 +723,30 @@ class Power(NoneParameterGate): NoneParameterGate.__init__(self, '{}^{}'.format(gate.name, round(t, 2))) self.matrix_value = fractional_matrix_power(gate.matrix(), t) + self.hermitian_property = HERMITIAN_PROPERTIES['do_hermitian'] def define_projectq_gate(self): """Define the corresponded projectq gate.""" self.projectq_gate = None + def get_cpp_obj(self): + mat = mb.dim2matrix(self.matrix()) + cpp_gate = mb.basic_gate(False, self.name, self.hermitian_property, + mat) + cpp_gate.daggered = self.daggered + cpp_gate.obj_qubits = self.obj_qubits + cpp_gate.ctrl_qubits = self.ctrl_qubits + return cpp_gate + I = IGate() X = XGate() Y = YGate() Z = ZGate() H = HGate() -S = PhaseShift(np.pi / 2) +S = SGate() +T = TGate() SWAP = SWAPGate() +ISWAP = ISWAPGate() CNOT = CNOTGate() +BARRIER = BarrierGate(show=False) diff --git a/mindquantum/core/gates/measurement.py b/mindquantum/core/gates/measurement.py new file mode 100644 index 0000000000000000000000000000000000000000..bc418df1e8900da796129aa477b12252a49837cb --- /dev/null +++ b/mindquantum/core/gates/measurement.py @@ -0,0 +1,330 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""Basic module for quantum gate.""" +from collections.abc import Iterable +import numpy as np +from rich.console import Console +from mindquantum import mqbackend as mb +from mindquantum.io.display import measure_text_drawer +from .basic import NoneParameterGate + + +class Measure(NoneParameterGate): + """ + Measurement gate that measure quantum qubits. + + Args: + name (str): The key of this measurement gate. In a quantum circuit, the + key of different measurement gate should be unique. + + Examples: + >>> import numpy as np + >>> from mindquantum import qft, Circuit + >>> from mindquantum import Measure + >>> from mindquantum import Simulator + >>> circ = qft(range(2)) + >>> circ += Measure('q0').on(0) + >>> circ += Measure().on(1) + >>> circ + q0: ──H────PS(π/2)─────────@────M(q0)── + │ │ + q1: ──────────●───────H────@────M(q1)── + >>> sim = Simulator('projectq', circ.n_qubits) + >>> sim.apply_circuit(Circuit().h(0).x(1, 0)) + >>> sim + projectq simulator with 2 qubits. + Current quantum state: + √2/2¦00⟩ + √2/2¦11⟩ + >>> res = sim.sampling(circ, shots=2000) + >>> res + shots: 2000 + Keys: q1 q0│0.00 0.123 0.246 0.37 0.493 0.616 + ───────────┼───────────┴───────────┴───────────┴───────────┴───────────┴ + 00│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ + │ + 10│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ + │ + 11│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ + │ + {'00': 986, '10': 517, '11': 497} + >>> sim + projectq simulator with 2 qubits. + Current quantum state: + √2/2¦00⟩ + √2/2¦11⟩ + >>> sim.apply_circuit(circ[:-2]) + >>> sim + projectq simulator with 2 qubits. + Current quantum state: + √2/2¦00⟩ + (√2/4-√2/4j)¦10⟩ + (√2/4+√2/4j)¦11⟩ + >>> np.abs(sim.get_qs())**2 + array([0.5 , 0. , 0.25, 0.25]) + """ + def __init__(self, name=""): + self.key = name + NoneParameterGate.__init__(self, name) + self.name = 'M' + + def get_cpp_obj(self): + out = mb.get_measure_gate(self.key) + out.obj_qubits = self.obj_qubits + return out + + def __str__(self): + info = "" + if self.key and self.obj_qubits: + info = f'({self.obj_qubits[0]}, key={self.key})' + elif self.key: + info = f'(key={self.key})' + elif self.obj_qubits: + info = f'({self.obj_qubits[0]})' + return f"Measure{info}" + + def __repr__(self): + return self.__str__() + + def on(self, obj_qubits, ctrl_qubits=None): + """ + Apply this measurement gate on which qubit. + + Args: + obj_qubits (int): A non negative int that referring to its index number. + ctrl_qubits (int): Should be None for measure gate. + + Examples: + >>> from mindquantum import Circuit, Measure + >>> from mindquantum import Simulator + >>> sim = Simulator('projectq', 2) + >>> circ = Circuit().h(0).x(1, 0) + >>> circ += Measure('q0').on(0) + >>> circ += Measure('q1').on(1) + >>> circ + q0: ──H────●────M(q0)── + │ + q1: ───────X────M(q1)── + >>> res = sim.apply_circuit(circ) + >>> res + shots: 1 + Keys: q1 q0│0.00 0.2 0.4 0.6 0.8 1.0 + ───────────┼───────────┴───────────┴───────────┴───────────┴───────────┴ + 11│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ + │ + {'11': 1} + >>> sim + projectq simulator with 2 qubits. + Current quantum state: + 1¦11⟩ + """ + if ctrl_qubits is not None: + raise ValueError("Measure gate can not have control qubit") + if obj_qubits is None: + raise ValueError("The object qubit of measurement can not be none") + if not isinstance(obj_qubits, int): + raise ValueError("The object qubit of measurement must be a \ +non-negative integer referring to its index number") + if obj_qubits < 0: + raise ValueError("The object qubit of measurement must be a \ +non-negative integer referring to its index number") + new_gate = Measure(self.key) + new_gate.obj_qubits = [obj_qubits] + if not new_gate.key: + new_gate.key = f'q{obj_qubits}' + return new_gate + + def __hash__(self): + return hash(self.key) + + def __eq__(self, other): + if self.key == other.key: + return True + return False + + def hermitian(self): + """Hermitian gate of measure return its self""" + return self.__class__(self.key).on(self.obj_qubits[0]) + + def check_obj_qubits(self): + if not self.obj_qubits: + raise ValueError("Empty measure obj qubit") + if len(self.obj_qubits) > 1: + raise ValueError("Measure gate only apply on a single qubit") + + def define_projectq_gate(self): + raise NotImplementedError + + +class MeasureResult: + """ + Measurement result container + + Examples: + >>> from mindquantum import qft + >>> from mindquantum import Simulator + >>> sim = Simulator('projectq', 2) + >>> res = sim.sampling(qft(range(2)).measure_all(), shots=1000) + >>> res + shots: 1000 + Keys: q1 q0│0.00 0.065 0.131 0.196 0.261 0.326 + ───────────┼───────────┴───────────┴───────────┴───────────┴───────────┴ + 00│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ + │ + 01│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ + │ + 10│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ + │ + 11│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ + │ + {'00': 250, '01': 235, '10': 261, '11': 254} + >>> res.data + {'00': 250, '01': 235, '10': 261, '11': 254} + """ + def __init__(self): + self.measures = [] + self.keys = [] + self.samples = np.array([]) + self.bit_string_data = {} + self.shots = 0 + + def add_measure(self, measure): + """ + Add a measurement gate into this measurement result container. Measure key + should be unique in this measurement result container. + + Args: + measure (Union[Iterable, Measure]): One or more measure gates. + """ + if not isinstance(measure, Iterable): + measure = [measure] + for m in measure: + if not isinstance(m, Measure): + raise ValueError("Measurement gates need to \ +be objects of class 'Measurement' ") + for m in measure: + if m.key in self.keys: + raise ValueError(f"Measure key {m.key} already defined.") + self.measures.append(m) + self.keys.append(m.key) + + @property + def keys_map(self): + return {i: j for j, i in enumerate(self.keys)} + + def collect_data(self, samples): + """ + collect the measured bit string + + Args: + samples (numpy.ndarray): A two dimensional (N x M) numpy array that stores + the sampling bit string in 0 or 1, where N represents the number of shot + times, and M represents the number of keys in this measurement container + """ + self.samples = samples + out = {} + res = np.fliplr(self.samples) + self.shots = len(self.samples) + for s in res: + s = ''.join([str(i) for i in s]) + if s in out: + out[s] += 1 + else: + out[s] = 1 + keys = sorted(list(out.keys())) + self.bit_string_data = {key: out[key] for key in keys} + + def select_keys(self, *keys): + """ + Select certain measurement keys from this measurement container + + Args: + keys (tuple[str]): The key you want to select. + + Examples: + >>> from mindquantum import Simulator + >>> from mindquantum import qft, H + >>> circ = qft(range(2)).measure('q0_0', 0).measure('q1_0', 1) + >>> circ.h(0).measure('q0_1', 0) + >>> circ + q0: ──H────PS(π/2)─────────@────M(q0_0)────H────M(q0_1)── + │ │ + q1: ──────────●───────H────@────M(q1_0)────────────────── + >>> sim = Simulator('projectq', circ.n_qubits) + >>> res = sim.sampling(circ, shots=500) + >>> new_res = res.select_keys('q0_1', 'q1_0') + >>> new_res + shots: 500 + Keys: q1_0 q0_1│0.00 0.07 0.139 0.209 0.278 0.348 + ───────────────┼───────────┴───────────┴───────────┴───────────┴───────────┴ + 00│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ + │ + 01│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ + │ + 10│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ + │ + 11│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ + │ + {'00': 115, '01': 121, '10': 125, '11': 139} + """ + for key in keys: + if key not in self.keys: + raise ValueError(f'{key} not in this measure result.') + keys_map = self.keys_map + idx = [keys_map[key] for key in keys] + samples = self.samples[:, idx] + res = MeasureResult() + res.add_measure([self.measures[i] for i in idx]) + res.collect_data(samples) + return res + + @property + def data(self): + """ + Get the sampling data. + + Returns: + dict: The samping data. + """ + return self.bit_string_data + + def __str__(self): + return self.__repr__() + + def __repr__(self): + from mindquantum.io.display._config import _MEA_RES_STYLE + res = measure_text_drawer(self) + res.append(self.data.__str__()) + s = '\n'.join(res) + console = Console(record=True) + if not console.is_jupyter: + with console.capture() as capture: + console.print(s, style=_MEA_RES_STYLE['style']) + s = capture.get() + return s + + def _repr_html_(self): + """repr for jupyter notebook""" + from mindquantum.io.display._config import _MEA_RES_STYLE + from mindquantum.io.display._config import MEA_HTML_FORMAT + res = measure_text_drawer(self) + res.append(self.data.__str__()) + s = '\n'.join(res) + console = Console(record=True) + with console.capture() as _: + console.print(s, style=_MEA_RES_STYLE['style']) + s = console.export_html(code_format=MEA_HTML_FORMAT, inline_styles=True) + return '\n'.join(s.split('\n')[1:]) diff --git a/mindquantum/core/operators/__init__.py b/mindquantum/core/operators/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d76c7c220c52e565f655f2d051ecc30a0ba23528 --- /dev/null +++ b/mindquantum/core/operators/__init__.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +""" +MindQuantum operators library. An operator is composed of a combination of one or more basic gates. + +Contains classes representing: +- Qubit operators +- Fermion operators +- TimeEvolution operator + +""" +from mindquantum.third_party.interaction_operator import InteractionOperator +from .fermion_operator import FermionOperator +from .hamiltonian import Hamiltonian +from .polynomial_tensor import PolynomialTensor +from .projector import Projector +from .qubit_excitation_operator import QubitExcitationOperator +from .qubit_operator import QubitOperator +from .time_evolution import TimeEvolution +from .utils import count_qubits +from .utils import commutator +from .utils import normal_ordered +from .utils import get_fermion_operator +from .utils import number_operator +from .utils import hermitian_conjugated +from .utils import up_index +from .utils import down_index +from .utils import sz_operator + +__all__ = [ + "FermionOperator", "Hamiltonian", "PolynomialTensor", "Projector", + "QubitExcitationOperator", "QubitOperator", "TimeEvolution", + "count_qubits", "commutator", "normal_ordered", "get_fermion_operator", + "number_operator", "hermitian_conjugated", "up_index", "down_index", + "sz_operator" +] +__all__.append('InteractionOperator') +__all__.sort() diff --git a/mindquantum/ops/_base_operator.py b/mindquantum/core/operators/_base_operator.py similarity index 99% rename from mindquantum/ops/_base_operator.py rename to mindquantum/core/operators/_base_operator.py index c5a5461ad055056f649da4b93d034a18a6ac4cc5..e944170a322c58d0b2bde76bb5c1285a9448e513 100644 --- a/mindquantum/ops/_base_operator.py +++ b/mindquantum/core/operators/_base_operator.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Portions Copyright 2021 Huawei Technologies Co., Ltd # Portions Copyright 2017 The OpenFermion Developers. # Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,7 +22,7 @@ import copy from abc import ABCMeta, abstractmethod import numpy as np -from mindquantum.parameterresolver import ParameterResolver as PR +from mindquantum.core.parameterresolver import ParameterResolver as PR EQ_TOLERANCE = 1e-8 @@ -130,7 +131,7 @@ class _Operator(metaclass=ABCMeta): return () if isinstance(terms[0], int): self._validate_term(tuple(terms)) - return (terms,) + return (terms, ) for sub_term in terms: self._validate_term(sub_term) diff --git a/mindquantum/ops/fermion_operator.py b/mindquantum/core/operators/fermion_operator.py similarity index 80% rename from mindquantum/ops/fermion_operator.py rename to mindquantum/core/operators/fermion_operator.py index 0084d32543fc397e2c794ddb165e4d5122f7ca9f..837cf7acf3b7d65f4d24005ef16a68142529c1fd 100644 --- a/mindquantum/ops/fermion_operator.py +++ b/mindquantum/core/operators/fermion_operator.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Portions Copyright 2021 Huawei Technologies Co., Ltd # Portions Copyright 2017 The OpenFermion Developers. # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,7 +15,7 @@ # ============================================================================ """This module is generated the Fermion Operator""" -from mindquantum.parameterresolver import ParameterResolver as PR +from mindquantum.core.parameterresolver import ParameterResolver as PR from ._base_operator import _Operator @@ -22,24 +23,19 @@ def _check_valid_fermion_operator_term(term): """Check valid fermion operator term.""" if term is not None and term != '': if not isinstance(term, (str, tuple)): - raise ValueError( - 'Fermion operator requires a string or a tuple, but get {}'. - format(type(term))) + raise ValueError('Fermion operator requires a string or a tuple, but get {}'.format(type(term))) if isinstance(term, str): terms = term.split(' ') for t in terms: - if (t.endswith('^') - and not t[:-1].isdigit()) or (not t.endswith('^') - and not t.isdigit()): + if (t.endswith('^') and not t[:-1].isdigit()) or (not t.endswith('^') and not t.isdigit()): if t: - raise ValueError( - 'Invalid fermion operator term {}'.format(t)) + raise ValueError('Invalid fermion operator term {}'.format(t)) if isinstance(term, tuple): for t in term: - if len(t) != 2 or not isinstance(t[0], int) or not isinstance( - t[1], int) or t[0] < 0 or t[1] not in [0, 1]: - raise ValueError( - 'Invalid fermion operator term {}'.format(t)) + if len(t) != 2 or not isinstance(t[0], int) or not isinstance(t[1], int) or t[0] < 0 or t[1] not in [ + 0, 1 + ]: + raise ValueError('Invalid fermion operator term {}'.format(t)) class FermionOperator(_Operator): @@ -56,19 +52,21 @@ class FermionOperator(_Operator): coefficient for the corresponding single operators Default: 1.0. Examples: - >>> from mindquantum.ops import FermionOperator + >>> from mindquantum.core.operators import FermionOperator >>> a_p_dagger = FermionOperator('1^') >>> a_p_dagger 1.0 [1^] >>> a_q = FermionOperator('0') >>> a_q 1.0 [0] - >>> zero= FermionOperator() - >>> 0 + >>> zero = FermionOperator() + >>> zero + 0 >>> identity= FermionOperator('') - >>> 1.0 [] - >>> # check with coefficient + >>> identity + 1.0 [] >>> para_op = FermionOperator('0 1^', 'x') + >>> para_op x [0 1^] >>> para_dt = {'x':2} >>> op = para_op.subs(para_dt) @@ -127,15 +125,12 @@ class FermionOperator(_Operator): index = int(sub_term) if operator not in self.operators: - raise ValueError( - 'Invalid type of operator {}.' - 'The Fermion operator should be one of this {}'.format( - operator, self.operators)) + raise ValueError('Invalid type of operator {}.' + 'The Fermion operator should be one of this {}'.format(operator, self.operators)) if index < 0: raise ValueError("Invalid index {}.The qubit index should be\ non negative integer".format(self.operators)) - terms_to_tuple.append( - (index, map_operator_to_integer_rep(operator))) + terms_to_tuple.append((index, map_operator_to_integer_rep(operator))) # check the commutate terms with same index in the list and # replace it with the corresponding commutation relationship return tuple(terms_to_tuple) @@ -151,8 +146,7 @@ class FermionOperator(_Operator): for term, coeff in sorted(self.terms.items()): term_cnt += 1 if isinstance(coeff, PR): - tmp_string = '{} ['.format( - coeff.expression()) # begin of the '[' + tmp_string = '{} ['.format(coeff.expression()) # begin of the '[' else: tmp_string = '{} ['.format(coeff) # begin of the '[' # deal with this situation (1,'X') or [1, 'X'] @@ -165,23 +159,19 @@ class FermionOperator(_Operator): elif isinstance(term[0], int): index, operator = term if operator in self.operators: - tmp_string += '{}{} '.format(index, - self.operators[operator]) + tmp_string += '{}{} '.format(index, self.operators[operator]) else: for sub_term in term: index, operator = sub_term # check validity, if checked before, # then we can take away this step if operator in self.operators: - tmp_string += '{}{} '.format(index, - self.operators[operator]) + tmp_string += '{}{} '.format(index, self.operators[operator]) if term_cnt < len(self.terms): - string_rep += '{}] +\n'.format( - tmp_string.strip()) # end of the ']' + string_rep += '{}] +\n'.format(tmp_string.strip()) # end of the ']' else: - string_rep += '{}] '.format( - tmp_string.strip()) # end of the ']' + string_rep += '{}] '.format(tmp_string.strip()) # end of the ']' return string_rep @@ -197,7 +187,7 @@ class FermionOperator(_Operator): FermionOperator, the imag part of this fermion operator. Examples: - >>> from mindquantum.ops import FermionOperator + >>> from mindquantum.core.operators import FermionOperator >>> f = FermionOperator('0', 1 + 2j) + FermionOperator('0^', 'a') >>> f.imag.compress() 2.0 [0] @@ -217,7 +207,7 @@ class FermionOperator(_Operator): FermionOperator, the real part of this fermion operator. Examples: - >>> from mindquantum.ops import FermionOperator + >>> from mindquantum.core.operators import FermionOperator >>> f = FermionOperator('0', 1 + 2j) + FermionOperator('0^', 'a') >>> f.real.compress() 1.0 [0] + @@ -236,7 +226,7 @@ class FermionOperator(_Operator): FermionOperator, the normal ordered FermionOperator. Exmples: - >>> from mindquantum.ops import FermionOperator + >>> from mindquantum.core.operators import FermionOperator >>> origin = FermionOperator('0 1^') >>> origin 1.0 [0 1^] @@ -272,8 +262,7 @@ def _normal_ordered_term(term, coefficient): # And generate the new term if left_sub_term[0] == right_sub_term[0]: new_term = term[:(j - 1)] + term[(j + 1):] - ordered_term += _normal_ordered_term( - new_term, -1 * coefficient) + ordered_term += _normal_ordered_term(new_term, -1 * coefficient) elif left_sub_term[1] == right_sub_term[1]: # If indice are same,evaluate it to zero. if left_sub_term[0] == right_sub_term[0]: @@ -284,8 +273,7 @@ def _normal_ordered_term(term, coefficient): coefficient = -1 * coefficient # Add the term and return. - ordered_term += FermionOperator(_fermion_tuple_to_string(tuple(term)), - coefficient) + ordered_term += FermionOperator(_fermion_tuple_to_string(tuple(term)), coefficient) return ordered_term diff --git a/mindquantum/core/operators/hamiltonian.py b/mindquantum/core/operators/hamiltonian.py new file mode 100644 index 0000000000000000000000000000000000000000..75d90cd8764a33cfad2d9b96eec903dfbf526a73 --- /dev/null +++ b/mindquantum/core/operators/hamiltonian.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""Hamiltonian module.""" + +import scipy.sparse as sp +import numpy as np +from projectq.ops import QubitOperator as pq_operator +from openfermion.ops import QubitOperator as of_operator +from mindquantum import mqbackend as mb + +MODE = {'origin': 0, 'backend': 1, 'frontend': 2} +EDOM = {0: 'origin', 1: 'backend', 2: 'frontend'} + + +class Hamiltonian: + """ + A QubitOperator hamiltonian wrapper. + + Args: + hamiltonian (QubitOperator): The pauli word qubit operator. + + Examples: + >>> from mindquantum.core.operators import QubitOperator + >>> from mindquantum import Hamiltonian + >>> ham = Hamiltonian(QubitOperator('Z0 Y1', 0.3)) + """ + def __init__(self, hamiltonian): + from mindquantum.core.operators import QubitOperator as hiq_operator + from mindquantum.core.operators.utils import count_qubits + support_type = (pq_operator, of_operator, hiq_operator, sp.csr_matrix) + if not isinstance(hamiltonian, support_type): + raise TypeError("Require a QubitOperator or a csr_matrix, but get {}!".format(type(hamiltonian))) + if isinstance(hamiltonian, sp.csr_matrix): + if len(hamiltonian.shape) != 2 or hamiltonian.shape[0] != hamiltonian.shape[1]: + raise ValueError( + f"Hamiltonian requires a two dimension square csr_matrix, but get shape {hamiltonian.shape}") + if np.log2(hamiltonian.shape[0]) % 1 != 0: + raise ValueError(f"size of hamiltonian sparse matrix should be power of 2, but get {hamiltonian.shape}") + self.hamiltonian = hiq_operator('') + self.sparse_mat = hamiltonian + self.how_to = MODE['frontend'] + self.n_qubits = int(np.log2(self.sparse_mat.shape[0])) + else: + self.hamiltonian = hamiltonian + self.sparse_mat = sp.csr_matrix(np.eye(2, dtype=np.complex64)) + self.how_to = MODE['origin'] + self.n_qubits = count_qubits(hamiltonian) + self.ham_termlist = [(i, j) for i, j in self.hamiltonian.terms.items()] + + def __str__(self): + return self.hamiltonian.__str__() + + def __repr__(self): + return self.hamiltonian.__repr__() + + def sparse(self, n_qubits=1): + """ + Calculate the sparse matrix of this hamiltonian in pqc operator + + Args: + n_qubits (int): The total qubit of this hamiltonian, only need when mode is + 'frontend'. Default: 1. + """ + if EDOM[self.how_to] != 'origin': + raise ValueError('Already a sparse hamiltonian.') + if n_qubits < self.n_qubits: + raise ValueError(f"Can not sparse a {self.n_qubits} qubits hamiltonian to {n_qubits} hamiltonian.") + self.n_qubits = n_qubits + self.how_to = MODE['backend'] + return self + + def get_cpp_obj(self, hermitian=False): + """ + get_cpp_obj + + Args: + hermitian (bool): Whether to get the cpp object of this hamiltonian in hermitian version. + """ + if not hermitian: + if not hasattr(self, 'ham_cpp'): + if self.how_to == MODE['origin']: + ham = mb.hamiltonian(self.ham_termlist) + elif self.how_to == MODE['backend']: + ham = mb.hamiltonian(self.ham_termlist, self.n_qubits) + else: + dim = self.sparse_mat.shape[0] + nnz = self.sparse_mat.nnz + csr_mat = mb.csr_hd_matrix(dim, nnz, self.sparse_mat.indptr, self.sparse_mat.indices, + self.sparse_mat.data) + ham = mb.hamiltonian(csr_mat, self.n_qubits) + self.ham_cpp = ham + return self.ham_cpp + if self.how_to == MODE['backend'] or self.how_to == MODE['origin']: + return self.get_cpp_obj() + if not hasattr(self, 'herm_ham_cpp'): + herm_sparse_mat = self.sparse_mat.conjugate().T.tocsr() + dim = herm_sparse_mat.shape[0] + nnz = herm_sparse_mat.nnz + csr_mat = mb.csr_hd_matrix(dim, nnz, herm_sparse_mat.indptr, herm_sparse_mat.indices, herm_sparse_mat.data) + self.herm_ham_cpp = mb.hamiltonian(csr_mat, self.n_qubits) + return self.herm_ham_cpp + + +__all__ = ['Hamiltonian'] diff --git a/mindquantum/ops/polynomial_tensor.py b/mindquantum/core/operators/polynomial_tensor.py similarity index 99% rename from mindquantum/ops/polynomial_tensor.py rename to mindquantum/core/operators/polynomial_tensor.py index e1acf69b84907f35160f1d7cd308ef09ba9a2c9c..8edf55748368ea3cde73022d136e7cbcf1612c8a 100644 --- a/mindquantum/ops/polynomial_tensor.py +++ b/mindquantum/core/operators/polynomial_tensor.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Portions Copyright (c) 2020 Huawei Technologies Co.,ltd. # Portions Copyright 2017 The OpenFermion Developers. # @@ -67,7 +68,7 @@ class PolynomialTensor: Examples: >>> import numpy as np - >>> from mindquantum.ops import PolynomialTensor + >>> from mindquantum.core.operators import PolynomialTensor >>> constant = 1 >>> one_body_term = np.array([[1,0],[0,1]]) >>> two_body_term = two_body_term = np.array([[[[1,0],[0,1]],[[1,0],[0,1]]],[[[1,0],[0,1]],[[1,0],[0,1]]]]) @@ -453,7 +454,7 @@ class PolynomialTensor: the divisor is 0.'.format(type(self))) return quotient - # ba careful with this function + # be careful with this function def __div__(self, divisor): """ For compatibility with Python 2. """ return self.__truediv__(divisor) @@ -491,3 +492,6 @@ class PolynomialTensor: def __repr__(self): return str(self) + + +__all__ = ['PolynomialTensor'] diff --git a/mindquantum/gate/projector.py b/mindquantum/core/operators/projector.py similarity index 83% rename from mindquantum/gate/projector.py rename to mindquantum/core/operators/projector.py index 8bbcc995e7fafbbbcc0163073d6c848557534a92..e826716c66b1788db97f647dc8d0d9ba41239cac 100644 --- a/mindquantum/gate/projector.py +++ b/mindquantum/core/operators/projector.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,9 +23,7 @@ def _check_projector_str(proj): raise TypeError(f"Projector requires a string, but get {type(proj)}!") for i in proj: if i not in ['0', '1', 'I']: - raise ValueError( - f"Unkonw character '{i}' for a projector. Projector must onstructed by '0', '1' and 'I'." - ) + raise ValueError(f"Unkonw character '{i}' for a projector. Projector must onstructed by '0', '1' and 'I'.") class Projector: @@ -45,7 +44,7 @@ class Projector: proj (str): The string format of the projector. Examples: - >>> from mindquantum.gate import Projector + >>> from mindquantum.core.operators import Projector >>> p = Projector('II010') >>> p I2 ⊗ ¦010⟩⟨010¦ @@ -67,10 +66,3 @@ class Projector: def __repr__(self): return self.__str__() - - def mindspore_data(self): - """ - Generate projector information for PQC operator. - """ - m_data = {'projectors': self.proj} - return m_data diff --git a/mindquantum/ops/qubit_excitation_operator.py b/mindquantum/core/operators/qubit_excitation_operator.py similarity index 82% rename from mindquantum/ops/qubit_excitation_operator.py rename to mindquantum/core/operators/qubit_excitation_operator.py index 06fe38be3cc265610fcd19f9466027520d37bba5..856e6fa569d088b3fc770d8725e1dcb43c30666d 100644 --- a/mindquantum/ops/qubit_excitation_operator.py +++ b/mindquantum/core/operators/qubit_excitation_operator.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,9 +15,9 @@ # ============================================================================ """This module implements qubit-excitation operators""" -from mindquantum.ops.fermion_operator import FermionOperator -from mindquantum.ops.qubit_operator import QubitOperator -from mindquantum.parameterresolver import ParameterResolver as PR +from mindquantum.core.operators.fermion_operator import FermionOperator +from mindquantum.core.operators.qubit_operator import QubitOperator +from mindquantum.core.parameterresolver import ParameterResolver as PR from ._base_operator import _Operator @@ -24,25 +25,20 @@ def _check_valid_qubit_excitation_operator_term(term): """Check valid qubit excitation operator term.""" if term is not None and term != '': if not isinstance(term, (str, tuple)): - raise ValueError( - 'Qubit excitation operator requires a string or a tuple, \ + raise ValueError('Qubit excitation operator requires a string or a tuple, \ but get {}'.format(type(term))) if isinstance(term, str): terms = term.split(' ') for t in terms: - if (t.endswith('^') - and not t[:-1].isdigit()) or (not t.endswith('^') - and not t.isdigit()): + if (t.endswith('^') and not t[:-1].isdigit()) or (not t.endswith('^') and not t.isdigit()): if t: - raise ValueError( - 'Invalid qubit excitation operator term {}'.format( - t)) + raise ValueError('Invalid qubit excitation operator term {}'.format(t)) if isinstance(term, tuple): for t in term: - if len(t) != 2 or not isinstance(t[0], int) or not isinstance( - t[1], int) or t[0] < 0 or t[1] not in [0, 1]: - raise ValueError( - 'Invalid qubit excitation operator term {}'.format(t)) + if len(t) != 2 or not isinstance(t[0], int) or not isinstance(t[1], int) or t[0] < 0 or t[1] not in [ + 0, 1 + ]: + raise ValueError('Invalid qubit excitation operator term {}'.format(t)) class QubitExcitationOperator(_Operator): @@ -64,9 +60,8 @@ class QubitExcitationOperator(_Operator): coefficient for the corresponding single operators Default: 1.0. Examples: - >>> from mindquantum.hiqfermion.transforms import Transform - >>> from mindquantum.ops import QubitExcitationOperator - >>> from mindquantum.hiqfermion.transforms import Transform + >>> from mindquantum.algorithm.nisq.chem import Transform + >>> from mindquantum.core.operators import QubitExcitationOperator >>> op = QubitExcitationOperator(((4, 1), (1, 0), (0, 0)), 2.5) >>> op 2.5 [Q4^ Q1 Q0] @@ -119,7 +114,7 @@ class QubitExcitationOperator(_Operator): according to the definition of Qubit excitation operators. Examples: - >>> from mindquantum.ops import QubitExcitationOperator + >>> from mindquantum.core.operators import QubitExcitationOperator >>> op = QubitExcitationOperator("7^ 1") >>> op.to_qubit_operator() 0.25 [X1 X7] + @@ -178,15 +173,13 @@ class QubitExcitationOperator(_Operator): index = int(sub_term) if operator not in self.operators: - raise ValueError( - 'Invalid type of operator {}.' - 'The Qubit excitation operator should be one of this {}'. - format(operator, self.operators)) + raise ValueError('Invalid type of operator {}.' + 'The Qubit excitation operator should be one of this {}'.format( + operator, self.operators)) if index < 0: raise ValueError("Invalid index {}.The qubit index should be\ non negative integer".format(self.operators)) - terms_to_tuple.append( - (index, map_operator_to_integer_rep(operator))) + terms_to_tuple.append((index, map_operator_to_integer_rep(operator))) # check the commutate terms with same index in the list and # replace it with the corresponding commutation relationship return tuple(terms_to_tuple) @@ -203,8 +196,7 @@ class QubitExcitationOperator(_Operator): for term, coeff in sorted(self.terms.items()): term_cnt += 1 if isinstance(coeff, PR): - tmp_string = '{} ['.format( - coeff.expression()) # begin of the '[' + tmp_string = '{} ['.format(coeff.expression()) # begin of the '[' else: tmp_string = '{} ['.format(coeff) # begin of the '[' # deal with this situation (1,'X') or [1, 'X'] @@ -217,23 +209,19 @@ class QubitExcitationOperator(_Operator): elif isinstance(term[0], int): index, operator = term if operator in self.operators: - tmp_string += 'Q{}{} '.format(index, - self.operators[operator]) + tmp_string += 'Q{}{} '.format(index, self.operators[operator]) else: for sub_term in term: index, operator = sub_term # check validity, if checked before, # then we can take away this step if operator in self.operators: - tmp_string += 'Q{}{} '.format(index, - self.operators[operator]) + tmp_string += 'Q{}{} '.format(index, self.operators[operator]) if term_cnt < len(self.terms): - string_rep += '{}] +\n'.format( - tmp_string.strip()) # end of the ']' + string_rep += '{}] +\n'.format(tmp_string.strip()) # end of the ']' else: - string_rep += '{}] '.format( - tmp_string.strip()) # end of the ']' + string_rep += '{}] '.format(tmp_string.strip()) # end of the ']' return string_rep @@ -249,7 +237,7 @@ class QubitExcitationOperator(_Operator): QubitExcitationOperator, the image part of this qubit excitation operator. Examples: - >>> from mindquantum.ops import QubitExcitationOperator + >>> from mindquantum.core.operators import QubitExcitationOperator >>> f = QubitExcitationOperator(((1, 0),), 1 + 2j) >>> f += QubitExcitationOperator(((1, 1),), 'a') >>> f.imag.compress() @@ -270,7 +258,7 @@ class QubitExcitationOperator(_Operator): QubitExcitationOperator, the real part of this qubit excitation operator. Examples: - >>> from mindquantum.ops import QubitExcitationOperator + >>> from mindquantum.core.operators import QubitExcitationOperator >>> f = QubitExcitationOperator(((1, 0),), 1 + 2j) >>> f += QubitExcitationOperator(((1, 1),), 'a') >>> f.real.compress() @@ -290,7 +278,7 @@ class QubitExcitationOperator(_Operator): QubitExcitationOperator, the normal ordered operator. Examples: - >>> from mindquantum.ops import QubitExcitationOperator + >>> from mindquantum.core.operators import QubitExcitationOperator >>> op = QubitExcitationOperator("7 1^") >>> op 1.0 [Q7 Q1^] @@ -338,8 +326,7 @@ def _normal_ordered_term(term, coefficient): term[j], term[j - 1] = left_sub_term, right_sub_term # Add the term and return. - ordered_term += QubitExcitationOperator( - _qubit_excitation_tuple_to_string(tuple(term)), coefficient) + ordered_term += QubitExcitationOperator(_qubit_excitation_tuple_to_string(tuple(term)), coefficient) return ordered_term @@ -351,3 +338,6 @@ def _qubit_excitation_tuple_to_string(term): else: s.append(str(i[0])) return ' '.join(s) + + +__all__ = ['QubitExcitationOperator'] diff --git a/mindquantum/ops/qubit_operator.py b/mindquantum/core/operators/qubit_operator.py similarity index 96% rename from mindquantum/ops/qubit_operator.py rename to mindquantum/core/operators/qubit_operator.py index b4c9c0e0cfec9087f3c2d7c6634576ad719bb242..39d38ba6bd8efbe726c3d4af83509b83fdaf2d50 100644 --- a/mindquantum/ops/qubit_operator.py +++ b/mindquantum/core/operators/qubit_operator.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Portions Copyright (c) 2020 Huawei Technologies Co.,ltd. # Portions Copyright 2017 The OpenFermion Developers. @@ -18,7 +19,7 @@ # Apache 2.0 license. """This is the module for the Qubit Operator. """ -from mindquantum.parameterresolver import ParameterResolver as PR +from mindquantum.core.parameterresolver import ParameterResolver as PR from ._base_operator import _Operator EQ_TOLERANCE = 1e-8 @@ -94,10 +95,9 @@ class QubitOperator(_Operator): represent by a string or a symbol or a parameter resolver. Default: 1.0. Examples: - >>> from mindquantum.ops import QubitOperator + >>> from mindquantum.core.operators import QubitOperator >>> ham = ((QubitOperator('X0 Y3', 0.5) - + 0.6 * QubitOperator('X0 Y3'))) - # Equivalently + ... + 0.6 * QubitOperator('X0 Y3'))) >>> ham2 = QubitOperator('X0 Y3', 0.5) >>> ham2 += 0.6 * QubitOperator('X0 Y3') >>> ham2 @@ -180,7 +180,7 @@ class QubitOperator(_Operator): QubitOperator, the real part of this qubit operator. Examples: - >>> from mindquantum.ops import QubitOperator + >>> from mindquantum.core.operators import QubitOperator >>> f = QubitOperator('X0', 1 + 2j) + QubitOperator('Y0', 'a') >>> f.real.compress() 1.0 [X0] + @@ -201,7 +201,7 @@ class QubitOperator(_Operator): QubitOperator, the imag part of this qubit operator. Examples: - >>> from mindquantum.ops import QubitOperator + >>> from mindquantum.core.operators import QubitOperator >>> f = QubitOperator('X0', 1 + 2j) + QubitOperator('Y0', 'a') >>> f.imag.compress() 2.0 [X0] @@ -293,3 +293,6 @@ class QubitOperator(_Operator): def __repr__(self): return str(self) + + +__all__ = ['QubitOperator'] diff --git a/mindquantum/circuit/time_evolution.py b/mindquantum/core/operators/time_evolution.py similarity index 52% rename from mindquantum/circuit/time_evolution.py rename to mindquantum/core/operators/time_evolution.py index 8fbed0dea799aa0e8ab825810acb015de0d14cd0..0625e4b782074d6da44467313966cf3372412421 100644 --- a/mindquantum/circuit/time_evolution.py +++ b/mindquantum/core/operators/time_evolution.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,10 +15,9 @@ # ============================================================================ """Circuit for time evolution.""" -from mindquantum.ops import QubitOperator -from mindquantum.parameterresolver import ParameterResolver -from .circuit import Circuit -from .uccsd import decompose_single_term_time_evolution +from mindquantum.core.operators import QubitOperator +from mindquantum.core.parameterresolver import ParameterResolver +from mindquantum.core.circuit.utils import decompose_single_term_time_evolution class TimeEvolution: @@ -28,7 +28,7 @@ class TimeEvolution: .. math:: - \left|\varphi(t)\right>=e^{-itH}\left|\varphi(0)\right> + \left|\varphi(t)\right>=e^{-iHt}\left|\varphi(0)\right> Note: The hamiltonian should be a parameterized or non parameterized @@ -43,17 +43,28 @@ class TimeEvolution: Default: None. Examples: - >>> from mindquantum.circuit import TimeEvolution - >>> from mindquantum.ops import QubitOperator - >>> h = QubitOperator('Z0 Z1', 'p') - >>> TimeEvolution(h).circuit - X(1 <-: 0) - RZ(2*p|1) - X(1 <-: 0) + >>> from mindquantum.core.operators import TimeEvolution, QubitOperator + >>> q1 = QubitOperator('Z0 Y1', 'a') + >>> q2 = QubitOperator('X0 Z1', 'b') + >>> ops1 = q1 + q2 + >>> ops2 = q2 + q1 + >>> TimeEvolution(ops1).circuit + q0: ─────────────●───────────────●───────H────────●───────────────●────H── + │ │ │ │ + q1: ──RX(π/2)────X────RZ(2*a)────X────RX(7π/2)────X────RZ(2*b)────X─────── + >>> TimeEvolution(ops2).circuit + q0: ──H────●───────────────●───────H───────●───────────────●────────────── + │ │ │ │ + q1: ───────X────RZ(2*b)────X────RX(π/2)────X────RZ(2*a)────X────RX(7π/2)── """ def __init__(self, ops: QubitOperator, time=None): + from mindquantum.utils.type_value_check import _num_type if time is None: time = 1 + if not isinstance(time, _num_type): + if not isinstance(time, (dict, ParameterResolver)): + raise TypeError(f"time requires a number or a dict or a ParameterResolver, but get {type(time)}") + time = ParameterResolver(time) self.time = time if isinstance(time, dict): self.time = ParameterResolver(time) @@ -62,6 +73,7 @@ class TimeEvolution: @property def circuit(self): """Get the first order trotter decomposition circuit of this time evolution operator.""" + from ..circuit import Circuit circ = Circuit() for k, v in self.ops.terms.items(): pr_tmp = self.time * v diff --git a/mindquantum/utils/utils_operator.py b/mindquantum/core/operators/utils.py similarity index 89% rename from mindquantum/utils/utils_operator.py rename to mindquantum/core/operators/utils.py index 3c16474bb3e6966d30c16327387fa0f3f1475c16..ab473e0ee9a335316529da83c3c2d7daeb6298c9 100644 --- a/mindquantum/utils/utils_operator.py +++ b/mindquantum/core/operators/utils.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Portions Copyright (c) 2020 Huawei Technologies Co.,ltd. # Portions Copyright 2017 The OpenFermion Developers. # @@ -16,10 +17,10 @@ import projectq.ops as pjops import openfermion.ops as ofops -from mindquantum.ops.fermion_operator import FermionOperator -from mindquantum.ops.qubit_operator import QubitOperator -from mindquantum.ops.qubit_excitation_operator import QubitExcitationOperator -from mindquantum.ops.polynomial_tensor import PolynomialTensor +from mindquantum.core.operators.fermion_operator import FermionOperator +from mindquantum.core.operators.qubit_operator import QubitOperator +from mindquantum.core.operators.qubit_excitation_operator import QubitExcitationOperator +from mindquantum.core.operators.polynomial_tensor import PolynomialTensor def count_qubits(operator): @@ -41,8 +42,8 @@ def count_qubits(operator): TypeError: Operator of invalid type. Examples: - >>> from mindquantum.ops import QubitOperator,FermionOperator - >>> from mindquantum.utils import count_qubits + >>> from mindquantum.core.operators import QubitOperator,FermionOperator + >>> from mindquantum.core.operators.utils import count_qubits >>> qubit_op = QubitOperator("X1 Y2") >>> count_qubits(qubit_op) 3 @@ -59,7 +60,7 @@ def count_qubits(operator): for term in operator.terms: # a tuple compose of single (qubit_index,operator) subterms if term == (): - qubit_index = (0,) + qubit_index = (0, ) else: qubit_index, _ = zip(*term) num_qubits = max(max(qubit_index) + 1, @@ -86,8 +87,8 @@ def commutator(left_operator, right_operator): TypeError: operator_a and operator_b are not of the same type. Examples: - >>> from mindquantum.ops import QubitOperator,FermionOperator - >>> from mindquantum.utils import commutator + >>> from mindquantum.core.operators import QubitOperator,FermionOperator + >>> from mindquantum.core.operators import commutator >>> qub_op1 = QubitOperator("X1 Y2") >>> qub_op2 = QubitOperator("X1 Z2") >>> commutator(qub_op1, qub_op1) @@ -162,8 +163,8 @@ def normal_ordered(fermion_operator): FermionOperator, the normal_ordered FermionOperator. Examples: - >>> from mindquantum.ops import FermionOperator - >>> from mindquantum.utils import normal_ordered + >>> from mindquantum.core.operators import FermionOperator + >>> from mindquantum.core.operators import normal_ordered >>> op = FermionOperator("3 4^", 'a') >>> normal_ordered(op) -a [4^ 3] @@ -179,6 +180,9 @@ def normal_ordered(fermion_operator): def get_fermion_operator(operator): """Convert the tensor (PolynomialTensor) to FermionOperator. + Args: + operator (PolynomialTensor): The `PolynomialTensor` you want to convert to `FermionOperator`. + Returns: fermion_operator, An instance of the FermionOperator class. """ @@ -206,8 +210,8 @@ def number_operator(n_modes=None, mode=None, coefficient=1.): FermionOperator, a fermionic number operator for the reverse_jordan_wigner transform. Examples: - >>> from mindquantum.ops import FermionOperator - >>> from mindquantum.utils import number_operator + >>> from mindquantum.core.operators import FermionOperator + >>> from mindquantum.core.operators import number_operator >>> nmode = 3 >>> number_operator(nmode) 1.0 [0^ 0] + @@ -240,8 +244,8 @@ def hermitian_conjugated(operator): the hermitian form of the input operator. Examples: - >>> from mindquantum.ops import QubitOperator - >>> from mindquantum.utils import hermitian_conjugated + >>> from mindquantum.core.operators import QubitOperator + >>> from mindquantum.core.operators import hermitian_conjugated >>> q = QubitOperator('X0', {'a' : 2j}) >>> hermitian_conjugated(q) -2.0*I*a [X0] @@ -290,7 +294,7 @@ def up_index(index): An integer that is the index of the associated spin-up orbital. Examples: - >>> from mindquantum.utils import up_index + >>> from mindquantum.core.operators import up_index >>> up_index(1) 2 """ @@ -309,7 +313,7 @@ def down_index(index): An integer that is the index of the associated spin-down orbital. Examples: - >>> from mindquantum.utils import down_index + >>> from mindquantum.core.operators import down_index >>> down_index(1) 3 """ @@ -332,7 +336,7 @@ def sz_operator(n_spatial_orbitals): while the spin_down(beta) corresponds to odd index.rpartition() Examples: - >>> from mindquantum.utils import sz_operator + >>> from mindquantum.core.operators import sz_operator >>> sz_operator(3) 0.5 [0^ 0] + -0.5 [1^ 1] + diff --git a/mindquantum/parameterresolver/__init__.py b/mindquantum/core/parameterresolver/__init__.py similarity index 97% rename from mindquantum/parameterresolver/__init__.py rename to mindquantum/core/parameterresolver/__init__.py index 2718839ff0b402cd4f5fa547300f17c940cafddc..e777ba2e5667cf28a3ed93d46bc52c0012357787 100644 --- a/mindquantum/parameterresolver/__init__.py +++ b/mindquantum/core/parameterresolver/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,5 +17,4 @@ from .parameterresolver import ParameterResolver - __all__ = ["ParameterResolver"] diff --git a/mindquantum/parameterresolver/parameterresolver.py b/mindquantum/core/parameterresolver/parameterresolver.py similarity index 80% rename from mindquantum/parameterresolver/parameterresolver.py rename to mindquantum/core/parameterresolver/parameterresolver.py index 3b8874b702fd941ff7a5fe983c40b0347acc5b81..2a767ea14700bcfa5aa0b1f591ca2e7a833f3a57 100644 --- a/mindquantum/parameterresolver/parameterresolver.py +++ b/mindquantum/core/parameterresolver/parameterresolver.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +19,8 @@ from collections.abc import Iterable from copy import deepcopy import numpy as np import sympy as sp - +from mindquantum import mqbackend as mb +from mindquantum.utils.type_value_check import _num_type class ParameterResolver(dict): """ @@ -46,21 +48,19 @@ class ParameterResolver(dict): if data is None: data = {} if not isinstance(data, (dict, ParameterResolver)): - raise TypeError( - "Require a dict or a ParameterResolver, but get {}!".format( - type(data))) + raise TypeError("Require a dict or a ParameterResolver, but get {}!".format(type(data))) for k, v in data.items(): if not isinstance(k, str): - raise TypeError( - "Parameter name should be a string, but get {}!".format( - type(k))) + raise TypeError("Parameter name should be a string, but get {}!".format(type(k))) if not isinstance(v, _num_type): - raise TypeError( - "Require a number, but get {}, which is {}!".format( - v, type(v))) + raise TypeError("Require a number, but get {}, which is {}!".format(v, type(v))) super(ParameterResolver, self).__init__(data) self.no_grad_parameters = set() - self.requires_grad_parameters = set(self.para_name) + self.requires_grad_parameters = set(self.params_name) + + def get_cpp_obj(self): + """Get cpp obj of this parameter resolver""" + return mb.parameter_resolver(self, self.no_grad_parameters, self.requires_grad_parameters) def __setitem__(self, keys, values): """ @@ -78,9 +78,8 @@ class ParameterResolver(dict): """ if isinstance(keys, str): if not isinstance(values, _num_type): - raise TypeError( - "Parameter value should be a number, but get {}, which is {}!" - .format(values, type(values))) + raise TypeError("Parameter value should be a number, but get {}, which is {}!".format( + values, type(values))) super().__setitem__(keys, values) self.requires_grad_parameters.add(keys) elif isinstance(keys, Iterable): @@ -91,9 +90,7 @@ class ParameterResolver(dict): for i, k in enumerate(keys): self.__setitem__(k, values[i]) else: - raise TypeError( - "Parameter name should be a string, but get {}!".format( - type(keys))) + raise TypeError("Parameter name should be a string, but get {}!".format(type(keys))) def __add__(self, pr): """ @@ -113,8 +110,7 @@ class ParameterResolver(dict): 3*a + 3*b """ if not isinstance(pr, ParameterResolver): - raise ValueError( - 'Require a parameter resolver, but get {}.'.format(type(pr))) + raise ValueError('Require a parameter resolver, but get {}.'.format(type(pr))) res = self * 1 pr = pr * 1 for k, v in pr.items(): @@ -129,12 +125,13 @@ class ParameterResolver(dict): Subtraction a parameter resolver with other parameter. Returns: - :class:`mindquantum.parameterresolver.ParameterResolver` + :class:`mindquantum.core.parameterresolver.ParameterResolver` Args: pr (ParameterResolver): The parameter resolver need to subtract. Examples: + >>> from mindquantum import ParameterResolver >>> pr1 = ParameterResolver({'a': 1}) >>> pr2 = ParameterResolver({'a': 2, 'b': 3}) >>> (pr1 - pr2).expression() @@ -150,6 +147,7 @@ class ParameterResolver(dict): ParameterResolver, the negative version. Examples: + >>> from mindquantum import ParameterResolver >>> pr1 = ParameterResolver({'a': 1}) >>> (-pr1).expression() -a @@ -161,12 +159,13 @@ class ParameterResolver(dict): Parameter support inplace multiply. Returns: - :class:`mindquantum.parameterresolver.ParameterResolver` + :class:`mindquantum.core.parameterresolver.ParameterResolver` Args: num (number): Multiply factor. Examples: + >>> from mindquantum import ParameterResolver >>> pr = ParameterResolver({'a': 1, 'b': 2}) >>> pr *= 2 >>> pr @@ -185,12 +184,13 @@ class ParameterResolver(dict): Multiply num with every value of parameter resolver. Returns: - :class:`mindquantum.parameterresolver.ParameterResolver` + :class:`mindquantum.core.parameterresolver.ParameterResolver` Args: num (number): Multiply factor. Examples: + >>> from mindquantum import ParameterResolver >>> pr1 = ParameterResolver({'a': 1, 'b': 2}) >>> pr2 = pr1 * 2 >>> pr2 @@ -206,7 +206,7 @@ class ParameterResolver(dict): def __rmul__(self, num): """ - See :class:`mindquantum.parameterresolver.ParameterResolver.__mul__`. + See :class:`mindquantum.core.parameterresolver.ParameterResolver.__mul__`. """ return self.__mul__(num) @@ -217,7 +217,7 @@ class ParameterResolver(dict): return super().__eq__(other) and no_grad_eq and requires_grad_eq @property - def para_name(self): + def params_name(self): """ Get the parameters name. @@ -227,7 +227,7 @@ class ParameterResolver(dict): Examples: >>> from mindquantum import ParameterResolver >>> pr = ParameterResolver({'a': 1, 'b': 2}) - >>> pr.para_name + >>> pr.params_name ['a', 'b'] """ return list(self.keys()) @@ -265,7 +265,7 @@ class ParameterResolver(dict): {'a', 'b'} """ self.no_grad_parameters = set() - self.requires_grad_parameters = set(self.para_name) + self.requires_grad_parameters = set(self.params_name) return self def no_grad(self): @@ -282,7 +282,7 @@ class ParameterResolver(dict): >>> pr.requires_grad_parameters set() """ - self.no_grad_parameters = set(self.para_name) + self.no_grad_parameters = set(self.params_name) self.requires_grad_parameters = set() return self @@ -306,12 +306,9 @@ class ParameterResolver(dict): """ for name in names: if not isinstance(name, str): - raise TypeError("name should be a string, but get {}!".format( - type(name))) + raise TypeError("name should be a string, but get {}!".format(type(name))) if name not in self: - raise KeyError( - "Parameter {} not in this parameter resolver!".format( - name)) + raise KeyError("Parameter {} not in this parameter resolver!".format(name)) while name in self.no_grad_parameters: self.no_grad_parameters.remove(name) while name not in self.requires_grad_parameters: @@ -337,12 +334,9 @@ class ParameterResolver(dict): """ for name in names: if not isinstance(name, str): - raise TypeError("name should be a string, but get {}!".format( - type(name))) + raise TypeError("name should be a string, but get {}!".format(type(name))) if name not in self: - raise KeyError( - "Parameter {} not in this parameter resolver!".format( - name)) + raise KeyError("Parameter {} not in this parameter resolver!".format(name)) while name not in self.no_grad_parameters: self.no_grad_parameters.add(name) while name in self.requires_grad_parameters: @@ -358,7 +352,7 @@ class ParameterResolver(dict): Raises: ValueError: If some parameters require grad and not require grad in - other parameter resolver and vise versa. + other parameter resolver and vice versa. Examples: >>> from mindquantum import ParameterResolver @@ -373,35 +367,14 @@ class ParameterResolver(dict): """ _check_pr_type(others) super().update(others) - conflict = (self.no_grad_parameters & others.requires_grad_parameters - ) | (others.no_grad_parameters - & self.requires_grad_parameters) + conflict = (self.no_grad_parameters & others.requires_grad_parameters) | (others.no_grad_parameters + & self.requires_grad_parameters) if conflict: - raise ValueError( - "Parameter conflict, {} require grad in some parameter \ + raise ValueError("Parameter conflict, {} require grad in some parameter \ resolver and not require grad in other parameter resolver ".format(conflict)) self.no_grad_parameters.update(others.no_grad_parameters) self.requires_grad_parameters.update(others.requires_grad_parameters) - def mindspore_data(self): - """ - Generate data for PQC operator. - - Returns: - Dict. - """ - m_data = { - 'gate_params_names': [], - 'gate_coeff': [], - 'gate_requires_grad': [] - } - for k, v in self.items(): - m_data['gate_params_names'].append(k) - m_data['gate_coeff'].append(float(v)) - m_data['gate_requires_grad'].append( - k in self.requires_grad_parameters) - return m_data - def expression(self): """ Get the expression of this parameter resolver. @@ -410,7 +383,7 @@ resolver and not require grad in other parameter resolver ".format(conflict)) sympy.Expr, the symbol expression of this parameter resolver. Examples: - >>> from mindquantum.parameterresolver import ParameterResolver as PR + >>> from mindquantum.core.parameterresolver import ParameterResolver as PR >>> pr = PR({'a' : 2, 'b' : 0.3}) >>> pr.expression() 2*a + 0.3*b @@ -428,7 +401,7 @@ resolver and not require grad in other parameter resolver ".format(conflict)) ParameterResolver, the conjugate version of this parameter resolver. Examples: - >>> from mindquantum.parameterresolver import ParameterResolver as PR + >>> from mindquantum.core.parameterresolver import ParameterResolver as PR >>> pr = PR({'a' : 1, 'b': 1j}) >>> pr.conjugate().expression() a - 1.0*I*b @@ -450,15 +423,14 @@ resolver and not require grad in other parameter resolver ".format(conflict)) numbers.Number, the combination result. Examples: - >>> from mindquantum import ParameterResolver - >>> pr1 = ParameterResolver({'a': 1, 'b': 2}) - >>> pr2 = ParameterResolver({'a': 2, 'b': 3}) - >>> pr1.combination(pr2) + >>> from mindquantum import ParameterResolver + >>> pr1 = ParameterResolver({'a': 1, 'b': 2}) + >>> pr2 = ParameterResolver({'a': 2, 'b': 3}) + >>> pr1.combination(pr2) + 8 """ if not isinstance(pr, (ParameterResolver, dict)): - raise ValueError( - 'Require a parameter resolver or a dict, but get {}.'.format( - type(pr))) + raise ValueError('Require a parameter resolver or a dict, but get {}.'.format(type(pr))) res = 0 for k, v in self.items(): if k not in pr: @@ -475,7 +447,7 @@ resolver and not require grad in other parameter resolver ".format(conflict)) ParameterResolver, the real part of this parameter resolver. Examples: - >>> from mindquantum.parameterresolver import ParameterResolver as PR + >>> from mindquantum.core.parameterresolver import ParameterResolver as PR >>> pr = PR({'a': 1.2 + 1.3j}) >>> pr.real() {'a': 1.2} @@ -494,7 +466,7 @@ resolver and not require grad in other parameter resolver ".format(conflict)) ParameterResolver, the image part of this parameter resolver. Examples: - >>> from mindquantum.parameterresolver import ParameterResolver as PR + >>> from mindquantum.core.parameterresolver import ParameterResolver as PR >>> pr = PR({'a': 1.2 + 1.3j}) >>> pr.imag() {'a': 1.3} @@ -507,8 +479,4 @@ resolver and not require grad in other parameter resolver ".format(conflict)) def _check_pr_type(pr): if not isinstance(pr, ParameterResolver): - raise TypeError("Require a ParameterResolver, but get {}".format( - type(pr))) - - -_num_type = (int, float, complex, np.int32, np.int64, np.float32, np.float64) + raise TypeError("Require a ParameterResolver, but get {}".format(type(pr))) diff --git a/mindquantum/core/third_party/__init__.py b/mindquantum/core/third_party/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..5111cf3868282a78344dbd8b911172fc1a38b04f --- /dev/null +++ b/mindquantum/core/third_party/__init__.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Third-party modules for MindQuantum eDSL.""" + +import pkgutil +import importlib + +# Allow extending this namespace. +__path__ = pkgutil.extend_path(__path__, __name__) + +# Automatically import all submodules and bring their exported symbols (__all__) into our namespace +__all__ = [] +for (_, pkg_name, _) in pkgutil.iter_modules(path=__path__): + imported_module = importlib.import_module('.' + pkg_name, package=__name__) + + if hasattr(imported_module, '__all__'): + __all__.extend(imported_module.__all__) + for symbol_name in imported_module.__all__: + globals().setdefault(symbol_name, + getattr(imported_module, symbol_name)) + else: + for attr_name in dir(imported_module): + if attr_name[0] != '_': + __all__.append(attr_name) + globals().setdefault(attr_name, + getattr(imported_module, attr_name)) + +__all__.sort() diff --git a/mindquantum/engine/__init__.py b/mindquantum/engine/__init__.py index e8e5b87d9af0d14cb6b3e25b5683f77e5d983891..37a8b69417374f10d682f3829fb4bc3404d1819c 100644 --- a/mindquantum/engine/__init__.py +++ b/mindquantum/engine/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +19,5 @@ from .circuitengine import BasicQubit from .circuitengine import CircuitEngine from .circuitengine import circuit_generator - -__all__ = ['BasicQubit', 'CircuitEngine', 'circuit_generator'] - +__all__ = ["BasicQubit", "CircuitEngine", "circuit_generator"] __all__.sort() diff --git a/mindquantum/engine/circuitengine.py b/mindquantum/engine/circuitengine.py index 50e3fc601000715803f8c90cf830f978449a4e1e..a94ef125e6f10806267502c9927eb6f17c4c339e 100644 --- a/mindquantum/engine/circuitengine.py +++ b/mindquantum/engine/circuitengine.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,7 +15,7 @@ # ============================================================================ """Simple engine to generate parameterized quantum circuit.""" -from mindquantum.circuit import Circuit +from mindquantum.core.circuit import Circuit class BasicQubit: @@ -101,17 +102,18 @@ class CircuitEngine: n_qubits (int): qubit number of quantum circuit. Examples: - >>> import mindquantum.gate as G + >>> import mindquantum.core.gates as G >>> from mindquantum.engine import circuit_generator >>> @circuit_generator(2,prefix='p') >>> def ansatz(qubits, prefix): >>> G.X | (qubits[0], qubits[1]) >>> G.RX(prefix+'_0') | qubits[1] >>> print(ansatz) - X(1 <-: 0) - RX(p_0|1) + q0: ──●───────────── + │ + q1: ──X────RX(p_0)── >>> print(type(ansatz)) - + """ def deco(fn): eng = CircuitEngine() diff --git a/mindquantum/framework/__init__.py b/mindquantum/framework/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..81eaf16ddc4eb86b55f99bc77a9c10b3c79c13ed --- /dev/null +++ b/mindquantum/framework/__init__.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""Quantum neural networks operators and cells.""" +import warnings + +__all__ = [] +try: + import mindspore + from .layer import MQAnsatzOnlyLayer + from .layer import MQN2AnsatzOnlyLayer + from .layer import MQLayer + from .layer import MQN2Layer + from .operations import MQOps + from .operations import MQN2Ops + from .operations import MQAnsatzOnlyOps + from .operations import MQN2AnsatzOnlyOps + from .operations import MQEncoderOnlyOps + from .operations import MQN2EncoderOnlyOps + __all__.extend([ + "MQAnsatzOnlyLayer", "MQN2AnsatzOnlyLayer", "MQLayer", "MQN2Layer", "MQOps", "MQN2Ops", "MQAnsatzOnlyOps", + "MQN2AnsatzOnlyOps", "MQEncoderOnlyOps", "MQN2EncoderOnlyOps" + ]) +except ImportError: + warnings.warn("MindSpore not installed, you may not be able to use hybrid quantum classical neural network.") + +__all__.sort() diff --git a/mindquantum/framework/layer.py b/mindquantum/framework/layer.py new file mode 100644 index 0000000000000000000000000000000000000000..ca7ba2faf2cd3ceb38b908346073f17b36038a2e --- /dev/null +++ b/mindquantum/framework/layer.py @@ -0,0 +1,250 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""Mindspore quantum simulator layer.""" +import mindspore as ms +import mindspore.nn as nn +from mindspore.common.parameter import Parameter +from mindspore.common.initializer import initializer +from .operations import MQOps +from .operations import MQN2Ops +from .operations import MQAnsatzOnlyOps +from .operations import MQN2AnsatzOnlyOps + + +class MQLayer(nn.Cell): + """ + MindQuantum trainable layer. The parameters of ansatz circuit are trainable parameters. + + Args: + expectation_with_grad (GradOpsWrapper): a grad ops that receive encoder data and + ansatz data and return the expectation value and gradient value of parameters + respect to expectation. + weight (Union[Tensor, str, Initializer, numbers.Number]): The trainable weight parameter or ansatz circuit. + Default: 'normal'. + + Inputs: + - **enc_data** (Tensor) - Tensor of encoder data that you want to encode into quantum state. + + Outputs: + Tensor, The expectation value of the hamiltonian. + + Supported Platforms: + ``GPU``, ``CPU`` + + Examples: + >>> import numpy as np + >>> from mindquantum import Circuit, Hamiltonian, QubitOperator + >>> from mindquantum import Simulator, MQLayer + >>> import mindspore as ms + >>> ms.context.set_context(mode=ms.context.PYNATIVE_MODE, device_target="CPU") + >>> enc = Circuit().ry('a', 0) + >>> ans = Circuit().h(0).rx('b', 0) + >>> ham = Hamiltonian(QubitOperator('Z0')) + >>> sim = Simulator('projectq', 1) + >>> grad_ops = sim.get_expectation_with_grad(ham, enc+ans, + ... encoder_params_name=['a'], + ... ansatz_params_name=['b']) + >>> enc_data = ms.Tensor(np.array([[0.1]])) + >>> net = MQLayer(grad_ops) + >>> opti = ms.nn.Adam(net.trainable_params(), learning_rate=0.1) + >>> train_net = ms.nn.TrainOneStepCell(net, opti) + >>> for i in range(100): + ... train_net(enc_data) + >>> net.weight.asnumpy() + array([-3.1424556], dtype=float32) + >>> net(enc_data) + Tensor(shape=[1, 1], dtype=Float32, value= + [[-9.98333767e-02]]) + """ + def __init__(self, expectation_with_grad, weight='normal'): + super(MQLayer, self).__init__() + self.evolution = MQOps(expectation_with_grad) + weight_size = len( + self.evolution.expectation_with_grad.ansatz_params_name) + self.weight = Parameter(initializer(weight, + weight_size, + dtype=ms.float32), + name='ansatz_weight') + + def construct(self, x): + return self.evolution(x, self.weight) + + +class MQN2Layer(nn.Cell): + """ + MindQuantum trainable layer. The parameters of ansatz circuit are trainable parameters. + This layer will calculate the square of absolute value of expectation automatically. + + Args: + expectation_with_grad (GradOpsWrapper): a grad ops that receive encoder data and + ansatz data and return the square of absolute value of expectation value and + gradient value of parameters respect to expectation. + weight (Union[Tensor, str, Initializer, numbers.Number]): The trainable weight parameter or ansatz circuit. + Default: 'normal'. + + Inputs: + - **enc_data** (Tensor) - Tensor of encoder data that you want to encode into quantum state. + + Outputs: + Tensor, The square of absolute value of expectation value of the hamiltonian. + + Supported Platforms: + ``GPU``, ``CPU`` + + Examples: + >>> import numpy as np + >>> from mindquantum import Circuit, Hamiltonian, QubitOperator + >>> from mindquantum import Simulator, MQN2Layer + >>> import mindspore as ms + >>> ms.context.set_context(mode=ms.context.PYNATIVE_MODE, device_target="CPU") + >>> enc = Circuit().ry('a', 0) + >>> ans = Circuit().h(0).rx('b', 0) + >>> ham = Hamiltonian(QubitOperator('Z0')) + >>> sim = Simulator('projectq', 1) + >>> grad_ops = sim.get_expectation_with_grad(ham, enc+ans, + ... encoder_params_name=['a'], + ... ansatz_params_name=['b']) + >>> enc_data = ms.Tensor(np.array([[0.1]])) + >>> net = MQN2Layer(grad_ops) + >>> opti = ms.nn.Adam(net.trainable_params(), learning_rate=0.1) + >>> train_net = ms.nn.TrainOneStepCell(net, opti) + >>> for i in range(100): + ... train_net(enc_data) + >>> net.weight.asnumpy() + array([-1.56476], dtype=float32) + >>> net(enc_data) + Tensor(shape=[1, 1], dtype=Float32, value= + [[ 3.63158676e-07]]) + """ + def __init__(self, expectation_with_grad, weight='normal'): + super(MQN2Layer, self).__init__() + self.evolution = MQN2Ops(expectation_with_grad) + weight_size = len( + self.evolution.expectation_with_grad.ansatz_params_name) + self.weight = Parameter(initializer(weight, + weight_size, + dtype=ms.float32), + name='ansatz_weight') + + def construct(self, x): + return self.evolution(x, self.weight) + + +class MQAnsatzOnlyLayer(nn.Cell): + """ + MindQuantum trainable layer. The parameters of ansatz circuit are trainable parameters. + + Args: + expectation_with_grad (GradOpsWrapper): a grad ops that receive encoder data and + ansatz data and return the expectation value and gradient value of parameters + respect to expectation. + weight (Union[Tensor, str, Initializer, numbers.Number]): The trainable weight parameter or ansatz circuit. + Default: 'normal'. + + Outputs: + Tensor, The expectation value of the hamiltonian. + + Supported Platforms: + ``GPU``, ``CPU`` + + Examples: + >>> import numpy as np + >>> from mindquantum import Circuit, Hamiltonian, QubitOperator + >>> from mindquantum import Simulator, MQAnsatzOnlyLayer + >>> import mindspore as ms + >>> ms.context.set_context(mode=ms.context.PYNATIVE_MODE, device_target="CPU") + >>> circ = Circuit().ry('a', 0).h(0).rx('b', 0) + >>> ham = Hamiltonian(QubitOperator('Z0')) + >>> sim = Simulator('projectq', 1) + >>> grad_ops = sim.get_expectation_with_grad(ham, circ) + >>> net = MQAnsatzOnlyLayer(grad_ops) + >>> opti = ms.nn.Adam(net.trainable_params(), learning_rate=0.1) + >>> train_net = ms.nn.TrainOneStepCell(net, opti) + >>> for i in range(100): + ... train_net() + >>> net.weight.asnumpy() + array([-1.5724511e+00, 1.3100551e-04], dtype=float32) + >>> net() + Tensor(shape=[1], dtype=Float32, value= [-9.99998629e-01]) + """ + def __init__(self, expectation_with_grad, weight='normal'): + super(MQAnsatzOnlyLayer, self).__init__() + self.evolution = MQAnsatzOnlyOps(expectation_with_grad) + weight_size = len( + self.evolution.expectation_with_grad.ansatz_params_name) + self.weight = Parameter(initializer(weight, + weight_size, + dtype=ms.float32), + name='ansatz_weight') + + def construct(self): + return self.evolution(self.weight) + + +class MQN2AnsatzOnlyLayer(nn.Cell): + """ + MindQuantum trainable layer. The parameters of ansatz circuit are trainable parameters. + This layer will calculate the square of absolute value of expectation automatically. + + Args: + expectation_with_grad (GradOpsWrapper): a grad ops that receive encoder data and + ansatz data and return the expectation value and gradient value of parameters + respect to expectation. + weight (Union[Tensor, str, Initializer, numbers.Number]): The trainable weight parameter or ansatz circuit. + Default: 'normal'. + + Inputs: + - **enc_data** (Tensor) - Tensor of encoder data that you want to encode into quantum state. + + Outputs: + Tensor, The expectation value of the hamiltonian. + + Supported Platforms: + ``GPU``, ``CPU`` + + Examples: + >>> import numpy as np + >>> from mindquantum import Circuit, Hamiltonian, QubitOperator + >>> from mindquantum import Simulator, MQN2AnsatzOnlyLayer + >>> import mindspore as ms + >>> ms.set_seed(43) + >>> ms.context.set_context(mode=ms.context.PYNATIVE_MODE, device_target="CPU") + >>> circ = Circuit().ry('a', 0).h(0).rx('b', 0) + >>> ham = Hamiltonian(QubitOperator('Z0')) + >>> sim = Simulator('projectq', 1) + >>> grad_ops = sim.get_expectation_with_grad(ham, circ) + >>> net = MQN2AnsatzOnlyLayer(grad_ops) + >>> opti = ms.nn.Adam(net.trainable_params(), learning_rate=0.1) + >>> train_net = ms.nn.TrainOneStepCell(net, opti) + >>> for i in range(100): + ... train_net() + >>> net.weight.asnumpy() + array([ 0.05957536, -1.5686935 ], dtype=float32) + >>> net() + Tensor(shape=[1], dtype=Float32, value= [ 1.56753845e-08]) + """ + def __init__(self, expectation_with_grad, weight='normal'): + super(MQN2AnsatzOnlyLayer, self).__init__() + self.evolution = MQN2AnsatzOnlyOps(expectation_with_grad) + weight_size = len( + self.evolution.expectation_with_grad.ansatz_params_name) + self.weight = Parameter(initializer(weight, + weight_size, + dtype=ms.float32), + name='ansatz_weight') + + def construct(self): + return self.evolution(self.weight) diff --git a/mindquantum/framework/operations.py b/mindquantum/framework/operations.py new file mode 100644 index 0000000000000000000000000000000000000000..fb758b6792d09b0f0b5d6d42b9c6f3e61dd691ec --- /dev/null +++ b/mindquantum/framework/operations.py @@ -0,0 +1,430 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""Mindspore quantum simulator operator.""" +import numpy as np +import mindspore as ms +from mindspore import context +import mindspore.nn as nn +from mindquantum.simulator import GradOpsWrapper + + +class MQOps(nn.Cell): + """ + MindQuantum operator that get the expectation of a hamiltonian on a quantum + state evaluated by a parameterized quantum circuit (PQC). This PQC should contains + a encoder circuit and an ansatz circuit. This ops is `PYNATIVE_MODE` supported only. + + Args: + expectation_with_grad (GradOpsWrapper): a grad ops that receive encoder data and + ansatz data and return the expectation value and gradient value of parameters + respect to expectation. + + Inputs: + - **enc_data** (Tensor) - Tensor of encoder data that you want to encode into quantum state. + - **ans_data** (Tensor) - Tensor for ansatz circuit. + + Outputs: + Tensor, The expectation value of the hamiltonian. + + Supported Platforms: + ``GPU``, ``CPU`` + + Examples: + >>> import numpy as np + >>> from mindquantum import Circuit, Hamiltonian, QubitOperator + >>> from mindquantum import Simulator, MQOps + >>> import mindspore as ms + >>> ms.context.set_context(mode=ms.context.PYNATIVE_MODE, device_target="CPU") + >>> enc = Circuit().ry('a', 0) + >>> ans = Circuit().h(0).rx('b', 0) + >>> ham = Hamiltonian(QubitOperator('Z0')) + >>> sim = Simulator('projectq', 1) + >>> grad_ops = sim.get_expectation_with_grad(ham, enc+ans, + ... encoder_params_name=['a'], + ... ansatz_params_name=['b']) + >>> enc_data = np.array([[0.1]]) + >>> ans_data = np.array([0.2]) + >>> f, g_enc, g_ans = grad_ops(enc_data, ans_data) + >>> f + array([[0.0978434+0.j]]) + >>> net = MQOps(grad_ops) + >>> f_ms = net(ms.Tensor(enc_data), ms.Tensor(ans_data)) + >>> f_ms + Tensor(shape=[1, 1], dtype=Float32, value= + [[ 9.78433937e-02]]) + """ + def __init__(self, expectation_with_grad): + super(MQOps, self).__init__() + _mode_check(self) + _check_grad_ops(expectation_with_grad) + self.expectation_with_grad = expectation_with_grad + + def extend_repr(self): + return self.expectation_with_grad.str + + def construct(self, enc_data, ans_data): + enc_data = enc_data.asnumpy() + ans_data = ans_data.asnumpy() + f, g_enc, g_ans = self.expectation_with_grad(enc_data, ans_data) + f = ms.Tensor(np.real(f), dtype=ms.float32) + self.g_enc = np.real(g_enc) + self.g_ans = np.real(g_ans) + return f + + def bprop(self, enc_data, ans_data, out, dout): + dout = dout.asnumpy() + enc_grad = np.einsum('smp,sm->sp', self.g_enc, dout) + ans_grad = np.einsum('smp,sm->p', self.g_ans, dout) + return ms.Tensor(enc_grad, dtype=ms.float32), ms.Tensor(ans_grad, dtype=ms.float32) + + +class MQN2Ops(nn.Cell): + r""" + MindQuantum operator that get the square of absolute value of expectation of a hamiltonian + on a quantum state evaluated by a parameterized quantum circuit (PQC). This PQC should contains + a encoder circuit and an ansatz circuit. This ops is `PYNATIVE_MODE` supported only. + + .. math: + + O = \left|\left<0\right| U^\dagger_l H U_\r\left|0\right>\right|^2 + + Args: + expectation_with_grad (GradOpsWrapper): a grad ops that receive encoder data and + ansatz data and return the square of absolute value of expectation value and + gradient value of parameters respect to expectation. + + Inputs: + - **enc_data** (Tensor) - Tensor of encoder data that you want to encode into quantum state. + - **ans_data** (Tensor) - Tensor for ansatz circuit. + + Outputs: + Tensor, The square of absolute value of expectation value of the hamiltonian. + + Supported Platforms: + ``GPU``, ``CPU`` + + Examples: + >>> import numpy as np + >>> from mindquantum import Circuit, Hamiltonian, QubitOperator + >>> from mindquantum import Simulator, MQN2Ops + >>> import mindspore as ms + >>> ms.context.set_context(mode=ms.context.PYNATIVE_MODE, device_target="CPU") + >>> enc = Circuit().ry('a', 0) + >>> ans = Circuit().h(0).rx('b', 0) + >>> ham = Hamiltonian(QubitOperator('Z0')) + >>> sim = Simulator('projectq', 1) + >>> grad_ops = sim.get_expectation_with_grad(ham, enc+ans, + ... encoder_params_name=['a'], + ... ansatz_params_name=['b']) + >>> enc_data = np.array([[0.1]]) + >>> ans_data = np.array([0.2]) + >>> f, g_enc, g_ans = grad_ops(enc_data, ans_data) + >>> np.abs(f) ** 2 + array([[0.00957333]]) + >>> net = MQN2Ops(grad_ops) + >>> f_ms = net(ms.Tensor(enc_data), ms.Tensor(ans_data)) + >>> f_ms + Tensor(shape=[1, 1], dtype=Float32, value= + [[ 9.57333017e-03]]) + """ + def __init__(self, expectation_with_grad): + super(MQN2Ops, self).__init__() + _mode_check(self) + _check_grad_ops(expectation_with_grad) + self.expectation_with_grad = expectation_with_grad + + def extend_repr(self): + return self.expectation_with_grad.str + + def construct(self, enc_data, ans_data): + enc_data = enc_data.asnumpy() + ans_data = ans_data.asnumpy() + f, g_enc, g_ans = self.expectation_with_grad(enc_data, ans_data) + self.f = f + f = ms.Tensor(np.abs(f)**2, dtype=ms.float32) + self.g_enc = g_enc + self.g_ans = g_ans + return f + + def bprop(self, enc_data, ans_data, out, dout): + dout = dout.asnumpy() + enc_grad = 2 * np.real(np.einsum('smp,sm,sm->sp', self.g_enc, dout, np.conj(self.f))) + ans_grad = 2 * np.real(np.einsum('smp,sm,sm->p', self.g_ans, dout, np.conj(self.f))) + return ms.Tensor(enc_grad, dtype=ms.float32), ms.Tensor(ans_grad, dtype=ms.float32) + + +class MQAnsatzOnlyOps(nn.Cell): + r""" + MindQuantum operator that get the expectation of a hamiltonian + on a quantum state evaluated by a parameterized quantum circuit (PQC). This PQC should + contains an ansatz circuit only. This ops is `PYNATIVE_MODE` supported only. + + Args: + expectation_with_grad (GradOpsWrapper): a grad ops that receive encoder data and + ansatz data and return the expectation value and gradient value of parameters + respect to expectation. + + Inputs: + - **ans_data** (Tensor) - Tensor for ansatz circuit. + + Outputs: + Tensor, The expectation value of the hamiltonian. + + Supported Platforms: + ``GPU``, ``CPU`` + + Examples: + >>> import numpy as np + >>> from mindquantum import Circuit, Hamiltonian, QubitOperator + >>> from mindquantum import Simulator, MQAnsatzOnlyOps + >>> import mindspore as ms + >>> ms.context.set_context(mode=ms.context.PYNATIVE_MODE, device_target="CPU") + >>> circ = Circuit().ry('a', 0).h(0).rx('b', 0) + >>> ham = Hamiltonian(QubitOperator('Z0')) + >>> sim = Simulator('projectq', 1) + >>> grad_ops = sim.get_expectation_with_grad(ham, circ) + >>> data = np.array([0.1, 0.2]) + >>> f, g = grad_ops(data) + >>> f + array([[0.0978434+0.j]]) + >>> net = MQAnsatzOnlyOps(grad_ops) + >>> f_ms = net(ms.Tensor(data)) + >>> f_ms + Tensor(shape=[1], dtype=Float32, value= [ 9.78433937e-02]) + """ + def __init__(self, expectation_with_grad): + super(MQAnsatzOnlyOps, self).__init__() + _mode_check(self) + _check_grad_ops(expectation_with_grad) + self.expectation_with_grad = expectation_with_grad + + def extend_repr(self): + return self.expectation_with_grad.str + + def construct(self, x): + x = x.asnumpy() + f, g = self.expectation_with_grad(x) + f = ms.Tensor(np.real(f[0]), dtype=ms.float32) + self.g = np.real(g[0]) + return f + + def bprop(self, x, out, dout): + dout = dout.asnumpy() + grad = dout @ self.g + return ms.Tensor(grad, dtype=ms.float32) + + +class MQN2AnsatzOnlyOps(nn.Cell): + r""" + MindQuantum operator that get the square of absolute value of expectation of a hamiltonian + on a quantum state evaluated by a parameterized quantum circuit (PQC). This PQC should + contains an ansatz circuit only. This ops is `PYNATIVE_MODE` supported only. + + Args: + expectation_with_grad (GradOpsWrapper): a grad ops that receive encoder data and + ansatz data and return the square of absolute value of expectation value and + gradient value of parameters respect to expectation. + + Inputs: + - **ans_data** (Tensor) - Tensor for ansatz circuit. + + Outputs: + Tensor, The square of absolute value of expectation value of the hamiltonian. + + Supported Platforms: + ``GPU``, ``CPU`` + + Examples: + >>> import numpy as np + >>> from mindquantum import Circuit, Hamiltonian, QubitOperator + >>> from mindquantum import Simulator, MQN2AnsatzOnlyOps + >>> import mindspore as ms + >>> ms.context.set_context(mode=ms.context.PYNATIVE_MODE, device_target="CPU") + >>> circ = Circuit().ry('a', 0).h(0).rx('b', 0) + >>> ham = Hamiltonian(QubitOperator('Z0')) + >>> sim = Simulator('projectq', 1) + >>> grad_ops = sim.get_expectation_with_grad(ham, circ) + >>> data = np.array([0.1, 0.2]) + >>> f, g = grad_ops(data) + >>> np.abs(f) ** 2 + array([[0.00957333]]) + >>> net = MQN2AnsatzOnlyOps(grad_ops) + >>> f_ms = net(ms.Tensor(data)) + >>> f_ms + Tensor(shape=[1], dtype=Float32, value= [ 9.57333017e-03]) + """ + def __init__(self, expectation_with_grad): + super(MQN2AnsatzOnlyOps, self).__init__() + _mode_check(self) + _check_grad_ops(expectation_with_grad) + self.expectation_with_grad = expectation_with_grad + + def extend_repr(self): + return self.expectation_with_grad.str + + def construct(self, x): + x = x.asnumpy() + f, g = self.expectation_with_grad(x) + self.f = f[0] + f = ms.Tensor(np.abs(f[0])**2, dtype=ms.float32) + self.g = g[0] + return f + + def bprop(self, x, out, dout): + dout = dout.asnumpy() + grad = 2 * np.real(np.einsum('m,m,mp->p', np.conj(self.f), dout, self.g)) + return ms.Tensor(grad, dtype=ms.float32) + + +class MQEncoderOnlyOps(nn.Cell): + r""" + MindQuantum operator that get the expectation of a hamiltonian + on a quantum state evaluated by a parameterized quantum circuit (PQC). This PQC should + contains a encoder circuit only. This ops is `PYNATIVE_MODE` supported only. + + Args: + expectation_with_grad (GradOpsWrapper): a grad ops that receive encoder data and + ansatz data and return the expectation value and gradient value of parameters + respect to expectation. + + Inputs: + - **ans_data** (Tensor) - Tensor of encoder data that you want to encode into quantum state. + + Outputs: + Tensor, The expectation value of the hamiltonian. + + Supported Platforms: + ``GPU``, ``CPU`` + + Examples: + >>> import numpy as np + >>> from mindquantum import Circuit, Hamiltonian, QubitOperator + >>> from mindquantum import Simulator, MQEncoderOnlyOps + >>> import mindspore as ms + >>> ms.context.set_context(mode=ms.context.PYNATIVE_MODE, device_target="CPU") + >>> circ = Circuit().ry('a', 0).h(0).rx('b', 0) + >>> ham = Hamiltonian(QubitOperator('Z0')) + >>> sim = Simulator('projectq', 1) + >>> grad_ops = sim.get_expectation_with_grad(ham, circ, encoder_params_name=circ.params_name) + >>> data = np.array([[0.1, 0.2], [0.3, 0.4]]) + >>> f, g = grad_ops(data) + >>> f + array([[0.0978434 +0.j], + [0.27219214+0.j]]) + >>> net = MQEncoderOnlyOps(grad_ops) + >>> f_ms = net(ms.Tensor(data)) + >>> f_ms + Tensor(shape=[2, 1], dtype=Float32, value= + [[ 9.78433937e-02], + [ 2.72192121e-01]]) + """ + def __init__(self, expectation_with_grad): + super(MQEncoderOnlyOps, self).__init__() + _mode_check(self) + _check_grad_ops(expectation_with_grad) + self.expectation_with_grad = expectation_with_grad + + def extend_repr(self): + return self.expectation_with_grad.str + + def construct(self, x): + x = x.asnumpy() + f, g = self.expectation_with_grad(x) + f = ms.Tensor(np.real(f), dtype=ms.float32) + self.g = np.real(g) + return f + + def bprop(self, x, out, dout): + dout = dout.asnumpy() + grad = np.einsum('smp,sm->sp', self.g, dout) + return ms.Tensor(grad, dtype=ms.float32) + + +class MQN2EncoderOnlyOps(nn.Cell): + r""" + MindQuantum operator that get the square of absolute value of expectation of a hamiltonian + on a quantum state evaluated by a parameterized quantum circuit (PQC). This PQC should + contains a encoder circuit only. This ops is `PYNATIVE_MODE` supported only. + + Args: + expectation_with_grad (GradOpsWrapper): a grad ops that receive encoder data and + ansatz data and return the square of absolute value of expectation value and + gradient value of parameters respect to expectation. + + Inputs: + - **ans_data** (Tensor) - Tensor of encoder data that you want to encode into quantum state. + + Outputs: + Tensor, The square of absolute value of expectation value of the hamiltonian. + + Supported Platforms: + ``GPU``, ``CPU`` + + Examples: + >>> import numpy as np + >>> from mindquantum import Circuit, Hamiltonian, QubitOperator + >>> from mindquantum import Simulator, MQN2EncoderOnlyOps + >>> import mindspore as ms + >>> ms.context.set_context(mode=ms.context.PYNATIVE_MODE, device_target="CPU") + >>> circ = Circuit().ry('a', 0).h(0).rx('b', 0) + >>> ham = Hamiltonian(QubitOperator('Z0')) + >>> sim = Simulator('projectq', 1) + >>> grad_ops = sim.get_expectation_with_grad(ham, circ, encoder_params_name=circ.params_name) + >>> data = np.array([[0.1, 0.2], [0.3, 0.4]]) + >>> f, g = grad_ops(data) + >>> np.abs(f) ** 2 + array([[0.00957333], + [0.07408856]]) + >>> net = MQN2EncoderOnlyOps(grad_ops) + >>> f_ms = net(ms.Tensor(data)) + >>> f_ms + Tensor(shape=[2, 1], dtype=Float32, value= + [[ 9.57333017e-03], + [ 7.40885586e-02]]) + """ + def __init__(self, expectation_with_grad): + super(MQN2EncoderOnlyOps, self).__init__() + _mode_check(self) + _check_grad_ops(expectation_with_grad) + self.expectation_with_grad = expectation_with_grad + + def extend_repr(self): + return self.expectation_with_grad.str + + def construct(self, x): + x = x.asnumpy() + f, g = self.expectation_with_grad(x) + self.f = f + f = ms.Tensor(np.abs(f)**2, dtype=ms.float32) + self.g = g + return f + + def bprop(self, x, out, dout): + dout = dout.asnumpy() + grad = 2 * np.real(np.einsum('smp,sm,sm->sp', self.g, dout, np.conj(self.f))) + return ms.Tensor(grad, dtype=ms.float32) + + +def _mode_check(self): + if context.get_context('mode') != context.PYNATIVE_MODE: + raise RuntimeError(f'{self.__class__} is `PYNATIVE_MODE` supported only. Run command below to set context\n\ + import mindspore as ms\n\ + ms.context.set_context(mode=ms.context.PYNATIVE_MODE, device_target="CPU")') + + +def _check_grad_ops(expectation_with_grad): + if not isinstance(expectation_with_grad, GradOpsWrapper): + raise ValueError(f'expectation_with_grad requires a GradOpsWrapper, but get {type(expectation_with_grad)}') diff --git a/mindquantum/gate/hamiltonian.py b/mindquantum/gate/hamiltonian.py deleted file mode 100644 index 847690230b31a344acd09369a2fa9fb4277c1fd8..0000000000000000000000000000000000000000 --- a/mindquantum/gate/hamiltonian.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright 2021 Huawei Technologies Co., Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ============================================================================ -"""Hamiltonian module.""" - -from projectq.ops import QubitOperator as pq_operator -from openfermion.ops import QubitOperator as of_operator -from mindquantum.ops import QubitOperator as hiq_operator - - -class Hamiltonian: - """ - A QubitOperator hamiltonian wrapper. - - Args: - hamiltonian (QubitOperator): The pauli word qubit operator. - - Examples: - >>> from mindquantum.ops import QubitOperator - >>> from mindquantum import Hamiltonian - >>> ham = Hamiltonian(QubitOperator('Z0 Y1', 0.3)) - >>> ham.mindspore_data() - {'hams_pauli_coeff': [0.3], - 'hams_pauli_word': [['Z', 'Y']], - 'hams_pauli_qubit': [[0, 1]]} - """ - def __init__(self, hamiltonian): - if not isinstance(hamiltonian, - (pq_operator, of_operator, hiq_operator)): - raise TypeError("Require a QubitOperator, but get {}!".format( - type(hamiltonian))) - self.hamiltonian = hamiltonian - self.ham_termlist = [(i, j) for i, j in hamiltonian.terms.items()] - - def __str__(self): - return self.hamiltonian.__str__() - - def __repr__(self): - return self.hamiltonian.__repr__() - - def mindspore_data(self): - """ - Generate hamiltonian information for PQC operator. - """ - m_data = { - "hams_pauli_coeff": [], - "hams_pauli_word": [], - "hams_pauli_qubit": [] - } - for term, coeff in self.ham_termlist: - m_data["hams_pauli_coeff"].append(float(coeff)) - m_data["hams_pauli_word"].append([]) - m_data["hams_pauli_qubit"].append([]) - for qubit, word in term: - m_data["hams_pauli_qubit"][-1].append(qubit) - m_data["hams_pauli_word"][-1].append(word) - return m_data diff --git a/tests/st/test_gate/test_projector.py b/mindquantum/io/__init__.py similarity index 32% rename from tests/st/test_gate/test_projector.py rename to mindquantum/io/__init__.py index 11ca2ece15d2b6f7791034953b1ba92804ca7db5..164d4bc4a78cead892d6daff21c1d3f3bcce5a9b 100644 --- a/tests/st/test_gate/test_projector.py +++ b/mindquantum/io/__init__.py @@ -1,7 +1,8 @@ -# Copyright (c) 2020 Huawei Technologies Co.,ltd. +# -*- coding: utf-8 -*- +# Copyright 2021 # # Licensed under the Apache License, Version 2.0 (the "License"); -# You may not use this file except in compliance with the License. +# you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 @@ -11,31 +12,15 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""The test projector.""" +"""Input/Output module for MindQuantum.""" -from mindquantum.gate import Projector -from mindquantum.circuit import UN -from mindquantum import H, RX, RY, RZ -from mindquantum.nn import generate_evolution_operator -from mindquantum.nn import MindQuantumAnsatzOnlyOperator -import mindspore as ms -import numpy as np +from . import display +from . import qasm +from .display import * +from .qasm import * +from .beauty_print import bprint - -def test_projector(): - pro = Projector('II010') - assert str(pro) == 'I2 ⊗ ¦010⟩⟨010¦' - - -def test_projector_checked_by_evo(): - circ = UN(H, 3) + RX('a').on(0) + RY('b').on(1) + RZ('c').on(2) - evo = generate_evolution_operator(circ) - a, b, c = 0.3, 0.5, 0.9 - data = ms.Tensor(np.array([a, b, c]).astype(np.float32)) - state = evo(data) - proj = [Projector('I10'), Projector('I10')] - poi = [int(i, 2) for i in ['010', '110']] - pqc = MindQuantumAnsatzOnlyOperator(circ.para_name, circ, proj) - pob = pqc(data) - pob_exp = np.sum(np.abs(state[poi])**2) - assert np.allclose(pob.asnumpy(), [[pob_exp], [pob_exp]]) +__all__ = ['bprint'] +__all__.extend(display.__all__) +__all__.extend(qasm.__all__) +__all__.sort() diff --git a/mindquantum/utils/beauty_print.py b/mindquantum/io/beauty_print.py similarity index 97% rename from mindquantum/utils/beauty_print.py rename to mindquantum/io/beauty_print.py index 0c90e695341038fc4c13743478d7bdbb25dc2002..da63f674f2225be9c3aafe9c06f76932d617841c 100644 --- a/mindquantum/utils/beauty_print.py +++ b/mindquantum/io/beauty_print.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -50,7 +51,7 @@ def bprint(strings: list, list, a list of formatted string. Examples: - >>> from mindquantum.utils import bprint + >>> from mindquantum.io import bprint >>> title='Info of Bob' >>> o = bprint(['Name:Bob', 'Age:17', 'Nationality:China'], >>> title=title) @@ -104,3 +105,6 @@ def bprint(strings: list, output.extend(strings) output.append(bot) return output + + +__all__ = ['bprint'] diff --git a/mindquantum/hiqfermion/__init__.py b/mindquantum/io/display/__init__.py similarity index 75% rename from mindquantum/hiqfermion/__init__.py rename to mindquantum/io/display/__init__.py index 69a85ec4de09ec8330a9e7b7c020f561c011396d..03216730d916baa14fe913ed572d4bbb5ea8c0e6 100644 --- a/mindquantum/hiqfermion/__init__.py +++ b/mindquantum/io/display/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,7 +13,12 @@ # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================ -"""HiQFermion module""" +"""display""" -from . import transforms -from . import ucc +from .circuit_text_drawer import brick_model +from .measure_res_drawer import measure_text_drawer + +__all__ = [] +# __all__ = ['brick_model', 'measure_text_drawer'] + +__all__.sort() diff --git a/mindquantum/circuit/__init__.py b/mindquantum/io/display/_config.py similarity index 40% rename from mindquantum/circuit/__init__.py rename to mindquantum/io/display/_config.py index 6deb9de8e5bca67b9af83795ebbb4b5960f979b9..acc65e9a9bf9dc10b66c59b12676e2bd72f518fd 100644 --- a/mindquantum/circuit/__init__.py +++ b/mindquantum/io/display/_config.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,34 +13,44 @@ # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================ +"""Configuration""" + +CIRCUIT_HTML_FORMAT = """\ +
+
{code}
""" -Circuit. -Quantum circuit module. +MEA_HTML_FORMAT = """\ +
+
{code}
""" -from .circuit import Circuit -from .circuit import pauli_word_to_circuits -from .module_circuit import UN, SwapParts, U3 -from .uccsd import generate_uccsd -from .uccsd import decompose_single_term_time_evolution -from .time_evolution import TimeEvolution -from .high_level_ops import controlled -from .high_level_ops import dagger -from .high_level_ops import apply -from .high_level_ops import add_prefix -from .high_level_ops import change_param_name -from .high_level_ops import A -from .high_level_ops import AP -from .high_level_ops import C -from .high_level_ops import CPN -from .high_level_ops import D -from .state_evolution import StateEvolution -from .quantum_fourier import qft +_res_text_drawer_config = { + 'vline': '│', + 'max_size': 60, + 'spilit': 5, + 'hline': '─', + 'cross_mask': '┼', + 'axis_mask': '┴', + 'box_high': '▓', + 'box_low': '▒', + 'deci': 3, +} + +_text_drawer_config = { + 'ctrl_mask': '●', #⨉ + 'circ_line': '─', + 'ctrl_line': '│', + 'cross_mask': '┼', + 'v_n': 1, + 'swap_mask': ['@', '@'], # ✖, ⨯⨯ + 'edge_num': 2, + 'barrier': '‖' +} + +_text_drawer_config['edge'] = _text_drawer_config[ + 'circ_line'] * _text_drawer_config['edge_num'] -__all__ = [ - 'Circuit', 'StateEvolution', 'TimeEvolution', 'U3', 'UN', 'SwapParts', - 'qft', 'pauli_word_to_circuits', 'decompose_single_term_time_evolution', - 'generate_uccsd', 'controlled', 'dagger', 'apply', 'add_prefix', - 'change_param_name', 'C', 'D', 'A', 'AP', 'CPN' -] +_CIRCUIT_STYLE = {'style': 'blue bold'} +_MEA_RES_STYLE = {'style': 'yellow'} +_DAGGER_MASK = '†' diff --git a/mindquantum/io/display/circuit_text_drawer.py b/mindquantum/io/display/circuit_text_drawer.py new file mode 100644 index 0000000000000000000000000000000000000000..83c845a7c82f8bde6f064edad0b21b28f4e19d5d --- /dev/null +++ b/mindquantum/io/display/circuit_text_drawer.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""Text draw a circuit""" +import numpy as np +from ._config import _text_drawer_config +from ._config import _DAGGER_MASK as _dm + + +def _get_qubit_range(gate): + """_get_qubit_range""" + out = [] + out.extend(gate.obj_qubits) + out.extend(gate.ctrl_qubits) + return out + + +def brick_model(circ, qubits_name=None): + """Split a circuit into layers.""" + from mindquantum import gates as G + n = circ.n_qubits + if qubits_name is None: + qubits_name = list(range(n)) + v_n = _text_drawer_config['v_n'] + blocks = [] + qubit_hight = np.zeros(n, dtype=int) + for gate in circ: + if isinstance(gate, G.BarrierGate): + qrange = range(n) + else: + qrange = _get_qubit_range(gate) + max_hight = np.max(qubit_hight[range(min(qrange), max(qrange) + 1)]) + if len(blocks) <= max_hight: + blocks.append([]) + blocks[max_hight].append(gate) + qubit_hight[range(min(qrange), max(qrange) + 1)] = max_hight + 1 + blocks = [_single_block_drawer(i, n) for i in blocks] + res = {} + max_q = 0 + for i in range(n): + res[i * (v_n + 1)] = f'q{qubits_name[i]}: ' + max_q = max(max_q, len(res[i * (v_n + 1)])) + for i in range(n): + res[i * (v_n + 1)] = res[i * (v_n + 1)].ljust(max_q, ' ') + if i != n - 1: + for j in range(v_n): + res[i * (v_n + 1) + j + 1] = ' ' * max_q + for block in blocks: + for k, v in block.items(): + res[k] += v + return [res[i].rstrip() for i in range((n - 1) * (v_n + 1) + 1)] + + +def _single_gate_drawer(gate): + """_single_gate_drawer""" + from mindquantum import gates as G + from mindquantum.core.gates.basic import HERMITIAN_PROPERTIES + if isinstance(gate, G.CNOTGate): + gate = G.X.on(*gate.obj_qubits) + main_text = gate.name + if isinstance(gate, G.SWAPGate): + main_text = _text_drawer_config['swap_mask'][0] + elif issubclass(gate.__class__, G.ParameterGate): + if isinstance(gate, (G.SGate, G.TGate)): + main_text = gate.name + ('†' if gate.daggered else '') + else: + main_text = str(gate.__class__(gate.coeff)) + elif isinstance(gate, G.Measure): + main_text = f"{gate.name}({gate.key})" + elif isinstance(gate, G.NoneParameterGate): + if gate.hermitian_property == HERMITIAN_PROPERTIES['do_hermitian']: + if gate.daggered: + main_text += _dm + main_text = _text_drawer_config['edge'] + main_text + _text_drawer_config['edge'] + res = {} + for i in gate.obj_qubits: + res[i] = main_text + if isinstance(gate, G.SWAPGate): + max_idx = max(gate.obj_qubits) + lower_txt = _text_drawer_config['swap_mask'][1] + res[max_idx] = _text_drawer_config['edge'] + lower_txt + _text_drawer_config['edge'] + for i in gate.ctrl_qubits: + res[i] = _text_drawer_config['ctrl_mask'] + res[i] = res[i].center(len(main_text), _text_drawer_config['circ_line']) + res['len'] = len(main_text) + return res + + +def _single_block_drawer(block, n_qubits): + """single block drawer""" + from mindquantum import gates as G + v_n = _text_drawer_config['v_n'] + text_gates = {} + if isinstance(block[0], G.BarrierGate): + if not block[0].show: + tmp = '' + else: + tmp = _text_drawer_config['barrier'] + for i in range((n_qubits - 1) * v_n + n_qubits): + text_gates[i] = tmp + return text_gates + for gate in block: + text_gate = _single_gate_drawer(gate) + qrange = _get_qubit_range(gate) + for q in range(min(qrange), max(qrange) + 1): + ind = q * (v_n + 1) + if q in qrange: + text_gates[ind] = text_gate[q] + else: + text_gates[ind] = _text_drawer_config['cross_mask'] + text_gates[ind] = text_gates[ind].center(text_gate['len'], _text_drawer_config['circ_line']) + for q in range(min(qrange), max(qrange)): + for i in range(v_n): + ind = q * (v_n + 1) + i + 1 + text_gates[ind] = _text_drawer_config['ctrl_line'] + text_gates[ind] = text_gates[ind].center(text_gate['len'], ' ') + max_l = max([len(j) for j in text_gates.values()]) + for k, v in text_gates.items(): + if len(v) != max_l: + if k % (v_n + 1) == 0: + text_gates[k] = text_gates[k].center(max_l, _text_drawer_config['circ_line']) + else: + text_gates[k] = text_gates[k].center(max_l, ' ') + for i in range((n_qubits - 1) * v_n + n_qubits): + if i not in text_gates: + if i % (v_n + 1) == 0: + text_gates[i] = _text_drawer_config['circ_line'] * max_l + else: + text_gates[i] = ' ' * max_l + return text_gates diff --git a/mindquantum/io/display/measure_res_drawer.py b/mindquantum/io/display/measure_res_drawer.py new file mode 100644 index 0000000000000000000000000000000000000000..667dc41c3d3d59ca46aa485f2b043060fcf815b6 --- /dev/null +++ b/mindquantum/io/display/measure_res_drawer.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""Text measure result""" +# from mindquantum.core.gates import MeasureResult +import math +from ._config import _res_text_drawer_config + + +def _trans(v, l, m): + return math.ceil(v / m * l) + + +def measure_text_drawer(res): + """Draw measure result.""" + max_size = _res_text_drawer_config['max_size'] + vline = _res_text_drawer_config['vline'] + hline = _res_text_drawer_config['hline'] + cross_mask = _res_text_drawer_config['cross_mask'] + box_high = _res_text_drawer_config['box_high'] + box_low = _res_text_drawer_config['box_low'] + axis_mask = _res_text_drawer_config['axis_mask'] + split = _res_text_drawer_config['spilit'] + deci = _res_text_drawer_config['deci'] + keys = res.keys + max_shot = max(res.data.values()) + max_prop = max_shot / res.shots + if max_prop == 0: + max_prop = 1 + if max_prop / 0.8 > 1: + max_prop = 1 + else: + max_prop /= 0.8 + ket_exp = 'Keys: ' + ket_exp += ' '.join(keys[::-1]) + s = [f'shots: {res.shots}'] + s.append(ket_exp + vline) + axis_num = '' + axis = '' + for i in range(split): + axis_num = str(round(max_prop / split * (split - i), deci)).rjust( + int(max_size / split)) + axis_num + axis = axis_mask.rjust(int(max_size / split), hline) + axis + axis_num = '0.00' + axis_num[4:] + s[-1] += axis_num + s.append(hline * len(ket_exp) + cross_mask + axis) + for k, v in res.data.items(): + state = k + state = state.rjust(len(ket_exp)) + state += vline + state += (box_high if v == max_shot else box_low) * _trans( + v / res.shots, max_size, max_prop) + s.append(state) + s.append(' ' * len(ket_exp) + vline) + return s diff --git a/mindquantum/io/qasm/__init__.py b/mindquantum/io/qasm/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c5e7f92289ce82ea96474f1a6793c817cfeb736e --- /dev/null +++ b/mindquantum/io/qasm/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""QASM flavors support (OpenQASM, HiQASM, etc.)""" + +from .openqasm import OpenQASM +from .hiqasm import random_hiqasm +from .hiqasm import HiQASM + +__all__ = ['OpenQASM', 'random_hiqasm', 'HiQASM'] + +__all__.sort() diff --git a/mindquantum/io/qasm/hiqasm.py b/mindquantum/io/qasm/hiqasm.py new file mode 100644 index 0000000000000000000000000000000000000000..bf76aa9ff88d067b6d833349ebce8fa5e0c04484 --- /dev/null +++ b/mindquantum/io/qasm/hiqasm.py @@ -0,0 +1,460 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""hiqqasm""" +import numpy as np +from .openqasm import _find_qubit_id +from .openqasm import u3 + +HIQASM_GATE_SET = { + '0.1': { + 'np': ['X', 'Y', 'Z', 'S', 'T', 'H', 'CNOT', 'CZ', 'ISWAP', 'CCNOT'], + 'p': [ + 'RX', 'RY', 'RZ', 'U', 'CRX', 'CRY', 'CRZ', 'XX', 'YY', 'ZZ', + 'CCRX', 'CCRY', 'CCRZ' + ], + } +} + + +def random_hiqasm(n_qubits, gate_num, version='0.1', seed=42): + """ + Generate random hiqasm supported circuit. + + Args: + n_qubits (int): Total number of qubit in this quantum circuit. + gate_num (int): Total number of gate in this quantum circuit. + version (str): version of HIQASM. Default: '0.1'. + seed (int): The random seed to generate this random quantum circuit. + + Returns: + str, quantum in HIQASM format. + + Examples: + >>> from mindquantum.io.qasm import random_hiqasm + >>> from mindquantum.io.qasm import HiQASM + >>> hiqasm_str = random_hiqasm(2, 5) + >>> hiqasm = HiQASM() + >>> circuit = hiqasm.from_string(hiqasm_str) + >>> circuit + q0: ──RZ(-2.513)────RZ(-3.012)────RX(0.738)────M(k0)─────────── + │ + q1: ──────S───────────────────────────●──────────Z──────M(k1)── + """ + if not isinstance(n_qubits, (int, np.int64)) or n_qubits < 1: + raise ValueError( + f'qubit number should be a non negative int, but get {n_qubits}.') + if not isinstance(gate_num, (int, np.int64)) or gate_num < 1: + raise ValueError( + f'gate number should be a non negative int, but get {n_qubits}.') + np.random.seed(seed) + gate_set = HIQASM_GATE_SET[version] + np_set = gate_set['np'] + p_set = gate_set['p'] + if version == '0.1': + qasm = [ + '# HIQASM 0.1', '# Instruction stdins', '', + f'ALLOCATE q {n_qubits}', 'RESET q' + ] + if n_qubits == 1: + np_set = np_set[:6] + p_set = p_set[:4] + elif n_qubits == 2: + np_set = np_set[:9] + p_set = p_set[:10] + while len(qasm) - 5 < gate_num: + g_set = [np_set, p_set][int(np.random.choice([0, 1]))] + gate = np.random.choice(g_set) + pval = np.random.uniform(-np.pi, np.pi, 3) + qidx = np.arange(n_qubits) + np.random.shuffle(qidx) + if gate in ['X', 'Y', 'Z', 'S', 'T', 'H']: + qasm.append(f'{gate} q[{qidx[0]}]') + elif gate in ['CNOT', 'CZ', 'ISWAP']: + qasm.append(f'{gate} q[{qidx[0]}],q[{qidx[1]}]') + elif gate == 'CCNOT': + qasm.append('{} q[{}],q[{}],q[{}]'.format(gate, *qidx[:3])) + elif gate in ['RX', 'RY', 'RZ']: + qasm.append(f'{gate} q[{qidx[0]}] {pval[0]}') + elif gate == 'U': + qasm.append('U q[{}] {},{},{}'.format(qidx[0], *pval)) + elif gate in ['CRX', 'CRY', 'CRZ', 'XX', 'YY', 'ZZ']: + qasm.append('{} q[{}],q[{}] {}'.format(gate, *qidx[:2], + pval[0])) + elif gate in ['CCRX', 'CCRY', 'CCRZ']: + qasm.append('{} q[{}],q[{}],q[{}] {}'.format( + gate, *qidx[:3], pval[0])) + else: + raise NotImplementedError( + f"gate {gate} not implement in HIQASM {version}") + qasm.append('MEASURE q') + qasm.append('DEALLOCATE q') + qasm.append('') + return '\n'.join(qasm) + raise NotImplementedError(f'version {version} not implemented') + + +class HiQASM: + """ + Convert a circuit to hiqasm format. + + Examples: + >>> import numpy as np + >>> from mindquantum.io.qasm import HiQASM + >>> from mindquantum.core import Circuit + >>> circuit = Circuit().rx(0.3, 0).z(0, 1).zz(np.pi, [0, 1]) + >>> hiqasm = HiQASM() + >>> circuit_str = hiqasm.to_string(circuit) + >>> print(circuit_str[68: 80]) + CZ q[1],q[0] + >>> circuit_2 = hiqasm.from_string(circuit_str) + >>> circuit_2 + q0: ──RX(3/10)────Z────ZZ(π)── + │ │ + q1: ──────────────●────ZZ(π)── + """ + def __init__(self): + from mindquantum import Circuit + self.circuit = Circuit() + self.cmds = [] + + def _filter(self, cmds): + """filter empty cmds and head.""" + out = [] + version = None + n_qubits = None + for cmd in cmds: + cmd = cmd.strip() + if not cmd: + continue + if _startswithany(cmd, '#', 'ALLOCATE', 'RESET', 'DEALLOCATE'): + if cmd.startswith('# HIQASM'): + version = cmd.split(' ')[-1] + if cmd.startswith('ALLOCATE q '): + n_qubits = int(cmd.split(' ')[-1]) + continue + out.append(cmd) + if n_qubits is None: + raise ValueError('Can not find qubit number in qasm') + if version is None: + raise ValueError('Can not find version in qasm') + return out, version, n_qubits + + def to_string(self, circuit, version='0.1'): + """ + Convert circuit to hiqasm. + + Args: + circuit (Circuit): The quantum circuit you want to translated to HiQASM. + version (str): The HiQASM version you want to use. Default: '0.1'. + + Returns: + str, The HiQASM format of input circuit. + + Raises: + TypeError: if `circuit` is not a Circuit. + TypeError: if `version` is not a str. + NotImplementedError: if HiQASM version not implement. + ValueError: if gate not implement in this version. + """ + from mindquantum import gates as G + from mindquantum.core import Circuit + if not isinstance(circuit, Circuit): + raise TypeError( + f"circuit requires Circuit, but get {type(circuit)}.") + if not isinstance(version, str): + raise TypeError(f"version requires a str, but get {type(version)}") + if version == '0.1': + if circuit.parameterized: + raise ValueError( + "Cannot convert parameterized circuit to HIQASM") + self.circuit = circuit + self.cmds = [ + f"# HIQASM {version}", "# Instruction stdins", "", + f'ALLOCATE q {circuit.n_qubits}', 'RESET q' + ] + for gate in circuit: + ctrl_qubits = gate.ctrl_qubits + n_ctrl_qubits = len(ctrl_qubits) + obj_qubits = gate.obj_qubits + n_obj_qubits = len(obj_qubits) + if n_ctrl_qubits > 2: + raise ValueError( + f"HIQASM do not support more than two control qubits gate: {gate}" + ) + if n_obj_qubits > 2: + raise ValueError( + f"HIQASM do not support more than two object qubit gate: {gate}" + ) + if self._to_string_non_parametric(gate, ctrl_qubits, + obj_qubits, version): + pass + elif self._to_string_parametric(gate, ctrl_qubits, obj_qubits, + version): + pass + elif isinstance(gate, G.ISWAPGate): + if n_ctrl_qubits == 0: + self.cmds.append( + f'ISWAP q[{obj_qubits[0]}],q[{obj_qubits[1]}]') + else: + _not_implement(version, gate) + elif isinstance(gate, G.Measure): + if n_ctrl_qubits == 0: + self.cmds.append(f'MEASURE q[{obj_qubits[0]}]') + else: + _not_implement(version, gate) + else: + _not_implement(version, gate) + self.cmds.append('DEALLOCATE q') + self.cmds.append('') + else: + raise NotImplementedError + return '\n'.join(self.cmds) + + def _to_string_non_parametric(self, gate, ctrl_qubits, obj_qubits, + version): + """Conversion of simple gates to string""" + from mindquantum.core import gates as G + n_ctrl_qubits = len(ctrl_qubits) + + if isinstance(gate, G.XGate): + if n_ctrl_qubits == 0: + self.cmds.append(f'X q[{obj_qubits[0]}]') + elif n_ctrl_qubits == 1: + self.cmds.append( + f'CNOT q[{ctrl_qubits[0]}],q[{obj_qubits[0]}]') + elif n_ctrl_qubits == 2: + self.cmds.append( + f'CCNOT q[{ctrl_qubits[0]}],q[{ctrl_qubits[1]}],q[{obj_qubits[0]}]' + ) + else: + _not_implement(version, gate) + elif isinstance(gate, G.YGate): + if n_ctrl_qubits == 0: + self.cmds.append(f'Y q[{obj_qubits[0]}]') + else: + _not_implement(version, gate) + elif isinstance(gate, G.ZGate): + if n_ctrl_qubits == 0: + self.cmds.append(f'Z q[{obj_qubits[0]}]') + elif n_ctrl_qubits == 1: + self.cmds.append(f'CZ q[{ctrl_qubits[0]}],q[{obj_qubits[0]}]') + else: + _not_implement(version, gate) + elif isinstance(gate, G.SGate): + if n_ctrl_qubits == 0: + if gate.daggered: + _not_implement(version, gate) + self.cmds.append(f'S q[{obj_qubits[0]}]') + else: + _not_implement(version, gate) + elif isinstance(gate, G.TGate): + if n_ctrl_qubits == 0: + if gate.daggered: + _not_implement(version, gate) + self.cmds.append(f'T q[{obj_qubits[0]}]') + else: + _not_implement(version, gate) + elif isinstance(gate, G.HGate): + if n_ctrl_qubits == 0: + self.cmds.append(f'H q[{obj_qubits[0]}]') + else: + _not_implement(version, gate) + else: + return False + return True + + def _to_string_parametric(self, gate, ctrl_qubits, obj_qubits, version): + """Conversion of parametric gates to string""" + from mindquantum.core import gates as G + n_ctrl_qubits = len(ctrl_qubits) + + if isinstance(gate, (G.RX, G.RY, G.RZ)): + if n_ctrl_qubits == 0: + self.cmds.append( + f'{gate.name} q[{obj_qubits[0]}] {gate.coeff}') + elif n_ctrl_qubits == 1: + self.cmds.append( + f'C{gate.name} q[{ctrl_qubits[0]}],q[{obj_qubits[0]}] {gate.coeff}' + ) + elif n_ctrl_qubits == 2: + self.cmds.append( + f'CC{gate.name} q[{ctrl_qubits[0]}],q[{ctrl_qubits[1]}],q[{obj_qubits[0]}] {gate.coeff}' + ) + else: + _not_implement(version, gate) + elif isinstance(gate, (G.XX, G.YY, G.ZZ)): + if n_ctrl_qubits == 0: + self.cmds.append( + f'{gate.name} q[{obj_qubits[0]}],q[{obj_qubits[1]}] {gate.coeff}' + ) + else: + _not_implement(version, gate) + else: + return False + return True + + def from_string(self, string): + """ + Read a hiqasm string + + Args: + string (str): The HiQASM string of a Circuit. + + Returns: + Circuit, The quantum circuit translated from HiQASM string. + """ + cmds = string.split('\n') + self.cmds, version, n_qubits = self._filter(cmds) + if version == '0.1': + self._trans_v01(self.cmds, n_qubits) + else: + raise ValueError(f'HIQASM {version} not implement yet') + return self.circuit + + def from_file(self, file_name): + """ + Read a hiqasm file. + + Args: + file_name (str): The path of file that stored quantum circuit in HiQASM format. + + Returns: + Circuit, the quantum circuit translated from HiQASM file. + """ + with open(file_name, 'r') as f: + cmds = f.readlines() + self.from_string('\n'.join(cmds)) + return self.circuit + + def to_file(self, file_name, circuit, version='0.1'): + """ + Convert a quantum circuit to HiQASM format and save in file. + + Args: + file_name (str): The file name you want to save the HiQASM file. + circuit (Circuit): The Circuit you want to convert. + version (str): The version of HiQASM. Default: '0.1'. + + Raises: + TypeError: if `file_name` is not a str. + TypeError: if `circuit` is not a Circuit. + TypeError: if `version` is not a str. + """ + from mindquantum.core import Circuit + if not isinstance(file_name, str): + raise TypeError( + f'file_name requires a str, but get {type(file_name)}') + if not isinstance(circuit, Circuit): + raise TypeError( + f"circuit requires a Circuit, but get {type(circuit)}") + if not isinstance(version, str): + raise TypeError(f'version requires a str, but get {type(version)}') + cs = self.to_string(circuit, version) + with open(file_name, 'w') as f: + f.writelines(cs) + print(f"write circuit to {file_name} finished!") + + def _trans_v01(self, cmds, n_qubits): + """Trans method for hiqasm version 0.1""" + from mindquantum import Circuit + import mindquantum.core.gates as G + self.circuit = Circuit() + for cmd in cmds: + q = _find_qubit_id(cmd) + if cmd.startswith('CNOT '): + self.circuit.x(q[1], q[0]) + elif cmd.startswith('CZ '): + self.circuit.z(q[1], q[0]) + elif cmd.startswith('ISWAP '): + self.circuit += G.ISWAP.on(q[:2]) + elif cmd.startswith('CCNOT '): + self.circuit.x(q[-1], q[:2]) + elif cmd.startswith('CRX '): + self.circuit.rx(*_extr_parameter(cmd), q[1], q[0]) + elif cmd.startswith('CRY '): + self.circuit.ry(*_extr_parameter(cmd), q[1], q[0]) + elif cmd.startswith('CRZ '): + self.circuit.rz(*_extr_parameter(cmd), q[1], q[0]) + elif cmd.startswith('XX '): + self.circuit.xx(*_extr_parameter(cmd), q[:2]) + elif cmd.startswith('YY '): + self.circuit.yy(*_extr_parameter(cmd), q[:2]) + elif cmd.startswith('ZZ '): + self.circuit.zz(*_extr_parameter(cmd), q[:2]) + elif cmd.startswith('CCRX '): + self.circuit.rx(*_extr_parameter(cmd), q[-1], q[:2]) + elif cmd.startswith('CCRY '): + self.circuit.ry(*_extr_parameter(cmd), q[-1], q[:2]) + elif cmd.startswith('CCRZ '): + self.circuit.rz(*_extr_parameter(cmd), q[-1], q[:2]) + elif cmd.startswith('MEASURE '): + q = _find_qubit_id(cmd) + if q: + self.circuit.measure(f'k{self.circuit.all_measures.size}', + q[0]) + else: + for midx in range(n_qubits): + self.circuit.measure( + f'k{self.circuit.all_measures.size}', midx) + elif self._trans_v01_single_qubit(cmd, q[0]): + pass + else: + raise ValueError(f"transfer cmd {cmd} not implement yet!") + + def _trans_v01_single_qubit(self, cmd, qubit): + """Trans method for hiqasm version 0.1 (single-qubit gates)""" + from mindquantum.core import gates as G + if cmd.startswith('H '): + self.circuit.h(qubit) + elif cmd.startswith('X '): + self.circuit.x(qubit) + elif cmd.startswith('Y '): + self.circuit.y(qubit) + elif cmd.startswith('Z '): + self.circuit.z(qubit) + elif cmd.startswith('S '): + self.circuit += G.S.on(qubit) + elif cmd.startswith('T '): + self.circuit += G.T.on(qubit) + elif cmd.startswith('U '): + self.circuit += u3(*_extr_parameter(cmd), qubit) + elif cmd.startswith('RX '): + self.circuit.rx(*_extr_parameter(cmd), qubit) + elif cmd.startswith('RY '): + self.circuit.ry(*_extr_parameter(cmd), qubit) + elif cmd.startswith('RZ '): + self.circuit.rz(*_extr_parameter(cmd), qubit) + else: + return False + return True + + +def _extr_parameter(cmd): + """extra parameter for parameterized gate in hiqasm cmd""" + return [float(i) for i in cmd.split(' ')[-1].split(',')] + + +def _startswithany(cmd, *s): + """Checkout whether cmd starts with any string in s""" + for i in s: + if cmd.startswith(i): + return True + return False + + +def _not_implement(version, gate): + """not implement error""" + raise ValueError(f'{gate} not implement in HIQASM {version}') diff --git a/mindquantum/io/qasm/openqasm.py b/mindquantum/io/qasm/openqasm.py new file mode 100644 index 0000000000000000000000000000000000000000..cffc8ce9abaad8aa04dc09124efe68b5f2f1d0a6 --- /dev/null +++ b/mindquantum/io/qasm/openqasm.py @@ -0,0 +1,256 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""openqasm""" +import numpy as np + + +def _find_qubit_id(cmd): + """Find qubit id in openqasm cmd""" + left = [] + right = [] + for i, j in enumerate(cmd): + if j == '[': + left.append(i) + elif j == ']': + right.append(i) + if len(left) != len(right): + raise ValueError(f"Parsing failed for cmd {cmd}") + idx = [] + for l, r in zip(left, right): + idx.append(int(cmd[l + 1:r])) + return idx + + +def _extr_parameter(cmd): + """extra parameter for parameterized gate in openqasm cmd""" + l = cmd.find('(') + r = cmd.find(')') + if l == -1 or r == -1: + raise ValueError(f"no parameter found in cmd {cmd}") + all_expre = cmd[l + 1:r] + all_expre = all_expre.split(',') + out = [] + for expre in all_expre: + if 'pi' in expre: + expre = expre.replace('pi', str(np.pi)) + if '*' in expre: + tmp = expre.split('*') + if len(tmp) != 2: + raise ValueError(f"cannot parse cmd {cmd}") + expre = str(float(tmp[0]) * float(tmp[1])) + if '/' in expre: + tmp = expre.split('/') + if len(tmp) != 2: + raise ValueError(f"cannot parse cmd {cmd}") + expre = str(float(tmp[0]) / float(tmp[1])) + out.append(float(expre)) + return out[0] if len(all_expre) == 1 else out + + +def u3(theta, psi, lambd, q): + """decomp u3 gate""" + from mindquantum import Circuit + circ = Circuit().rz(psi + 3 * np.pi, q) + circ.rx(np.pi / 2, q).rz(theta + np.pi, q) + circ.rx(np.pi / 2, q).rz(lambd, q) + return circ + + +def u1(lambd, q): + """openqasm u1 gate""" + from mindquantum import Circuit + return Circuit().rz(lambd, q) + + +def isgateinstance(gate, gates): + """Check whether gate is any instance of supported gate type""" + if isinstance(gates, list): + gates = (gates, ) + for gate_test in gates: + for g in gate_test: + if isinstance(gate, g): + return True + return False + + +class OpenQASM: + """ + Convert a circuit to openqasm format + """ + def __init__(self): + from mindquantum import Circuit + self.circuit = Circuit() + self.cmds = [] + + def to_string(self, circuit, version="2.0"): + """ + Convert circuit to hiqasm. + + Args: + circuit (Circuit): The quantum circuit you want to translated to openqasm. + version (str): The HiQASM version you want to use. Default: '2.0'. + + Returns: + str, The HiQASM format of input circuit. + + Raises: + TypeError: if circuit is not a Circuit. + TypeError: if version is not a str. + NotImplementedError: if openqasm version not implement. + ValueError: if gate not implement in this version. + """ + from mindquantum import gates as G + from mindquantum.core import Circuit + if not isinstance(circuit, Circuit): + raise TypeError(f"circuit requires Circuit, but get {type(circuit)}.") + if not isinstance(version, str): + raise TypeError(f"version requires a str, but get {type(version)}") + single_np = [G.XGate, G.YGate, G.ZGate] + single_p = [G.RX, G.RY, G.RZ, G.PhaseShift] + double_np = [G.SWAPGate, G.CNOTGate] + double_p = [G.XX, G.YY, G.ZZ] + if version == "2.0": + self.circuit = circuit + self.cmds = [f"OPENQASM {version};", "include \"qelib1.inc\";"] + self.cmds.append(f"qreg q[{circuit.n_qubits}];") + for gate in self.circuit: + if isgateinstance(gate, (single_np, single_p)): + if len(gate.ctrl_qubits) > 1: + raise ValueError(f"Multiple control for gate {gate} not implement") + if isgateinstance(gate, single_np): + obj = gate.obj_qubits[0] + if gate.ctrl_qubits: + ctrl = gate.ctrl_qubits[0] + self.cmds.append(f"c{gate.name.lower()} q[{ctrl}],q[{obj}];") + else: + self.cmds.append(f"{gate.name.lower()} q[{obj}];") + else: + obj = gate.obj_qubits[0] + p = gate.coeff + if gate.ctrl_qubits: + ctrl = gate.ctrl_qubits[0] + self.cmds.append(f"c{gate.name.lower()}({p}) q[{ctrl}],q[{obj}];") + else: + self.cmds.append(f"{gate.name.lower()}({p}) q[{obj}];") + if isgateinstance(gate, (double_np, double_p)): + if gate.ctrl_qubits: + raise ValueError(f"control two qubits gate {gate} not implement") + if isgateinstance(gate, double_np): + obj = gate.obj_qubits + if isinstance(gate, G.SWAPGate): + self.cmds.append(f"cx q[{obj[1]}],q[{obj[0]}];") + self.cmds.append(f"cx q[{obj[0]}],q[{obj[1]}];") + self.cmds.append(f"cx q[{obj[1]}],q[{obj[0]}];") + if isinstance(gate, G.CNOTGate): + self.cmds.append(f"cx q[{obj[1]}],q[{obj[0]}];") + else: + obj = gate.obj_qubits + p = gate.coeff + self.cmds.append(f"{gate.name.lower()}({p}) q[{obj[0]}],q[{obj[1]}];") + return self.cmds + raise NotImplementedError(f"openqasm version {version} not implement") + + def to_file(self, file_name, circuit, version="2.0"): + """ + Convert a quantum circuit to openqasm format and save in file. + + Args: + file_name (str): The file name you want to save the openqasm file. + circuit (Circuit): The Circuit you want to convert. + version (str): The version of openqasm. Default: '2.0'. + + Raises: + TypeError: if `file_name` is not a str. + TypeError: if `circuit` is not a Circuit. + TypeError: if `version` is not a str. + """ + from mindquantum.core import Circuit + if not isinstance(file_name, str): + raise TypeError(f'file_name requires a str, but get {type(file_name)}') + if not isinstance(circuit, Circuit): + raise TypeError(f"circuit requires a Circuit, but get {type(circuit)}") + if not isinstance(version, str): + raise TypeError(f'version requires a str, but get {type(version)}') + self.to_string(circuit, version) + with open(file_name, 'w') as f: + f.writelines("\n".join(self.cmds)) + print(f"write circuit to {file_name} finished!") + + def from_file(self, file_name): + """ + Read a openqasm file. + + Args: + file_name (str): The path of file that stored quantum circuit in openqasm format. + + Returns: + Circuit, the quantum circuit translated from openqasm file. + """ + with open(file_name, 'r') as f: + cmds = f.readlines() + self.cmds, version = self._filter(cmds) + if version == '2.0': + self._trans_v2(self.cmds) + else: + raise ValueError(f"OPENQASM {version} not implement yet") + + def _filter(self, cmds): + """ + filter empty cmds and head. + """ + out = [] + version = None + for cmd in cmds: + cmd = cmd.strip() + if not cmd or cmd.startswith('//') or cmd.startswith('include') or cmd.startswith("qreg"): + pass + elif cmd.startswith('OPENQASM'): + version = cmd.split(' ')[-1][:-1] + else: + out.append(cmd[:-1]) + return out, version + + def _trans_v2(self, cmds): + """ + trans method for openqasm version 2 + """ + from mindquantum import Circuit + from mindquantum.core.circuit import controlled + self.circuit = Circuit() + for cmd in cmds: + q = _find_qubit_id(cmd) + if cmd.startswith("h "): + self.circuit.h(q[0]) + elif cmd.startswith("x "): + self.circuit.x(q[0]) + elif cmd.startswith("y "): + self.circuit.y(q[0]) + elif cmd.startswith("cx "): + self.circuit.x(q[1], q[0]) + elif cmd.startswith("cz "): + self.circuit.z(*q[::-1]) + elif cmd.startswith("rz("): + self.circuit.rz(_extr_parameter(cmd), q[0]) + elif cmd.startswith("ry("): + self.circuit.ry(_extr_parameter(cmd), q[0]) + elif cmd.startswith("rx("): + self.circuit.rx(_extr_parameter(cmd), q[0]) + elif cmd.startswith("u3("): + self.circuit += u3(*_extr_parameter(cmd), q[0]) + elif cmd.startswith("cu1("): + self.circuit += controlled(u1(_extr_parameter(cmd), q[1]))(q[0]) + else: + raise ValueError(f"transfer cmd {cmd} not implement yet!") diff --git a/mindquantum/nn/_check_qnn_input.py b/mindquantum/nn/_check_qnn_input.py deleted file mode 100644 index 1552857a2a40d4ddace7462a450e2e36dfa8a45d..0000000000000000000000000000000000000000 --- a/mindquantum/nn/_check_qnn_input.py +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright 2021 Huawei Technologies Co., Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ============================================================================ -"""Check input for quantum neural network.""" - -from collections.abc import Iterable -from mindquantum.circuit import Circuit - - -def _check_circuit(circuit, msg): - if not isinstance(circuit, Circuit): - raise TypeError("{} requires a quantum circuit, but get {}!".format( - msg, type(circuit))) - - -def _check_non_parameterized_circuit(circuit: Circuit): - if not isinstance(circuit, Circuit): - raise TypeError( - "Requires a non parameterized quantum circuit, but get {}!".format( - type(circuit))) - for g in circuit: - if g.isparameter: - raise ValueError( - "Requires a non parameterized quantum circuit, but {} is parameterized gate!" - .format(g)) - - -def _check_type_or_iterable_type(inputs, require, msg): - if not isinstance(inputs, Iterable): - if not isinstance(inputs, require): - raise TypeError( - "{msg} requires {req} or several {req}s, but get {inp}!". - format(msg=msg, req=require, inp=type(inputs))) - else: - for inp in inputs: - if not isinstance(inp, require): - raise TypeError( - "{msg} requires {req} or several {req}s, but {inp} is not {req}!" - .format(msg=msg, req=require, inp=inp)) - - -def _check_list_of_string(inputs, msg): - if not isinstance(inputs, list): - raise TypeError("{} requires a list of string, but get {}!".format( - msg, type(inputs))) - for inp in inputs: - if not isinstance(inp, str): - raise TypeError( - "{} requires a list of string, but {} is not string.".format( - msg, inp)) - - -def _check_parameters_of_circuit(encoder_params_names, ansatz_params_names, - circuit: Circuit): - _check_list_of_string(encoder_params_names, 'Encoder parameter names') - _check_list_of_string(ansatz_params_names, 'Ansatz parameter names') - all_names = [] - all_names.extend(encoder_params_names) - all_names.extend(ansatz_params_names) - circ_names = circuit.para_name - if not set(all_names) == set(circ_names): - raise ValueError( - "Parameter names you input not match with parameters in circuit.") diff --git a/mindquantum/nn/evolution.py b/mindquantum/nn/evolution.py deleted file mode 100644 index 34027ad38207a698db3beabbdd2b63da5c01a9a3..0000000000000000000000000000000000000000 --- a/mindquantum/nn/evolution.py +++ /dev/null @@ -1,147 +0,0 @@ -# Copyright 2021 Huawei Technologies Co., Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ============================================================================ -"""Mindspore quantum simulator evolution operator.""" - -import numpy as np -from mindquantum.ops import QubitOperator -from mindspore import Tensor -from mindspore.ops.primitive import PrimitiveWithInfer -from mindspore.ops.primitive import prim_attr_register -from mindspore._checkparam import Validator as validator -from mindspore.common import dtype as mstype -from mindquantum.gate import Hamiltonian -from ._check_qnn_input import _check_circuit -from ._check_qnn_input import _check_non_parameterized_circuit -from ._check_qnn_input import _check_type_or_iterable_type -from ._check_qnn_input import _check_parameters_of_circuit - - -class Evolution(PrimitiveWithInfer): - r""" - Inputs of this operation is generated by MindQuantum framework. - - Inputs: - - **n_qubits** (int) - The qubit number of quantum simulator. - - **param_names** (list[str]) - The parameters names. - - **gate_names** (list[str]) - The name of each gate. - - **gate_matrix** (list[list[list[list[float]]]]) - Real part and image part of the matrix of quantum gate. - - **gate_obj_qubits** (list[list[int]]) - Object qubits of each gate. - - **gate_ctrl_qubits** (list[list[int]]) - Control qubits of each gate. - - **gate_params_names** (list[list[str]]) - Parameter names of each gate. - - **gate_coeff** (list[list[float]]) - Coefficient of eqch parameter of each gate. - - **gate_requires_grad** (list[list[bool]]) - Whether to calculate gradient of parameters of gates. - - **hams_pauli_coeff** (list[list[float]]) - Coefficient of pauli words. - - **hams_pauli_word** (list[list[list[str]]]) - Pauli words. - - **hams_pauli_qubit** (list[list[list[int]]]) - The qubit that pauli matrix act on. - - Outputs: - - **Quantum state** (Tensor) - The quantum state after evolution. - - Supported Platforms: - ``CPU`` - """ - @prim_attr_register - def __init__(self, n_qubits, param_names, gate_names, gate_matrix, - gate_obj_qubits, gate_ctrl_qubits, gate_params_names, - gate_coeff, gate_requires_grad, hams_pauli_coeff, - hams_pauli_word, hams_pauli_qubit): - """Initialize Evolutino""" - self.init_prim_io_names(inputs=['param_data'], outputs=['state']) - self.n_qubits = n_qubits - - def check_shape_size(self, param_data): - if len(param_data) != 1: - raise ValueError("PQC input param_data should have dimension size \ -equal to 1, but got {}.".format(len(param_data))) - - def infer_shape(self, param_data): - self.check_shape_size(param_data) - return [1 << self.n_qubits, 2] - - def infer_dtype(self, param_data): - args = {'param_data': param_data} - validator.check_tensors_dtypes_same_and_valid(args, mstype.float_type, - self.name) - return param_data - - def __call__(self, tmp=None): - if tmp is None: - if self.param_names: - raise ValueError( - "Parameterized circuit shuold have parameter input.") - tmp = Tensor(np.array([0]).astype(np.float32)) - state = super().__call__(tmp) - state = state.asnumpy() - state = state[:, 0] + state[:, 1] * 1j - return state - - -def generate_evolution_operator(circuit, param_names=None, hams=None): - """ - A method to generate a parameterized quantum circuit simulation operator. - - Args: - circuit (Circuit): The whole circuit combined with - encoder circuit and ansatz circuit, can be a parameterized circuit - or a non parameterized circuit. - param_names (list[str]): The list of parameter names, if None, than the - circuit should be a non parameterized circuit, otherwise, param_names will - be take from circuit. Default: None. - hams (Union[Hamiltonian, list[Hamiltonian]]): The measurement - hamiltonian. If None, than no hamiltonians will be applied on the - final quantum state. Default: None. - - Returns: - Evolution, A parameterized quantum circuit simulator operator supported by mindspore framework. - - Examples: - >>> import numpy as np - >>> from mindspore import Tensor - >>> import mindquantum.gate as G - >>> from mindquantum import Circuit - >>> from mindquantum.nn import generate_evolution_operator - >>> circ = Circuit(G.RX('a').on(0)) - >>> evol = generate_evolution_operator(circ, ['a']) - >>> state = evol(Tensor(np.array([0.5]).astype(np.float32))) - array([0.9689124+0.j , 0. -0.24740396j], dtype=complex64) - >>> G.RX(0.5).matrix()[:, 0] - array([0.96891242+0.j , 0. -0.24740396j]) - """ - if param_names is None: - param_names = circuit.para_name - if not param_names: - _check_non_parameterized_circuit(circuit) - else: - _check_circuit(circuit, 'circuit') - _check_parameters_of_circuit([], param_names, circuit) - if hams is not None: - _check_type_or_iterable_type(hams, Hamiltonian, 'Hamiltonian') - if isinstance(hams, Hamiltonian): - hams = [hams] - if hams is None: - ham_ms_data = Hamiltonian(QubitOperator()).mindspore_data() - else: - ham_ms_data = {} - for ham in hams: - for k, v in ham.mindspore_data().items(): - if k not in ham_ms_data: - ham_ms_data[k] = [v] - else: - ham_ms_data[k].append(v) - evol = Evolution(circuit.n_qubits, - param_names=param_names, - **circuit.mindspore_data(), - **ham_ms_data) - return evol diff --git a/mindquantum/nn/mindquantum_ansatz_only_layer.py b/mindquantum/nn/mindquantum_ansatz_only_layer.py deleted file mode 100644 index 296b9b95a8b84543d3319395c35b4bc419675b60..0000000000000000000000000000000000000000 --- a/mindquantum/nn/mindquantum_ansatz_only_layer.py +++ /dev/null @@ -1,169 +0,0 @@ -# Copyright 2021 Huawei Technologies Co., Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ============================================================================ -"""Basic mindquanutm neural layer with ansatz only.""" - -import numpy as np -from mindspore import Tensor -from mindquantum.circuit import Circuit -from .mindquantum_layer import MindQuantumLayer - - -class MindQuantumAnsatzOnlyOperator(MindQuantumLayer): - """ - An Ansatz only Mindquantum operator. - - This operator only need an ansatz circuit and the parameter data for ansatz - circuit. - - Args: - param_names (list[str]): Parameters names of ansatz circuit. - The order of this parameters is the same as the order of trainable - parameters. - circuit (Circuit): The ansatz circuit. - measurements (Union[Hamiltonian, list[Hamiltonian], Projector, list[Projector]]): - Hamiltonian or a list of Hamiltonian for measurement. - n_threads (int): Number of threads for data parallel. Default: 1. - - Inputs: - - **input** (Tensor) - Tensor of shape :math:`(E_{in}, )`, - where :math:`E_{in}` is the number of parameters in ansatz circuit. - - Outputs: - Tensor of shape :math:`(1, 1)`. - - Supported Platforms: - ``CPU`` - - Examples: - >>> from mindquantum import Circuit, RX, Hamiltonian - >>> from mindquantum.ops import QubitOperator - >>> from mindquantum.nn import MindQuantumAnsatzOnlyOperator - >>> from mindspore import Tensor - >>> import numpy as np - >>> circuit = Circuit(RX('a').on(0)) - >>> ham = Hamiltonian(QubitOperator('Z0')) - >>> mea = MindQuantumAnsatzOnlyOperator(circuit.para_name, circuit, ham) - >>> data = Tensor(np.array([0.5]).astype(np.float32)) - >>> mea(data) - Tensor(shape=[1, 1], dtype=Float32, value= - [[ 8.77582550e-01]]) - """ - def __init__(self, param_names, circuit, measurements, n_threads=1): - circuit, dummy_para = _add_dummy_encoder(circuit) - super(MindQuantumAnsatzOnlyOperator, - self).__init__([dummy_para], param_names, circuit, measurements, - 'normal', n_threads) - self.fake_data = Tensor(np.array([[0]]).astype(np.float32)) - del self.weight - - def construct(self, data): - x, _, _ = self.pqc(self.fake_data, data) - return x - - -def _add_dummy_encoder(circ): - """add a dummy parameterized gate""" - para_name = circ.para_name - index = 0 - while True: - name = f'_d_{index}' - if name not in para_name: - dummy_circ = Circuit().rx(name, 0).no_grad() - return dummy_circ + circ, name - index += 1 - - -class MindQuantumAnsatzOnlyLayer(MindQuantumLayer): - """ - An ansatz only trainable Mindquantum layer. - - A mindquantum layer simulate a parameterized quantum circuit and get the - measurement result. The quantum circuit is construct only by an ansatz circuit. - - Args: - param_names (list[str]): Parameters names of ansatz circuit. - The order of this parameters is the same as the order of trainable - parameters. - circuit (Circuit): The ansatz circuit. - measurements (Union[Hamiltonian, list[Hamiltonian], Projector, list[Projector]]): - Hamiltonian or a list of Hamiltonian for measurement. - weight_init (Union[Tensor, str, Initializer, numbers.Number]): The - trainable weight_init parameter. The dtype is same as input x. The - values of str refer to the function `initializer`. - Default: 'normal'. - n_threads (int): Number of threads for data parallel. Default: 1. - - Inputs: - No inputs needed. - - Outputs: - Tensor of shape :math:`(1, 1)`. - - Supported Platforms: - ``CPU`` - - Examples: - >>> from mindquantum import Circuit, H, RX, RY, RZ, Hamiltonian - >>> from mindquantum.nn import MindQuantumAnsatzOnlyLayer - >>> from mindquantum.ops import QubitOperator - >>> from mindspore import Tensor - >>> import mindspore.nn as nn - >>> import numpy as np - >>> circuit = Circuit([H.on(0), RZ(0.4).on(0), RX('a').on(0), RY('b').on(0)]) - >>> ham = Hamiltonian(QubitOperator('Z0')) - >>> init = Tensor(np.array([0, 0]).astype(np.float32)) - >>> net = MindQuantumAnsatzOnlyLayer(circuit.para_name, circuit, ham, init) - >>> opti = nn.Adam(net.trainable_params(), learning_rate=0.8) - >>> train_net = nn.TrainOneStepCell(net, opti) - >>> for i in range(1000): - ... train_net() - >>> net() - Tensor(shape=[1, 1], dtype=Float32, value= - [[-1.00000000e+00]]) - >>> net.weight.asnumpy() - array([-4.712389 , 1.9707963], dtype=float32) - - """ - def __init__(self, - param_names, - circuit, - measurements, - weight_init='normal', - n_threads=1): - circuit, dummy_para = _add_dummy_encoder(circuit) - super(MindQuantumAnsatzOnlyLayer, - self).__init__([dummy_para], param_names, circuit, measurements, - weight_init, n_threads) - self.fake_data = Tensor(np.array([[0]]).astype(np.float32)) - - def construct(self): - x, _, _ = self.pqc(self.fake_data, self.weight) - return x - - def final_state(self, measurements=None): - """ - Get the quantum state after evolution. - - Args: - measurements (Hamiltonian): Hamiltonian for measurement. If None, no hamiltonians - will be used. Default: None. - - Returns: - numpy.ndarray, the final quantum state. - """ - fake_data = Tensor(np.array([]).astype(np.float32)) - return super().final_state(fake_data, - self.weight, - measurements=measurements) diff --git a/mindquantum/nn/mindquantum_layer.py b/mindquantum/nn/mindquantum_layer.py deleted file mode 100644 index 7c16f6b14f5e81ea13c0e276beee08d6d1a7c0e4..0000000000000000000000000000000000000000 --- a/mindquantum/nn/mindquantum_layer.py +++ /dev/null @@ -1,141 +0,0 @@ -# Copyright 2021 Huawei Technologies Co., Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ============================================================================ -"""Basic mindquanutm neural layer.""" - -import numpy as np -from mindspore import Tensor -import mindspore.nn as nn -from mindspore.common.parameter import Parameter -from mindspore.common.initializer import initializer -from .pqc import generate_pqc_operator -from .evolution import generate_evolution_operator - - -class MindQuantumLayer(nn.Cell): - """ - A trainable Mindquantum layer. - - A mindquantum layer simulate a parameterized quantum circuit and get the - measurement result. The quantum circuit is construct by a encode circuit - and a trainable ansatz circuit. The encode circuit will encode classical - data into quantum state, and the trainable ansatz circuit apply on the - quantum state. - - Args: - encoder_params_names (list[str]): Parameters names of encoder circuit. - The order of this parameters is the same as the order of data - passed in construct. - ansatz_params_names (list[str]): Parameters names of ansatz circuit. - The order of this parameters is the same as the order of trainable - parameters. - circuit (Circuit): Quantum circuit construct by - encode circuit and ansatz circuit. - measurements (Union[Hamiltonian, list[Hamiltonian], Projector, list[Projector]]): - Hamiltonian or a list of Hamiltonian for measurement. - weight_init (Union[Tensor, str, Initializer, numbers.Number]): The - trainable weight_init parameter. The dtype is same as input x. The - values of str refer to the function `initializer`. - Default: 'normal'. - n_threads (int): Number of threads for data parallel. Default: 1. - - Inputs: - - **input** (Tensor) - Tensor of shape :math:`(N, E_{in})`, where :math:`N` is batch - size, :math:`E_{in}` is the number of parameters in encoder circuit. - - Outputs: - Tensor of shape :math:`(N, H_{out})`, where :math:`H_{out}` is the - number of hamiltonians. - - Supported Platforms: - ``CPU`` - - Examples: - >>> from mindquantum.ops import QubitOperator - >>> from mindquantum.nn import MindQuantumLayer - >>> from mindquantum import Circuit, Hamiltonian - >>> import mindquantum.gate as G - >>> encoder_circ = Circuit([G.RX('a').on(0)]) - >>> encoder_circ.no_grad() - >>> ansatz = Circuit([G.RY('b').on(0)]) - >>> ham = Hamiltonian(QubitOperator('Z0')) - >>> net = MindQuantumLayer(['a'], ['b'], encoder_circ + ansatz, ham) - >>> res = net(Tensor(np.array([[1.0]]).astype(np.float32))) - >>> res.asnumpy() - array([[0.54030216]], dtype=float32) - """ - def __init__(self, - encoder_params_names, - ansatz_params_names, - circuit, - measurements, - weight_init='normal', - n_threads=1): - super(MindQuantumLayer, self).__init__() - self.circuit = circuit - self.measurements = measurements - self.encoder_params_names = encoder_params_names - self.ansatz_params_names = ansatz_params_names - self.pqc = generate_pqc_operator(encoder_params_names, - ansatz_params_names, - circuit, - measurements, - n_threads=n_threads) - self.weight = Parameter(initializer(weight_init, - len(ansatz_params_names)), - name="weight") - - def final_state(self, - encoder_data, - ansatz_data=None, - circuit=None, - measurements=None): - """ - Get the quantum state after evolution. - - Args: - encoder_data (Tensor): A one dimension tensor for encoder circuit. - ansatz_data (Tensor): A one dimension tensor for ansatz circuit. If - ansatz_data is None, then the traind parameter will be used. - Default: None. - circuit (Circuit): Quantum circuit construct - by encode circuit and ansatz circuit. If None, the circuit for - train will be used. Default: None. - measurements (list[Hamiltonian]): Hamiltonian or a - list of Hamiltonian for measurement. If None, no hamiltonians - will be used. Default: None. - - Returns: - numpy.ndarray, the final quantum state. - """ - if circuit is None: - circuit = self.circuit - if ansatz_data is None: - ansatz_data = self.weight - if len(encoder_data.shape) != 1: - raise ValueError("Except a one dimension tensor for encoder_data!") - data = np.array([]) - data = np.append(data, encoder_data.asnumpy()) - data = np.append(data, ansatz_data.asnumpy()) - data = Tensor(data.astype(np.float32)) - param_names = [] - param_names.extend(self.encoder_params_names) - param_names.extend(self.ansatz_params_names) - evol = generate_evolution_operator(circuit, param_names, measurements) - state = evol(data) - return state - - def construct(self, x): - x, _, _ = self.pqc(x, self.weight) - return x diff --git a/mindquantum/nn/pqc.py b/mindquantum/nn/pqc.py deleted file mode 100644 index e8381f306dc0f25e04554a6764ee10c808351f74..0000000000000000000000000000000000000000 --- a/mindquantum/nn/pqc.py +++ /dev/null @@ -1,212 +0,0 @@ -# Copyright 2021 Huawei Technologies Co., Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ============================================================================ -"""Mindspore quantum simulator operator.""" - -from mindspore.ops.primitive import PrimitiveWithInfer -from mindspore.ops.primitive import prim_attr_register -from mindspore._checkparam import Validator as validator -from mindspore.common import dtype as mstype -from mindspore.ops._grad.grad_base import bprop_getters -import mindspore.ops as P -from mindquantum.circuit import Circuit -from mindquantum.gate import Hamiltonian, IGate -from mindquantum.gate import Projector -from mindquantum.ops import QubitOperator -from mindquantum.utils import count_qubits -from ._check_qnn_input import _check_type_or_iterable_type -from ._check_qnn_input import _check_circuit -from ._check_qnn_input import _check_parameters_of_circuit - - -class PQC(PrimitiveWithInfer): - r""" - Evaluate a parameterized quantum circuit and calculate the gradient of each parameters. - - Inputs of this operation is generated by MindQuantum framework. - - Inputs: - - **n_qubits** (int) - The qubit number of quantum simulator. - - **encoder_params_names** (list[str]) - The parameters names of encoder circuit. - - **ansatz_params_names** (list[str]) - The parameters names of ansatz circuit. - - **gate_names** (list[str]) - The name of each gate. - - **gate_matrix** (list[list[list[list[float]]]]) - Real part and image part of the matrix of quantum gate. - - **gate_obj_qubits** (list[list[int]]) - Object qubits of each gate. - - **gate_ctrl_qubits** (list[list[int]]) - Control qubits of each gate. - - **gate_params_names** (list[list[str]]) - Parameter names of each gate. - - **gate_coeff** (list[list[float]]) - Coefficient of eqch parameter of each gate. - - **gate_requires_grad** (list[list[bool]]) - Whether to calculate gradient of parameters of gates. - - **hams_pauli_coeff** (list[list[float]]) - Coefficient of pauli words. - - **hams_pauli_word** (list[list[list[str]]]) - Pauli words. - - **hams_pauli_qubit** (list[list[list[int]]]) - The qubit that pauli matrix act on. - - **is_projector** (bool) - Whether the measurement operator is a subspace bitstring measurement. - - **projectors** (Union[str, list[str]]) - The projector bitstrings. - - **n_threads** (int) - Thread to evaluate input data. - - Outputs: - - **expected_value** (Tensor) - The expected value of hamiltonian. - - **g1** (Tensor) - Gradient of encode circuit parameters. - - **g2** (Tensor) - Gradient of ansatz circuit parameters. - - Supported Platforms: - ``CPU`` - """ - @prim_attr_register - def __init__(self, n_qubits, encoder_params_names, ansatz_params_names, - gate_names, gate_matrix, gate_obj_qubits, gate_ctrl_qubits, - gate_params_names, gate_coeff, gate_requires_grad, - hams_pauli_coeff, hams_pauli_word, hams_pauli_qubit, - is_projector, projectors, n_threads): - """Initialize PQC""" - self.init_prim_io_names( - inputs=['encoder_data', 'ansatz_data'], - outputs=['results', 'encoder_gradient', 'ansatz_gradient']) - if is_projector: - self.n_meas = len(projectors) - else: - self.n_meas = len(hams_pauli_coeff) - - def check_shape_size(self, encoder_data, ansatz_data): - """check shape size""" - if len(encoder_data) != 2: - raise ValueError( - "PQC input encoder_data should have dimension size \ -equal to 2, but got {}.".format(len(encoder_data))) - if len(ansatz_data) != 1: - raise ValueError( - "PQC input ansatz_data should have dimension size \ -equal to 1, but got {}.".format(len(ansatz_data))) - if encoder_data[1] != len(self.encoder_params_names): - raise ValueError( - f'Input encoder_data length {encoder_data[1]} \ -mismatch with circuit encoder parameter number {len(self.encoder_params_names)}') - if ansatz_data[0] != len(self.ansatz_params_names): - raise ValueError( - f'Input ansatz_data length {ansatz_data[1]} \ -mismatch with circuit ansatz parameter number {len(self.ansatz_params_names)}') - - def infer_shape(self, encoder_data, ansatz_data): - self.check_shape_size(encoder_data, ansatz_data) - return [encoder_data[0], self.n_meas], [ - encoder_data[0], self.n_meas, - len(self.encoder_params_names) - ], [encoder_data[0], self.n_meas, - len(self.ansatz_params_names)] - - def infer_dtype(self, encoder_data, ansatz_data): - args = {'encoder_data': encoder_data, 'ansatz_data': ansatz_data} - validator.check_tensors_dtypes_same_and_valid(args, mstype.float_type, - self.name) - return encoder_data, encoder_data, encoder_data - - -@bprop_getters.register(PQC) -def bprop_pqc(self): - """Grad definition for `PQC` operation.""" - t = P.Transpose() - mul = P.Mul() - sum_ = P.ReduceSum() - - def bprop(encoder_data, ansatz_data, out, dout): - dx = t(out[1], (2, 0, 1)) - dx = mul(dout[0], dx) - dx = sum_(dx, 2) - dx = t(dx, (1, 0)) - dy = P.tensor_dot(dout[0], out[2], ((0, 1), (0, 1))) - return dx, dy - - return bprop - - -def generate_pqc_operator(encoder_params_names, - ansatz_params_names, - circuit: Circuit, - measurements, - n_threads=1): - """ - A method to generate a parameterized quantum circuit simulation operator. - - Args: - encoder_params_names (list[str]): The list of parameter names for - encoder circuit. - ansatz_params_names (list[str]): The list of parameter names for ansatz - circuit. - circuit (Circuit): The whole circuit combined with - encoder circuit and ansatz circuit. - measurements (Union[Hamiltonian, list[Hamiltonian], Projector, list[Projector]]): The measurement - operators, would be hamiltonians or projectors. - n_threads (int): Number of threads for data parallelize. Default: 1. - - Returns: - PQC, A parameterized quantum circuit simulator operator supported by mindspore framework. - - Examples: - >>> from mindquantum.ops import QubitOperator - >>> from mindquantum import Circuit - >>> import mindquantum.gate as G - >>> from mindquantum.nn import generate_pqc_operator - >>> encoder_circ = Circuit([G.RX('a').on(0)]) - >>> ansatz_circ = Circuit([G.RY('b').on(0)]) - >>> circ = encoder_circ + ansatz_circ - >>> ham = G.Hamiltonian(QubitOperator('Z0')) - >>> pqc = generate_pqc_operator(['a'], ['b'], circ, ham) - """ - _check_circuit(circuit, 'Circuit') - _check_type_or_iterable_type(measurements, (Hamiltonian, Projector), - 'Hamiltonian or Projector') - _check_parameters_of_circuit(encoder_params_names, ansatz_params_names, - circuit) - if not isinstance(n_threads, int) or n_threads <= 0: - raise TypeError( - "n_threads requires a positive int, but get {}".format(n_threads)) - is_projector = False - if isinstance(measurements, (Projector, Hamiltonian)): - measurements = [measurements] - hams = [Hamiltonian(QubitOperator())] - pros = [Projector('')] - if isinstance(measurements[0], Hamiltonian): - hams = measurements - n_qubits_ham = count_qubits(hams[0].hamiltonian) - if n_qubits_ham > circuit.n_qubits: - circuit += IGate().on(n_qubits_ham - 1) - if isinstance(measurements[0], Projector): - for pro in measurements: - if pro.n_qubits != circuit.n_qubits: - raise ValueError( - f"The qubit of projector {pro} not match with quantum circuit." - ) - is_projector = True - pros = measurements - ham_ms_data = {} - for ham in hams: - for k, v in ham.mindspore_data().items(): - if k not in ham_ms_data: - ham_ms_data[k] = [v] - else: - ham_ms_data[k].append(v) - proj_ms_data = {} - for pro in pros: - for k, v in pro.mindspore_data().items(): - if k not in proj_ms_data: - proj_ms_data[k] = [v] - else: - proj_ms_data[k].append(v) - return PQC(circuit.n_qubits, - encoder_params_names=encoder_params_names, - ansatz_params_names=ansatz_params_names, - **circuit.mindspore_data(), - **ham_ms_data, - is_projector=is_projector, - **proj_ms_data, - n_threads=n_threads) diff --git a/mindquantum/simulator/__init__.py b/mindquantum/simulator/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..dbeaa73455d275f50e5d6a4c2faa35ea91d6381a --- /dev/null +++ b/mindquantum/simulator/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""Simulator.""" + +from .simulator import Simulator +from .simulator import GradOpsWrapper +from .simulator import get_supported_simulator + +__all__ = ['Simulator', 'GradOpsWrapper', 'get_supported_simulator'] +__all__.sort() diff --git a/mindquantum/simulator/simulator.py b/mindquantum/simulator/simulator.py new file mode 100644 index 0000000000000000000000000000000000000000..5fbaef0f35f21ecfe86f0f6c2417d016676e5238 --- /dev/null +++ b/mindquantum/simulator/simulator.py @@ -0,0 +1,653 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""Simulator.""" +from mindquantum.core.gates.basic import BasicGate +import numpy as np +from mindquantum.core.circuit import Circuit +from mindquantum.core.operators import Hamiltonian +from mindquantum.core.operators.hamiltonian import MODE +from mindquantum.core.parameterresolver import ParameterResolver +from mindquantum.core.gates import MeasureResult +from mindquantum.core.gates import Measure +from mindquantum.core.gates import BarrierGate +from mindquantum.utils import ket_string +from mindquantum import mqbackend as mb +from mindquantum.utils.type_value_check import _check_input_type +from mindquantum.utils.type_value_check import _check_value_should_not_less +from mindquantum.utils.type_value_check import _check_value_should_between_close_set +from mindquantum.utils.type_value_check import _check_and_generate_pr_type + +SUPPORTED_SIMULATOR = ['projectq'] + + +def get_supported_simulator(): + """ + Get simulator name that supported by MindQuantum. + + Returns: + list, The supported simulator list. + """ + return SUPPORTED_SIMULATOR + + +class Simulator: + """ + Quantum simulator that simulate quantum circuit. + + Args: + backend (str): which backend you want. The supported backend can be found + in SUPPORTED_SIMULATOR + n_qubits (int): number of quantum simulator. + seed (int): the random seed for this simulator. Default: 42. + + Raises: + TypeError: if `backend` is not str. + TypeError: if `n_qubits` is not int. + TypeError: if `seed` is not int. + ValueError: if `backend` is not supported. + ValueError: if `n_qubits` is negative. + ValueError: if `seed` is less than 0 or great than 2**23 - 1. + + Examples: + >>> from mindquantum import Simulator + >>> from mindquantum import qft + >>> sim = Simulator('projectq', 2) + >>> sim.apply_circuit(qft(range(2))) + >>> sim.get_qs() + array([0.5+0.j, 0.5+0.j, 0.5+0.j, 0.5+0.j]) + """ + def __init__(self, backend, n_qubits, seed=42): + _check_input_type('backend', str, backend) + _check_input_type('n_qubits', int, n_qubits) + _check_value_should_not_less('n_qubits', 0, n_qubits) + _check_input_type('seed', int, seed) + _check_value_should_between_close_set('seed', 0, 2**23 - 1, seed) + if backend not in SUPPORTED_SIMULATOR: + raise ValueError(f"backend {backend} not supported!") + self.backend = backend + self.seed = seed + self.n_qubits = n_qubits + if backend == 'projectq': + self.sim = mb.projectq(seed, n_qubits) + + def __str__(self): + state = self.get_qs() + s = f"{self.backend} simulator with {self.n_qubits} qubit{'s' if self.n_qubits > 1 else ''}." + s += f"\nCurrent quantum state:\n" + if self.n_qubits < 4: + s += '\n'.join(ket_string(state)) + else: + s += state.__str__() + return s + + def __repr__(self): + return self.__str__() + + def reset(self): + """ + Reset simulator to zero state. + + Examples: + >>> from mindquantum import Simulator + >>> from mindquantum import qft + >>> sim = Simulator('projectq', 2) + >>> sim.apply_circuit(qft(range(2))) + >>> sim.reset() + >>> sim.get_qs() + array([1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j]) + """ + self.sim.reset() + + def flush(self): + """ + Flush gate that works for projectq simulator. The projectq simulator + will cache several gate and fushion these gate into a bigger gate, and + than act on the quantum state. The flush command will ask the simulator + to fushion currently stored gate and act on the quantum state. + + Examples: + >>> from mindquantum import Simulator + >>> from mindquantum import H + >>> sim = Simulator('projectq', 1) + >>> sim.apply_gate(H.on(0)) + >>> sim.flush() + """ + if self.backend == 'projectq': + self.sim.run() + + def apply_gate(self, gate, pr=None): + """ + Apply a gate on this simulator, can be a quantum gate or a measurement operator + + Args: + gate (BasicGate): The gate you want to apply. + pr (Union[numbers.Number, numpy.ndarray, ParameterResolver, list]): The + parameter for parameterized gate. Default: None. + + Returns: + int or None, if the gate if a measure gate, then return a collapsed state, Otherwise + return None. + + Raises: + TypeError: if `gate` is not a BasicGate. + ValueError: if any qubit of `gate` is higher than simulator qubits. + ValueError: if `gate` is parameterized, but no parameter supplied. + TypeError: the `pr` is not a ParameterResolver if `gate` is parameterized. + + Examples: + >>> import numpy as np + >>> from mindquantum import Simulator + >>> from mindquantum import RY, Measure + >>> sim = Simulator('projectq', 1) + >>> sim.apply_gate(RY('a').on(0), np.pi/2) + >>> sim.get_qs() + array([0.70710678+0.j, 0.70710678+0.j]) + >>> sim.apply_gate(Measure().on(0)) + 1 + >>> sim.get_qs() + array([0.+0.j, 1.+0.j]) + """ + _check_input_type('gate', BasicGate, gate) + if not isinstance(gate, BarrierGate): + gate_max = max(max(gate.obj_qubits, gate.ctrl_qubits)) + if self.n_qubits < gate_max: + raise ValueError(f"qubits of gate {gate} is higher than simulator qubits.") + if isinstance(gate, Measure): + return self.sim.apply_measure(gate.get_cpp_obj()) + if pr is None: + if gate.parameterized: + raise ValueError("apply a parameterized gate needs a parameter_resolver") + self.sim.apply_gate(gate.get_cpp_obj()) + else: + pr = _check_and_generate_pr_type(pr, gate.coeff.params_name) + self.sim.apply_gate(gate.get_cpp_obj(), pr.get_cpp_obj(), False) + return None + + def apply_circuit(self, circuit, pr=None): + """ + Apply a circuit on this simulator. + + Args: + circuit (Circuit): The quantum circuit you want to apply on this simulator. + pr (Union[ParameterResolver, dict, numpy.ndarray, list, numbers.Number]): The + parameter resolver for this circuit. If the circuit is not parameterized, + this arg should be None. Default: None. + + Returns: + MeasureResult or None, if the circuit has measure gate, then return a MeasureResult, + otherwise return None. + + Examples: + >>> import numpy as np + >>> from mindquantum import Circuit, H + >>> from mindquantum import Simulator + >>> sim = Simulator('projectq', 2) + >>> sim.apply_circuit(Circuit().un(H, 2)) + >>> sim.apply_circuit(Circuit().ry('a', 0).ry('b', 1), np.array([1.1, 2.2])) + >>> sim + projectq simulator with 2 qubits. + Current quantum state: + -0.0721702531972066¦00⟩ + -0.30090405886869676¦01⟩ + 0.22178317006196263¦10⟩ + 0.9246947752567126¦11⟩ + >>> sim.apply_circuit(Circuit().measure(0).measure(1)) + shots: 1 + Keys: q1 q0│0.00 0.2 0.4 0.6 0.8 1.0 + ───────────┼───────────┴───────────┴───────────┴───────────┴───────────┴ + 11│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ + │ + {'11': 1} + """ + _check_input_type('circuit', Circuit, circuit) + if self.n_qubits < circuit.n_qubits: + raise ValueError(f"Circuit has {circuit.n_qubits} qubits, which is more than simulator qubits.") + if circuit.has_measure_gate: + res = MeasureResult() + res.add_measure(circuit.all_measures.keys()) + if circuit.params_name: + if pr is None: + raise ValueError("Applying a parameterized circuit needs a parameter_resolver") + pr = _check_and_generate_pr_type(pr, circuit.params_name) + else: + pr = ParameterResolver() + if circuit.has_measure_gate: + samples = np.array( + self.sim.apply_circuit_with_measure(circuit.get_cpp_obj(), pr.get_cpp_obj(), res.keys_map)) + samples = samples.reshape((1, -1)) + res.collect_data(samples) + return res + if circuit.params_name: + self.sim.apply_circuit(circuit.get_cpp_obj(), pr.get_cpp_obj()) + else: + self.sim.apply_circuit(circuit.get_cpp_obj()) + return None + + def sampling(self, circuit, pr=None, shots=1, seed=None): + """ + Samping the measure qubit in circuit. Sampling do not change the origin quantum + state of this simulator. + + Args: + circuit (Circuit): The circuit that you want to evolution and do sampling. + pr (Union[None, dict, ParameterResolver]): The parameter + resolver for this circuit, if this circuit is a parameterized circuit. + Default: None. + shots (int): How many shots you want to sampling this circuit. Default: 1 + seed (int): Random seed for random sampling. Default: None. + + Returns: + MeasureResult, the measure result of sampling. + + Examples: + >>> from mindquantum import Circuit, Measure + >>> from mindquantum import Simulator + >>> circ = Circuit().ry('a', 0).ry('b', 1) + >>> circ += Measure('q0_0').on(0) + >>> circ += Measure('q0_1').on(0) + >>> circ += Measure('q1').on(1) + >>> sim = Simulator('projectq', circ.n_qubits) + >>> res = sim.sampling(circ, {'a': 1.1, 'b': 2.2}, shots=100, seed=42) + >>> res + shots: 100 + Keys: q1 q0_1 q0_0│0.00 0.122 0.245 0.367 0.49 0.612 + ──────────────────┼───────────┴───────────┴───────────┴───────────┴───────────┴ + 000│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ + │ + 011│▒▒▒▒▒▒▒ + │ + 100│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ + │ + 111│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ + │ + {'000': 17, '011': 8, '100': 49, '111': 26} + """ + if not isinstance(circuit, Circuit): + raise TypeError(f"sampling circuit need a quantum circuit but get {type(circuit)}") + if self.n_qubits < circuit.n_qubits: + raise ValueError(f"Circuit has {circuit.n_qubits} qubits, which is more than simulator qubits.") + if not isinstance(shots, int) or shots < 0: + raise ValueError(f"sampling shot should be non negative int, but get {shots}") + if circuit.parameterized: + if pr is None: + raise ValueError("Sampling a parameterized circuit need a ParameterResolver") + pr = ParameterResolver(pr) + else: + pr = ParameterResolver() + if seed is None: + seed = self.seed + elif not isinstance(seed, int) or seed < 0 or seed > 2**23 - 1: + raise ValueError(f"seed must be between 0 and 2**23 - 1") + res = MeasureResult() + res.add_measure(circuit.all_measures.keys()) + samples = np.array(self.sim.sampling(circuit.get_cpp_obj(), pr.get_cpp_obj(), shots, res.keys_map, + seed)).reshape((shots, -1)) + res.collect_data(samples) + return res + + def apply_hamiltonian(self, hamiltonian: Hamiltonian): + """ + Apply hamiltonian to a simulator, this hamiltonian can be + hermitian or non hermitian. + + Notes: + The quantum state may be not a normalized quantum state after apply hamiltonian. + + Args: + hamiltonian (Hamiltonian): the hamiltonian you want to apply. + + Examples: + >>> from mindquantum import Simulator + >>> from mindquantum import Circuit, Hamiltonian + >>> from mindquantum.core.operators import QubitOperator + >>> import scipy.sparse as sp + >>> sim = Simulator('projectq', 1) + >>> sim.apply_circuit(Circuit().h(0)) + >>> sim.get_qs() + array([0.70710678+0.j, 0.70710678+0.j]) + >>> ham1 = Hamiltonian(QubitOperator('Z0')) + >>> sim.apply_hamiltonian(ham1) + >>> sim.get_qs() + array([ 0.70710678+0.j, -0.70710678+0.j]) + + >>> sim.reset() + >>> ham2 = Hamiltonian(sp.csr_matrix([[1, 2], [3, 4]])) + >>> sim.apply_hamiltonian(ham2) + >>> sim.get_qs() + array([1.+0.j, 3.+0.j]) + """ + + _check_input_type('hamiltonian', Hamiltonian, hamiltonian) + _check_hamiltonian_qubits_number(hamiltonian, self.n_qubits) + self.sim.apply_hamiltonian(hamiltonian.get_cpp_obj()) + + def get_expectation(self, hamiltonian): + r""" + Get expectation of the given hamiltonian. The hamiltonian could be non hermitian. + + .. math:: + + E = \left<\psi\right|H\left|\psi\right> + + Args: + hamiltonian (Hamiltonian): The hamiltonian you want to get expectation. + + Returns: + numbers.Number, the expectation value. + + Examples: + >>> from mindquantum.core.operators import QubitOperator + >>> from mindquantum import Circuit, Simulator + >>> from mindquantum import Hamiltonian + >>> sim = Simulator('projectq', 1) + >>> sim.apply_circuit(Circuit().ry(1.2, 0)) + >>> ham = Hamiltonian(QubitOperator('Z0')) + >>> sim.get_expectation(ham) + (0.36235775447667357+0j) + """ + if not isinstance(hamiltonian, Hamiltonian): + raise TypeError(f"hamiltonian requires a Hamiltonian, but got {type(hamiltonian)}") + _check_hamiltonian_qubits_number(hamiltonian, self.n_qubits) + return self.sim.get_expectation(hamiltonian.get_cpp_obj()) + + def get_qs(self, ket=False): + """ + Get current quantum state of this simulator. + + Args: + ket (bool): Whether to return the quantum state in ket format or not. + Default: False. + + Returns: + numpy.ndarray, the current quantum state. + + Examples: + >>> from mindquantum import qft, Simulator + >>> sim = Simulator('projectq', 2) + >>> sim.apply_circuit(qft(range(2))) + >>> sim.get_qs() + array([0.5+0.j, 0.5+0.j, 0.5+0.j, 0.5+0.j]) + """ + state = np.array(self.sim.get_qs()) + if ket: + return '\n'.join(ket_string(state)) + return state + + def set_qs(self, vec): + """ + Set quantum state for this simulation. + + Args: + vec (numpy.ndarray): the quantum state that you want. + + Examples: + >>> from mindquantum import Simulator + >>> import numpy as np + >>> sim = Simulator('projectq', 1) + >>> sim.get_qs() + array([1.+0.j, 0.+0.j]) + >>> sim.set_qs(np.array([1, 1])) + >>> sim.get_qs() + array([0.70710678+0.j, 0.70710678+0.j]) + """ + if not isinstance(vec, np.ndarray): + raise TypeError(f"quantum state must be a ndarray, but get {type(vec)}") + if len(vec.shape) != 1: + raise ValueError(f"vec requires a 1-dimensional array, but get {vec.shape}") + n_qubits = np.log2(vec.shape[0]) + if n_qubits % 1 != 0: + raise ValueError(f"vec size {vec.shape[0]} is not power of 2") + n_qubits = int(n_qubits) + if self.n_qubits != n_qubits: + raise ValueError(f"{n_qubits} qubits vec does not match with simulation qubits ({self.n_qubits})") + self.sim.set_qs(vec / np.sqrt(np.sum(np.abs(vec)**2))) + + def get_expectation_with_grad(self, + hams, + circ_right, + circ_left=None, + encoder_params_name=None, + ansatz_params_name=None, + parallel_worker=None): + r""" + Get a function that return the forward value and gradient w.r.t circuit parameters. + This method is designed to calculate the expectation and its gradient shown as below. + + .. math:: + + E = \left<\psi\right|U_l^\dagger H U_r \left|\psi\right> + + where :math:`U_l` is circ_left, :math:`U_r` is circ_right, :math:`H` is hams + and :math:`\left|\psi\right>` is the current quantum state of this simulator. + + Args: + hams (Hamiltonian): The hamiltonian that need to get expectation. + circ_right (Circuit): The :math:`U_r` circuit described above. + circ_left (Circuit): The :math:`U_l` circuit described above. By default, this circuit + will be none, and in this situation, :math:`U_l` will be equals to + :math:`U_r`. Default: None. + encoder_params_name (list[str]): To specific which parameters belongs to encoder, + that will encoder the input data into quantum state. The encoder data + can be a batch. + ansatz_params_name (list[str]): To specific which parameters belongs to ansatz, + that will be trained during training. + parallel_worker (int): The parallel worker numbers. The parallel workers can handle + batch in parallel threads. + + Returns: + GradOpsWrapper, a grad ops wrapper than contains information to generate this grad ops. + + Examples: + >>> import numpy as np + >>> from mindquantum import Simulator, Hamiltonian + >>> from mindquantum import Circuit + >>> from mindquantum.core.operators import QubitOperator + >>> circ = Circuit().ry('a', 1) + >>> ham = Hamiltonian(QubitOperator('Z0')) + >>> sim = Simulator('projectq', 1) + >>> grad_ops = sim.get_expectation_with_grad(ham, circ) + >>> grad_ops(np.array([1.0])) + (array([[0.54030231+0.j]]), array([[[-0.84147098+0.j]]])) + """ + if isinstance(hams, Hamiltonian): + hams = [hams] + elif not isinstance(hams, list): + raise ValueError(f"hams requires a Hamiltonian or a list of Hamiltonian, but get {type(hams)}") + for h_tmp in hams: + if not isinstance(h_tmp, Hamiltonian): + raise TypeError(f"hams's element should be a Hamiltonian, but get {type(h_tmp)}") + _check_hamiltonian_qubits_number(h_tmp, self.n_qubits) + if not isinstance(circ_right, Circuit): + raise ValueError(f"Quantum circuit need a Circuit, but get {type(circ_right)}") + if circ_left is not None and not isinstance(circ_left, Circuit): + raise ValueError(f"Quantum circuit need a Circuit, but get {type(circ_left)}") + if circ_left is None: + circ_left = Circuit() + if circ_left.has_measure_gate or circ_right.has_measure_gate: + raise ValueError("circuit for variational algorithm cannot have measure gate") + if parallel_worker is not None and not isinstance(parallel_worker, int): + raise ValueError(f"parallel_worker need a integer, but get {type(parallel_worker)}") + if encoder_params_name is None and ansatz_params_name is None: + encoder_params_name = [] + ansatz_params_name = [i for i in circ_right.params_name] + for i in circ_left.params_name: + if i not in ansatz_params_name: + ansatz_params_name.append(i) + if encoder_params_name is not None and not isinstance(encoder_params_name, list): + raise ValueError(f"encoder_params_name requires a list of str, but get {type(encoder_params_name)}") + if ansatz_params_name is not None and not isinstance(ansatz_params_name, list): + raise ValueError(f"ansatz_params_name requires a list of str, but get {type(ansatz_params_name)}") + if encoder_params_name is None: + encoder_params_name = [] + if ansatz_params_name is None: + ansatz_params_name = [] + s1 = set(circ_right.params_name) | set(circ_left.params_name) + s2 = set(encoder_params_name) | set(ansatz_params_name) + if s1 - s2 or s2 - s1: + raise ValueError("encoder_params_name and ansatz_params_name are different with circuit parameters") + circ_n_qubits = max(circ_left.n_qubits, circ_right.n_qubits) + if self.n_qubits < circ_n_qubits: + raise ValueError(f"Simulator has {self.n_qubits} qubits, but circuit has {circ_n_qubits} qubits.") + version = "both" + if not ansatz_params_name: + version = "encoder" + if not encoder_params_name: + version = "ansatz" + + def grad_ops(*inputs): + if version == "both" and len(inputs) != 2: + raise ValueError("Need two inputs!") + if version in ("encoder", "ansatz") and len(inputs) != 1: + raise ValueError("Need one input!") + if version == "both": + _check_encoder(inputs[0], len(encoder_params_name)) + _check_ansatz(inputs[1], len(ansatz_params_name)) + batch_threads, mea_threads = _thread_balance(inputs[0].shape[0], len(hams), parallel_worker) + inputs0 = inputs[0] + inputs1 = inputs[1] + if version == "encoder": + _check_encoder(inputs[0], len(encoder_params_name)) + batch_threads, mea_threads = _thread_balance(inputs[0].shape[0], len(hams), parallel_worker) + inputs0 = inputs[0] + inputs1 = np.array([]) + if version == "ansatz": + _check_ansatz(inputs[0], len(ansatz_params_name)) + batch_threads, mea_threads = _thread_balance(1, len(hams), parallel_worker) + inputs0 = np.array([[]]) + inputs1 = inputs[0] + if circ_left: + f_g1_g2 = self.sim.non_hermitian_measure_with_grad([i.get_cpp_obj() for i in hams], + [i.get_cpp_obj(hermitian=True) for i in hams], + circ_right.get_cpp_obj(), + circ_right.get_cpp_obj(hermitian=True), + circ_left.get_cpp_obj(), + circ_left.get_cpp_obj(hermitian=True), inputs0, + inputs1, encoder_params_name, ansatz_params_name, + batch_threads, mea_threads) + else: + f_g1_g2 = self.sim.hermitian_measure_with_grad([i.get_cpp_obj() + for i in hams], circ_right.get_cpp_obj(), + circ_right.get_cpp_obj(hermitian=True), inputs0, inputs1, + encoder_params_name, ansatz_params_name, batch_threads, + mea_threads) + res = np.array(f_g1_g2) + if version == 'both': + f = res[:, :, 0] + g1 = res[:, :, 1:1 + len(encoder_params_name)] + g2 = res[:, :, 1 + len(encoder_params_name):] + return f, g1, g2 + f = res[:, :, 0] + g = res[:, :, 1:] + return f, g + + grad_wrapper = GradOpsWrapper(grad_ops, hams, circ_right, circ_left, encoder_params_name, ansatz_params_name, + parallel_worker) + s = f'{self.n_qubits} qubit' + ('' if self.n_qubits == 1 else 's') + s += f' {self.backend} VQA Operator' + grad_wrapper.set_str(s) + return grad_wrapper + + +def _check_encoder(data, encoder_params_size): + if not isinstance(data, np.ndarray): + raise ValueError(f"encoder parameters need numpy array, but get {type(data)}") + data_shape = data.shape + if len(data_shape) != 2: + raise ValueError("encoder data requires a two dimension numpy array") + if data_shape[1] != encoder_params_size: + raise ValueError(f"encoder parameters size do not match with encoder parameters name,\ +need {encoder_params_size} but get {data_shape[1]}.") + + +def _check_ansatz(data, ansatz_params_size): + """check ansatz""" + if not isinstance(data, np.ndarray): + raise ValueError(f"ansatz parameters need numpy array, but get {type(data)}") + data_shape = data.shape + if len(data_shape) != 1: + raise ValueError("ansatz data requires a one dimension numpy array") + if data_shape[0] != ansatz_params_size: + raise ValueError(f"ansatz parameters size do not match with ansatz parameters name,\ +need {ansatz_params_size} but get {data_shape[0]}") + + +def _thread_balance(n_prs, n_meas, parallel_worker): + """threa balance""" + if parallel_worker is None: + parallel_worker = n_meas * n_prs + if n_meas * n_prs <= parallel_worker: + batch_threads = n_prs + mea_threads = n_meas + else: + if n_meas < n_prs: + batch_threads = min(n_prs, parallel_worker) + mea_threads = min(n_meas, max(1, parallel_worker // batch_threads)) + else: + mea_threads = min(n_meas, parallel_worker) + batch_threads = min(n_prs, max(1, parallel_worker // mea_threads)) + return batch_threads, mea_threads + + +def _check_hamiltonian_qubits_number(hamiltonian, sim_qubits): + """check hamiltonian qubits number""" + if hamiltonian.how_to != MODE['origin']: + if hamiltonian.n_qubits != sim_qubits: + raise ValueError(f"Hamiltonian qubits is {hamiltonian.n_qubits}, not match \ +with simulator qubits number {sim_qubits}") + else: + if hamiltonian.n_qubits > sim_qubits: + raise ValueError(f"Hamiltonian qubits is {hamiltonian.n_qubits}, which is bigger than simulator qubits.") + + +class GradOpsWrapper: + """ + Wrapper the gradient operator that with the information that generate this + gradient operator. + + Args: + grad_ops (Union[FunctionType, MethodType])): A function or a method + that return forward value and gradient w.r.t parameters. + hams (Hamiltonian): The hamiltonian that generate this grad ops. + circ_right (Circuit): The right circuit that generate this grad ops. + circ_left (Circuit): The left circuit that generate this grad ops. + encoder_params_name (list[str]): The encoder parameters name. + ansatz_params_name (list[str]): The ansatz parameters name. + parallel_worker (int): The number of parallel worker to run the batch. + """ + def __init__(self, grad_ops, hams, circ_right, circ_left, encoder_params_name, ansatz_params_name, parallel_worker): + self.grad_ops = grad_ops + self.hams = hams + self.circ_right = circ_right + self.circ_left = circ_left + self.encoder_params_name = encoder_params_name + self.ansatz_params_name = ansatz_params_name + self.parallel_worker = parallel_worker + self.str = '' + + def __call__(self, *args): + return self.grad_ops(*args) + + def set_str(self, s): + """ + Set expression for gradient operator. + + Args: + s (str): The string of QNN operator. + """ + self.str = s + + +__all__ = ['Simulator', 'get_supported_simulator', 'GradOpsWrapper'] diff --git a/mindquantum/src/CMakeLists.txt b/mindquantum/src/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..5e44f0a5045635961316d27257bba98a59fba7e6 --- /dev/null +++ b/mindquantum/src/CMakeLists.txt @@ -0,0 +1,69 @@ +# ============================================================================== +# +# Copyright 2021 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ============================================================================== + +# lint_cmake: -whitespace/indent + +# for pybind +pybind11_add_module(mqbackend ${CMAKE_CURRENT_SOURCE_DIR}/binding.cc) +target_compile_definitions(mqbackend PUBLIC $<$:ENABLE_PROJECTQ> + $<$:ENABLE_QUEST>) +target_include_directories(mqbackend PUBLIC ${CMAKE_CURRENT_LIST_DIR}) + +target_link_libraries(mqbackend PUBLIC ${PARALLEL_LIBS} "$<$:mq_projectq>" + "$<$:QuEST;mq_quest>" mq_base) + +set_output_directory_auto(mqbackend mindquantum) +python_install_set_rpath(mqbackend "") + +set(MQ_BASE_HEADERS + gate/basic_gate.h + gate/gates.h + hamiltonian/hamiltonian.h + matrix/two_dim_matrix.h + core/popcnt.h + pr/parameter_resolver.h + projector/projector.h + sparse/algo.h + sparse/csrhdmatrix.h + sparse/paulimat.h + sparse/sparse_utils.h + core/type.h + core/utils.h) +set(MQ_BASE_SOURCES utils.cc) +add_subdirectory(backends) + +if(ENABLE_CUDA) + add_library(mq_base STATIC ${MQ_BASE_SOURCES} ${MQ_BASE_HEADERS}) + + target_compile_definitions(mq_base PUBLIC GPUACCELERATED) + target_link_libraries(mq_base PUBLIC $,CUDA::cudart_static,CUDA::cudart>) +else() + add_library(mq_base STATIC ${MQ_BASE_SOURCES} ${MQ_BASE_HEADERS}) +endif() +target_include_directories(mq_base PUBLIC ${CMAKE_CURRENT_LIST_DIR}) + +if(WIN32) + get_filename_component(CXX_DIR ${CMAKE_CXX_COMPILER} PATH) + file(GLOB LIB_LIST ${CXX_DIR}/libstdc++-6.dll ${CXX_DIR}/libwinpthread-1.dll + ${CXX_DIR}/libssp-0.dll ${CXX_DIR}/libgcc_s_*-1.dll ${CXX_DIR}/libgomp-1.dll) + # install(FILES ${LIB_LIST} DESTINATION ${CMAKE_BINARY_DIR}) + foreach(WIN_DEP_LIB ${LIB_LIST}) + MESSAGE("COPYING ${WIN_DEP_LIB}") + file(COPY ${WIN_DEP_LIB} DESTINATION ${MQBACKEND_OUTPUT_DIR} FOLLOW_SYMLINK_CHAIN) + endforeach() +endif() \ No newline at end of file diff --git a/mindquantum/src/backends/CMakeLists.txt b/mindquantum/src/backends/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..607990e8d327f417e0c64115464af4cc8dc28495 --- /dev/null +++ b/mindquantum/src/backends/CMakeLists.txt @@ -0,0 +1,25 @@ +# ============================================================================== +# +# Copyright 2021 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ============================================================================== + +if(ENABLE_PROJECTQ) + add_subdirectory(projectq) +endif() + +if(ENABLE_QUEST) + add_subdirectory(quest) +endif() diff --git a/mindquantum/src/backends/projectq/CMakeLists.txt b/mindquantum/src/backends/projectq/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..d054138f1721e3b76d42693d47218eccb9697d50 --- /dev/null +++ b/mindquantum/src/backends/projectq/CMakeLists.txt @@ -0,0 +1,25 @@ +# ============================================================================== +# +# Copyright 2021 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ============================================================================== + +add_library(mq_projectq INTERFACE) +target_sources(mq_projectq INTERFACE projectq.h projectq_utils.h) + +target_compile_definitions(mq_projectq INTERFACE INTRIN) +target_compile_options(mq_projectq INTERFACE -ffast-math -mavx2) +target_include_directories(mq_projectq INTERFACE ${CMAKE_CURRENT_LIST_DIR}) +target_link_libraries(mq_projectq INTERFACE projectq) diff --git a/mindquantum/src/backends/projectq/projectq.h b/mindquantum/src/backends/projectq/projectq.h new file mode 100644 index 0000000000000000000000000000000000000000..63d0d140c3839dff3e88e37c1a02619d3f5fcbb1 --- /dev/null +++ b/mindquantum/src/backends/projectq/projectq.h @@ -0,0 +1,487 @@ +/** + * Copyright 2021 Huawei Technologies Co., Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef MINDQUANTUM_BACKENDS_PROJECTQ_PROJECTQ_H_ +#define MINDQUANTUM_BACKENDS_PROJECTQ_PROJECTQ_H_ + +#include + +#include +#include +#include +#include +#include + +#include "backends/projectq/projectq_utils.h" +#include "core/utils.h" +#include "gate/basic_gate.h" +#include "hamiltonian/hamiltonian.h" +#include "pr/parameter_resolver.h" +#include "projectq/backends/_sim/_cppkernels/simulator.hpp" + +namespace mindquantum { +namespace projectq { +template +class Projectq : public Simulator { + private: + unsigned n_qubits_; + VT ordering_; + unsigned len_; + RndEngine rnd_eng_; + std::function rng_; + + public: + Projectq() : Simulator(1, 1), n_qubits_(1), rnd_eng_(1) { + for (unsigned i = 0; i < n_qubits_; i++) { + ordering_.push_back(i); + } + len_ = (1UL << (n_qubits_ + 1)); + std::uniform_real_distribution dist(0., 1.); + rng_ = std::bind(dist, std::ref(rnd_eng_)); + } + + Projectq(unsigned seed, unsigned N) : Simulator(seed, N), n_qubits_(N), rnd_eng_(seed) { + for (unsigned i = 0; i < n_qubits_; i++) { + ordering_.push_back(i); + } + len_ = (1UL << (n_qubits_ + 1)); + std::uniform_real_distribution dist(0., 1.); + rng_ = std::bind(dist, std::ref(rnd_eng_)); + } + Projectq(unsigned seed, unsigned N, calc_type *vec) : Simulator(seed, N), n_qubits_(N), rnd_eng_(seed) { + for (unsigned i = 0; i < n_qubits_; i++) { + ordering_.push_back(i); + } + len_ = (1UL << (n_qubits_ + 1)); + set_wavefunction(vec, ordering_); + std::uniform_real_distribution dist(0., 1.); + rng_ = std::bind(dist, std::ref(rnd_eng_)); + } + void InitializeSimulator() { + if (vec_ != NULL) { + free(vec_); + } + vec_ = (StateVector) calloc(len_, sizeof(calc_type)); + vec_[0] = 1; + } + + void InitializeSimulator(const VT> &circ) { + Projectq::InitializeSimulator(); + Projectq::ApplyCircuit(circ); + } + + void InitializeSimulator(CTP vec) { + } + + void SetState(VT> vec) { + set_wavefunction(reinterpret_cast(vec.data()), ordering_); + } + + void ApplyGate(const BasicGate &gate) { + Projectq::apply_controlled_gate(MCast(gate.base_matrix_.matrix_), VCast(gate.obj_qubits_), + VCast(gate.ctrl_qubits_)); + } + + void ApplyGate(const BasicGate &gate, const ParameterResolver &pr, bool diff = false) { + T theta = LinearCombine(pr, gate.params_); + if (diff) { + Projectq::apply_controlled_gate(MCast(gate.param_diff_matrix_(theta).matrix_), VCast(gate.obj_qubits_), + VCast(gate.ctrl_qubits_)); + } else { + Projectq::apply_controlled_gate(MCast(gate.param_matrix_(theta).matrix_), VCast(gate.obj_qubits_), + VCast(gate.ctrl_qubits_)); + } + } + + unsigned ApplyMeasure(const BasicGate &gate) { + run(); + auto qubit = gate.obj_qubits_[0]; + auto mask = (1UL << qubit); + calc_type zero_amps = 0; + // #pragma omp parallel for schedule(static) reduction(+ : zero_amps) + for (unsigned i = 0; i < (len_ >> 1); i++) { + if ((i & mask) == 0) { + zero_amps += vec_[2 * i] * vec_[2 * i] + vec_[2 * i + 1] * vec_[2 * i + 1]; + } + } + unsigned collapse = (static_cast(rng_() > zero_amps) << qubit); + auto norm = (collapse == 0) ? sqrt(zero_amps) : sqrt(1 - zero_amps); +#pragma omp parallel for schedule(static) + for (unsigned i = 0; i < (len_ >> 1); i++) { + if ((i & mask) == collapse) { + vec_[2 * i] /= norm; + vec_[2 * i + 1] /= norm; + } else { + vec_[2 * i] = 0; + vec_[2 * i + 1] = 0; + } + } + return (collapse >> qubit); + } + + void ApplyCircuit(const VT> &circ) { + for (auto &gate : circ) { + Projectq::ApplyGate(gate); + } + Projectq::run(); + } + + void ApplyCircuit(const VT> &circ, const ParameterResolver &pr) { + for (auto &gate : circ) { + if (gate.parameterized_) { + Projectq::ApplyGate(gate, pr); + } else { + Projectq::ApplyGate(gate); + } + } + Projectq::run(); + } + + VT Sampling(const VT> &circ, const ParameterResolver &pr, size_t shots, + const MST &key_map, unsigned seed) { + auto key_size = key_map.size(); + VT res(shots * key_size); + for (size_t i = 0; i < shots; i++) { + Projectq sim = Projectq(seed + i, n_qubits_, vec_); + auto res0 = sim.ApplyCircuitWithMeasure(circ, pr, key_map); + for (size_t j = 0; j < key_size; j++) { + res[i * key_size + j] = res0[j]; + } + } + return res; + } + + VT ApplyCircuitWithMeasure(const VT> &circ, const ParameterResolver &pr, + const MST &key_map) { + auto key_size = key_map.size(); + VT res(key_size); + for (auto &gate : circ) { + if (gate.is_measure_) { + auto collapse = ApplyMeasure(gate); + res[key_map.at(gate.name_)] = collapse; + } else if (gate.parameterized_) { + ApplyGate(gate, pr); + } else { + ApplyGate(gate); + } + } + return res; + } + + void ApplyHamiltonian(const Hamiltonian &ham) { + Projectq::run(); + if (ham.how_to_ == ORIGIN) { + Projectq::apply_qubit_operator(HCast(ham.ham_), Projectq::ordering_); + } else if (ham.how_to_ == BACKEND) { + Projectq::vec_ = sparse::Csr_Dot_Vec(ham.ham_sparse_main_, ham.ham_sparse_second_, + Projectq::vec_); + } else { + Projectq::vec_ = sparse::Csr_Dot_Vec(ham.ham_sparse_main_, Projectq::vec_); + } + } + + VT> RightSizeGrad(calc_type *left_vec, calc_type *right_vec, const Hamiltonian &ham, + const VT> &circ, const VT> &herm_circ, + const ParameterResolver &pr, const MST &p_map) { + VT> f_g(p_map.size() + 1, 0); + Projectq sim_left = Projectq(1, n_qubits_, left_vec); + sim_left.ApplyHamiltonian(ham); + f_g[0] = ComplexInnerProduct(sim_left.vec_, right_vec, static_cast(len_)); + Projectq sim_right = Projectq(1, n_qubits_, right_vec); + Projectq sim_right_tmp = Projectq(1, n_qubits_); + for (size_t j = 0; j < circ.size(); j++) { + if ((!herm_circ[j].parameterized_) || (herm_circ[j].params_.requires_grad_parameters_.size() == 0)) { + if (herm_circ[j].parameterized_) { + sim_left.ApplyGate(herm_circ[j], pr, false); + sim_right.ApplyGate(herm_circ[j], pr, false); + } else { + sim_left.ApplyGate(herm_circ[j]); + sim_right.ApplyGate(herm_circ[j]); + } + } else { + sim_right.ApplyGate(herm_circ[j], pr, false); + sim_right.run(); + sim_right_tmp.set_wavefunction(sim_right.vec_, ordering_); + sim_right_tmp.ApplyGate(circ[circ.size() - j - 1], pr, true); + sim_right_tmp.run(); + sim_left.run(); + CT gi = 0; + if (herm_circ[j].ctrl_qubits_.size() == 0) { + gi = ComplexInnerProduct(sim_left.vec_, sim_right_tmp.vec_, static_cast(len_)); + } else { + gi = ComplexInnerProductWithControl(sim_left.vec_, sim_right_tmp.vec_, + static_cast(len_), + GetControlMask(herm_circ[j].ctrl_qubits_)); + } + for (auto &it : herm_circ[j].params_.requires_grad_parameters_) { + f_g[1 + p_map.at(it)] += circ[circ.size() - j - 1].params_.data_.at(it) * gi; + } + sim_left.ApplyGate(herm_circ[j], pr, false); + } + } + return f_g; + } + + CT GetExpectation(const Hamiltonian &ham) { + Projectq sim = Projectq(1, n_qubits_, vec_); + sim.ApplyHamiltonian(ham); + auto out = ComplexInnerProduct(sim.vec_, vec_, static_cast(len_)); + return out; + } + + VT>> HermitianMeasureWithGrad(const VT> &hams, const VT> &circ, + const VT> &herm_circ, const ParameterResolver &pr, + const MST &p_map, size_t mea_threads) { + auto n_hams = hams.size(); + auto n_params = pr.data_.size(); + VT>> output; + for (size_t i = 0; i < n_hams; i++) { + output.push_back({}); + for (size_t j = 0; j < n_params + 1; j++) { + output[i].push_back({0, 0}); + } + } + + Projectq sim = Projectq(1, n_qubits_, vec_); + sim.ApplyCircuit(circ, pr); + if (n_hams == 1) { + auto f_g = sim.RightSizeGrad(sim.vec_, sim.vec_, hams[0], circ, herm_circ, pr, p_map); + for (size_t g = 1; g < n_params + 1; g++) { + f_g[g] += std::conj(f_g[g]); + } + output[0] = f_g; + } else { + std::vector tasks; + tasks.reserve(mea_threads); + size_t end = 0; + size_t offset = n_hams / mea_threads; + size_t left = n_hams % mea_threads; + for (size_t i = 0; i < mea_threads; ++i) { + size_t start = end; + end = start + offset; + if (i < left) { + end += 1; + } + + auto task = [&, start, end]() { + for (size_t n = start; n < end; n++) { + auto f_g = sim.RightSizeGrad(sim.vec_, sim.vec_, hams[n], circ, herm_circ, pr, p_map); + for (size_t g = 1; g < n_params + 1; g++) { + f_g[g] += std::conj(f_g[g]); + } + output[n] = f_g; + } + }; + tasks.emplace_back(task); + } + for (auto &t : tasks) { + t.join(); + } + } + return output; + } + + VT>>> HermitianMeasureWithGrad(const VT> &hams, const VT> &circ, + const VT> &herm_circ, const VVT &enc_data, + const VT &ans_data, const VS &enc_name, const VS &ans_name, + size_t batch_threads, size_t mea_threads) { + auto n_hams = hams.size(); + auto n_prs = enc_data.size(); + auto n_params = enc_name.size() + ans_name.size(); + VT>>> output; + for (size_t i = 0; i < n_prs; i++) { + output.push_back({}); + for (size_t j = 0; j < n_hams; j++) { + output[i].push_back({}); + for (size_t k = 0; k < n_params + 1; k++) { + output[i][j].push_back({0, 0}); + } + } + } + MST p_map; + for (size_t i = 0; i < enc_name.size(); i++) { + p_map[enc_name[i]] = i; + } + for (size_t i = 0; i < ans_name.size(); i++) { + p_map[ans_name[i]] = i + enc_name.size(); + } + + if (n_prs == 1) { + ParameterResolver pr = ParameterResolver(); + pr.SetData(enc_data[0], enc_name); + pr.SetData(ans_data, ans_name); + output[0] = HermitianMeasureWithGrad(hams, circ, herm_circ, pr, p_map, mea_threads); + } else { + std::vector tasks; + tasks.reserve(batch_threads); + size_t end = 0; + size_t offset = n_prs / batch_threads; + size_t left = n_prs % batch_threads; + for (size_t i = 0; i < batch_threads; ++i) { + size_t start = end; + end = start + offset; + if (i < left) { + end += 1; + } + auto task = [&, start, end]() { + for (size_t n = start; n < end; n++) { + ParameterResolver pr = ParameterResolver(); + pr.SetData(enc_data[n], enc_name); + pr.SetData(ans_data, ans_name); + auto f_g = HermitianMeasureWithGrad(hams, circ, herm_circ, pr, p_map, mea_threads); + output[n] = f_g; + } + }; + tasks.emplace_back(task); + } + for (auto &t : tasks) { + t.join(); + } + } + return output; + } + + VT>> NonHermitianMeasureWithGrad(const VT> &hams, const VT> &herm_hams, + const VT> &left_circ, const VT> &herm_left_circ, + const VT> &right_circ, + const VT> &herm_right_circ, const ParameterResolver &pr, + const MST &p_map, size_t mea_threads) { + auto n_hams = hams.size(); + auto n_params = pr.data_.size(); + VT>> output; + for (size_t i = 0; i < n_hams; i++) { + output.push_back({}); + for (size_t j = 0; j < n_params + 1; j++) { + output[i].push_back({0, 0}); + } + } + Projectq sim = Projectq(1, n_qubits_, vec_); + sim.ApplyCircuit(right_circ, pr); + Projectq sim2 = Projectq(1, n_qubits_, vec_); + sim2.ApplyCircuit(left_circ, pr); + if (n_hams == 1) { + auto f_g1 = sim2.RightSizeGrad(sim.vec_, sim2.vec_, herm_hams[0], left_circ, herm_left_circ, pr, p_map); + auto f_g2 = sim.RightSizeGrad(sim2.vec_, sim.vec_, hams[0], right_circ, herm_right_circ, pr, p_map); + for (size_t g = 1; g < n_params + 1; g++) { + f_g2[g] += std::conj(f_g1[g]); + } + output[0] = f_g2; + } else { + std::vector tasks; + tasks.reserve(mea_threads); + size_t end = 0; + size_t offset = n_hams / mea_threads; + size_t left = n_hams % mea_threads; + for (size_t i = 0; i < mea_threads; i++) { + size_t start = end; + end = start + offset; + if (i < left) { + end += 1; + } + auto task = [&, start, end]() { + for (size_t n = start; n < end; n++) { + auto f_g1 = sim2.RightSizeGrad(sim.vec_, sim2.vec_, herm_hams[n], left_circ, herm_left_circ, pr, + p_map); + auto f_g2 = sim.RightSizeGrad(sim2.vec_, sim.vec_, hams[n], right_circ, herm_right_circ, pr, + p_map); + for (size_t g = 1; g < n_params + 1; g++) { + f_g2[g] += std::conj(f_g1[g]); + } + output[n] = f_g2; + } + }; + tasks.emplace_back(task); + } + for (auto &t : tasks) { + t.join(); + } + } + return output; + } + + VT>>> NonHermitianMeasureWithGrad(const VT> &hams, const VT> &herm_hams, + const VT> &left_circ, + const VT> &herm_left_circ, + const VT> &right_circ, + const VT> &herm_right_circ, const VVT &enc_data, + const VT &ans_data, const VS &enc_name, const VS &ans_name, + size_t batch_threads, size_t mea_threads) { + auto n_hams = hams.size(); + auto n_prs = enc_data.size(); + auto n_params = enc_name.size() + ans_name.size(); + VT>>> output; + for (size_t i = 0; i < n_prs; i++) { + output.push_back({}); + for (size_t j = 0; j < n_hams; j++) { + output[i].push_back({}); + for (size_t k = 0; k < n_params + 1; k++) { + output[i][j].push_back({0, 0}); + } + } + } + MST p_map; + for (size_t i = 0; i < enc_name.size(); i++) { + p_map[enc_name[i]] = i; + } + for (size_t i = 0; i < ans_name.size(); i++) { + p_map[ans_name[i]] = i + enc_name.size(); + } + if (n_prs == 1) { + ParameterResolver pr = ParameterResolver(); + pr.SetData(enc_data[0], enc_name); + pr.SetData(ans_data, ans_name); + output[0] = NonHermitianMeasureWithGrad(hams, herm_hams, left_circ, herm_left_circ, right_circ, + herm_right_circ, pr, p_map, mea_threads); + } else { + std::vector tasks; + tasks.reserve(batch_threads); + size_t end = 0; + size_t offset = n_prs / batch_threads; + size_t left = n_prs % batch_threads; + for (size_t i = 0; i < batch_threads; ++i) { + size_t start = end; + end = start + offset; + if (i < left) { + end += 1; + } + auto task = [&, start, end]() { + for (size_t n = start; n < end; n++) { + ParameterResolver pr = ParameterResolver(); + pr.SetData(enc_data[n], enc_name); + pr.SetData(ans_data, ans_name); + auto f_g = NonHermitianMeasureWithGrad(hams, herm_hams, left_circ, herm_left_circ, right_circ, + herm_right_circ, pr, p_map, mea_threads); + output[n] = f_g; + } + }; + tasks.emplace_back(task); + } + for (auto &t : tasks) { + t.join(); + } + } + return output; + } + + void PrintInfo() { + std::cout << n_qubits_ << " qubits simulator with currently quantum state at:" << std::endl; + for (unsigned i = 0; i < (len_ >> 1); i++) { + std::cout << "(" << vec_[2 * i] << ", " << vec_[2 * i + 1] << ")" << std::endl; + } + } +}; +} // namespace projectq +} // namespace mindquantum +#endif // MINDQUANTUM_BACKENDS_PROJECTQ_PROJECTQ_H_ diff --git a/mindquantum/src/backends/projectq/projectq_utils.h b/mindquantum/src/backends/projectq/projectq_utils.h new file mode 100644 index 0000000000000000000000000000000000000000..cf03bee51e2692e90ef871143d5025806e5288b4 --- /dev/null +++ b/mindquantum/src/backends/projectq/projectq_utils.h @@ -0,0 +1,61 @@ +/** + * Copyright 2021 Huawei Technologies Co., Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef MINDQUANTUM_BACKENDS_PROJECTQ_UTILS_H_ +#define MINDQUANTUM_BACKENDS_PROJECTQ_UTILS_H_ +#include +#include + +#include "core/utils.h" +#include "projectq/backends/_sim/_cppkernels/fusion.hpp" +#include "projectq/backends/_sim/_cppkernels/intrin/alignedallocator.hpp" +#include "projectq/backends/_sim/_cppkernels/simulator.hpp" + +namespace mindquantum { +namespace projectq { +inline VT VCast(const VT &a) { + return VT(a.begin(), a.end()); +} + +template +inline Fusion::Matrix MCast(const VVT> &m) { + Fusion::Matrix out; + for (size_t i = 0; i < m.size(); i++) { + std::vector> col; + for (auto &a : m[i]) { + // cppcheck-suppress useStlAlgorithm + col.push_back({a.real(), a.imag()}); + } + out.push_back(col); + } + return out; +} + +template +inline Simulator::ComplexTermsDict HCast(const VT> &ham_) { + Simulator::ComplexTermsDict res; + for (auto &pt : ham_) { + Simulator::Term term; + for (auto &pw : pt.first) { + // cppcheck-suppress useStlAlgorithm + term.push_back(std::make_pair(static_cast(pw.first), pw.second)); + } + res.push_back(std::make_pair(term, static_cast(pt.second))); + } + return res; +} +} // namespace projectq +} // namespace mindquantum +#endif // MINDQUANTUM_BACKENDS_PROJECTQ_H_ diff --git a/mindquantum/src/backends/quest/CMakeLists.txt b/mindquantum/src/backends/quest/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..3a6f30965ec618d14531f08e5fa837d579ceb097 --- /dev/null +++ b/mindquantum/src/backends/quest/CMakeLists.txt @@ -0,0 +1,23 @@ +# ============================================================================== +# +# Copyright 2021 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ============================================================================== + +add_library(mq_quest INTERFACE) +target_sources(mq_quest INTERFACE quest.h quest_utils.h) + +target_include_directories(mq_quest INTERFACE ${CMAKE_CURRENT_LIST_DIR}) +target_link_libraries(mq_quest INTERFACE QuEST) diff --git a/mindquantum/src/backends/quest/quest.h b/mindquantum/src/backends/quest/quest.h new file mode 100644 index 0000000000000000000000000000000000000000..7f2e7d3d5294d89ecb76ccc0911230c79b19bc08 --- /dev/null +++ b/mindquantum/src/backends/quest/quest.h @@ -0,0 +1,317 @@ +/** + * Copyright 2021 Huawei Technologies Co., Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef MINDQUANTUM_BACKENDS_QUEST_QUEST_H_ +#define MINDQUANTUM_BACKENDS_QUEST_QUEST_H_ +#include + +#include "QuEST.h" +#include "QuEST_internal.h" +#include "backends/quest/quest_utils.h" +#include "core/utils.h" +#include "gate/basic_gate.h" +#include "hamiltonian/hamiltonian.h" +#include "pr/parameter_resolver.h" + +namespace mindquantum { +namespace quest { +template +struct Quest { + int n_qubits_; + QuESTEnv env; + Qureg qubits; + explicit Quest(int n) : n_qubits_(n), env(createQuESTEnv()), qubits(createQureg(n, env)) { + initZeroState(qubits); + } + Quest(int n, Qureg ref_qureg) : n_qubits_(n), env(createQuESTEnv()), qubits(createQureg(n, env)) { + cloneQureg(qubits, ref_qureg); + } + Quest() : n_qubits_(1), env(createQuESTEnv()), qubits(createQureg(n_qubits_, env)) { + initZeroState(qubits); + } + ~Quest() { + destroyQureg(qubits, env); + destroyQuESTEnv(env); + } + void InitializeSimulator() { + initZeroState(qubits); + } + void PrintInfo() { + reportQuregParams(qubits); + reportQuESTEnv(env); + } + VT> GetVec() { + VT> result; + for (size_t i = 0; i < (1UL << n_qubits_); i++) { + result.push_back({getRealAmp(qubits, i), getImagAmp(qubits, i)}); + } + return result; + } + void ApplyGate(const BasicGate &gate) { + if (gate.ctrl_qubits_.size() == 0) { // no control + if (gate.name_ == gX) { + pauliX(qubits, gate.obj_qubits_[0]); + } else if (gate.name_ == gY) { + pauliY(qubits, gate.obj_qubits_[0]); + } else if (gate.name_ == gZ) { + pauliZ(qubits, gate.obj_qubits_[0]); + } else if (gate.name_ == gH) { + hadamard(qubits, gate.obj_qubits_[0]); + } else if (gate.name_ == gT) { + tGate(qubits, gate.obj_qubits_[0]); + } else if (gate.name_ == gS) { + sGate(qubits, gate.obj_qubits_[0]); + } else if (gate.name_ == gCNOT) { + controlledNot(qubits, gate.obj_qubits_[1], gate.obj_qubits_[0]); + } else if (gate.name_ == gSWAP) { + swapGate(qubits, gate.obj_qubits_[0], gate.obj_qubits_[1]); + } else if (gate.name_ == gRX) { + rotateX(qubits, gate.obj_qubits_[0], static_cast(gate.applied_value_)); + } else if (gate.name_ == gRY) { + rotateY(qubits, gate.obj_qubits_[0], static_cast(gate.applied_value_)); + } else if (gate.name_ == gRZ) { + rotateZ(qubits, gate.obj_qubits_[0], static_cast(gate.applied_value_)); + } else if (gate.name_ == gPS) { + phaseShift(qubits, gate.obj_qubits_[0], static_cast(gate.applied_value_)); + } else { + auto obj = Vec2Intptr(gate.obj_qubits_); + auto m = Dim2Matrix2ComplexMatrixN(gate.base_matrix_, gate.obj_qubits_.size()); + multiQubitUnitary(qubits, obj, static_cast(gate.obj_qubits_.size()), m); + if (obj != nullptr) { + free(obj); + } + destroyComplexMatrixN(m); + } + } else if (gate.ctrl_qubits_.size() == 1) { + if (gate.name_ == gX) { + controlledNot(qubits, gate.ctrl_qubits_[0], gate.obj_qubits_[0]); + } else { + auto obj = Vec2Intptr(gate.obj_qubits_); + auto m = Dim2Matrix2ComplexMatrixN(gate.base_matrix_, gate.obj_qubits_.size()); + controlledMultiQubitUnitary(qubits, gate.ctrl_qubits_[0], obj, + static_cast(gate.obj_qubits_.size()), m); + if (obj != nullptr) { + free(obj); + } + destroyComplexMatrixN(m); + } + } else { + auto ctrl = Vec2Intptr(gate.ctrl_qubits_); + auto obj = Vec2Intptr(gate.obj_qubits_); + auto m = Dim2Matrix2ComplexMatrixN(gate.base_matrix_, gate.obj_qubits_.size()); + int nctrl = static_cast(gate.ctrl_qubits_.size()); + int nobj = static_cast(gate.obj_qubits_.size()); + multiControlledMultiQubitUnitary(qubits, ctrl, nctrl, obj, nobj, m); + destroyComplexMatrixN(m); + if (obj != nullptr) { + free(obj); + } + if (ctrl != nullptr) { + free(ctrl); + } + } + } + void ApplyGate(const BasicGate &gate, const ParameterResolver &pr, bool diff = false) { + T theta = LinearCombine(pr, gate.params_); + if (diff) { + auto ctrl = Vec2Intptr(gate.ctrl_qubits_); + auto obj = Vec2Intptr(gate.obj_qubits_); + int nctrl = static_cast(gate.ctrl_qubits_.size()); + int nobj = static_cast(gate.obj_qubits_.size()); + if (nctrl == 0 && nobj == 1) { + auto m = Dim2Matrix2ComplexMatrix2(gate.param_diff_matrix_(theta)); + applyMatrix2(qubits, gate.obj_qubits_[0], m); + } else if (nctrl == 0 && nobj == 2) { + auto m = Dim2Matrix2ComplexMatrix4(gate.param_diff_matrix_(theta)); + applyMatrix4(qubits, gate.obj_qubits_[0], gate.obj_qubits_[1], m); + } else if (nctrl != 0) { + auto m = Dim2Matrix2ComplexMatrixN(gate.param_diff_matrix_(theta), gate.obj_qubits_.size()); + applyMultiControlledMatrixN(qubits, ctrl, nctrl, obj, nobj, m); + destroyComplexMatrixN(m); + } else { + auto m = Dim2Matrix2ComplexMatrixN(gate.param_diff_matrix_(theta), gate.obj_qubits_.size()); + applyMatrixN(qubits, obj, nobj, m); + destroyComplexMatrixN(m); + } + if (obj != nullptr) { + free(obj); + } + if (ctrl != nullptr) { + free(ctrl); + } + } else { + BasicGate gate_tmp = gate; + gate_tmp.ApplyValue(theta); + ApplyGate(gate_tmp); + } + } + void ApplyCircuit(const VT> &circ) { + for (auto &gate : circ) { + Quest::ApplyGate(gate); + } + } + unsigned ApplyMeasure(const BasicGate &gate) { + } + + VT Sampling(const VT> &circ, const ParameterResolver &pr, size_t shots, + const MST &key_map, unsigned seed) { + } + + void ApplyCircuit(const VT> &circ, const ParameterResolver &pr) { + for (auto &gate : circ) { + if (gate.parameterized_) { + Quest::ApplyGate(gate, pr); + } else { + Quest::ApplyGate(gate); + } + } + } + + void ApplyHamiltonian(const Hamiltonian &ham) { + if (ham.how_to_ == ORIGIN) { + Qureg qureg = createQureg(n_qubits_, env); + Qureg ori = createQureg(n_qubits_, env); + cloneQureg(ori, qubits); + initBlankState(qubits); + for (size_t i = 0; i < ham.ham_.size(); i++) { + cloneQureg(qureg, ori); + for (size_t j = 0; j < ham.ham_[i].first.size(); j++) { + if (ham.ham_[i].first[j].second == 'X') { + statevec_pauliX(qureg, ham.ham_[i].first[j].first); + } + if (ham.ham_[i].first[j].second == 'Y') { + statevec_pauliY(qureg, ham.ham_[i].first[j].first); + } + if (ham.ham_[i].first[j].second == 'Z') { + statevec_pauliZ(qureg, ham.ham_[i].first[j].first); + } + } + Complex coef = (Complex){.real = ham.ham_[i].second, .imag = 0}; + Complex iden = (Complex){.real = 1, .imag = 0}; + Complex zero = (Complex){.real = 0, .imag = 0}; + setWeightedQureg(coef, qureg, iden, qubits, zero, qubits); + } + destroyQureg(qureg, env); + destroyQureg(ori, env); + } else if (ham.how_to_ == BACKEND) { + Csr_Dot_Vec(ham.ham_sparse_main_, ham.ham_sparse_second_, qubits); + } else { + Csr_Dot_Vec(ham.ham_sparse_main_, qubits); + } + } + + CT GetExpectation(const Hamiltonian &ham) { + // auto quest_ham = HCast(ham, n_qubits_); + Quest sim = Quest(n_qubits_); + cloneQureg(sim.qubits, qubits); + sim.ApplyHamiltonian(ham); + CT value; + if (qubits.isDensityMatrix) { + value = {calcTotalProb(sim.qubits), 0}; + } else { + value = Complex2Complex(calcInnerProduct(qubits, sim.qubits)); + } + + return value; + } + + VT> RightSizeGrad(Qureg left_vec, Qureg right_vec, const Hamiltonian &ham, const VT> &circ, + const VT> &herm_circ, const ParameterResolver &pr, + const MST &p_map) { + VT> f_g(p_map.size() + 1, 0); + Quest sim_left = Quest(n_qubits_, left_vec); + sim_left.ApplyHamiltonian(ham); + f_g[0] = Complex2Complex(calcInnerProduct(sim_left.qubits, right_vec)); + Quest sim_right = Quest(n_qubits_, right_vec); + Quest sim_right_tmp = Quest(n_qubits_); + for (size_t j = 0; j < circ.size(); j++) { + if ((!herm_circ[j].parameterized_) || (herm_circ[j].params_.requires_grad_parameters_.size() == 0)) { + if (herm_circ[j].parameterized_) { + sim_left.ApplyGate(herm_circ[j], pr, false); + sim_right.ApplyGate(herm_circ[j], pr, false); + } else { + sim_left.ApplyGate(herm_circ[j]); + sim_right.ApplyGate(herm_circ[j]); + } + } else { + sim_right.ApplyGate(herm_circ[j], pr, false); + cloneQureg(sim_right_tmp.qubits, sim_right.qubits); + sim_right_tmp.ApplyGate(circ[circ.size() - j - 1], pr, true); + CT gi = 0; + // if (herm_circ[j].ctrl_qubits_.size() == 0) { + gi = Complex2Complex(calcInnerProduct(sim_left.qubits, sim_right_tmp.qubits)); + // } else { + // gi = ComplexInnerProductWithControl(sim_left.vec_, + // sim_right_tmp.vec_, static_cast(len_), + // GetControlMask(herm_circ[j].ctrl_qubits_)); + // } + for (auto &it : herm_circ[j].params_.requires_grad_parameters_) { + f_g[1 + p_map.at(it)] -= herm_circ[j].params_.data_.at(it) * gi; + } + sim_left.ApplyGate(herm_circ[j], pr, false); + } + } + return f_g; + } + + VT>> HermitianMeasureWithGrad(const VT> &hams, const VT> &circ, + const VT> &herm_circ, const ParameterResolver &pr, + const VT ¶ms_order, size_t mea_threads) { + auto n_hams = hams.size(); + auto n_params = pr.data_.size(); + MST p_map; + for (size_t i = 0; i < params_order.size(); i++) { + p_map[params_order[i]] = i; + } + VT>> output(n_hams); + Quest sim = Quest(n_qubits_, qubits); + auto t0 = NOW(); + sim.ApplyCircuit(circ, pr); + auto t1 = NOW(); + std::cout << "evolution time " << TimeDuration(t0, t1) << std::endl; + + // #pragma omp parallel for schedule(static) + for (size_t i = 0; i < n_hams; i++) { + auto t2 = NOW(); + auto f_g = sim.RightSizeGrad(sim.qubits, sim.qubits, hams[i], circ, herm_circ, pr, p_map); + auto t3 = NOW(); + std::cout << "grad " << TimeDuration(t2, t3) << std::endl; + for (size_t g = 1; g < n_params + 1; g++) { + f_g[g] += std::conj(f_g[g]); + } + output[i] = f_g; + } + return output; + } + + VT>>> HermitianMeasureWithGrad(const VT> &hams, const VT> &circ, + const VT> &herm_circ, const VT> &prs, + const VT ¶ms_order, size_t batch_threads, + size_t mea_threads) { + // auto n_hams = hams.size(); + auto n_prs = prs.size(); + // auto n_params = prs[0].data_.size(); + VT>>> output(n_prs); + // #pragma omp parallel for schedule(static) + for (size_t i = 0; i < n_prs; i++) { + auto f_g = HermitianMeasureWithGrad(hams, circ, herm_circ, prs[i], params_order, mea_threads); + output[i] = f_g; + } + return output; + } +}; +} // namespace quest +} // namespace mindquantum +#endif diff --git a/mindquantum/src/backends/quest/quest_utils.h b/mindquantum/src/backends/quest/quest_utils.h new file mode 100644 index 0000000000000000000000000000000000000000..fa9bfd419f6e846600162ad5d058b105bfa25789 --- /dev/null +++ b/mindquantum/src/backends/quest/quest_utils.h @@ -0,0 +1,218 @@ +/** + * Copyright 2021 Huawei Technologies Co., Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef MINDQUANTUM_BACKENDS_QUEST_UTILS_H_ +#define MINDQUANTUM_BACKENDS_QUEST_UTILS_H_ +#include + +#include "QuEST.h" +#include "QuEST_validation.h" +#include "core/utils.h" +#include "gate/basic_gate.h" +#include "sparse/csrhdmatrix.h" +#ifdef GPUACCELERATED +# include +#endif + +namespace mindquantum { +namespace quest { +using sparse::CsrHdMatrix; + +template +inline ComplexMatrixN Dim2Matrix2ComplexMatrixN(const Dim2Matrix &matrix, int n_qubits) { + auto m = createComplexMatrixN(n_qubits); + validateMatrixInit(m, __func__); + int dim = 1 << m.numQubits; + for (int i = 0; i < dim; i++) + for (int j = 0; j < dim; j++) { + m.real[i][j] = std::real(matrix.matrix_[i][j]); + m.imag[i][j] = std::imag(matrix.matrix_[i][j]); + } + return m; +} + +template +inline ComplexMatrix2 Dim2Matrix2ComplexMatrix2(const Dim2Matrix &matrix) { + ComplexMatrix2 m; + int dim = 1 << 1; + for (int i = 0; i < dim; i++) + for (int j = 0; j < dim; j++) { + m.real[i][j] = std::real(matrix.matrix_[i][j]); + m.imag[i][j] = std::imag(matrix.matrix_[i][j]); + } + return m; +} + +template +inline ComplexMatrix4 Dim2Matrix2ComplexMatrix4(const Dim2Matrix &matrix) { + ComplexMatrix4 m; + int dim = 1 << 2; + for (int i = 0; i < dim; i++) + for (int j = 0; j < dim; j++) { + m.real[i][j] = std::real(matrix.matrix_[i][j]); + m.imag[i][j] = std::imag(matrix.matrix_[i][j]); + } + return m; +} + +// void destroyComplexMatrix2(ComplexMatrix2 m) { +// int numRows = 1 << 1; +// for (int r = 0; r < numRows; r++) { +// free(m.real[r]); +// free(m.imag[r]); +// } +// free(m.real); +// free(m.imag); +// } + +// void destroyComplexMatrix4(ComplexMatrix4 m) { +// int numRows = 1 << 2; +// for (int r = 0; r < numRows; r++) { +// free(m.real[r]); +// free(m.imag[r]); +// } +// free(m.real); +// free(m.imag); +// } + +inline int *Vec2Intptr(const VT &vec) { + int *out = reinterpret_cast(malloc(sizeof(int) * vec.size())); + for (size_t i = 0; i < vec.size(); i++) { + out[i] = static_cast(vec[i]); + } + return out; +} +template +inline PauliHamil HCast(const Hamiltonian &ham, int n_qubits) { + PauliHamil quest_ham = createPauliHamil(n_qubits, ham.ham_.size()); + validateHamilParams(quest_ham.numQubits, quest_ham.numSumTerms, __func__); + + int i = 0; + for (int t = 0; t < quest_ham.numSumTerms; t++) { + quest_ham.termCoeffs[t] = static_cast(ham.ham_[t].second); + size_t curr = 0; + for (size_t q = 0; q < static_cast(quest_ham.numQubits); q++) { + if (curr < ham.ham_[t].first.size()) { + if (ham.ham_[t].first[curr].first == q) { + if (ham.ham_[t].first[curr].second == 'X') { + quest_ham.pauliCodes[i] = PAULI_X; + } else if (ham.ham_[t].first[curr].second == 'Y') { + quest_ham.pauliCodes[i] = PAULI_Y; + } else { + quest_ham.pauliCodes[i] = PAULI_Z; + } + curr++; + } else { + quest_ham.pauliCodes[i] = PAULI_I; + } + } else { + quest_ham.pauliCodes[i] = PAULI_I; + } + i++; + } + } + return quest_ham; +} + +template +inline CT Complex2Complex(Complex a) { + CT res = {a.real, a.imag}; + return res; +} + +template +inline void Csr_Dot_Vec(std::shared_ptr> a, Qureg qureg) { + auto dim = a->dim_; + auto vec_real = reinterpret_cast(malloc(sizeof(qreal) * dim)); + auto vec_imag = reinterpret_cast(malloc(sizeof(qreal) * dim)); + auto ori_real = reinterpret_cast(malloc(sizeof(qreal) * dim)); + auto ori_imag = reinterpret_cast(malloc(sizeof(qreal) * dim)); + // auto nnz = a->nnz_; + auto data = a->data_; + auto indptr = a->indptr_; + auto indices = a->indices_; +#ifdef GPUACCELERATED + cudaMemcpy(ori_real, qureg.deviceStateVec.real, dim * sizeof(qreal), cudaMemcpyDeviceToHost); + cudaMemcpy(ori_imag, qureg.deviceStateVec.imag, dim * sizeof(qreal), cudaMemcpyDeviceToHost); +#else +# pragma omp parallel for schedule(static) + for (Index i = 0; i < dim; ++i) { + ori_real[i] = statevec_getRealAmp(qureg, i); + ori_imag[i] = statevec_getImagAmp(qureg, i); + } +#endif +#pragma omp parallel for schedule(static) + for (Index i = 0; i < dim; i++) { + CT sum = {0.0, 0.0}; + for (Index j = indptr[i]; j < indptr[i + 1]; j++) { + sum += data[j] * CT(ori_real[indices[j]], ori_imag[indices[j]]); + } + vec_real[i] = std::real(sum); + vec_imag[i] = std::imag(sum); + } + initStateFromAmps(qureg, vec_real, vec_imag); + free(vec_real); + free(vec_imag); + free(ori_real); + free(ori_imag); +} + +template +inline void Csr_Dot_Vec(std::shared_ptr> a, std::shared_ptr> b, Qureg qureg) { + auto dim = a->dim_; + auto vec_real = reinterpret_cast(malloc(sizeof(qreal) * dim)); + auto vec_imag = reinterpret_cast(malloc(sizeof(qreal) * dim)); + auto ori_real = reinterpret_cast(malloc(sizeof(qreal) * dim)); + auto ori_imag = reinterpret_cast(malloc(sizeof(qreal) * dim)); + // auto nnz = a->nnz_; + auto data = a->data_; + auto indptr = a->indptr_; + auto indices = a->indices_; + auto data_b = b->data_; + auto indptr_b = b->indptr_; + auto indices_b = b->indices_; + +#ifdef GPUACCELERATED + cudaMemcpy(ori_real, qureg.deviceStateVec.real, dim * sizeof(qreal), cudaMemcpyDeviceToHost); + cudaMemcpy(ori_imag, qureg.deviceStateVec.imag, dim * sizeof(qreal), cudaMemcpyDeviceToHost); +#else +# pragma omp parallel for schedule(static) + for (Index i = 0; i < dim; ++i) { + ori_real[i] = statevec_getRealAmp(qureg, i); + ori_imag[i] = statevec_getImagAmp(qureg, i); + } +#endif +#pragma omp parallel for schedule(static) + for (Index i = 0; i < dim; i++) { + CT sum = {0.0, 0.0}; + for (Index j = indptr[i]; j < indptr[i + 1]; j++) { + sum += data[j] * CT(ori_real[indices[j]], ori_imag[indices[j]]); + } + for (Index j = indptr_b[i]; j < indptr_b[i + 1]; j++) { + sum += data_b[j] * CT(ori_real[indices_b[j]], ori_imag[indices_b[j]]); + } + vec_real[i] = std::real(sum); + vec_imag[i] = std::imag(sum); + } + initStateFromAmps(qureg, vec_real, vec_imag); + free(vec_real); + free(vec_imag); + free(ori_real); + free(ori_imag); +} + +} // namespace quest +} // namespace mindquantum +#endif diff --git a/mindquantum/src/binding.cc b/mindquantum/src/binding.cc new file mode 100644 index 0000000000000000000000000000000000000000..88a11b23aefa87e7cc12bb231b353da43798992a --- /dev/null +++ b/mindquantum/src/binding.cc @@ -0,0 +1,177 @@ +/** + * Copyright 2021 Huawei Technologies Co., Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include +#include +#include +#include +#include + +#ifdef ENABLE_PROJECTQ +# include "backends/projectq/projectq.h" +#endif +#ifdef ENABLE_QUEST +# include "backends/quest/quest.h" +#endif +#include "core/type.h" +#include "gate/gates.h" +#include "hamiltonian/hamiltonian.h" +#include "matrix/two_dim_matrix.h" +#include "pr/parameter_resolver.h" +#include "sparse/algo.h" +#include "sparse/csrhdmatrix.h" +#include "sparse/paulimat.h" + +namespace py = pybind11; +namespace mindquantum { +using mindquantum::sparse::Csr_Plus_Csr; +using mindquantum::sparse::GetPauliMat; +using mindquantum::sparse::PauliMat; +using mindquantum::sparse::PauliMatToCsrHdMatrix; +using mindquantum::sparse::SparseHamiltonian; +using mindquantum::sparse::TransposeCsrHdMatrix; + +#ifdef ENABLE_PROJECTQ +using mindquantum::projectq::Projectq; +#endif + +#ifdef ENABLE_QUEST +using mindquantum::quest::Quest; +#endif + +// Interface with python +PYBIND11_MODULE(mqbackend, m) { + m.doc() = "MindQuantum c plugin"; + + // matrix + py::class_, std::shared_ptr>>(m, "dim2matrix") + .def(py::init<>()) + .def(py::init> &>()); + + // basic gate + py::class_, std::shared_ptr>>(m, "basic_gate") + .def(py::init<>()) + .def(py::init>()) + .def(py::init()) + .def("PrintInfo", &BasicGate::PrintInfo) + .def("apply_value", &BasicGate::ApplyValue) + .def_readwrite("obj_qubits", &BasicGate::obj_qubits_) + .def_readwrite("ctrl_qubits", &BasicGate::ctrl_qubits_) + .def_readwrite("params", &BasicGate::params_) + .def_readwrite("daggered", &BasicGate::daggered_) + .def_readwrite("applied_value", &BasicGate::applied_value_) + .def_readwrite("base_matrix", &BasicGate::base_matrix_) + .def_readwrite("hermitian_prop", &BasicGate::hermitian_prop_); + m.def("get_gate_by_name", &GetGateByName); + m.def("get_measure_gate", &GetMeasureGate); + // parameter resolver + py::class_, std::shared_ptr>>(m, "parameter_resolver") + .def(py::init &, const SS &, const SS &>()) + .def(py::init<>()) + .def(py::init &, const VT &, const VT &>()) + .def_readonly("data", &ParameterResolver::data_) + .def_readonly("no_grad_parameters", &ParameterResolver::no_grad_parameters_) + .def_readonly("requires_grad_parameters", &ParameterResolver::requires_grad_parameters_); + + m.def("linear_combine", &LinearCombine); + + // pauli mat + py::class_, std::shared_ptr>>(m, "pauli_mat") + .def(py::init<>()) + .def(py::init &, Index>()) + .def_readonly("n_qubits", &PauliMat::n_qubits_) + .def_readonly("dim", &PauliMat::dim_) + .def_readwrite("coeff", &PauliMat::p_) + .def("PrintInfo", &PauliMat::PrintInfo); + + m.def("get_pauli_mat", &GetPauliMat); + + // csr_hd_matrix + py::class_, std::shared_ptr>>(m, "csr_hd_matrix") + .def(py::init<>()) + .def(py::init, py::array_t, py::array_t>>()) + .def("PrintInfo", &CsrHdMatrix::PrintInfo); + m.def("csr_plus_csr", &Csr_Plus_Csr); + m.def("transpose_csr_hd_matrix", &TransposeCsrHdMatrix); + m.def("pauli_mat_to_csr_hd_matrix", &PauliMatToCsrHdMatrix); + + // hamiltonian + py::class_, std::shared_ptr>>(m, "hamiltonian") + .def(py::init<>()) + .def(py::init> &>()) + .def(py::init> &, Index>()) + .def(py::init>, Index>()) + .def_readwrite("how_to", &Hamiltonian::how_to_) + .def_readwrite("n_qubits", &Hamiltonian::n_qubits_) + .def_readwrite("ham", &Hamiltonian::ham_) + .def_readwrite("ham_sparse_main", &Hamiltonian::ham_sparse_main_) + .def_readwrite("ham_sparse_second", &Hamiltonian::ham_sparse_second_); + m.def("sparse_hamiltonian", &SparseHamiltonian); + +#ifdef ENABLE_PROJECTQ + // projectq simulator + py::class_, std::shared_ptr>>(m, "projectq") + .def(py::init<>()) + .def(py::init()) + .def("reset", py::overload_cast<>(&Projectq::InitializeSimulator)) + .def("apply_measure", &Projectq::ApplyMeasure) + .def("apply_gate", py::overload_cast &>(&Projectq::ApplyGate)) + .def("apply_gate", + py::overload_cast &, const ParameterResolver &, bool>(&Projectq::ApplyGate)) + .def("apply_circuit", py::overload_cast> &>(&Projectq::ApplyCircuit)) + .def("apply_circuit", + py::overload_cast> &, const ParameterResolver &>(&Projectq::ApplyCircuit)) + .def("apply_circuit_with_measure", &Projectq::ApplyCircuitWithMeasure) + .def("sampling", &Projectq::Sampling) + .def("apply_hamiltonian", &Projectq::ApplyHamiltonian) + .def("get_expectation", &Projectq::GetExpectation) + .def("PrintInfo", &Projectq::PrintInfo) + .def("run", &Projectq::run) + .def("get_qs", &Projectq::cheat) + .def("set_qs", &Projectq::SetState) + .def("hermitian_measure_with_grad", + py::overload_cast> &, const VT> &, const VT> &, + const VVT &, const VT &, const VS &, const VS &, size_t, size_t>( + &Projectq::HermitianMeasureWithGrad)) + .def("non_hermitian_measure_with_grad", + py::overload_cast> &, const VT> &, const VT> &, + const VT> &, const VT> &, const VT> &, + const VVT &, const VT &, const VS &, const VS &, size_t, size_t>( + &Projectq::NonHermitianMeasureWithGrad)); +#endif + +#ifdef ENABLE_QUEST + // quest simulator + py::class_, std::shared_ptr>>(m, "quest") + .def(py::init()) + .def(py::init<>()) + .def("reset", &Quest::InitializeSimulator) + .def("PrintInfo", &Quest::PrintInfo) + .def("get_qs", &Quest::GetVec) + .def("apply_gate", py::overload_cast &>(&Quest::ApplyGate)) + .def("apply_gate", + py::overload_cast &, const ParameterResolver &, bool>(&Quest::ApplyGate)) + .def("apply_circuit", py::overload_cast> &>(&Quest::ApplyCircuit)) + .def("apply_circuit", + py::overload_cast> &, const ParameterResolver &>(&Quest::ApplyCircuit)) + .def("apply_hamiltonian", &Quest::ApplyHamiltonian) + .def("get_expectation", &Quest::GetExpectation) + .def("hermitian_measure_with_grad", + py::overload_cast> &, const VT> &, const VT> &, + const VT> &, const VT &, size_t, size_t>( + &Quest::HermitianMeasureWithGrad)); +#endif +} +} // namespace mindquantum diff --git a/mindquantum/src/core/popcnt.h b/mindquantum/src/core/popcnt.h new file mode 100644 index 0000000000000000000000000000000000000000..fc292d5395767c495acc5cbe6804fab7cca14d19 --- /dev/null +++ b/mindquantum/src/core/popcnt.h @@ -0,0 +1,29 @@ +/** + * Copyright 2021 Huawei Technologies Co., Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef MINDQUANTUM_POPCNT_H_ +#define MINDQUANTUM_POPCNT_H_ + +namespace mindquantum { +static char POPCNTTABLE[256] = { + 0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 1, 2, 2, 3, 2, + 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, + 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, + 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, + 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, + 5, 5, 6, 5, 6, 6, 7, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, + 6, 7, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8}; +} // namespace mindquantum +#endif // MINDQUANTUM_POPCNT_H_ diff --git a/mindquantum/src/core/type.h b/mindquantum/src/core/type.h new file mode 100644 index 0000000000000000000000000000000000000000..8c54432fcffc4b455d46260cca55c3c89db9c74f --- /dev/null +++ b/mindquantum/src/core/type.h @@ -0,0 +1,140 @@ +/** + * Copyright 2021 Huawei Technologies Co., Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef MINDQUANTUM_TYPE_H_ +#define MINDQUANTUM_TYPE_H_ +// #ifndef MQONLY +// #include "backend/kernel_compiler/cpu/quantum/quantum_simulator/popcnt.h" +// #else +// #include "./popcnt.h" +// #endif + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +namespace mindquantum { +#define PRECISION 1e-8 +#define COS1_2(theta) static_cast(cos(theta / 2)) +#define SIN1_2(theta) static_cast(sin(theta / 2)) + +using Index = int64_t; +#ifdef FLOAT +using MT = float; +#else +using MT = double; +#endif + +template +using VT = std::vector; + +template +using VVT = VT>; + +template +using VVVT = VT>; + +template +using CT = std::complex; + +template +using CTP = CT *; + +template +using MST = std::map; + +using SS = std::set; +using VS = VT; +using PauliWord = std::pair; + +template +using PauliTerm = std::pair, T>; + +using TimePoint = std::chrono::steady_clock::time_point; + +TimePoint NOW(); + +int TimeDuration(TimePoint start, TimePoint end); + +// void Duration() +struct PauliMask { + Index mask_x = 0; + Index mask_y = 0; + Index mask_z = 0; + Index num_x = 0; + Index num_y = 0; + Index num_z = 0; +}; + +const char kNThreads[] = "n_threads"; +const char kNQubits[] = "n_qubits"; +const char kParamNames[] = "param_names"; +const char kEncoderParamsNames[] = "encoder_params_names"; +const char kAnsatzParamsNames[] = "ansatz_params_names"; +const char kGateNames[] = "gate_names"; +const char kGateMatrix[] = "gate_matrix"; +const char kGateObjQubits[] = "gate_obj_qubits"; +const char kGateCtrlQubits[] = "gate_ctrl_qubits"; +const char kGateThetas[] = "gate_thetas"; +const char kNPG[] = "npg"; +const char kGateParamsNames[] = "gate_params_names"; +const char kGateCoeff[] = "gate_coeff"; +const char kGateRequiresGrad[] = "gate_requires_grad"; +const char kHamsPauliCoeff[] = "hams_pauli_coeff"; +const char kHamsPauliWord[] = "hams_pauli_word"; +const char kHamsPauliQubit[] = "hams_pauli_qubit"; +const char kHowTo[] = "how_to"; +const char kHamsSparseData[] = "hams_sparse_data"; +const char kHamsSparseIndice[] = "hams_sparse_indice"; +const char kHamsSparseIndptr[] = "hams_sparse_indptr"; +const char kIsProjector[] = "is_projector"; +const char kProjectors[] = "projectors"; + +enum SparseHow : int64_t { + ORIGIN = 0, + BACKEND, + FRONTEND, +}; +enum HermitianProp : int64_t { + SELFHERMITIAN = 0, + DOHERMITIAN, + PARAMSOPPOSITE, +}; +const char gX[] = "X"; +const char gY[] = "Y"; +const char gZ[] = "Z"; +const char gI[] = "I"; +const char gH[] = "H"; +const char gT[] = "T"; +const char gS[] = "S"; +const char gCNOT[] = "CNOT"; +const char gCZ[] = "CZ"; +const char gSWAP[] = "SWAP"; +const char gISWAP[] = "ISWAP"; +const char gRX[] = "RX"; +const char gRY[] = "RY"; +const char gRZ[] = "RZ"; +const char gPS[] = "PS"; +const char gXX[] = "XX"; +const char gYY[] = "YY"; +const char gZZ[] = "ZZ"; +} // namespace mindquantum +#endif // MINDQUANTUM_TYPE_H_ diff --git a/mindquantum/src/core/utils.h b/mindquantum/src/core/utils.h new file mode 100644 index 0000000000000000000000000000000000000000..30afc231355a6a2162b4689d537540ea16a32947 --- /dev/null +++ b/mindquantum/src/core/utils.h @@ -0,0 +1,143 @@ +/** + * Copyright 2021 Huawei Technologies Co., Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef MINDQUANTUM_UTILS_H_ +#define MINDQUANTUM_UTILS_H_ + +#ifdef ENABLE_OPENMP +# include +#endif // ENABLE_OPENMP // NOLINT + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/type.h" + +namespace mindquantum { +extern const VT> POLAR; +template +CT ComplexInnerProduct(const ST *v1, const ST *v2, Index len) { + // len is (1UL>>n_qubits)*2 + ST real_part = 0; + ST imag_part = 0; + auto size = len / 2; +#pragma omp parallel for reduction(+ : real_part, imag_part) + for (Index i = 0; i < size; i++) { + real_part += v1[2 * i] * v2[2 * i] + v1[2 * i + 1] * v2[2 * i + 1]; + imag_part += v1[2 * i] * v2[2 * i + 1] - v1[2 * i + 1] * v2[2 * i]; + } + + CT result = {static_cast(real_part), static_cast(imag_part)}; + return result; +} + +template +CT ComplexInnerProductWithControl(const ST *v1, const ST *v2, Index len, Index ctrlmask) { + // len is (1UL>>n_qubits)*2 + ST real_part = 0; + ST imag_part = 0; + auto size = len / 2; +#pragma omp parallel for reduction(+ : real_part, imag_part) + for (Index i = 0; i < size; i++) { + if ((i & ctrlmask) == ctrlmask) { + real_part += v1[2 * i] * v2[2 * i] + v1[2 * i + 1] * v2[2 * i + 1]; + imag_part += v1[2 * i] * v2[2 * i + 1] - v1[2 * i + 1] * v2[2 * i]; + } + } + CT result = {static_cast(real_part), static_cast(imag_part)}; + return result; +} + +Index GetControlMask(const VT &ctrls); + +PauliMask GetPauliMask(const VT &pws); + +// inline int CountOne(uint32_t n) { return __popcntd(n); } +// inline int CountOne(uint64_t n) { return __popcntq(n);} +inline int CountOne(uint32_t n) { + int result; + asm("popcnt %1,%0" : "=r"(result) : "r"(n)); + return result; +} + +inline int CountOne(int64_t n) { + uint32_t *p = reinterpret_cast(&n); + return CountOne(p[0]) + CountOne(p[1]); +} + +// inline int CountOne(uint64_t n) { +// uint8_t *p = reinterpret_cast(&n); +// return POPCNTTABLE[p[0]] + POPCNTTABLE[p[1]] + POPCNTTABLE[p[2]] + +// POPCNTTABLE[p[3]] + POPCNTTABLE[p[4]] + POPCNTTABLE[p[5]] + +// POPCNTTABLE[p[6]] + POPCNTTABLE[p[7]]; +// } + +// inline int CountOne(uint32_t n) { +// uint8_t *p = reinterpret_cast(&n); +// return POPCNTTABLE[p[0]] + POPCNTTABLE[p[1]] + POPCNTTABLE[p[2]] + +// POPCNTTABLE[p[3]]; +// } + +template +PauliTerm GenerateRandomPauliTerm(Index n_qubits) { + std::default_random_engine e(std::clock()); + std::uniform_real_distribution ut(-1.0, 1.0); + auto coeff = ut(e); + std::uniform_int_distribution uit(0, 3); + VT pws; + for (Index i = 0; i < n_qubits; i++) { + auto p = uit(e); + if (p != 3) { + pws.push_back(std::make_pair(i, (p + 'X'))); + } + } + return std::make_pair(pws, coeff); +} + +template +void ShowPauliTerm(const PauliTerm &pt) { + std::cout << pt.second << " ["; + for (Index i = 0; i < static_cast(pt.first.size()); i++) { + auto &pw = pt.first[i]; + std::cout << pw.second << pw.first; + if (i != static_cast(pt.first.size()) - 1) { + std::cout << " "; + } + } + std::cout << "]" << std::endl; +} + +TimePoint NOW(); +int TimeDuration(TimePoint start, TimePoint end); + +template +void PrintVec(T *vec, size_t len) { + auto cvec = reinterpret_cast>(vec); + for (size_t i = 0; i < len / 2; i++) { + std::cout << cvec[i] << std::endl; + } +} +} // namespace mindquantum +#endif // MINDQUANTUM_UTILS_H_ diff --git a/mindquantum/src/gate/basic_gate.h b/mindquantum/src/gate/basic_gate.h new file mode 100644 index 0000000000000000000000000000000000000000..249d6c880c24ba53f81c4983468abf7f6babca67 --- /dev/null +++ b/mindquantum/src/gate/basic_gate.h @@ -0,0 +1,126 @@ +/** + * Copyright 2021 Huawei Technologies Co., Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef MINDQUANTUM_GATE_basic_gate_H_ +#define MINDQUANTUM_GATE_basic_gate_H_ +#include +#include + +#include "core/utils.h" +#include "matrix/two_dim_matrix.h" +#include "pr/parameter_resolver.h" + +namespace mindquantum { +namespace py = pybind11; + +template +inline VVT> CastArray(const py::object& fun, T theta) { + py::array_t> a = fun(theta); + py::buffer_info buf = a.request(); + if (buf.ndim != 2) { + throw std::runtime_error("Gate matrix must be two dimension!"); + } + if (buf.shape[0] != buf.shape[1]) { + throw std::runtime_error("Gate matrix need a square matrix!"); + } + CTP ptr = static_cast>(buf.ptr); + + VVT> m; + for (size_t i = 0; i < buf.shape[0]; i++) { + m.push_back({}); + for (size_t j = 0; j < buf.shape[1]; j++) { + m[i].push_back(ptr[i * buf.shape[1] + j]); + } + } + return m; +} +template +struct BasicGate { + bool parameterized_ = false; + std::string name_; + VT obj_qubits_; + VT ctrl_qubits_; + ParameterResolver params_; + int64_t hermitian_prop_ = SELFHERMITIAN; + bool daggered_ = false; + T applied_value_ = 0; + bool is_measure_ = false; + Dim2Matrix base_matrix_; + std::function(T)> param_matrix_; + std::function(T)> param_diff_matrix_; + // Dim2Matrix (*param_matrix_)(T para); + // Dim2Matrix (*param_diff_matrix_)(T para); + void PrintInfo() { + if (!daggered_) { + std::cout << "Gate name: " << name_ << std::endl; + } else { + std::cout << "Gate name: " << name_ << " (daggered version)" << std::endl; + } + std::cout << "Parameterized: " << parameterized_ << std::endl; + if (!parameterized_) { + base_matrix_.PrintInfo(); + } + if (obj_qubits_.size() != 0) { + std::cout << "Obj qubits: "; + for (auto o : obj_qubits_) { + std::cout << o << " "; + } + std::cout << std::endl; + } + if (ctrl_qubits_.size() != 0) { + std::cout << "Control qubits: "; + for (auto o : ctrl_qubits_) { + std::cout << o << " "; + } + std::cout << std::endl; + } + } + void ApplyValue(T theta) { + if (parameterized_) { + parameterized_ = false; + applied_value_ = theta; + base_matrix_ = param_matrix_(theta); + } + } + BasicGate() { + } + BasicGate(bool parameterized, const std::string& name, int64_t hermitian_prop, Dim2Matrix base_matrix) + : parameterized_(parameterized), name_(name), hermitian_prop_(hermitian_prop), base_matrix_(base_matrix) { + } + BasicGate(bool parameterized, const std::string& name, int64_t hermitian_prop, + Dim2Matrix (*param_matrix)(T para), Dim2Matrix (*param_diff_matrix)(T para)) + : parameterized_(parameterized) + , name_(name) + , hermitian_prop_(hermitian_prop) + , param_matrix_(param_matrix) + , param_diff_matrix_(param_diff_matrix) { + } + BasicGate(const std::string& name, int64_t hermitian_prop, py::object matrix_fun, py::object diff_matrix_fun) + : parameterized_(true), name_(name), hermitian_prop_(hermitian_prop) { + param_matrix_ = [matrix_fun](T theta) { + auto matrix = CastArray(matrix_fun, theta); + Dim2Matrix res = Dim2Matrix(matrix); + return res; + }; + param_diff_matrix_ = [diff_matrix_fun](T theta) { + auto matirx = CastArray(diff_matrix_fun, theta); + Dim2Matrix res = Dim2Matrix(matirx); + return res; + }; + } +}; +} // namespace mindquantum +#endif // MINDQUANTUM_GATE_basic_gate_H_ diff --git a/mindquantum/src/gate/gates.h b/mindquantum/src/gate/gates.h new file mode 100644 index 0000000000000000000000000000000000000000..93f04b2feba3db5d9b78e309fd5ad35f8556bb74 --- /dev/null +++ b/mindquantum/src/gate/gates.h @@ -0,0 +1,222 @@ +/** + * Copyright 2021 Huawei Technologies Co., Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef MINDQUANTUM_GATE_GATES_H_ +#define MINDQUANTUM_GATE_GATES_H_ +#include + +#include + +#include "core/utils.h" +#include "gate/basic_gate.h" +#include "matrix/two_dim_matrix.h" + +namespace mindquantum { +template +BasicGate XGate = {false, gX, SELFHERMITIAN, Dim2Matrix{{{{0, 0}, {1, 0}}, {{1, 0}, {0, 0}}}}}; + +template +BasicGate YGate = {false, gY, SELFHERMITIAN, Dim2Matrix{{{{0, 0}, {0, -1}}, {{0, 1}, {0, 0}}}}}; + +template +BasicGate ZGate = {false, gZ, SELFHERMITIAN, Dim2Matrix{{{{1, 0}, {0, 0}}, {{0, 0}, {-1, 0}}}}}; + +template +BasicGate IGate = {false, gI, SELFHERMITIAN, Dim2Matrix{{{{1, 0}, {0, 0}}, {{0, 0}, {1, 0}}}}}; + +template +BasicGate HGate = {false, gH, SELFHERMITIAN, + Dim2Matrix{{{{static_cast(M_SQRT1_2), 0}, {static_cast(M_SQRT1_2), 0}}, + {{static_cast(M_SQRT1_2), 0}, {-static_cast(M_SQRT1_2), 0}}}}}; + +template +BasicGate TGate = { + false, gT, DOHERMITIAN, + Dim2Matrix{{{{1, 0}, {0, 0}}, {{0, 0}, {static_cast(M_SQRT1_2), static_cast(M_SQRT1_2)}}}}}; + +template +BasicGate SGate = {false, gS, DOHERMITIAN, Dim2Matrix{{{{1, 0}, {0, 0}}, {{0, 0}, {0, 1}}}}}; + +template +BasicGate CNOTGate = {false, gCNOT, DOHERMITIAN, + Dim2Matrix{{{{1, 0}, {0, 0}, {0, 0}, {0, 0}}, + {{0, 0}, {1, 0}, {0, 0}, {0, 0}}, + {{0, 0}, {0, 0}, {0, 0}, {1, 0}}, + {{0, 0}, {0, 0}, {1, 0}, {0, 0}}}}}; + +template +BasicGate CZGate = {false, gCZ, SELFHERMITIAN, + Dim2Matrix{{{{1, 0}, {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, 0}, {0, 0}, {-1, 0}}}}}; + +template +BasicGate SWAPGate = {false, gSWAP, SELFHERMITIAN, + Dim2Matrix{{{{1, 0}, {0, 0}, {0, 0}, {0, 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}}}}}; + +template +BasicGate ISWAPGate = {false, gISWAP, DOHERMITIAN, + Dim2Matrix{{{{1, 0}, {0, 0}, {0, 0}, {0, 0}}, + {{0, 0}, {0, 0}, {0, 1}, {0, 0}}, + {{0, 0}, {0, 1}, {0, 0}, {0, 0}}, + {{0, 0}, {0, 0}, {0, 0}, {1, 0}}}}}; + +template +BasicGate RXGate = { + true, gRX, PARAMSOPPOSITE, + [](T theta) { + return Dim2Matrix{{{{COS1_2(theta), 0}, {0, -SIN1_2(theta)}}, {{0, -SIN1_2(theta)}, {COS1_2(theta), 0}}}}; + }, + [](T theta) { + return Dim2Matrix{ + {{{-SIN1_2(theta) / 2, 0}, {0, -COS1_2(theta) / 2}}, {{0, -COS1_2(theta) / 2}, {-SIN1_2(theta) / 2, 0}}}}; + }}; + +template +BasicGate RYGate = { + true, gRY, PARAMSOPPOSITE, + [](T theta) { + return Dim2Matrix{{{{COS1_2(theta), 0}, {-SIN1_2(theta), 0}}, {{SIN1_2(theta), 0}, {COS1_2(theta), 0}}}}; + }, + [](T theta) { + return Dim2Matrix{ + {{{-SIN1_2(theta) / 2, 0}, {-COS1_2(theta) / 2, 0}}, {{COS1_2(theta) / 2, 0}, {-SIN1_2(theta) / 2, 0}}}}; + }}; + +template +BasicGate RZGate = { + true, gRZ, PARAMSOPPOSITE, + [](T theta) { + return Dim2Matrix{{{{COS1_2(theta), -SIN1_2(theta)}, {0, 0}}, {{0, 0}, {COS1_2(theta), SIN1_2(theta)}}}}; + }, + [](T theta) { + return Dim2Matrix{ + {{{-SIN1_2(theta) / 2, -COS1_2(theta) / 2}, {0, 0}}, {{0, 0}, {-SIN1_2(theta) / 2, COS1_2(theta) / 2}}}}; + }}; + +template +BasicGate PSGate = {true, gPS, PARAMSOPPOSITE, + [](T theta) { + return Dim2Matrix{{{{1, 0}, {0, 0}}, {{0, 0}, {COS1_2(2 * theta), SIN1_2(2 * theta)}}}}; + }, + [](T theta) { + return Dim2Matrix{{{{0, 0}, {0, 0}}, {{0, 0}, {-SIN1_2(2 * theta), COS1_2(2 * theta)}}}}; + }}; + +template +BasicGate XXGate = {true, gXX, PARAMSOPPOSITE, + [](T theta) { + return Dim2Matrix{{{{COS1_2(2 * theta), 0}, {0, 0}, {0, 0}, {0, -SIN1_2(2 * theta)}}, + {{0, 0}, {COS1_2(2 * theta), 0}, {0, -SIN1_2(2 * theta)}, {0, 0}}, + {{0, 0}, {0, -SIN1_2(2 * theta)}, {COS1_2(2 * theta), 0}, {0, 0}}, + {{0, -SIN1_2(2 * theta)}, {0, 0}, {0, 0}, {COS1_2(2 * theta), 0}}}}; + }, + [](T theta) { + return Dim2Matrix{{{{-SIN1_2(2 * theta), 0}, {0, 0}, {0, 0}, {0, -COS1_2(2 * theta)}}, + {{0, 0}, {-SIN1_2(2 * theta), 0}, {0, -COS1_2(2 * theta)}, {0, 0}}, + {{0, 0}, {0, -COS1_2(2 * theta)}, {-SIN1_2(2 * theta), 0}, {0, 0}}, + {{0, -COS1_2(2 * theta)}, {0, 0}, {0, 0}, {-SIN1_2(2 * theta), 0}}}}; + }}; + +template +BasicGate YYGate = {true, gYY, PARAMSOPPOSITE, + [](T theta) { + return Dim2Matrix{{{{COS1_2(2 * theta), 0}, {0, 0}, {0, 0}, {0, SIN1_2(2 * theta)}}, + {{0, 0}, {COS1_2(2 * theta), 0}, {0, -SIN1_2(2 * theta)}, {0, 0}}, + {{0, 0}, {0, -SIN1_2(2 * theta)}, {COS1_2(2 * theta), 0}, {0, 0}}, + {{0, SIN1_2(2 * theta)}, {0, 0}, {0, 0}, {COS1_2(2 * theta), 0}}}}; + }, + [](T theta) { + return Dim2Matrix{{{{-SIN1_2(2 * theta), 0}, {0, 0}, {0, 0}, {0, COS1_2(2 * theta)}}, + {{0, 0}, {-SIN1_2(2 * theta), 0}, {0, -COS1_2(2 * theta)}, {0, 0}}, + {{0, 0}, {0, -COS1_2(2 * theta)}, {-SIN1_2(2 * theta), 0}, {0, 0}}, + {{0, COS1_2(2 * theta)}, {0, 0}, {0, 0}, {-SIN1_2(2 * theta), 0}}}}; + }}; + +template +BasicGate ZZGate = {true, gZZ, PARAMSOPPOSITE, + [](T theta) { + return Dim2Matrix{{{{COS1_2(2 * theta), -SIN1_2(2 * theta)}, {0, 0}, {0, 0}, {0, 0}}, + {{0, 0}, {COS1_2(2 * theta), SIN1_2(2 * theta)}, {0, 0}, {0, 0}}, + {{0, 0}, {0, 0}, {COS1_2(2 * theta), SIN1_2(2 * theta)}, {0, 0}}, + {{0, 0}, {0, 0}, {0, 0}, {COS1_2(2 * theta), -SIN1_2(2 * theta)}}}}; + }, + [](T theta) { + return Dim2Matrix{{{{-SIN1_2(2 * theta), -COS1_2(2 * theta)}, {0, 0}, {0, 0}, {0, 0}}, + {{0, 0}, {-SIN1_2(2 * theta), COS1_2(2 * theta)}, {0, 0}, {0, 0}}, + {{0, 0}, {0, 0}, {-SIN1_2(2 * theta), COS1_2(2 * theta)}, {0, 0}}, + {{0, 0}, {0, 0}, {0, 0}, {-SIN1_2(2 * theta), -COS1_2(2 * theta)}}}}; + }}; + +template +BasicGate GetMeasureGate(const std::string& name) { + BasicGate out; + out.name_ = name; + out.is_measure_ = true; + return out; +} + +template +BasicGate GetGateByName(const std::string& name) { + BasicGate out; + if (name == gX) { + out = XGate; + } else if (name == gY) { + out = YGate; + } else if (name == gZ) { + out = ZGate; + } else if (name == gI) { + out = IGate; + } else if (name == gH) { + out = HGate; + } else if (name == gT) { + out = TGate; + } else if (name == gS) { + out = SGate; + } else if (name == gCNOT) { + out = CNOTGate; + } else if (name == gSWAP) { + out = SWAPGate; + } else if (name == gISWAP) { + out = ISWAPGate; + } else if (name == gCZ) { + out = CZGate; + } else if (name == gRX) { + out = RXGate; + } else if (name == gRY) { + out = RYGate; + } else if (name == gRZ) { + out = RZGate; + } else if (name == gPS) { + out = PSGate; + } else if (name == gXX) { + out = XXGate; + } else if (name == gYY) { + out = YYGate; + } else if (name == gZZ) { + out = ZZGate; + } else { + auto msg = name + " not implement in backend!"; + throw std::invalid_argument(msg); + } + return out; +} +} // namespace mindquantum +#endif // MINDQUANTUM_GATE_GATES_H_ diff --git a/mindquantum/src/hamiltonian/hamiltonian.h b/mindquantum/src/hamiltonian/hamiltonian.h new file mode 100644 index 0000000000000000000000000000000000000000..fe3eeca43c9d827c2c017b73d67e9ffc80e05e3b --- /dev/null +++ b/mindquantum/src/hamiltonian/hamiltonian.h @@ -0,0 +1,59 @@ +/** + * Copyright 2021 Huawei Technologies Co., Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef MINDQUANTUM_HAMILTONIAN_HAMILTONIAN_H_ +#define MINDQUANTUM_HAMILTONIAN_HAMILTONIAN_H_ +#include + +#include "core/utils.h" +#include "sparse/algo.h" + +namespace mindquantum { +using mindquantum::sparse::CsrHdMatrix; +using mindquantum::sparse::SparseHamiltonian; +using mindquantum::sparse::TransposeCsrHdMatrix; + +template +struct Hamiltonian { + int64_t how_to_; + Index n_qubits_; + VT> ham_; + std::shared_ptr> ham_sparse_main_; + std::shared_ptr> ham_sparse_second_; + + Hamiltonian() { + } + + explicit Hamiltonian(const VT> &ham) : how_to_(ORIGIN), ham_(ham) { + } + + Hamiltonian(const VT> &ham, Index n_qubits) : how_to_(BACKEND), n_qubits_(n_qubits), ham_(ham) { + if (n_qubits_ > 16) { + std::cout << "Sparsing hamiltonian ..." << std::endl; + } + ham_sparse_main_ = SparseHamiltonian(ham_, n_qubits_); + ham_sparse_second_ = TransposeCsrHdMatrix(ham_sparse_main_); + if (n_qubits_ > 16) { + std::cout << "Sparsing hamiltonian finished!" << std::endl; + } + } + + Hamiltonian(std::shared_ptr> csr_mat, Index n_qubits) + : n_qubits_(n_qubits), how_to_(FRONTEND), ham_sparse_main_(csr_mat) { + } +}; +} // namespace mindquantum +#endif // MINDQUANTUM_HAMILTONIAN_HAMILTONIAN_H_ diff --git a/mindquantum/src/matrix/two_dim_matrix.h b/mindquantum/src/matrix/two_dim_matrix.h new file mode 100644 index 0000000000000000000000000000000000000000..4ff2b020bb906eadf7f05fb0bb67c168fc77c9ef --- /dev/null +++ b/mindquantum/src/matrix/two_dim_matrix.h @@ -0,0 +1,63 @@ +/** + * Copyright 2021 Huawei Technologies Co., Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef MINDQUANTUM_MATRIX_TWO_DIM_MATRIX_H_ +#define MINDQUANTUM_MATRIX_TWO_DIM_MATRIX_H_ +#include +#include +#include + +#include "core/utils.h" +namespace mindquantum { +template +struct Dim2Matrix { + VVT> matrix_; + Dim2Matrix() { + } + explicit Dim2Matrix(const VVT> &m) : matrix_(m) { + } + void PrintInfo() { + if (matrix_.size() > 0) { + if (matrix_[0].size() > 0) { + std::cout << "<--Matrix of " << matrix_.size() << " X " << matrix_[0].size() << std::endl; + for (auto &col : matrix_) { + for (Index i = 0; i < static_cast(col.size()); i++) { + std::cout << col[i]; + if (i != static_cast(col.size() - 1)) { + std::cout << ", "; + } + } + std::cout << std::endl; + } + std::cout << "-->" << std::endl; + } + } + } +}; + +template +Dim2Matrix Dim2MatrixFromRI(const VT &real, const VT &imag) { + Dim2Matrix out; + for (Index i = 0; i < static_cast(real.size()); i++) { + out.matrix_.push_back({}); + for (Index j = 0; j < static_cast(real[i].size()); j++) { + out.matrix_[i].push_back(CT(std::stod(real[i][j]), std::stod(imag[i][j]))); + } + } + return out; +} +} // namespace mindquantum +#endif // MINDQUANTUM_MATRIX_TWO_DIM_MATRIX_H_ diff --git a/mindquantum/src/pr/parameter_resolver.h b/mindquantum/src/pr/parameter_resolver.h new file mode 100644 index 0000000000000000000000000000000000000000..fdae8f4a1a29d127c7eb150538984e1176cd561a --- /dev/null +++ b/mindquantum/src/pr/parameter_resolver.h @@ -0,0 +1,68 @@ +/** + * Copyright 2021 Huawei Technologies Co., Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef MINDQUANTUM_PR_PARAMETER_RESOLVER_H_ +#define MINDQUANTUM_PR_PARAMETER_RESOLVER_H_ +#include +#include +#include + +#include "core/utils.h" + +namespace mindquantum { +template +struct ParameterResolver { + MST data_; + SS no_grad_parameters_; + SS requires_grad_parameters_; + + ParameterResolver() { + } + ParameterResolver(const MST &data, const SS &ngp, const SS &rgp) + : data_(data), no_grad_parameters_(ngp), requires_grad_parameters_(rgp) { + } + ParameterResolver(const VT &names, const VT &coeffs, const VT &requires_grads) { + for (Index i = 0; i < static_cast(names.size()); ++i) { + data_[names[i]] = coeffs[i]; + if (requires_grads[i]) { + requires_grad_parameters_.insert(names[i]); + } else { + no_grad_parameters_.insert(names[i]); + } + } + } + void SetData(const VT &data, const VS &name) { + for (size_t i = 0; i < data.size(); i++) { + data_[name[i]] = data[i]; + } + } + void Times(T f) { + for (auto &it : data_) { + data_[it.first] = -it.second; + } + } +}; + +template +T LinearCombine(const ParameterResolver &pr_big, const ParameterResolver &pr_small) { + T res = 0; + for (typename MST::const_iterator i = pr_small.data_.begin(); i != pr_small.data_.end(); ++i) { + res += (pr_big.data_.at(i->first) * i->second); + } + return res; +} +} // namespace mindquantum +#endif // MINDQUANTUM_PR_PARAMETER_RESOLVER_H_ diff --git a/mindquantum/src/projector/projector.h b/mindquantum/src/projector/projector.h new file mode 100644 index 0000000000000000000000000000000000000000..c0aef2cf733ca5fa67c852b8d8ef218aa8b05f55 --- /dev/null +++ b/mindquantum/src/projector/projector.h @@ -0,0 +1,49 @@ +/** + * Copyright 2021 Huawei Technologies Co., Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef MINDQUANTUM_PROJECTOR_PROJECTOR_H_ +#define MINDQUANTUM_PROJECTOR_PROJECTOR_H_ +#include + +#include "core/utils.h" + +namespace mindquantum { +struct Projector { + std::string proj_str_; + Index n_qubits_; + Index mask1_; + Index mask2_; + + explicit Projector(const std::string &proj_str) : proj_str_(proj_str) { + n_qubits_ = static_cast(proj_str_.length()); + mask1_ = 0; + mask2_ = 0; + for (auto i : proj_str_) { + if (i == '1') { + mask1_ = mask1_ * 2 + 1; + } else { + mask1_ = mask1_ * 2; + } + if (i == '0') { + mask2_ = mask2_ * 2; + } else { + mask2_ = mask2_ * 2 + 1; + } + } + } +}; +} // namespace mindquantum +#endif // MINDQUANTUM_PROJECTOR_PROJECTOR_H_ diff --git a/mindquantum/src/sparse/algo.h b/mindquantum/src/sparse/algo.h new file mode 100644 index 0000000000000000000000000000000000000000..5068932c2670e84cd56b9b7b021ed69a3f6f5601 --- /dev/null +++ b/mindquantum/src/sparse/algo.h @@ -0,0 +1,194 @@ +/** + * Copyright 2021 Huawei Technologies Co., Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef MINDQUANTUM_SPARSE_ALGO_H_ +#define MINDQUANTUM_SPARSE_ALGO_H_ +#include + +#include "sparse/csrhdmatrix.h" +#include "sparse/paulimat.h" +#include "sparse/sparse_utils.h" + +namespace mindquantum { +namespace sparse { +template +std::shared_ptr> TransposeCsrHdMatrix(std::shared_ptr> a) { + auto &dim = a->dim_; + auto &nnz = a->nnz_; + auto &a_indices = a->indices_; + auto &a_indptr = a->indptr_; + auto &a_data = a->data_; + Index *indices = reinterpret_cast(malloc(sizeof(Index) * nnz)); + Index *indptr = reinterpret_cast(malloc(sizeof(Index) * (dim + 1))); + CTP data = reinterpret_cast>(malloc(sizeof(CT) * nnz)); + std::fill(indptr, indptr + dim, 0); + for (Index n = 0; n < nnz; n++) { + indptr[a_indices[n]]++; + } + for (Index col = 0, sum = 0; col < dim; col++) { + Index t = indptr[col]; + indptr[col] = sum; + sum += t; + } + indptr[dim] = nnz; + for (Index row = 0; row < dim; row++) { + for (Index jj = a_indptr[row]; jj < a_indptr[row + 1]; jj++) { + Index col = a_indices[jj]; + Index dest = indptr[col]; + indices[dest] = row; + data[dest] = std::conj(a_data[jj]); + indptr[col]++; + } + } + for (Index col = 0, last = 0; col <= dim; col++) { + Index t = indptr[col]; + indptr[col] = last; + last = t; + } + auto c = std::make_shared>(dim, nnz, indptr, indices, data); + return c; +} + +template +std::shared_ptr> PauliMatToCsrHdMatrix(std::shared_ptr> a) { + Index nnz = 0; + auto &col = a->col_; + auto &dim = a->dim_; + auto &coeff = a->coeff_; + auto &p = a->p_; +#pragma omp parallel for schedule(static) reduction(+ : nnz) + for (Index i = 0; i < dim; i++) { + if (i <= col[i]) { + nnz++; + } + } + Index *indptr = reinterpret_cast(malloc(sizeof(Index) * (dim + 1))); + Index *indices = reinterpret_cast(malloc(sizeof(Index) * nnz)); + CTP data = reinterpret_cast>(malloc(sizeof(CT) * nnz)); + indptr[0] = 0; + for (Index i = 0, j = 0; i < dim; i++) { + if (i <= col[i]) { + indptr[i + 1] = indptr[i] + 1; + if (i == col[i]) { + data[j] = p * POLAR[coeff[i]] * (T) (0.5); + } else { + data[j] = p * POLAR[coeff[i]]; + } + indices[j] = col[i]; + j++; + } else { + indptr[i + 1] = indptr[i]; + } + } + auto c = std::make_shared>(dim, nnz, indptr, indices, data); + return c; +} + +template +std::shared_ptr> GetPauliMat(const PauliTerm &pt, Index n_qubits) { + auto c = std::make_shared>(pt, n_qubits); + return c; +} + +template +std::shared_ptr> Csr_Plus_Csr(std::shared_ptr> a, std::shared_ptr> b) { + auto a_nnz = a->nnz_; + auto b_nnz = b->nnz_; + auto dim = a->dim_; + auto maxnnz = a_nnz + b_nnz; + CTP data = reinterpret_cast>(malloc(sizeof(CT) * maxnnz)); + Index *indices = reinterpret_cast(malloc(sizeof(Index) * maxnnz)); + Index *indptr = reinterpret_cast(malloc(sizeof(Index) * (dim + 1))); + csr_plus_csr(dim, a->indptr_, a->indices_, a->data_, b->indptr_, b->indices_, b->data_, indptr, indices, data); + auto nnz = indptr[dim]; + + auto c = std::make_shared>(dim, nnz, indptr, indices, data); + return c; +} + +template +std::shared_ptr> SparseHamiltonian(const VT> &hams, Index n_qubits) { + VT>> sp_hams(hams.size()); + +#pragma omp parallel for schedule(static) + for (Index i = 0; i < static_cast(hams.size()); i++) { + auto pm = GetPauliMat(hams[i], n_qubits); + sp_hams[i] = PauliMatToCsrHdMatrix(pm); + pm->Reset(); + } + Index tot = static_cast(hams.size()); + while (tot > 1) { + Index half = tot / 2 + tot % 2; +#pragma omp parallel for schedule(static) num_threads(half) + for (Index i = half; i < tot; i++) { + sp_hams[i - half] = Csr_Plus_Csr(sp_hams[i - half], sp_hams[i]); + sp_hams[i]->Reset(); + } + tot = half; + } + return sp_hams[0]; +} + +template +T2 *Csr_Dot_Vec(std::shared_ptr> a, T2 *vec) { + auto dim = a->dim_; + auto c_vec = reinterpret_cast>(vec); + auto new_vec = reinterpret_cast>(malloc(sizeof(CT) * dim)); + // auto nnz = a->nnz_; + auto data = a->data_; + auto indptr = a->indptr_; + auto indices = a->indices_; +#pragma omp parallel for schedule(static) + for (Index i = 0; i < dim; i++) { + CT sum = {0.0, 0.0}; + for (Index j = indptr[i]; j < indptr[i + 1]; j++) { + sum += data[j] * c_vec[indices[j]]; + } + new_vec[i] = sum; + } + free(vec); + return reinterpret_cast(new_vec); +} + +template +T2 *Csr_Dot_Vec(std::shared_ptr> a, std::shared_ptr> b, T2 *vec) { + auto dim = a->dim_; + auto c_vec = reinterpret_cast>(vec); + auto new_vec = reinterpret_cast>(malloc(sizeof(CT) * dim)); + // auto nnz = a->nnz_; + auto data = a->data_; + auto indptr = a->indptr_; + auto indices = a->indices_; + auto data_b = b->data_; + auto indptr_b = b->indptr_; + auto indices_b = b->indices_; + +#pragma omp parallel for schedule(static) + for (Index i = 0; i < dim; i++) { + CT sum = {0.0, 0.0}; + for (Index j = indptr[i]; j < indptr[i + 1]; j++) { + sum += data[j] * c_vec[indices[j]]; + } + for (Index j = indptr_b[i]; j < indptr_b[i + 1]; j++) { + sum += data_b[j] * c_vec[indices_b[j]]; + } + new_vec[i] = sum; + } + free(vec); + return reinterpret_cast(new_vec); +} +} // namespace sparse +} // namespace mindquantum +#endif // MINDQUANTUM_SPARSE_ALGO_H_ diff --git a/mindquantum/src/sparse/csrhdmatrix.h b/mindquantum/src/sparse/csrhdmatrix.h new file mode 100644 index 0000000000000000000000000000000000000000..59c994f720c76a28d0d849e995dcfe9690e33564 --- /dev/null +++ b/mindquantum/src/sparse/csrhdmatrix.h @@ -0,0 +1,111 @@ +/** + * Copyright 2021 Huawei Technologies Co., Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef MINDQUANTUM_SPARSE_CSR_HD_MATRIX_H_ +#define MINDQUANTUM_SPARSE_CSR_HD_MATRIX_H_ +#include +#include +#include + +#include "core/utils.h" + +namespace mindquantum { +namespace sparse { +namespace py = pybind11; +template +struct CsrHdMatrix { + Index dim_; + Index nnz_; + Index *indptr_; + Index *indices_; + CTP data_; + + void FreeMemory() { + if (indptr_ != nullptr) { + free(indptr_); + } + if (indices_ != nullptr) { + free(indices_); + } + if (data_ != nullptr) { + free(data_); + } + indptr_ = nullptr; + indices_ = nullptr; + data_ = nullptr; + } + void Reset() { + FreeMemory(); + indptr_ = nullptr; + indices_ = nullptr; + data_ = nullptr; + } + ~CsrHdMatrix() { + FreeMemory(); + } + CsrHdMatrix() : dim_(0), nnz_(0), indptr_(nullptr), indices_(nullptr), data_(nullptr) { + } + CsrHdMatrix(Index dim, Index nnz, Index *indptr, Index *indices, CTP data) + : dim_(dim), nnz_(nnz), indptr_(indptr), indices_(indices), data_(data) { + } + CsrHdMatrix(Index dim, Index nnz, py::array_t indptr, py::array_t indices, py::array_t> data) + : dim_(dim), nnz_(nnz) { + indptr_ = reinterpret_cast(malloc(indptr.size() * sizeof(Index))); + indices_ = reinterpret_cast(malloc(indices.size() * sizeof(Index))); + data_ = (CTP) malloc(data.size() * sizeof(CT)); + Index *indptr_py = static_cast(indptr.request().ptr); + Index *indices_py = static_cast(indices.request().ptr); + CTP data_py = static_cast *>(data.request().ptr); + for (size_t i = 0; i < data.size(); i++) { + indices_[i] = indices_py[i]; + data_[i] = data_py[i]; + } + for (size_t i = 0; i < indptr.size(); i++) { + indptr_[i] = indptr_py[i]; + } + } + void PrintInfo() { + std::cout << "<--Csr Half Diag Matrix with Dimension: "; + std::cout << dim_ << " X " << dim_ << ", and nnz: " << nnz_ << std::endl; + std::cout << " Data:\n "; + for (Index i = 0; i < nnz_; ++i) { + std::cout << data_[i]; + if (i != nnz_ - 1) { + std::cout << ","; + } + } + + std::cout << "\n indptr:\n "; + for (Index i = 0; i < dim_ + 1; i++) { + std::cout << indptr_[i]; + if (i != dim_) { + std::cout << ","; + } + } + + std::cout << "\n indices:\n "; + for (Index i = 0; i < nnz_; i++) { + std::cout << indices_[i]; + if (i != nnz_ - 1) { + std::cout << ","; + } + } + + std::cout << "-->\n\n"; + } +}; +} // namespace sparse +} // namespace mindquantum +#endif // MINDQUANTUM_SPARSE_CSR_HD_MATRIX_H_ diff --git a/mindquantum/src/sparse/paulimat.h b/mindquantum/src/sparse/paulimat.h new file mode 100644 index 0000000000000000000000000000000000000000..03c5ebea64aa76a68a31430a399728193304823b --- /dev/null +++ b/mindquantum/src/sparse/paulimat.h @@ -0,0 +1,89 @@ +/** + * Copyright 2021 Huawei Technologies Co., Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef MINDQUANTUM_SPARSE_PAULI_MAT_H_ +#define MINDQUANTUM_SPARSE_PAULI_MAT_H_ +#include "core/utils.h" +#include "pybind11/numpy.h" + +namespace mindquantum { +namespace sparse { +namespace py = pybind11; +template +struct PauliMat { + char *coeff_; + Index *col_; + Index n_qubits_; + Index dim_; + T p_; + + inline void FreeMemory() { + if (coeff_ != nullptr) { + free(coeff_); + } + if (col_ != nullptr) { + free(col_); + } + } + void Reset() { + FreeMemory(); + coeff_ = nullptr; + col_ = nullptr; + } + ~PauliMat() { + FreeMemory(); + } + PauliMat() : coeff_(nullptr), col_(nullptr), n_qubits_(0), dim_(0) { + } + PauliMat(const PauliTerm pt, Index n_qubits) : n_qubits_(n_qubits), p_(pt.second) { + dim_ = (1UL << n_qubits_); + coeff_ = reinterpret_cast(malloc(sizeof(char) * dim_)); + col_ = reinterpret_cast(malloc(sizeof(Index) * dim_)); + auto mask = GetPauliMask(pt.first); + auto mask_f = mask.mask_x | mask.mask_y; +#pragma omp parallel for schedule(static) + for (Index i = 0; i < dim_; i++) { + auto j = (i ^ mask_f); + col_[i] = j; + auto axis2power = CountOne(i & mask.mask_z); // -1 + auto axis3power = CountOne(i & mask.mask_y); // -1j + // (-1)^a2*(-1j)^a3*(1j)^a1=(1j)^2a2*(1j)^3a3*(1j)^a1=(1j)^(a1+2*a2+3*a3) + coeff_[j] = static_cast((mask.num_y + 2 * axis3power + 2 * axis2power) & 3); + } + } + void PrintInfo() { + std::cout << "<--Pauli Matrix with Dimension: "; + std::cout << dim_ << " X " << dim_ << std::endl; + std::cout << " Data:\n "; + for (Index i = 0; i < dim_; i++) { + std::cout << POLAR[coeff_[i]]; + if (i != dim_ - 1) { + std::cout << ","; + } + } + + std::cout << "\n Col:\n "; + for (Index i = 0; i < dim_; i++) { + std::cout << col_[i]; + if (i != dim_ - 1) { + std::cout << ","; + } + } + std::cout << "-->\n\n"; + } +}; +} // namespace sparse +} // namespace mindquantum +#endif // MINDQUANTUM_SPARSE_PAULI_MAT_H_ diff --git a/mindquantum/src/sparse/sparse_utils.h b/mindquantum/src/sparse/sparse_utils.h new file mode 100644 index 0000000000000000000000000000000000000000..344f3d7b2624f57f05316d03f976fa74e2e4314c --- /dev/null +++ b/mindquantum/src/sparse/sparse_utils.h @@ -0,0 +1,93 @@ +/** + * Copyright 2021 Huawei Technologies Co., Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef MINDQUANTUM_SPARSE_SPARSE_UTILS_H_ +#define MINDQUANTUM_SPARSE_SPARSE_UTILS_H_ +#include +#include + +#include "core/utils.h" + +namespace mindquantum { +namespace sparse { +template +void csr_plus_csr(Index dim, const Index *a_indptr, const Index *aj, const T *ad, const Index *b_indptr, + const Index *bj, const T *bd, Index *cp, Index *cj, T *cd) { + cp[0] = 0; + Index nnz = 0; + for (Index i = 0; i < dim; i++) { + Index ap = a_indptr[i]; + Index bp = b_indptr[i]; + Index a_end = a_indptr[i + 1]; + Index b_end = b_indptr[i + 1]; + + while (ap < a_end && bp < b_end) { + Index a_j = aj[ap]; + Index b_j = bj[bp]; + + if (a_j == b_j) { + T result = ad[ap] + bd[bp]; + if (std::abs(result) > PRECISION) { + cj[nnz] = a_j; + cd[nnz] = result; + nnz++; + } + ap++; + bp++; + } else if (a_j < b_j) { + T result = ad[ap]; + if (std::abs(result) > PRECISION) { + cj[nnz] = a_j; + cd[nnz] = result; + nnz++; + } + ap++; + } else { + T result = bd[bp]; + if (std::abs(result) > PRECISION) { + cj[nnz] = b_j; + cd[nnz] = result; + nnz++; + } + bp++; + } + } + + while (ap < a_end) { + T result = ad[ap]; + if (std::abs(result) > PRECISION) { + cj[nnz] = aj[ap]; + cd[nnz] = result; + nnz++; + } + ap++; + } + + while (bp < b_end) { + T result = bd[bp]; + if (std::abs(result) > PRECISION) { + cj[nnz] = bj[bp]; + cd[nnz] = result; + nnz++; + } + bp++; + } + + cp[i + 1] = nnz; + } +} +} // namespace sparse +} // namespace mindquantum +#endif // MINDQUANTUM_SPARSE_SPARSE_UTILS_H_ diff --git a/mindquantum/src/utils.cc b/mindquantum/src/utils.cc new file mode 100644 index 0000000000000000000000000000000000000000..0fe6e58ef1044ef0c45b9ef0577ca79031309d86 --- /dev/null +++ b/mindquantum/src/utils.cc @@ -0,0 +1,48 @@ +/** + * Copyright 2021 Huawei Technologies Co., Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "core/utils.h" + +namespace mindquantum { +const VT> POLAR = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}}; +TimePoint NOW() { + return std::chrono::steady_clock::now(); +} + +int TimeDuration(TimePoint start, TimePoint end) { + auto d = end - start; + return std::chrono::duration_cast(d).count(); +} + +Index GetControlMask(const VT &ctrls) { + Index ctrlmask = std::accumulate(ctrls.begin(), ctrls.end(), 0, [&](Index a, Index b) { return a | (1UL << b); }); + return ctrlmask; +} + +PauliMask GetPauliMask(const VT &pws) { + VT out = {0, 0, 0, 0, 0, 0}; + for (auto &pw : pws) { + for (Index i = 0; i < 3; i++) { + if (static_cast(pw.second - 'X') == i) { + out[i] += (1UL << pw.first); + out[3 + i] += 1; + } + } + } + PauliMask res = {out[0], out[1], out[2], out[3], out[4], out[5]}; + return res; +} +} // namespace mindquantum diff --git a/mindquantum/third_party/__init__.py b/mindquantum/third_party/__init__.py index 5e888335b2d78d0bb0a8103fd7810f361b38b875..ddc35f6bfe9c97ec3def421003e03671ff026ca7 100644 --- a/mindquantum/third_party/__init__.py +++ b/mindquantum/third_party/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2017 The OpenFermion Developers # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,3 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +"""Third-party modules for MindQuantum.""" + +# Allow extending this namespace. +from .unitary_cc import uccsd_singlet_get_packed_amplitudes +from .unitary_cc import uccsd_singlet_generator diff --git a/mindquantum/third_party/interaction_operator.py b/mindquantum/third_party/interaction_operator.py index 4af3a7465936925832d75b4e7410244b88469b31..615df3ea654a0ab73c127451cc7f6b1a44407280 100644 --- a/mindquantum/third_party/interaction_operator.py +++ b/mindquantum/third_party/interaction_operator.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2017 The OpenFermion Developers. # Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,7 +23,7 @@ Hamiltonian. It can be further used to construct the molecular Hamiltonian. # Note this module, we did not modify much of the OpenFermion file import itertools -from mindquantum.ops.polynomial_tensor import PolynomialTensor +from mindquantum.core.operators.polynomial_tensor import PolynomialTensor class InteractionOperator(PolynomialTensor): @@ -32,7 +33,7 @@ class InteractionOperator(PolynomialTensor): The Hamiltonian including one-body and two-body terms which conserve spin and parity. In this module, the stored coefficient could be represented the - molecualr Hamiltonians througth the FermionOperator class. + molecular Hamiltonians through the FermionOperator class. Note: The operators stored in this class has the form: diff --git a/mindquantum/third_party/unitary_cc.py b/mindquantum/third_party/unitary_cc.py index 972ef1d5546352220103311b872a5807c33b5c93..85271f8707798fccad877d76340ad3e1f93300c0 100644 --- a/mindquantum/third_party/unitary_cc.py +++ b/mindquantum/third_party/unitary_cc.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2017 The OpenFermion Developers. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,9 +16,8 @@ import itertools import numpy -from mindquantum.ops import FermionOperator from openfermion.utils.indexing import down_index, up_index -from mindquantum.parameterresolver import ParameterResolver as PR +from mindquantum.core.parameterresolver import ParameterResolver as PR def uccsd_singlet_get_packed_amplitudes(single_amplitudes, double_amplitudes, @@ -46,7 +46,7 @@ def uccsd_singlet_get_packed_amplitudes(single_amplitudes, double_amplitudes, Examples: >>> import numpy as np - >>> from mindquantum.hiqfermion.ucc import uccsd_singlet_get_packed_amplitudes + >>> from mindquantum.algorithm.nisq.chem import uccsd_singlet_get_packed_amplitudes >>> n_qubits, n_electrons = 4, 2 >>> np.random.seed(42) >>> ccsd_single_amps = np.random.random((4, 4)) @@ -131,7 +131,7 @@ def uccsd_singlet_generator(n_qubits, n_electrons, anti_hermitian=True): builds the UCCSD wavefunction. Examples: - >>> from mindquantum.hiqfermion.ucc import uccsd_singlet_generator + >>> from mindquantum.algorithm.nisq.chem import uccsd_singlet_generator >>> uccsd_singlet_generator(4, 2) -s_0 [0^ 2] + -d1_0 [0^ 2 1^ 3] + @@ -142,6 +142,7 @@ def uccsd_singlet_generator(n_qubits, n_electrons, anti_hermitian=True): s_0 [3^ 1] + d1_0 [3^ 1 2^ 0] """ + from mindquantum.core.operators import FermionOperator if n_qubits % 2 != 0: raise ValueError('The total number of spin-orbitals should be even.') diff --git a/mindquantum/utils/__init__.py b/mindquantum/utils/__init__.py index 61526bd067e83ce140d2f3dd37b702f7bb6e5ef2..8979da78f7bc849a9cad2e2266db73660673719c 100644 --- a/mindquantum/utils/__init__.py +++ b/mindquantum/utils/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,21 +15,11 @@ # ============================================================================ """Utils""" -from .beauty_print import bprint from .f import mod from .f import normalize from .f import random_state from .f import ket_string -from .utils_operator import (number_operator, normal_ordered, count_qubits, - commutator, get_fermion_operator, - hermitian_conjugated, up_index, down_index, - sz_operator) - -__all__ = [ - 'bprint', 'mod', 'normalize', 'random_state', 'number_operator', - 'normal_ordered', 'commutator', 'up_index', 'down_index', 'sz_operator', - 'hermitian_conjugated', 'ket_string', 'get_fermion_operator', - 'count_qubits' -] +from .f import random_circuit +__all__ = ['mod', 'normalize', 'random_state', 'ket_string', 'random_circuit'] __all__.sort() diff --git a/mindquantum/utils/f.py b/mindquantum/utils/f.py index a6b563d908bd5db20d9c87cb4aca72e608f773f0..9b82b4b018b0bfc3772802929db092e7fd3afb55 100644 --- a/mindquantum/utils/f.py +++ b/mindquantum/utils/f.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,14 +15,81 @@ # ============================================================================ """Useful functions""" +import fractions import numpy as np +from .type_value_check import _check_input_type +from .type_value_check import _check_value_should_not_less +from .type_value_check import _check_value_should_between_close_set + + +def random_circuit(n_qubits, gate_num, sd_rate=0.5, ctrl_rate=0.2, seed=42): + """ + Generate a random circuit. + + Args: + n_qubits (int): Number of qubits of random circuit. + gate_num (int): Number of gates in random circuit. + sd_rate (float): The rate of single qubit gate and double qubits gates. + ctrl_rate (float): The possibility that a gate has a control qubit. + seed (int): Random seed to generate random circuit. + + Examples: + >>> from mindquantum.utils import random_circuit + >>> random_circuit(3, 4, 0.5, 0.5, 100) + q1: ──Z────RX(0.944)────────●────────RX(-0.858)── + │ │ │ │ + q2: ──●────────●────────RZ(-2.42)────────●─────── + """ + from mindquantum import Circuit + import mindquantum.core.gates as G + _check_input_type('n_qubits', int, n_qubits) + _check_input_type('gate_num', int, gate_num) + _check_input_type('sd_rate', float, sd_rate) + _check_input_type('ctrl_rate', float, ctrl_rate) + _check_input_type('seed', int, seed) + _check_value_should_not_less('n_qubits', 1, n_qubits) + _check_value_should_not_less('gate_num', 1, gate_num) + _check_value_should_between_close_set('sd_rate', 0, 1, sd_rate) + _check_value_should_between_close_set('ctrl_rate', 0, 1, ctrl_rate) + _check_value_should_between_close_set('seed', 0, 2**32 - 1, seed) + if n_qubits == 1: + sd_rate = 1 + ctrl_rate = 0 + single = {'param': [G.RX, G.RY, G.RZ, G.PhaseShift], 'non_param': [G.X, G.Y, G.Z, G.H]} + double = {'param': [G.XX, G.YY, G.ZZ], 'non_param': [G.SWAP]} + c = Circuit() + np.random.seed(seed) + qubits = range(n_qubits) + for _ in range(gate_num): + if n_qubits == 1: + q1, q2 = int(qubits[0]), None + else: + q1, q2 = np.random.choice(qubits, 2, replace=False) + q1, q2 = int(q1), int(q2) + if np.random.random() < sd_rate: + if np.random.random() > ctrl_rate: + q2 = None + if np.random.random() < 0.5: + gate = np.random.choice(single['param']) + p = np.random.uniform(-np.pi * 2, np.pi * 2) + c += gate(p).on(q1, q2) + else: + gate = np.random.choice(single['non_param']) + c += gate.on(q1, q2) + else: + if np.random.random() < 0.75: + gate = np.random.choice(double['param']) + p = np.random.uniform(-np.pi * 2, np.pi * 2) + c += gate(p).on([q1, q2]) + else: + gate = np.random.choice(double['non_param']) + c += gate.on([q1, q2]) + return c def _check_num_array(vec, name): if not isinstance(vec, (np.ndarray, list)): - raise TypeError( - "{} requires a numpy.ndarray or a list of number, but get {}.". - format(name, type(vec))) + raise TypeError("{} requires a numpy.ndarray or a list of number, but get {}.".format(name, type(vec))) def mod(vec_in, axis=0): @@ -98,9 +166,7 @@ def random_state(shapes, norm_axis=0, comp=True, seed=None): [0.87252821+0.06923499j, 0.41946926+0.60691409j]]) """ if not isinstance(shapes, (int, tuple)): - raise TypeError( - "shape requires a int of a tuple of int, but get {}!".format( - type(shapes))) + raise TypeError("shape requires a int of a tuple of int, but get {}!".format(type(shapes))) if not isinstance(comp, bool): raise TypeError("comp requires a bool, but get {}!".format(comp)) np.random.seed(seed) @@ -120,24 +186,28 @@ def _index_to_bitstring(index, n, big_end=False): return s -def _common_exp(num, tol=1e-7): +def _common_exp(num, round_n=None): """common expressions.""" if num == 0: - return num - s2 = np.sqrt(2) - s3 = np.sqrt(3) - s5 = np.sqrt(5) - com = {2: s2, 3: s3, 5: s5} - for i, j in com.items(): - tmp_num = (j / num) - ceil = np.ceil(tmp_num) - floor = np.floor(tmp_num) - if np.abs(tmp_num - ceil) < tol or np.abs(tmp_num - floor) < tol: - frac = int(1 / (num / j)) - if frac > 0: - return f'√{i}/{frac}' - return f'-√{i}/{-frac}' - return num + return '0' + com = {'': 1, 'π': np.pi, '√2': np.sqrt(2), '√3': np.sqrt(3), '√5': np.sqrt(5)} + for k, v in com.items(): + left = str(fractions.Fraction(str(round(num / v, 9)))) + if len(left) < 5 or '/' not in left or left.startswith('1/') or left.startswith('-1/'): + tmp = left.split('/') + if not (len(tmp) == 2 and int(tmp[1]) > 5 and int(tmp[0]) > 5): + if tmp[0] == '1': + tmp[0] = k + if k == '': + tmp[0] = '1' + elif tmp[0] == '-1': + tmp[0] = f"-{k}" + if k == '': + tmp[0] = '-1' + else: + tmp[0] = f"{tmp[0]}{k}" + return '/'.join(tmp) + return str(num) if round_n is None else str(round(num, round_n)) def ket_string(state, tol=1e-7): @@ -155,10 +225,11 @@ def ket_string(state, tol=1e-7): >>> import numpy as np >>> from mindquantum.utils import ket_string >>> state = np.array([1, -1j])/np.sqrt(2) - >>> print('\\n'.join(ket_string(state))) - √2/2¦0⟩ - -√2/2j¦1⟩ + >>> print(ket_string(state)) + ['√2/2¦0⟩', '-√2/2j¦1⟩'] """ + if not isinstance(state, np.ndarray) or len(state.shape) != 1: + raise TypeError(f"state need a 1-D ndarray.") n = int(np.log2(len(state))) if len(state) < 2 and len(state) != (1 << n): raise ValueError("Invalid state size!") @@ -168,10 +239,15 @@ def ket_string(state, tol=1e-7): if np.abs(i) < tol: continue if np.abs(np.real(i)) < tol: - s.append(f'{_common_exp(np.imag(i), tol)}j¦{b}⟩') + s.append(f'{_common_exp(np.imag(i))}j¦{b}⟩') continue if np.abs(np.imag(i)) < tol: - s.append(f'{_common_exp(np.real(i), tol)}¦{b}⟩') + s.append(f'{_common_exp(np.real(i))}¦{b}⟩') continue - s.append(f'{i}¦{b}⟩') + i_real = _common_exp(np.real(i)) + i_imag = _common_exp(np.imag(i)) + if i_imag.startswith('-'): + s.append(f'({i_real}{i_imag}j)¦{b}⟩') + else: + s.append(f'({i_real}+{i_imag}j)¦{b}⟩') return s diff --git a/mindquantum/utils/type_value_check.py b/mindquantum/utils/type_value_check.py new file mode 100644 index 0000000000000000000000000000000000000000..8ce4400815c860f35a236630bdf55343682bc135 --- /dev/null +++ b/mindquantum/utils/type_value_check.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""type and value check helper""" +import numpy as np + +_num_type = (int, float, complex, np.int32, np.int64, np.float32, np.float64) + + +def _check_input_type(arg_msg, require_type, arg): + """check input type""" + if not isinstance(arg, require_type): + raise TypeError(f"{arg_msg} requires a {require_type}, but get {type(arg)}") + + +def _check_value_should_not_less(arg_msg, require_value, arg): + """check value should not less""" + if arg < require_value: + raise ValueError(f'{arg_msg} should be not less than {require_value}, but get {arg}') + + +def _check_value_should_between_close_set(arg_ms, min_value, max_value, arg): + """Check value should between""" + if arg < min_value or arg > max_value: + raise ValueError(f"{arg_ms} should between {min_value} and {max_value}, but get {arg}") + + +def _check_and_generate_pr_type(pr, names=None): + """_check_and_generate_pr_type""" + from mindquantum.core import ParameterResolver + if isinstance(pr, _num_type): + if len(names) != 1: + raise ValueError(f"number of given parameters value is less than parameters ({len(names)})") + pr = np.array([pr]) + _check_input_type('parameter', (ParameterResolver, np.ndarray, list, dict), pr) + if isinstance(pr, dict): + pr = ParameterResolver(pr) + elif isinstance(pr, (np.ndarray, list)): + pr = np.array(pr) + if len(pr) != len(names) or len(pr.shape) != 1: + raise ValueError(f"given parameter value size ({pr.shape}) not match with parameter size ({len(names)})") + pr = ParameterResolver(dict(zip(names, pr))) + + return pr diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..3b0b3e9f372a206466a6596d1b143636063f5bac --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,98 @@ +[build-system] +requires = ["setuptools>=42", "wheel", "pybind11>=2.6.0", "setuptools_scm[toml]>=3.4"] +build-backend = "setuptools.build_meta" + +# ============================================================================== + +[tool.black] + line-length = 120 + target-version = ['py36','py37','py38'] + skip-string-normalization = true + + +[tool.check-manifest] + ignore = [ + 'PKG-INFO', + '*.egg-info', + '*.egg-info/*', + 'setup.cfg', + '.hgtags', + '.hgsigs', + '.hgignore', + '.gitignore', + '.bzrignore', + '.gitattributes', + '.github/*', + '.travis.yml', + '*.mo', + '.clang-tidy', + '.clang-format', + '.gitmodules', + 'requirements.txt', + 'requirements_tests.txt', + 'VERSION.txt', + '.editorconfig', + '*.yml', + '*.yaml', + 'docs/*', + 'docs/images/*', + 'tests/*', + ] + + + +[tool.coverage] + [tool.coverage.run] + omit = [ + '*_test.py', + '*_fixtures.py' + ] + + +[tool.pylint] + [tool.pylint.master] + ignore-patterns = [ + '.*_test.py', + '.*_fixtures.py', + ] + + extension-pkg-whitelist = [ + ] + extension-pkg-allow-list = [ + ] + + [tool.pylint.basic] + good-names = [] + + [tool.pylint.format] + max-line-length = 120 + + [tool.pylint.messages_control] + disable = [ + 'no-name-in-module', # due to dynamic importing of symbols + 'fixme' + ] + + +# [tool.pytest.ini_options] + +# minversion = '6.0' +# addopts = '-pno:warnings' +# testpaths = ['mindquantum'] +# norecursedirs = 'third_party' +# mock_use_standalone_module = true + +[tool.isort] + +profile = "black" + + +[tool.setuptools_scm] +write_to = 'VERSION.txt' +write_to_template = '{version}' +local_scheme = 'no-local-version' +fallback_version = '0.3.0' + + +[tool.yapf] +column_limit = 120 diff --git a/requirements.txt b/requirements.txt index 47f315a217531d8d66b9aefda4ae56d16a11ff3f..1425f1be75eed52f57b28d19fd6c45742233b4fe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,8 @@ -numpy >= 1.17.0 -scipy >= 1.5.3 -projectq >= 0.5.1 +numpy>=1.17.0 +scipy>=1.5.3 +projectq>=0.5.1 openfermion>=1.0.0 -sympy >= 1.4 -matplotlib >= 3.1.3 +sympy>=1.4 +matplotlib>=3.1.3 +rich>=10.9.0 + diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000000000000000000000000000000000000..3999f29250d120ea8a8cd35955c381f3ec50d73a --- /dev/null +++ b/setup.cfg @@ -0,0 +1,81 @@ +[metadata] + +name = mindquantum +version = file:VERSION.txt + +author = The MindSpore Authors +author_email = contact@mindspore.cn + +description = A hybrid quantum-classic framework for quantum computing +long_description = file:README.md +long_description_content_type = text/markdown; charset=UTF-8 + +license = Apache License Version 2.0 +license_file = LICENSE + +requires_dist = setuptools + +url = https://www.mindspore.cn/ +home_page = https://www.mindspore.cn/ +download_url = https://gitee.com/mindspore/mindquantum/tags +project_urls = + Download = https://gitee.com/mindspore/mindquantum/tags + Source = https://gitee.com/mindspore/mindquantum + Issue-Tracker = https://gitee.com/mindspore/mindquantum/issues + +classifier = + License :: OSI Approved :: Apache Software License + Topic :: Software Development :: Libraries :: Python Modules + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + + +[options] + +zip_safe = False +include_package_data = True +packages = find: +python_requires = >= 3 + +setup_requires = + setuptools >= 42 + setuptools_scm[toml] + pybind11 >= 2.6.0 + +install_requires = + numpy >= 1.17.0 + scipy >= 1.5.3 + projectq >= 0.5.1 + openfermion>=1.0.0 + sympy >= 1.4 + matplotlib >= 3.1.3 + rich >= 10.9.0 + +[options.extras_require] + +# test = +# pytest + +[options.package_data] + +* = *.so, *.so*, *.pyd + + +# ============================================================================== + +[flake8] + +max-line-length = 120 +exclude = + .git + __pycache__ + build + dist +docstring-quotes = """ +eradicate-whitelist = # yapf: disable# yapf: enable + +# ============================================================================== diff --git a/setup.py b/setup.py index a7c73e12d365c009d826e1d36e10513c950b44ac..c8cc83666d197d2061e92cd56cf6770f6003b7e4 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,85 +13,498 @@ # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================ -"""Setup.""" +"""Setup.py file.""" + +import contextlib +import copy +import distutils.log +import errno +import hashlib +import itertools +import multiprocessing import os +import platform +import shutil import stat -from setuptools import setup -from setuptools import find_packages -from setuptools.command.egg_info import egg_info -from setuptools.command.build_py import build_py +import subprocess +import sys +from distutils.cmd import Command +from distutils.command.clean import clean +from distutils.file_util import copy_file -cur_dir = os.path.dirname(os.path.realpath(__file__)) -pkg_dir = os.path.join(cur_dir, 'build') +import setuptools +from setuptools.command.build_ext import build_ext +# ============================================================================== +# Helper variables + +on_rtd = os.environ.get('READTHEDOCS') == 'True' +cur_dir = os.path.dirname(os.path.realpath(__file__)) +ext_errors = (subprocess.CalledProcessError, FileNotFoundError) +cmake_extra_options = [] -def read_version(): - """generate python file""" - version_file = os.path.join(cur_dir, 'mindquantum/', 'version.py') - with open(version_file, 'r') as f: - version_ = f.readlines()[-1].strip().split()[-1][1:-1] - return version_ +# ============================================================================== +# Helper functions and classes -version = read_version() -def update_permissions(path): +@contextlib.contextmanager +def fdopen(fname, mode, perms=0o644): # pragma: no cover """ - Update permissions. + Context manager for opening files with correct permissions. Args: - path (str): Target directory path. + fname (str): Path to file to open for reading/writing + mode (str): Mode in which the file is opened (see help for builtin `open()`) + perms (int): Permission mask (see help for `os.open()`) """ - for dirpath, dirnames, filenames in os.walk(path): - for dirname in dirnames: - dir_fullpath = os.path.join(dirpath, dirname) - os.chmod( - dir_fullpath, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC - | stat.S_IRGRP | stat.S_IXGRP) - for filename in filenames: - file_fullpath = os.path.join(dirpath, filename) - os.chmod(file_fullpath, stat.S_IREAD) - - -class EggInfo(egg_info): - """Egg info.""" + if 'r' in mode: + flags = os.O_RDONLY + elif 'w' in mode: + flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC + elif 'a' in mode: + flags = os.O_WRONLY | os.O_CREAT + else: + raise RuntimeError(f'Unsupported mode: {mode}') + + file_object = open(os.open(fname, flags, perms), mode=mode, encoding='utf-8') + + try: + yield file_object + finally: + file_object.close() + + +def remove_tree(directory): + """Remove a directory and its subdirectories.""" + + def remove_read_only(func, path, exc_info): + excvalue = exc_info[1] + if func in (os.rmdir, os.remove) and excvalue.errno == errno.EACCES: + os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) + func(path) + else: + raise exc_info[0].with_traceback(exc_info[1], exc_info[2]) + + if os.path.exists(directory): + distutils.log.info(f'Removing {directory} (and everything under it)') + shutil.rmtree(directory, ignore_errors=False, onerror=remove_read_only) + + +def write_checksum(): + """Rename Python wheels on Windows.""" + if os.path.exists(os.path.join(cur_dir, 'output')): + whl = os.listdir(os.path.join(cur_dir, 'output')) + if whl: + whl_name = os.path.join(cur_dir, 'output', whl[0]) + with open(whl_name, 'rb') as f: + sha256obj = hashlib.sha256() + sha256obj.update(f.read()) + hash_value = sha256obj.hexdigest() + with open(whl_name + '.sha256', 'w') as f: + f.writelines(f'{hash_value} *{whl[0]}') + + +def important_msgs(*msgs): + """Print an important message.""" + print('*' * 75) + for msg in msgs: + print(msg) + print('*' * 75) + + +def get_extra_cmake_options(): + """ + Parse CMake options from python3 setup.py command line. + + Read --unset, --set, -A and -G options from the command line and add them as cmake switches. + """ + _cmake_extra_options = [] + + opt_key = None + + has_generator = False + + argv = copy.deepcopy(sys.argv) + # parse command line options and consume those we care about + for arg in argv: + if opt_key == 'G': + has_generator = True + _cmake_extra_options += ['-G', arg.strip()] + elif opt_key == 'A': + _cmake_extra_options += ['-A', arg.strip()] + elif opt_key == 'unset': + _cmake_extra_options.append(f'-D{arg.strip()}:BOOL=OFF') + elif opt_key == 'set': + _cmake_extra_options.append(f'-D{arg.strip()}:BOOL=ON') + + if opt_key: + sys.argv.remove(arg) + opt_key = None + continue + + if arg in ['--unset', '--set', '--compiler-flags']: + opt_key = arg[2:].lower() + sys.argv.remove(arg) + continue + if arg in ['-A']: + opt_key = arg[1:] + sys.argv.remove(arg) + continue + if arg in ['-G']: + opt_key = arg[1:] + sys.argv.remove(arg) + continue + + # If no explicit CMake Generator specification, prefer MinGW Makefiles on Windows + if (not has_generator) and (platform.system() == "Windows"): + _cmake_extra_options += ['-G', "MinGW Makefiles"] + + return _cmake_extra_options + + +# ============================================================================== + + +def get_python_executable(): + """Retrieve the path to the Python executable.""" + try: + root_path = os.environ['VIRTUAL_ENV'] + python = os.path.basename(sys.executable) + python_path = os.path.join(root_path, python) + if os.path.exists(python_path): + return python_path + return os.path.join(root_path, 'bin', python) + except KeyError: + return sys.executable + + +def get_cmake_command(): + """Retrieve the path to the CMake executable.""" + with fdopen(os.devnull, 'w') as devnull: + try: + subprocess.check_call(['cmake', '--version'], stdout=devnull, stderr=devnull) + return ['cmake'] + except (OSError, subprocess.CalledProcessError): + pass + + # CMake not in PATH, should have installed Python CMake module + # -> try to find out where it is + try: + root_path = os.environ['VIRTUAL_ENV'] + python = os.path.basename(sys.executable) + except KeyError: + root_path, python = os.path.split(sys.executable) + + search_paths = [root_path, os.path.join(root_path, 'bin'), os.path.join(root_path, 'Scripts')] + + # First try executing CMake directly + for base_path in search_paths: + try: + cmake_cmd = os.path.join(base_path, 'cmake') + subprocess.check_call([cmake_cmd, '--version'], stdout=devnull, stderr=devnull) + return [cmake_cmd] + except (OSError, subprocess.CalledProcessError): + pass + + # That did not work: try calling it through Python + for base_path in search_paths: + try: + cmake_cmd = [python, os.path.join(base_path, 'cmake')] + subprocess.check_call(cmake_cmd + ['--version'], stdout=devnull, stderr=devnull) + return cmake_cmd + except (OSError, subprocess.CalledProcessError): + pass + + # Nothing worked -> give up! + return None + + +# ============================================================================== + + +class BuildFailed(Exception): + """Extension raised if the build fails for any reason.""" + + def __init__(self): + """Initialize a BuildFailed exception.""" + super().__init__() + self.cause = sys.exc_info()[1] # work around py 2/3 different syntax + + +# ============================================================================== + + +class CMakeExtension(setuptools.Extension): # pylint: disable=too-few-public-methods + """Class defining a C/C++ Python extension to be compiled using CMake.""" + + def __init__(self, pymod, target=None, optional=False): + """ + Initialize a CMakeExtension object. + + Args: + src_dir (string): Path to source directory + target (string): Name of target + pymod (string): Name of compiled Python module + optional (bool): (optional) If true, not building this extension is not considered an error + """ + # NB: the main source directory is the one containing the setup.py file + self.src_dir = os.path.realpath('') + self.pymod = pymod + self.target = target if target is not None else pymod.split('.')[-1] + + self.lib_filepath = os.path.join(*pymod.split('.')) + super().__init__(pymod, sources=[], optional=optional) + + +# ------------------------------------------------------------------------------ + + +class CMakeBuildExt(build_ext): + """Custom build_ext command class.""" + + user_options = build_ext.user_options + [ + ('no-arch-native', None, 'Do not use the -march=native flag when compiling'), + ('clean-build', None, 'Build in a clean build environment'), + ] + + boolean_options = build_ext.boolean_options + ['no-arch-native', 'clean-build'] + + def initialize_options(self): + """Initialize all options of this custom command.""" + build_ext.initialize_options(self) + self.no_arch_native = None + self.clean_build = None + + def build_extensions(self): + """Build a C/C++ extension using CMake.""" + # pylint: disable=attribute-defined-outside-init + if on_rtd: + important_msgs('skipping CMake build on ReadTheDocs') + return + self.cmake_cmd = get_cmake_command() + if self.cmake_cmd is None: + raise RuntimeError('Unable to locate the CMake command!') + distutils.log.info('using cmake command: ' + ' '.join(self.cmake_cmd)) + + self.configure_extensions() + build_ext.build_extensions(self) + + def configure_extensions(self): + """Run a CMake configuration and generation step for one extension.""" + # pylint: disable=attribute-defined-outside-init + + def _src_dir_pred(ext): + return ext.src_dir + + cmake_args = [ + '-DPython_EXECUTABLE:FILEPATH=' + get_python_executable(), + '-DBUILD_TESTING:BOOL=OFF', + '-DIN_PLACE_BUILD:BOOL=OFF', + '-DIS_PYTHON_BUILD:BOOL=ON', + '-DCMAKE_VERBOSE_MAKEFILE:BOOL=ON', + '-DVERSION_INFO="{self.distribution.get_version()}"', + ] # yapf: disable + + if self.no_arch_native: + cmake_args += ['-DUSE_NATIVE_INTRINSICS=OFF'] + + cfg = 'Debug' if self.debug else 'Release' + self.build_args = ['--config', cfg] + + if platform.system() == "Windows": + # self.build_args += ['--', '/m'] + pass + else: + cmake_args += ['-DCMAKE_BUILD_TYPE=' + cfg] + if platform.system() == "Darwin" and 'TRAVIS' in os.environ: + self.build_args += ['--'] + else: + self.build_args += [ + f'-j {self.parallel if self.parallel else multiprocessing.cpu_count()}', + '--', + ] + + cmake_args.extend(cmake_extra_options) + + env = os.environ.copy() + + # This can in principle handle the compilation of extensions outside the main CMake directory (ie. outside the + # one containing this setup.py file) + for src_dir, extensions in itertools.groupby(sorted(self.extensions, key=_src_dir_pred), key=_src_dir_pred): + self.cmake_configure_build(src_dir, extensions, cmake_args, env) + + def cmake_configure_build(self, src_dir, extensions, cmake_args, env): + """Run a CMake build command for a list of extensions.""" + args = cmake_args.copy() + for ext in extensions: + dest_path = os.path.realpath(os.path.dirname(self.get_ext_fullpath(ext.lib_filepath))) + args.append(f'-D{ext.target.upper()}_OUTPUT_DIR={dest_path}') + + build_temp = self._get_temp_dir(src_dir) + if self.clean_build: + remove_tree(build_temp) + if not os.path.exists(build_temp): + os.makedirs(build_temp) + + distutils.log.info(f' Configuring from {src_dir} '.center(80, '-')) + distutils.log.info(f'CMake command: {" ".join(self.cmake_cmd + [src_dir] + args)}') + distutils.log.info(f' cwd: {build_temp}') + try: + subprocess.check_call(self.cmake_cmd + [src_dir] + args, cwd=build_temp, env=env) + except ext_errors as err: + raise BuildFailed() from err + finally: + distutils.log.info(f' End configuring from {src_dir} '.center(80, '-')) + + def build_extension(self, ext): + """Build a single C/C++ extension using CMake.""" + distutils.log.info(f' Building {ext.pymod} '.center(80, '-')) + distutils.log.info( + 'CMake command: {" ".join(self.cmake_cmd + ["--build", ".", "--target", ext.target] + self.build_args)}' + ) + distutils.log.info(f' cwd: {self._get_temp_dir(ext.src_dir)}') + try: + subprocess.check_call( + self.cmake_cmd + ['--build', '.', '--target', ext.target] + self.build_args, + cwd=self._get_temp_dir(ext.src_dir), + ) + except ext_errors as err: + if not ext.optional: + raise BuildFailed() from err + distutils.log.info(f'Failed to compile optional extension {ext.target} (not an error)') + finally: + distutils.log.info(f' End building {ext.pymod} '.center(80, '-')) + + def copy_extensions_to_source(self): + """Copy the extensions.""" + # pylint: disable=protected-access + + build_py = self.get_finalized_command('build_py') + for ext in self.extensions: + fullname = self.get_ext_fullname(ext.name) + filename = self.get_ext_filename(fullname) + modpath = fullname.split('.') + package = '.'.join(modpath[:-1]) + package_dir = build_py.get_package_dir(package) + dest_filename = os.path.join(package_dir, os.path.basename(filename)) + src_filename = os.path.join(self.build_lib, filename) + + # Always copy, even if source is older than destination, to ensure + # that the right extensions for the current Python/platform are + # used. + if os.path.exists(src_filename) or not ext.optional: + copy_file(src_filename, dest_filename, verbose=self.verbose, dry_run=self.dry_run) + if ext._needs_stub: + self.write_stub(package_dir or os.curdir, ext, True) + + def get_outputs(self): + """ + Get the list of files generated during a build. + + Mainly defined to properly handle optional extensions. + """ + self.check_extensions_list(self.extensions) + outputs = [] + for ext in self.extensions: + if os.path.exists(self.get_ext_fullpath(ext.name)) or not ext.optional: + outputs.append(self.get_ext_fullpath(ext.name)) + return outputs + + def _get_temp_dir(self, src_dir): + return os.path.join(self.build_temp, os.path.basename(src_dir)) + + +# ============================================================================== + + +class Clean(clean): + """Custom clean command.""" + def run(self): - super().run() - egg_info_dir = os.path.join(cur_dir, 'mindquantum.egg-info') - update_permissions(egg_info_dir) + """Run the clean command.""" + # Execute the classic clean command + clean.run(self) + import glob # pylint: disable=import-outside-toplevel + + pkg_name = self.distribution.get_name().replace('-', '_') + info = glob.glob(f'{pkg_name}.egg-info') + if info: + remove_tree(info[0]) + + +# ============================================================================== + + +class GenerateRequirementFile(Command): + """A custom command to list the dependencies of the current.""" + description = 'List the dependencies of the current package' + user_options = [ + ('include-all-extras', None, 'Include all "extras_require" into the list'), + ('include-extras=', None, 'Include some of extras_requires into the list (comma separated)'), + ] + + boolean_options = ['include-all-extras'] + + def initialize_options(self): + """Initialize this command's options.""" + self.include_extras = None + self.include_all_extras = None + self.extra_pkgs = [] + + def finalize_options(self): + """Finalize this command's options.""" + if self.include_extras: + include_extras = self.include_extras.split(',') + else: + include_extras = [] + + try: + for name, pkgs in self.distribution.extras_require.items(): + if self.include_all_extras or name in include_extras: + self.extra_pkgs.extend(pkgs) + + except TypeError: # Mostly for old setuptools (< 30.x) + for name, pkgs in self.distribution.command_options['options.extras_require'].items(): + if self.include_all_extras or name in include_extras: + self.extra_pkgs.extend(pkgs) -class BuildPy(build_py): - """BuildPy.""" def run(self): - super().run() - mindquantum_dir = os.path.join(pkg_dir, 'lib', 'mindquantum') - update_permissions(mindquantum_dir) - - -with open('requirements.txt', 'r') as f_requirements: - requirements = f_requirements.readlines() -requirements = [r.strip() for r in requirements] - -setup(name='mindquantum', - version=version, - author='The MindSpore Authors', - author_email='contact@mindspore.cn', - url='https://www.mindspore.cn/', - download_url='https://gitee.com/mindspore/mindquantum/tags', - project_urls={ - 'Sources': 'https://gitee.com/mindspore/mindquantum', - 'Issue Tracker': 'https://gitee.com/mindspore/mindquantum/issues', - }, - description= - "A hybrid quantum-classic framework for quantum machine learning", - license='Apache 2.0', - packages=find_packages(), - include_package_data=True, - cmdclass={ - 'egg_info': EggInfo, - 'build_py': BuildPy, - }, - install_requires=requirements, - classifiers=['License :: OSI Approved :: Apache Software License']) -print(find_packages()) + """Execute this command.""" + with fdopen('requirements.txt', 'w') as req_file: + try: + for pkg in self.distribution.install_requires: + req_file.write(f'{pkg}\n') + except TypeError: # Mostly for old setuptools (< 30.x) + for pkg in self.distribution.command_options['options']['install_requires']: + req_file.write(f'{pkg}\n') + req_file.write('\n') + for pkg in self.extra_pkgs: + req_file.write(f'{pkg}\n') + + +# ============================================================================== + + +ext_modules = [ + CMakeExtension(pymod='mindquantum.libQuEST', target='QuEST', optional=True), + CMakeExtension(pymod='mindquantum.mqbackend'), +] + + +if __name__ == '__main__': + remove_tree(os.path.join(cur_dir, 'output')) + cmake_extra_options.extend(get_extra_cmake_options()) + setuptools.setup( + use_scm_version={'local_scheme': 'no-local-version'}, + setup_requires=['setuptools_scm'], + cmdclass={ + 'build_ext': CMakeBuildExt, + 'clean': Clean, + 'gen_reqfile': GenerateRequirementFile, + }, + ext_modules=ext_modules, + ) + + write_checksum() diff --git a/tests/cmake-ldtest/.gitignore b/tests/cmake-ldtest/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..a0f00082c8e5d428fcf98979e38e626b810213b7 --- /dev/null +++ b/tests/cmake-ldtest/.gitignore @@ -0,0 +1 @@ +CMakeLists.txt diff --git a/tests/cmake-ldtest/CMakeLists.txt.in b/tests/cmake-ldtest/CMakeLists.txt.in new file mode 100644 index 0000000000000000000000000000000000000000..eda0109582de0e1d4063f81295099454cedee4d4 --- /dev/null +++ b/tests/cmake-ldtest/CMakeLists.txt.in @@ -0,0 +1,46 @@ +# ============================================================================== +# +# Copyright 2021 +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. +# +# ============================================================================== + +# lint_cmake: -whitespace/indent + +cmake_minimum_required(VERSION 3.14) +set(CMAKE_MODULE_PATH @CMAKE_MODULE_PATH@) + +@CMAKE_EXTRA_CONTENT@ + +project(cmake-ldtest @LANGS@) + +set(CMAKE_SUPPRESS_REGENERATION 1) +set(CMAKE_VERBOSE_MAKEFILE @CMAKE_VERBOSE_MAKEFILE@) + +foreach(_flag ${LINKER_FLAGS}) + add_link_options("LINKER:${_flag}") +endforeach() + +set(CMAKE_BUILD_SKIP_RPATH @CMAKE_BUILD_SKIP_RPATH@) +set(CMAKE_INSTALL_RPATH_USE_LINK_PATH @CMAKE_INSTALL_RPATH_USE_LINK_PATH@) +set(CMAKE_BUILD_WITH_INSTALL_RPATH @CMAKE_BUILD_WITH_INSTALL_RPATH@) + +add_library(shared_lib_@LANG@ SHARED ${CMAKE_CURRENT_LIST_DIR}/shared_lib.cpp) +set_target_properties(shared_lib_@LANG@ PROPERTIES INSTALL_RPATH "$ORIGIN/.") +set_source_files_properties(${CMAKE_CURRENT_LIST_DIR}/shared_lib.cpp PROPERTIES LANGUAGE @LANG@) + +# ~~~ +# add_executable(shared_test ${CMAKE_CURRENT_LIST_DIR}/shared_test.cpp) +# target_link_libraries(shared_test PUBLIC shared_lib@LANG@) +# ~~~ diff --git a/tests/cmake-ldtest/shared_lib.cpp b/tests/cmake-ldtest/shared_lib.cpp new file mode 100644 index 0000000000000000000000000000000000000000..31c806614fd3351cf6f6a37a758e5a769f8f793f --- /dev/null +++ b/tests/cmake-ldtest/shared_lib.cpp @@ -0,0 +1,19 @@ +// Copyright 2021 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "shared_lib.hpp" + +int foo() { + return 42; // NOLINT +} diff --git a/tests/cmake-ldtest/shared_lib.hpp b/tests/cmake-ldtest/shared_lib.hpp new file mode 100644 index 0000000000000000000000000000000000000000..e5edc79ca011f82d7dba3f57d77f361fc07fbaf1 --- /dev/null +++ b/tests/cmake-ldtest/shared_lib.hpp @@ -0,0 +1,20 @@ +// Copyright 2021 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef SHARED_LIB_HPP +#define SHARED_LIB_HPP + +int foo(); + +#endif /* SHARED_LIB_HPP */ diff --git a/tests/cmake-ldtest/shared_test.cpp b/tests/cmake-ldtest/shared_test.cpp new file mode 100644 index 0000000000000000000000000000000000000000..ed9f02a1f604159554e93b1073c0a5ed1e0f885e --- /dev/null +++ b/tests/cmake-ldtest/shared_test.cpp @@ -0,0 +1,22 @@ +// Copyright 2021 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include "shared_lib.hpp" + +int main() { + std::cout << foo() << std::endl; + return 0; +} diff --git a/tests/st/__init__.py b/tests/st/__init__.py index 6228b7132697d24157a4052193061e9913f031c4..ed94b4aa0914af596c080cb797ec71c7acaf863d 100644 --- a/tests/st/__init__.py +++ b/tests/st/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/st/runtest.sh b/tests/st/runtest.sh index 916f7a2606b57fea6e6bba50e794e8cba7ef1dd5..81a8a8d15db23db7bb21da9b8cf6722fa54a2168 100644 --- a/tests/st/runtest.sh +++ b/tests/st/runtest.sh @@ -23,8 +23,8 @@ ST_PATH="$PROJECT_DIR/tests/st" run_test() { echo "Start to run test." cd "$PROJECT_DIR" || exit - pytest "$ST_PATH" + python -m pytest "$ST_PATH" echo "Test all use cases success." } -run_test \ No newline at end of file +run_test diff --git a/tests/st/test_ansatz/test_hardware_efficient.py b/tests/st/test_algorithm/test_nisq/test_chem/test_hardware_efficient.py similarity index 65% rename from tests/st/test_ansatz/test_hardware_efficient.py rename to tests/st/test_algorithm/test_nisq/test_chem/test_hardware_efficient.py index 0939c52c9e32dcf596b4064f23e3c11a701e9d5c..ba6b40d6e6d2d2ec8977727e3997ea49f72ec11d 100644 --- a/tests/st/test_ansatz/test_hardware_efficient.py +++ b/tests/st/test_algorithm/test_nisq/test_chem/test_hardware_efficient.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,15 +16,18 @@ """Test hardware efficient ansatz""" import os + os.environ['OMP_NUM_THREADS'] = '8' import numpy as np import mindspore as ms -from mindquantum.ansatz import HardwareEfficientAnsatz -from mindquantum.nn import MindQuantumAnsatzOnlyLayer as MAL -from mindquantum.gate import Hamiltonian, RX, RY, X -from mindquantum.ops import QubitOperator +from mindquantum.algorithm import HardwareEfficientAnsatz +from mindquantum.framework import MQAnsatzOnlyLayer +from mindquantum.core.gates import RX, RY, X +from mindquantum.core.operators import Hamiltonian +from mindquantum.core.operators import QubitOperator +from mindquantum.simulator import Simulator -ms.context.set_context(mode=ms.context.GRAPH_MODE, device_target="CPU") +ms.context.set_context(mode=ms.context.PYNATIVE_MODE, device_target="CPU") def test_hardware_efficient(): @@ -31,10 +35,12 @@ def test_hardware_efficient(): n_qubits = 3 hea = HardwareEfficientAnsatz(n_qubits, [RX, RY, RX], X, 'all', depth) ham = QubitOperator('Z0 Z1 Z2') + sim = Simulator('projectq', hea.circuit.n_qubits) + f_g_ops = sim.get_expectation_with_grad(Hamiltonian(ham), hea.circuit) ms.set_seed(42) - net = MAL(hea.circuit.para_name, hea.circuit, Hamiltonian(ham)) + net = MQAnsatzOnlyLayer(f_g_ops) opti = ms.nn.Adagrad(net.trainable_params(), learning_rate=4e-1) train_net = ms.nn.TrainOneStepCell(net, opti) for i in range(3): - res = train_net().asnumpy()[0, 0] + res = train_net().asnumpy()[0] assert np.allclose(round(res, 4), -0.7588) diff --git a/tests/st/test_ansatz/test_qubit_ucc.py b/tests/st/test_algorithm/test_nisq/test_chem/test_qubit_ucc.py similarity index 73% rename from tests/st/test_ansatz/test_qubit_ucc.py rename to tests/st/test_algorithm/test_nisq/test_chem/test_qubit_ucc.py index 71dad08df0d27e6f623d4aee1d0e85da8ed3ab01..31dc89dc38af3cd537ccc2a8da05e50a4628ba9c 100644 --- a/tests/st/test_ansatz/test_qubit_ucc.py +++ b/tests/st/test_algorithm/test_nisq/test_chem/test_qubit_ucc.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,16 +16,19 @@ """Test unitary coupled-cluster ansatz""" import os + os.environ['OMP_NUM_THREADS'] = '8' import numpy as np import mindspore as ms -from mindquantum.ansatz import QubitUCCAnsatz -from mindquantum.circuit import Circuit -from mindquantum.nn import MindQuantumAnsatzOnlyLayer as MAL -from mindquantum.gate import Hamiltonian, X -from mindquantum.ops import QubitOperator +from mindquantum.algorithm.nisq.chem import QubitUCCAnsatz +from mindquantum.core.circuit import Circuit +from mindquantum.framework import MQAnsatzOnlyLayer +from mindquantum.core.operators import Hamiltonian +from mindquantum.core.operators import QubitOperator +from mindquantum.core.gates import X +from mindquantum.simulator import Simulator -ms.context.set_context(mode=ms.context.GRAPH_MODE, device_target="CPU") +ms.context.set_context(mode=ms.context.PYNATIVE_MODE, device_target="CPU") def test_quccsd(): @@ -50,17 +54,20 @@ def test_quccsd(): vir_orb = [1] generalized = False trotter_step = 4 - ucc = QubitUCCAnsatz(n_qubits, n_electrons, - occ_orb, vir_orb, - generalized, trotter_step) + ucc = QubitUCCAnsatz(n_qubits, n_electrons, occ_orb, vir_orb, generalized, + trotter_step) total_circuit = Circuit() for i in range(n_electrons): total_circuit += X.on(i) total_circuit += ucc.circuit - net = MAL(total_circuit.para_name, total_circuit, Hamiltonian(ham.real)) + + sim = Simulator('projectq', total_circuit.n_qubits) + f_g_ops = sim.get_expectation_with_grad(Hamiltonian(ham.real), + total_circuit) + net = MQAnsatzOnlyLayer(f_g_ops) opti = ms.nn.Adagrad(net.trainable_params(), learning_rate=4e-2) train_net = ms.nn.TrainOneStepCell(net, opti) for i in range(100): - res = train_net().asnumpy()[0, 0] + res = train_net().asnumpy()[0] print(res) assert np.allclose(round(res, 4), -0.9486) diff --git a/tests/st/test_hiqfermion/test_quccsd.py b/tests/st/test_algorithm/test_nisq/test_chem/test_quccsd.py similarity index 49% rename from tests/st/test_hiqfermion/test_quccsd.py rename to tests/st/test_algorithm/test_nisq/test_chem/test_quccsd.py index f53477d347ee67f02691b42c7356c49e9edb9dad..625b345607288d7dbf35a8ecc6416593795ad1f8 100644 --- a/tests/st/test_hiqfermion/test_quccsd.py +++ b/tests/st/test_algorithm/test_nisq/test_chem/test_quccsd.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,51 +15,42 @@ # ============================================================================ """Tests for the qUCCSD generator and related functions""" -from mindquantum.circuit import TimeEvolution -from mindquantum.utils import count_qubits -from mindquantum.hiqfermion.ucc import quccsd_generator +from mindquantum.core.operators import TimeEvolution +from mindquantum.core.operators.utils import count_qubits +from mindquantum.algorithm.nisq.chem import quccsd_generator def test_quccsd(): + """ + Description: Test quccsd + Expectation: + """ h2_quccsd = quccsd_generator(4, 2) h2_quccsd_terms = set(list(h2_quccsd.terms)) - h2_quccsd_terms_check = set([((3, 1), (0, 0)), - ((3, 1), (1, 0)), - ((1, 1), (2, 0)), - ((1, 1), (3, 0)), - ((2, 1), (0, 0)), - ((2, 1), (1, 0)), - ((0, 1), (2, 0)), - ((0, 1), (3, 0)), - ((3, 1), (2, 1), (1, 0), (0, 0)), - ((1, 1), (0, 1), (3, 0), (2, 0))]) + h2_quccsd_terms_check = set([((3, 1), (0, 0)), ((3, 1), (1, 0)), ((1, 1), (2, 0)), ((1, 1), (3, 0)), + ((2, 1), (0, 0)), ((2, 1), (1, 0)), ((0, 1), (2, 0)), ((0, 1), (3, 0)), + ((3, 1), (2, 1), (1, 0), (0, 0)), ((1, 1), (0, 1), (3, 0), (2, 0))]) assert h2_quccsd_terms == h2_quccsd_terms_check lih_quccsd = quccsd_generator(12, 4) - lih_quccsd_circuit = TimeEvolution( - lih_quccsd.to_qubit_operator().imag, 1).circuit - n_params_lih = len(lih_quccsd_circuit.para_name) + lih_quccsd_circuit = TimeEvolution(lih_quccsd.to_qubit_operator().imag, 1).circuit + n_params_lih = len(lih_quccsd_circuit.params_name) assert n_params_lih == 200 - lih_quccgsd_cas = quccsd_generator(12, 4, - occ_orb=[1], vir_orb=[2, 3], - generalized=True) + lih_quccgsd_cas = quccsd_generator(12, 4, occ_orb=[1], vir_orb=[2, 3], generalized=True) assert count_qubits(lih_quccgsd_cas) == 8 - lih_quccgsd_cas_circuit = TimeEvolution( - lih_quccgsd_cas.to_qubit_operator().imag, 1).circuit - n_params_lih = len(lih_quccgsd_cas_circuit.para_name) + lih_quccgsd_cas_circuit = TimeEvolution(lih_quccgsd_cas.to_qubit_operator().imag, 1).circuit + n_params_lih = len(lih_quccgsd_cas_circuit.params_name) assert n_params_lih == 135 # qUCCSD with fully occupied orbitals should lead to zero parameters he2_quccsd = quccsd_generator(4, 4) - he2_quccsd_circuit = TimeEvolution( - he2_quccsd.to_qubit_operator().imag, 1).circuit - n_params_he2 = len(he2_quccsd_circuit.para_name) + he2_quccsd_circuit = TimeEvolution(he2_quccsd.to_qubit_operator().imag, 1).circuit + n_params_he2 = len(he2_quccsd_circuit.params_name) assert n_params_he2 == 0 # qUCCGSD will not be affected by the occupancy numbers he2_quccsd = quccsd_generator(4, 4, generalized=True) - he2_quccsd_circuit = TimeEvolution( - he2_quccsd.to_qubit_operator().imag, 1).circuit - n_params_he2 = len(he2_quccsd_circuit.para_name) + he2_quccsd_circuit = TimeEvolution(he2_quccsd.to_qubit_operator().imag, 1).circuit + n_params_he2 = len(he2_quccsd_circuit.params_name) assert n_params_he2 == 27 diff --git a/tests/st/test_hiqfermion/test_transforms.py b/tests/st/test_algorithm/test_nisq/test_chem/test_transforms.py similarity index 80% rename from tests/st/test_hiqfermion/test_transforms.py rename to tests/st/test_algorithm/test_nisq/test_chem/test_transforms.py index 1e9e9fbbad46103c1289f0791316cf5b229bc428..e2cfb89023289146a53500e60a227ca634e574a9 100644 --- a/tests/st/test_hiqfermion/test_transforms.py +++ b/tests/st/test_algorithm/test_nisq/test_chem/test_transforms.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (c) 2020 Huawei Technologies Co.,ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,13 +16,14 @@ Test the transforms in the hiqfermion module. """ -from mindquantum.hiqfermion.transforms.transform import Transform -from mindquantum.ops import FermionOperator +from mindquantum.algorithm.nisq.chem import Transform +from mindquantum.core.operators import FermionOperator + def test_transform(): - """Test different transform. - Note we need add the pyscfcalculator to build the Hamiltonian for the - test of bravyi-kitaev superfast transform. + """ + Description: Test transform + Expectation: """ op1 = FermionOperator('1^') op_transform = Transform(op1) @@ -36,4 +38,3 @@ def test_transform(): op1_ternary_tree = op_transform.ternary_tree() assert str(op1_ternary_tree) == '0.5 [X0 Z1] +\n-0.5j [Y0 X2] ' - #TODO bravyi_kitaev_superfast() diff --git a/tests/st/test_ansatz/test_ucc.py b/tests/st/test_algorithm/test_nisq/test_chem/test_ucc.py similarity index 73% rename from tests/st/test_ansatz/test_ucc.py rename to tests/st/test_algorithm/test_nisq/test_chem/test_ucc.py index 59821713767a6044c38bf862f624030a32990668..7e85df47b135f663b61b91b99734cac8da139943 100644 --- a/tests/st/test_ansatz/test_ucc.py +++ b/tests/st/test_algorithm/test_nisq/test_chem/test_ucc.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,16 +16,19 @@ """Test unitary coupled-cluster ansatz""" import os + os.environ['OMP_NUM_THREADS'] = '8' import numpy as np import mindspore as ms -from mindquantum.ansatz import UCCAnsatz -from mindquantum.circuit import Circuit -from mindquantum.nn import MindQuantumAnsatzOnlyLayer as MAL -from mindquantum.gate import Hamiltonian, X -from mindquantum.ops import QubitOperator +from mindquantum.algorithm.nisq.chem import UCCAnsatz +from mindquantum.core.circuit import Circuit +from mindquantum.framework import MQAnsatzOnlyLayer +from mindquantum.core.gates import X +from mindquantum.core.operators import Hamiltonian +from mindquantum.core.operators import QubitOperator +from mindquantum.simulator import Simulator -ms.context.set_context(mode=ms.context.GRAPH_MODE, device_target="CPU") +ms.context.set_context(mode=ms.context.PYNATIVE_MODE, device_target="CPU") def test_uccsd(): @@ -50,16 +54,18 @@ def test_uccsd(): vir_orb = [1] generalized = True trotter_step = 2 - ucc = UCCAnsatz(n_qubits, n_electrons, - occ_orb, vir_orb, - generalized, trotter_step) + ucc = UCCAnsatz(n_qubits, n_electrons, occ_orb, vir_orb, generalized, + trotter_step) total_circuit = Circuit() for i in range(n_electrons): total_circuit += X.on(i) total_circuit += ucc.circuit - net = MAL(total_circuit.para_name, total_circuit, Hamiltonian(ham.real)) + sim = Simulator('projectq', total_circuit.n_qubits) + f_g_ops = sim.get_expectation_with_grad(Hamiltonian(ham.real), + total_circuit) + net = MQAnsatzOnlyLayer(f_g_ops) opti = ms.nn.Adagrad(net.trainable_params(), learning_rate=4e-2) train_net = ms.nn.TrainOneStepCell(net, opti) for i in range(50): - res = train_net().asnumpy()[0, 0] + res = train_net().asnumpy()[0] assert np.allclose(round(res, 4), -0.9486) diff --git a/tests/st/test_algorithm/test_nisq/test_chem/test_uccsd.py b/tests/st/test_algorithm/test_nisq/test_chem/test_uccsd.py new file mode 100644 index 0000000000000000000000000000000000000000..247ccab558fcb7f9079b2e70b7b47a4f7970b4e3 --- /dev/null +++ b/tests/st/test_algorithm/test_nisq/test_chem/test_uccsd.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""Test uccsd.""" +import numpy as np +from mindquantum.algorithm.nisq.chem import generate_uccsd +from mindquantum.core import gates as G + +def test_generate_uccsd(): + """ + Description: Test generate_uccsd + Expectation: + """ + circ, init_amp, params_name, ham, n_q, n_e = generate_uccsd('./tests/st/LiH.hdf5') + circ = circ.remove_barrier() + assert len(circ) == 4416 + assert circ[2000] == G.X.on(9, 8) + assert np.allclose(init_amp[-5], 0.001687182323430231) + assert len(params_name) == 20 + assert len(ham.terms) == 631 + assert n_q == 12 + assert n_e == 4 diff --git a/tests/st/test_hiqfermion/test_uccsd0.py b/tests/st/test_algorithm/test_nisq/test_chem/test_uccsd0.py similarity index 74% rename from tests/st/test_hiqfermion/test_uccsd0.py rename to tests/st/test_algorithm/test_nisq/test_chem/test_uccsd0.py index 0b1f72b9b684679cb822265195dc6060e44b5ab2..fc2e37db1eff07163e8e3c967aeff9bac9a9d886 100644 --- a/tests/st/test_hiqfermion/test_uccsd0.py +++ b/tests/st/test_algorithm/test_nisq/test_chem/test_uccsd0.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,14 +15,18 @@ # ============================================================================ """Tests for the UCCSD0 generator and related functions""" -from mindquantum.circuit import TimeEvolution -from mindquantum.hiqfermion.transforms import Transform -from mindquantum.utils import count_qubits -from mindquantum.hiqfermion.ucc import uccsd0_singlet_generator -from mindquantum.hiqfermion.ucc.uccsd0 import spin_adapted_t1, spin_adapted_t2 +from mindquantum.core.operators import TimeEvolution +from mindquantum.algorithm.nisq.chem import Transform +from mindquantum.core.operators.utils import count_qubits +from mindquantum.algorithm.nisq.chem import uccsd0_singlet_generator +from mindquantum.algorithm.nisq.chem.uccsd0 import spin_adapted_t1, spin_adapted_t2 def test_spin_adapted_t1(): + """ + Description: Test spin adapted t1 + Expectation: + """ t1_20 = spin_adapted_t1(2, 0)[0] assert str(t1_20) == '1.0 [4^ 0] +\n1.0 [5^ 1] ' t1_00 = spin_adapted_t1(0, 0)[0] @@ -29,7 +34,10 @@ def test_spin_adapted_t1(): def test_spin_adapted_t2(): - """Use a 8-qubit 4-electron system as an example.""" + """ + Description: Test spin adapted t2 + Expectation: + """ t2_3210_list = spin_adapted_t2([3, 2], [1, 0]) assert len(t2_3210_list) == 2 term1 = set(list(t2_3210_list[0].terms)) @@ -51,12 +59,14 @@ def test_spin_adapted_t2(): def test_uccsd0(): + """ + Description: Test uccsd0 + Expectation: + """ h2_uccsd0 = uccsd0_singlet_generator(4, 2) h2_uccsd0_terms = set(list(h2_uccsd0.terms)) - h2_uccsd0_terms_check = set([((2, 1), (0, 0)), - ((3, 1), (1, 0)), - ((0, 1), (2, 0)), - ((1, 1), (3, 0)), + h2_uccsd0_terms_check = set([((2, 1), (0, 0)), ((3, 1), (1, 0)), + ((0, 1), (2, 0)), ((1, 1), (3, 0)), ((3, 1), (2, 1), (1, 0), (0, 0)), ((1, 1), (0, 1), (3, 0), (2, 0))]) assert h2_uccsd0_terms == h2_uccsd0_terms_check @@ -64,31 +74,33 @@ def test_uccsd0(): lih_uccsd0 = uccsd0_singlet_generator(12, 4) lih_uccsd0_circuit = TimeEvolution( Transform(lih_uccsd0).jordan_wigner().imag, 1).circuit - n_params_lih = len(lih_uccsd0_circuit.para_name) + n_params_lih = len(lih_uccsd0_circuit.params_name) assert n_params_lih == 44 # cas means complete active space - lih_uccgsd0_cas = uccsd0_singlet_generator(12, 4, - occ_orb=[1], vir_orb=[2, 3], + lih_uccgsd0_cas = uccsd0_singlet_generator(12, + 4, + occ_orb=[1], + vir_orb=[2, 3], generalized=True) # The max index of affected qubits in the ansatz is 7 = 8-1. # Does not mean the number of qubits in Hamiltonian is reduced to 8. assert count_qubits(lih_uccgsd0_cas) == 8 lih_uccgsd0_cas_circuit = TimeEvolution( Transform(lih_uccgsd0_cas).jordan_wigner().imag, 1).circuit - n_params_lih_cas = len(lih_uccgsd0_cas_circuit.para_name) + n_params_lih_cas = len(lih_uccgsd0_cas_circuit.params_name) assert n_params_lih_cas == 24 # UCCSD with fully occupied orbitals should lead to 0 parameters he2_uccsd = uccsd0_singlet_generator(4, 4) he2_uccsd_circuit = TimeEvolution( Transform(he2_uccsd).jordan_wigner().imag, 1).circuit - n_params_he2 = len(he2_uccsd_circuit.para_name) + n_params_he2 = len(he2_uccsd_circuit.params_name) assert n_params_he2 == 0 # UCCGSD will not be affected by the occupancy number he2_uccgsd = uccsd0_singlet_generator(4, 4, generalized=True) he2_uccgsd_circuit = TimeEvolution( Transform(he2_uccgsd).jordan_wigner().imag, 1).circuit - n_params_he2_gsd = len(he2_uccgsd_circuit.para_name) + n_params_he2_gsd = len(he2_uccgsd_circuit.params_name) assert n_params_he2_gsd == 5 diff --git a/tests/st/test_ansatz/test_max_2_sat.py b/tests/st/test_algorithm/test_nisq/test_qaoa/test_max_2_sat.py similarity index 67% rename from tests/st/test_ansatz/test_max_2_sat.py rename to tests/st/test_algorithm/test_nisq/test_qaoa/test_max_2_sat.py index 1f773ca444554de99fc5f86c9641b851a72259b0..4c21d24f0cf2ad97f42dca68ead06b8ab388bccc 100644 --- a/tests/st/test_ansatz/test_max_2_sat.py +++ b/tests/st/test_algorithm/test_nisq/test_qaoa/test_max_2_sat.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,26 +16,30 @@ """Test max_2_sat""" import os + os.environ['OMP_NUM_THREADS'] = '8' import numpy as np import mindspore as ms -from mindquantum.ansatz import Max2SATAnsatz -from mindquantum.nn import MindQuantumAnsatzOnlyLayer as MAL -from mindquantum.gate import Hamiltonian +from mindquantum.algorithm.nisq.qaoa import Max2SATAnsatz +from mindquantum.framework import MQAnsatzOnlyLayer +from mindquantum.core.operators import Hamiltonian +from mindquantum.simulator import Simulator -ms.context.set_context(mode=ms.context.GRAPH_MODE, device_target="CPU") +ms.context.set_context(mode=ms.context.PYNATIVE_MODE, device_target="CPU") def test_max_2_sat(): clauses = [(1, 2), (1, -2), (-1, 2), (-1, -2), (1, 3)] depth = 3 max2sat = Max2SATAnsatz(clauses, depth) + sim = Simulator('projectq', max2sat.circuit.n_qubits) + ham = max2sat.hamiltonian + f_g_ops = sim.get_expectation_with_grad(Hamiltonian(ham), max2sat.circuit) ms.set_seed(42) - net = MAL(max2sat.circuit.para_name, max2sat.circuit, - Hamiltonian(max2sat.hamiltonian)) + net = MQAnsatzOnlyLayer(f_g_ops) opt = ms.nn.Adagrad(net.trainable_params(), learning_rate=4e-1) train_net = ms.nn.TrainOneStepCell(net, opt) ret = 0 for i in range(100): - ret = train_net().asnumpy()[0, 0] + ret = train_net().asnumpy()[0] assert np.allclose(round(ret, 3), 1) diff --git a/tests/st/test_ansatz/test_max_cut.py b/tests/st/test_algorithm/test_nisq/test_qaoa/test_max_cut.py similarity index 67% rename from tests/st/test_ansatz/test_max_cut.py rename to tests/st/test_algorithm/test_nisq/test_qaoa/test_max_cut.py index bb86b52f221bd6850915d73c9246ef350e0de17d..e528672d78a1d35f647003feae67aa61d8f7c15e 100644 --- a/tests/st/test_ansatz/test_max_cut.py +++ b/tests/st/test_algorithm/test_nisq/test_qaoa/test_max_cut.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,25 +16,29 @@ """Test max_cut""" import os + os.environ['OMP_NUM_THREADS'] = '8' import numpy as np import mindspore as ms -from mindquantum.ansatz import MaxCutAnsatz -from mindquantum.nn import MindQuantumAnsatzOnlyLayer as MAL -from mindquantum.gate import Hamiltonian +from mindquantum.algorithm.nisq.qaoa import MaxCutAnsatz +from mindquantum.framework import MQAnsatzOnlyLayer +from mindquantum.core.operators import Hamiltonian +from mindquantum.simulator import Simulator -ms.context.set_context(mode=ms.context.GRAPH_MODE, device_target="CPU") +ms.context.set_context(mode=ms.context.PYNATIVE_MODE, device_target="CPU") def test_max_cut(): graph = [(0, 1), (1, 2), (2, 3), (3, 4), (1, 4)] depth = 3 maxcut = MaxCutAnsatz(graph, depth) + sim = Simulator('projectq', maxcut.circuit.n_qubits) + ham = maxcut.hamiltonian + f_g_ops = sim.get_expectation_with_grad(Hamiltonian(-ham), maxcut.circuit) ms.set_seed(42) - net = MAL(maxcut.circuit.para_name, maxcut.circuit, - Hamiltonian(-maxcut.hamiltonian)) + net = MQAnsatzOnlyLayer(f_g_ops) opti = ms.nn.Adagrad(net.trainable_params(), learning_rate=4e-1) train_net = ms.nn.TrainOneStepCell(net, opti) for i in range(50): - cut = -train_net().asnumpy()[0, 0] + cut = -train_net().asnumpy()[0] assert np.allclose(round(cut, 3), 4.831) diff --git a/tests/st/test_nn/test_mindquantum.py b/tests/st/test_applications/test_chem_net.py similarity index 32% rename from tests/st/test_nn/test_mindquantum.py rename to tests/st/test_applications/test_chem_net.py index 9cae5b8a7d129c79dabaac26275a017a014ec5bc..378b601005f99b2e921c30ecb2a99612391a6421 100644 --- a/tests/st/test_nn/test_mindquantum.py +++ b/tests/st/test_applications/test_chem_net.py @@ -1,47 +1,19 @@ -# Copyright 2021 Huawei Technologies Co., Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ============================================================================ -"""Test mindquantum.""" - import os + os.environ['OMP_NUM_THREADS'] = '8' -from mindquantum.ops import QubitOperator -import numpy as np import mindspore as ms -import mindquantum.gate as G -from mindquantum.nn import MindQuantumLayer, MindQuantumAnsatzOnlyLayer -from mindquantum.circuit import generate_uccsd -from mindquantum import Circuit, Hamiltonian - - -def test_mindquantumlayer_forward(): - """Test mindquantum layer.""" - encoder = Circuit() - ansatz = Circuit() - encoder += G.RX('e1').on(0) - ansatz += G.RY('a').on(0) - ham = Hamiltonian(QubitOperator('Z0')) - ms.set_seed(42) - ms.context.set_context(mode=ms.context.GRAPH_MODE, device_target="CPU") - net = MindQuantumLayer(['e1'], ['a'], encoder + ansatz, ham) - encoder_data = ms.Tensor(np.array([[0.5]]).astype(np.float32)) - res = net(encoder_data) - assert round(float(res.asnumpy()[0, 0]), 3) == 0.878 +from mindquantum.algorithm.nisq.chem import generate_uccsd +from mindquantum import Circuit, Simulator, Hamiltonian +from mindquantum.core import gates as G +from mindquantum.framework import MQAnsatzOnlyLayer -def test_vqe_convergence(): - ms.context.set_context(mode=ms.context.GRAPH_MODE, device_target="CPU") +def test_vqe_net(): + """ + Description: Test vqe + Expectation: + """ + ms.context.set_context(mode=ms.context.PYNATIVE_MODE, device_target="CPU") ansatz_circuit, \ init_amplitudes, \ ansatz_parameter_names, \ @@ -50,11 +22,10 @@ def test_vqe_convergence(): './tests/st/H4.hdf5', th=-1) hf_circuit = Circuit([G.X.on(i) for i in range(n_electrons)]) vqe_circuit = hf_circuit + ansatz_circuit - molecule_pqcnet = MindQuantumAnsatzOnlyLayer( - ansatz_parameter_names, vqe_circuit, - Hamiltonian(hamiltonian_qubitop.real)) - optimizer = ms.nn.Adagrad(molecule_pqcnet.trainable_params(), - learning_rate=4e-2) + sim = Simulator('projectq', vqe_circuit.n_qubits) + f_g_ops = sim.get_expectation_with_grad(Hamiltonian(hamiltonian_qubitop.real), vqe_circuit) + molecule_pqcnet = MQAnsatzOnlyLayer(f_g_ops) + optimizer = ms.nn.Adagrad(molecule_pqcnet.trainable_params(), learning_rate=4e-2) train_pqcnet = ms.nn.TrainOneStepCell(molecule_pqcnet, optimizer) eps = 1e-8 energy_diff = 1. diff --git a/tests/st/test_circuit/test_circuit.py b/tests/st/test_core/test_circuit/test_circuit.py similarity index 37% rename from tests/st/test_circuit/test_circuit.py rename to tests/st/test_core/test_circuit/test_circuit.py index bbfe655b27e4475e1019556708ec848fb3358318..b9dfb514b8aef8520c11aa5def11b0f0f68d677f 100644 --- a/tests/st/test_circuit/test_circuit.py +++ b/tests/st/test_core/test_circuit/test_circuit.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,25 +14,16 @@ # limitations under the License. # ============================================================================ """Test circuit.""" - -from mindquantum.ops import QubitOperator import numpy as np -from mindquantum import Circuit -import mindquantum.gate as G -from mindquantum.circuit import pauli_word_to_circuits -from mindquantum.circuit import decompose_single_term_time_evolution -from mindquantum.circuit import UN, SwapParts, generate_uccsd -from mindquantum.circuit import TimeEvolution - - -def test_time_evolution(): - h = QubitOperator('Z0 Z1', 'p') - circ = TimeEvolution(h).circuit - circ_exp = Circuit([G.X.on(1, 0), G.RZ({'p': 2}).on(1), G.X.on(1, 0)]) - assert circ == circ_exp +from mindquantum import Circuit, Simulator, ParameterResolver +from mindquantum import gates as G -def test_circuit(): +def test_circuit_qubits_grad(): + """ + Description: Test circuit basic operations + Expectation: + """ circuit1 = Circuit() circuit1 += G.RX('a').on(0) circuit1 *= 2 @@ -46,26 +38,13 @@ def test_circuit(): circuit3.requires_grad() assert len(circuit3[1].coeff.requires_grad_parameters) == 1 assert len(circuit3.parameter_resolver()) == 1 - assert circuit3.mindspore_data() == { - 'gate_names': ['npg', 'RX', 'RX', 'npg'], - 'gate_matrix': [[[['0.7071067811865475', '0.7071067811865475'], - ['0.7071067811865475', '-0.7071067811865475']], - [['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.0', '0.0']], - [['0.0', '0.0'], ['0.0', '0.0']]], - [[['0.0', '1.0'], ['1.0', '0.0']], - [['0.0', '0.0'], ['0.0', '0.0']]]], - 'gate_obj_qubits': [[0], [0], [0], [0]], - 'gate_ctrl_qubits': [[], [], [], [1]], - 'gate_params_names': [[], ['a'], ['a'], []], - 'gate_coeff': [[], [1.0], [1.0], []], - 'gate_requires_grad': [[], [True], [True], []] - } def test_circuit_apply(): + """ + Description: Test apply value to parameterized circuit + Expectation: + """ circuit = Circuit() circuit += G.RX('a').on(0, 1) circuit += G.H.on(0) @@ -74,34 +53,16 @@ def test_circuit_apply(): assert circuit == circuit_exp -def test_pauli_word_to_circuits(): - circ = pauli_word_to_circuits(QubitOperator('Z0 Y1')) - assert circ == Circuit([G.Z.on(0), G.Y.on(1)]) - - -def test_un(): - circ = UN(G.X, [3, 4, 5], [0, 1, 2]) - assert circ[-1] == G.X.on(5, 2) - - -def test_swappart(): - circ = SwapParts([1, 2, 3], [4, 5, 6], 0) - assert circ[-1] == G.SWAP([3, 6], 0) - - -def test_decompose_single_term_time_evolution(): - circ = decompose_single_term_time_evolution(QubitOperator('Z0 Z1'), - {'a': 1}) - assert circ == Circuit([G.X.on(1, 0), G.RZ({'a': 2}).on(1), G.X.on(1, 0)]) - - -def test_generate_uccsd(): - circ, init_amp, para_name, ham, n_q, n_e = generate_uccsd( - './tests/st/LiH.hdf5') - assert len(circ) == 4416 - assert circ[2000] == G.X.on(9, 8) - assert np.allclose(init_amp[-5], 0.001687182323430231) - assert len(para_name) == 20 - assert len(ham.terms) == 631 - assert n_q == 12 - assert n_e == 4 +def test_evolution_state(): + """ + test + Description: + Expectation: + """ + a, b = 0.3, 0.5 + circ = Circuit([G.RX('a').on(0), G.RX('b').on(1)]) + s = Simulator('projectq', circ.n_qubits) + s.apply_circuit(circ, ParameterResolver({'a': a, 'b': b})) + state = s.get_qs() + state_exp = [0.9580325796404553, -0.14479246283091116j, -0.2446258794777393j, -0.036971585637570345] + assert np.allclose(state, state_exp) diff --git a/tests/st/test_circuit/test_high_level_ops.py b/tests/st/test_core/test_circuit/test_circuit_utils.py similarity index 66% rename from tests/st/test_circuit/test_high_level_ops.py rename to tests/st/test_core/test_circuit/test_circuit_utils.py index 43a99d3b80423b6dadc8b99dd067b5558ce399c5..cbb102b46f7e06663d487f253facbc8b74d1dca9 100644 --- a/tests/st/test_circuit/test_high_level_ops.py +++ b/tests/st/test_core/test_circuit/test_circuit_utils.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,17 +13,38 @@ # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================ -"""Test high level ops.""" - +"""Test Circuit utils.""" import numpy as np +from mindquantum import pauli_word_to_circuits +from mindquantum import decompose_single_term_time_evolution +from mindquantum import QubitOperator +import mindquantum.core.gates as G from mindquantum import Circuit, X, H, RX -from mindquantum.circuit import controlled as C -from mindquantum.circuit import dagger as D -from mindquantum.circuit import apply as A -from mindquantum.circuit import AP -from mindquantum.circuit import CPN -from mindquantum.circuit import StateEvolution -from mindquantum.circuit import qft +from mindquantum.core.circuit.utils import controlled as C +from mindquantum.core.circuit.utils import dagger as D +from mindquantum.core.circuit.utils import apply as A +from mindquantum.core.circuit.utils import AP +from mindquantum.core.circuit.utils import CPN +from mindquantum.algorithm.library import qft + + +def test_pauli_word_to_circuits(): + """ + Description: test pauli word to circuits + Expectation: + """ + circ = pauli_word_to_circuits(QubitOperator('Z0 Y1')) + assert circ == Circuit([G.Z.on(0), G.Y.on(1)]) + + +def test_decompose_single_term_time_evolution(): + """ + Description: Test decompose_single_term_time_evolution + Expectation: + """ + circ = decompose_single_term_time_evolution(QubitOperator('Z0 Z1'), {'a': 1}) + circ = circ.remove_barrier() + assert circ == Circuit([G.X.on(1, 0), G.RZ({'a': 2}).on(1), G.X.on(1, 0)]) def test_apply(): @@ -94,50 +116,13 @@ def test_state_evol(): circuit_exp += X.on(0, 4) assert circuit_exp == circuit pr = {'a_0': 1, 'a_1': 2, 'a_2': 3, 'a_3': 4} - fs1 = StateEvolution(circuit).final_state(pr) - fs2 = StateEvolution(circuit.apply_value(pr)).final_state() + fs1 = circuit.get_qs(pr=pr) + fs2 = circuit.apply_value(pr).get_qs() assert np.allclose(fs1, fs2) - np.random.seed(42) - sampling = StateEvolution(circuit.apply_value(pr)).sampling(shots=100) - sampling_exp = { - '00000': 0, - '00001': 0, - '00010': 0, - '00011': 0, - '00100': 0, - '00101': 0, - '00110': 0, - '00111': 0, - '01000': 0, - '01001': 0, - '01010': 0, - '01011': 0, - '01100': 0, - '01101': 0, - '01110': 0, - '01111': 0, - '10000': 0, - '10001': 0, - '10010': 0, - '10011': 0, - '10100': 0, - '10101': 2, - '10110': 0, - '10111': 2, - '11000': 0, - '11001': 0, - '11010': 0, - '11011': 0, - '11100': 7, - '11101': 44, - '11110': 4, - '11111': 41 - } - assert sampling == sampling_exp def test_qft(): c = qft(range(4)) - s = StateEvolution(c).final_state() + s = c.get_qs() s_exp = np.ones(2**4) * 0.25 assert np.allclose(s, s_exp) diff --git a/tests/st/test_core/test_circuit/test_module_circuit.py b/tests/st/test_core/test_circuit/test_module_circuit.py new file mode 100644 index 0000000000000000000000000000000000000000..3eca6122734c78ffa0aae7b349846bc1551be6af --- /dev/null +++ b/tests/st/test_core/test_circuit/test_module_circuit.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""Test module_circuit.""" +import numpy as np +from mindquantum import UN, SwapParts, U3 +from mindquantum import Circuit +from mindquantum.core import gates as G + + +def test_un(): + """ + Description: Test UN + Expectation: + """ + circ = UN(G.X, [3, 4, 5], [0, 1, 2]) + assert circ[-1] == G.X.on(5, 2) + + +def test_swappart(): + """ + Description: Test SwapPart + Expectation: + """ + circ = SwapParts([1, 2, 3], [4, 5, 6], 0) + assert circ[-1] == G.SWAP([3, 6], 0) + + +def test_u3(): + """ + Description: Test U3 + Expectation: + """ + circ = U3('a', 'b', 'c', 0) + circ_exp = Circuit().rz('a', 0).rx(-np.pi / 2, 0).rz('b', 0).rx(np.pi / 2, 0).rz('c', 0) + assert circ == circ_exp diff --git a/tests/st/test_gate/test_gate.py b/tests/st/test_core/test_gates/test_gate.py similarity index 48% rename from tests/st/test_gate/test_gate.py rename to tests/st/test_core/test_gates/test_gate.py index 72f4048c3a9fa5598fb1cefbe22c0b95d240914c..f1593ff41b8923668e11d2dddf153473a0528921 100644 --- a/tests/st/test_gate/test_gate.py +++ b/tests/st/test_core/test_gates/test_gate.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,152 +16,152 @@ """Test gate.""" import pytest -from mindquantum.ops import QubitOperator import numpy as np from scipy.linalg import expm -from mindquantum.gate import Hamiltonian -import mindquantum.gate as G - - -def test_hamiltonian(): - """Test hamiltonian""" - ham = Hamiltonian(QubitOperator('Z0 Y1', 0.3)) - assert ham.ham_termlist == [(((0, 'Z'), (1, 'Y')), 0.3)] - assert ham.mindspore_data() == { - 'hams_pauli_coeff': [0.3], - 'hams_pauli_word': [['Z', 'Y']], - 'hams_pauli_qubit': [[0, 1]] - } +import mindquantum.core.gates as G def test_rotate_pauli(): + """ + Description: Test rotate pauli + Expectation: + """ gates = { 'rx': [ - G.RX('angle').on(0), lambda phi: np.array([[ - np.cos(phi / 2), -1j * np.sin(phi / 2) - ], [-1j * np.sin(phi / 2), np.cos(phi / 2)]]) + G.RX('angle').on(0), lambda phi: np.array([[np.cos(phi / 2), -1j * np.sin(phi / 2)], + [-1j * np.sin(phi / 2), np.cos(phi / 2)]]) ], 'ry': [ - G.RY('angle').on(0), - lambda phi: np.array([[np.cos(phi / 2), -np.sin( - phi / 2)], [np.sin(phi / 2), np.cos(phi / 2)]]) + G.RY('angle').on(0), lambda phi: np.array([[np.cos(phi / 2), -np.sin(phi / 2)], + [np.sin(phi / 2), np.cos(phi / 2)]]) ], - 'rz': [ - G.RZ('angle').on(0), - lambda phi: np.array([[np.exp(-1j * phi / 2), 0], - [0, np.exp(1j * phi / 2)]]) - ] + 'rz': [G.RZ('angle').on(0), lambda phi: np.array([[np.exp(-1j * phi / 2), 0], [0, np.exp(1j * phi / 2)]])] } angle = 0.5 for name, rs in gates.items(): - assert np.allclose(rs[0].matrix({'angle': angle}), - rs[1](angle)) - assert np.allclose(rs[0].diff_matrix({'angle': angle}), - 0.5 * rs[1](angle + np.pi)) - assert np.allclose(rs[0].hermitian().matrix({'angle': angle}), - rs[1](-angle)) - assert np.allclose(rs[0].hermitian().diff_matrix({'angle': angle}), - 0.5 * rs[1](-angle - np.pi)) + assert np.allclose(rs[0].matrix({'angle': angle}), rs[1](angle)) + assert np.allclose(rs[0].diff_matrix({'angle': angle}), 0.5 * rs[1](angle + np.pi)) + assert np.allclose(rs[0].hermitian().matrix({'angle': angle}), rs[1](-angle)) + assert np.allclose(rs[0].hermitian().diff_matrix({'angle': angle}), 0.5 * rs[1](-angle - np.pi)) def test_phase_shift(): + """ + Description: Test phase shift + Expectation: + """ angle = 0.5 f = lambda theta: np.array([[1, 0], [0, np.exp(1.0j * theta)]]) assert np.allclose(G.PhaseShift(angle).matrix(), f(angle)) def test_trap_ion_gate(): + """ + Description: Test trap ion gate + Expectation: + """ angle = 0.5 xx = [ G.XX("angle").on((0, 1)), lambda angle: expm(-1j * angle * np.array( - [[0. + 0.j, 0. + 0.j, 0. + 0.j, 1. + 0.j], - [0. + 0.j, 0. + 0.j, 1. + 0.j, 0. + 0.j], - [0. + 0.j, 1. + 0.j, 0. + 0.j, 0. + 0.j], - [1. + 0.j, 0. + 0.j, 0. + 0.j, 0. + 0.j]])) + [[0. + 0.j, 0. + 0.j, 0. + 0.j, 1. + 0.j], [0. + 0.j, 0. + 0.j, 1. + 0.j, 0. + 0.j], + [0. + 0.j, 1. + 0.j, 0. + 0.j, 0. + 0.j], [1. + 0.j, 0. + 0.j, 0. + 0.j, 0. + 0.j]])) ] yy = [ G.YY("angle").on((0, 1)), lambda angle: expm(-1j * angle * np.array( - [[0. + 0.j, 0. + 0.j, 0. + 0.j, -1. + 0.j], - [0. + 0.j, 0. + 0.j, 1. + 0.j, 0. + 0.j], - [0. + 0.j, 1. + 0.j, 0. + 0.j, 0. + 0.j], - [-1. + 0.j, 0. + 0.j, 0. + 0.j, 0. + 0.j]])) + [[0. + 0.j, 0. + 0.j, 0. + 0.j, -1. + 0.j], [0. + 0.j, 0. + 0.j, 1. + 0.j, 0. + 0.j], + [0. + 0.j, 1. + 0.j, 0. + 0.j, 0. + 0.j], [-1. + 0.j, 0. + 0.j, 0. + 0.j, 0. + 0.j]])) ] zz = [ - G.ZZ("angle").on((0, 1)), lambda angle: expm(-1j * angle * np.array([[ - 1., 0., 0., 0. - ], [0., -1., 0., 0.], [0., 0., -1., 0.], [0., 0., 0., 1.]])) + G.ZZ("angle").on((0, 1)), lambda angle: expm(-1j * angle * np.array([[1., 0., 0., 0.], [0., -1., 0., 0.], + [0., 0., -1., 0.], [0., 0., 0., 1.]])) ] for g in [xx, yy, zz]: - assert np.allclose(g[0].matrix({'angle': angle}), - g[1](angle)) - assert np.allclose(g[0].diff_matrix({'angle': angle}), - g[1](angle + np.pi / 2)) + assert np.allclose(g[0].matrix({'angle': angle}), g[1](angle)) + assert np.allclose(g[0].diff_matrix({'angle': angle}), g[1](angle + np.pi / 2)) def test_pauli_gate(): + """ + Description: Test pauli gate + Expectation: + """ gates = { 'X': [ G.X, np.array([[0. + 0.j, 1. + 0.j], [1. + 0.j, 0. + 0.j]]), - lambda phi: np.array([[np.cos(phi / 2), -1j * np.sin(phi / 2)], - [-1j * np.sin(phi / 2), - np.cos(phi / 2)]]) + lambda phi: np.array([[np.cos(phi / 2), -1j * np.sin(phi / 2)], [-1j * np.sin(phi / 2), + np.cos(phi / 2)]]) ], 'Y': [ G.Y, np.array([[0. + 0.j, 0. - 1.j], [0. + 1.j, 0. + 0.j]]), - lambda phi: np.array([[np.cos(phi / 2), -np.sin( - phi / 2)], [np.sin(phi / 2), np.cos(phi / 2)]]) + lambda phi: np.array([[np.cos(phi / 2), -np.sin(phi / 2)], [np.sin(phi / 2), + np.cos(phi / 2)]]) ], 'Z': [ G.Z, np.array([[1. + 0.j, 0. + 0.j], [0. + 0.j, -1. + 0.j]]), - lambda phi: np.array([[np.exp(-1j * phi / 2), 0], - [0, np.exp(1j * phi / 2)]]) + lambda phi: np.array([[np.exp(-1j * phi / 2), 0], [0, np.exp(1j * phi / 2)]]) ] } angle = 0.5 for name, ps in gates.items(): assert np.allclose(ps[0].matrix(), ps[1]) - assert np.allclose((ps[0]**angle).matrix(), - ps[2](angle * np.pi)) + assert np.allclose((ps[0]**angle).matrix(), ps[2](angle * np.pi)) def test_identity(): - assert np.allclose(G.I.matrix(), - np.array([[1. + 0.j, 0. + 0.j], [0. + 0.j, 1. + 0.j]])) + """ + Description: Test identity + Expectation: + """ + assert np.allclose(G.I.matrix(), np.array([[1. + 0.j, 0. + 0.j], [0. + 0.j, 1. + 0.j]])) def test_hadamard(): - assert np.allclose( - G.H.matrix(), - np.array([[0.70710678 + 0.j, 0.70710678 + 0.j], - [0.70710678 + 0.j, -0.70710678 + 0.j]])) - - -def test_power(): + """ + Description: Test hadamard + Expectation: + """ + assert np.allclose(G.H.matrix(), + np.array([[0.70710678 + 0.j, 0.70710678 + 0.j], [0.70710678 + 0.j, -0.70710678 + 0.j]])) + + +def test_power_ops(): + """ + Description: Test power + Expectation: + """ angle = 0.3 frac = 0.4 - assert np.allclose( - G.Power(G.RX(angle), frac).matrix(), - G.RX(angle * frac).matrix()) + assert np.allclose(G.Power(G.RX(angle), frac).matrix(), G.RX(angle * frac).matrix()) def test_swap(): + """ + Description: Test swap + Expectation: + """ assert np.allclose( G.SWAP.matrix(), - np.array([[1. + 0.j, 0. + 0.j, 0. + 0.j, 0. + 0.j], - [0. + 0.j, 0. + 0.j, 1. + 0.j, 0. + 0.j], - [0. + 0.j, 1. + 0.j, 0. + 0.j, 0. + 0.j], - [0. + 0.j, 0. + 0.j, 0. + 0.j, 1. + 0.j]])) + np.array([[1. + 0.j, 0. + 0.j, 0. + 0.j, 0. + 0.j], [0. + 0.j, 0. + 0.j, 1. + 0.j, 0. + 0.j], + [0. + 0.j, 1. + 0.j, 0. + 0.j, 0. + 0.j], [0. + 0.j, 0. + 0.j, 0. + 0.j, 1. + 0.j]])) def test_univ_mat_gate(): + """ + Description: Test UnivMathGate + Expectation: + """ mat = np.random.uniform(size=(2, 2)) assert np.allclose(G.UnivMathGate('univ', mat).matrix(), mat) def test_gate_obj_mismatch(): + """ + Description: Test raise gate obj mismatch + Expectation: + """ with pytest.raises(Exception, match=r"requires \d+ qubits"): G.X((0, 1)) with pytest.raises(Exception, match=r"requires \d+ qubits"): @@ -172,6 +173,10 @@ def test_gate_obj_mismatch(): def test_gate_obj_ctrl_overlap(): + """ + Description: Test gate obj ctrl overlap + Expectation: + """ with pytest.raises(Exception, match=r"cannot have same qubits"): G.X(1, 1) with pytest.raises(Exception, match=r"cannot have same qubits"): diff --git a/tests/st/test_ops/test_fermion_ops.py b/tests/st/test_core/test_operators/test_fermion_ops.py similarity index 80% rename from tests/st/test_ops/test_fermion_ops.py rename to tests/st/test_core/test_operators/test_fermion_ops.py index 2697b19c3914e656ceed4ba4f9d00cc48fb38fc5..a6968ec3f6239004e22ae7490aa257e042f0ca9a 100644 --- a/tests/st/test_ops/test_fermion_ops.py +++ b/tests/st/test_core/test_operators/test_fermion_ops.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,10 +15,14 @@ # ============================================================================ """Test fermion operator.""" -from mindquantum.ops import FermionOperator +from mindquantum.core.operators import FermionOperator def test_fermion_ops_num_coeff(): + """ + Description: Test fermion ops num coeff + Expectation: + """ # check the creation operator a_p_dagger = FermionOperator('1^') assert str(a_p_dagger) == '1.0 [1^] ' @@ -36,6 +41,10 @@ def test_fermion_ops_num_coeff(): def test_power(): + """ + Description: Test fermion operator power + Expectation: + """ # check power and multiply w = (1 + 2j) * FermionOperator(' 4^ 3 9 3^ ') + 4 * FermionOperator(' 2 ') w_2 = w * w @@ -44,6 +53,10 @@ def test_power(): def test_normal_order(): + """ + Description: Test normal order + Expectation: + """ origin = FermionOperator('0 1^') normal_order = FermionOperator('1^ 0', -1) @@ -52,6 +65,10 @@ def test_normal_order(): def test_multiplier(): + """ + Description: Test multiplier + Expectation: + """ origin = FermionOperator('0 1^') after_mul = FermionOperator('0 1^', 2) assert after_mul == 2 * origin @@ -70,6 +87,10 @@ def test_multiplier(): def test_add_sub(): + """ + Description: Test add and sub + Expectation: + """ # Test in place add w1 = FermionOperator(' 4^ 3 9 3^ ') + 4 * FermionOperator(' 2 ') w2 = 4 * FermionOperator(' 2 ') @@ -78,6 +99,10 @@ def test_add_sub(): def test_compress(): + """ + Description: Test compress + Expectation: + """ # test compress w1 = FermionOperator('4^ 3') + FermionOperator('2', 1e-9) w2 = FermionOperator('4^ 3') @@ -91,6 +116,10 @@ def test_compress(): def test_constant(): + """ + Description: Test constant + Expectation: + """ # test constant w1 = FermionOperator('4^ 3 9 3^') + 6.0 * FermionOperator( '2 3^') + 2.0 * FermionOperator('') @@ -98,6 +127,10 @@ def test_constant(): def test_para_operators(): + """ + Description: Test para operators + Expectation: + """ para_op = FermionOperator('0 1^', 'x') assert str(para_op) == 'x [0 1^] ' @@ -108,5 +141,9 @@ def test_para_operators(): def test_eq(): + """ + Description: Test equal + Expectation: + """ a = FermionOperator('0 1^', 'x') assert a.subs({'x': 1}) == FermionOperator('0 1^') diff --git a/mindquantum/version.py b/tests/st/test_core/test_operators/test_hamiltonian.py similarity index 67% rename from mindquantum/version.py rename to tests/st/test_core/test_operators/test_hamiltonian.py index ba961bc85646a9e7a0d7c0c7646b87fbcf11f3f1..fcdf471bef2818b7a9d289bee3da23a1c1c101a7 100644 --- a/mindquantum/version.py +++ b/tests/st/test_core/test_operators/test_hamiltonian.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,5 +13,16 @@ # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================ -"""Version""" -__version__ = '0.2.0' +"""Test Hamiltonian.""" + +from mindquantum import Hamiltonian +from mindquantum import QubitOperator + + +def test_hamiltonian(): + """ + Description: Test Hamiltonian + Expectation: + """ + ham = Hamiltonian(QubitOperator('Z0 Y1', 0.3)) + assert ham.ham_termlist == [(((0, 'Z'), (1, 'Y')), 0.3)] diff --git a/tests/st/test_utils/test_utils_operator.py b/tests/st/test_core/test_operators/test_operators_utils.py similarity index 89% rename from tests/st/test_utils/test_utils_operator.py rename to tests/st/test_core/test_operators/test_operators_utils.py index 16ff4917ec034bd04a57ee09c9e20288fbc84dfb..8e0d8322d4f5aca08ab4b373c6ce9605f3015c9a 100644 --- a/tests/st/test_utils/test_utils_operator.py +++ b/tests/st/test_core/test_operators/test_operators_utils.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,12 +13,11 @@ # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================ -"""Test utils_operator.""" +"""Test operator_utils.""" -from mindquantum.ops import QubitOperator, FermionOperator, QubitExcitationOperator -from mindquantum.utils import (count_qubits, normal_ordered, commutator, - number_operator, up_index, down_index, - hermitian_conjugated) +from mindquantum.core.operators import QubitOperator, FermionOperator, QubitExcitationOperator +from mindquantum.core.operators.utils import (count_qubits, normal_ordered, commutator, number_operator, up_index, + down_index, hermitian_conjugated) def test_count_qubits(): diff --git a/tests/st/test_ops/test_polynomial_tensor.py b/tests/st/test_core/test_operators/test_polynomial_tensor.py similarity index 90% rename from tests/st/test_ops/test_polynomial_tensor.py rename to tests/st/test_core/test_operators/test_polynomial_tensor.py index f22a2b56b48307339d77c2a837f239e9a6b5acab..1f32f8ca17d028567378d95967273b6b4610365a 100644 --- a/tests/st/test_ops/test_polynomial_tensor.py +++ b/tests/st/test_core/test_operators/test_polynomial_tensor.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (c) 2020 Huawei Technologies Co.,ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,11 +17,14 @@ Test the polynomial_tensor in the ops module. """ import numpy as np -from mindquantum.ops import PolynomialTensor +from mindquantum.core.operators import PolynomialTensor def test_polynomial_tensor(): - """"Test the polynomial_tensor class""" + """ + Description: Test polynomial tensor + Expectation: + """ constant = 1 one_body_term = np.array([[1, 0], [0, 1]]) two_body_term = np.array([[[[1, 0], [0, 1]], [[1, 0], [0, 1]]], diff --git a/tests/st/test_ops/test_qubit_excitation_ops.py b/tests/st/test_core/test_operators/test_qubit_excitation_ops.py similarity index 77% rename from tests/st/test_ops/test_qubit_excitation_ops.py rename to tests/st/test_core/test_operators/test_qubit_excitation_ops.py index 56d94b8b9514d61eafed8c0996c685aeda99ad40..1fd9e3c89e8b6f7c23f79a9fe0b1c8d3cfd69493 100644 --- a/tests/st/test_ops/test_qubit_excitation_ops.py +++ b/tests/st/test_core/test_operators/test_qubit_excitation_ops.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,12 +15,15 @@ # ============================================================================ """Test qubit excitation operator.""" -from mindquantum.ops import QubitExcitationOperator -from mindquantum.ops import QubitOperator, FermionOperator +from mindquantum.core.operators import QubitExcitationOperator +from mindquantum.core.operators import QubitOperator, FermionOperator def test_qubit_excitation_ops_num_coeff(): - # check the creation operator + """ + Description: check the creation operator + Expectation: + """ a_p_dagger = QubitExcitationOperator('1^') assert str(a_p_dagger) == '1.0 [Q1^] ' @@ -37,7 +41,10 @@ def test_qubit_excitation_ops_num_coeff(): def test_power(): - # check power and multiply + """ + Description: check power and multiply + Expectation: + """ w = (1 + 2j) * QubitExcitationOperator(' 4^ 3 9 3^ ') + \ 4 * QubitExcitationOperator(' 2 ') w_2 = w * w @@ -46,6 +53,10 @@ def test_power(): def test_normal_order(): + """ + Description: Test normal order + Expectation: + """ origin = QubitExcitationOperator('0 1^') # Coefficient will not be affected for qubit-excitation operators normal_order = QubitExcitationOperator('1^ 0', 1) @@ -54,6 +65,10 @@ def test_normal_order(): def test_multiplier(): + """ + Description: Test multiplier + Expectation: + """ origin = QubitExcitationOperator('0 1^') after_mul = QubitExcitationOperator('0 1^', 2) assert after_mul == 2 * origin @@ -72,6 +87,10 @@ def test_multiplier(): def test_add_sub(): + """ + Description: Test add ans sub + Expectation: + """ # Test in place add w1 = QubitExcitationOperator(' 4^ 3 9 3^ ') + \ 4 * QubitExcitationOperator(' 2 ') @@ -81,7 +100,10 @@ def test_add_sub(): def test_compress(): - # test compress + """ + Description: Test compress + Expectation: + """ w1 = QubitExcitationOperator('4^ 3') + \ QubitExcitationOperator('2', 1e-9) w2 = QubitExcitationOperator('4^ 3') @@ -95,13 +117,20 @@ def test_compress(): def test_constant(): - # test constant + """ + Description: Test constant + Expectation: + """ w1 = QubitExcitationOperator('4^ 3 9 3^') + 6.0 * QubitExcitationOperator( '2 3^') + 2.0 * QubitExcitationOperator('') assert w1.constant == 2.0 def test_para_operators(): + """ + Description: Test para operators + Expectation: + """ para_op = QubitExcitationOperator('0 1^', 'x') assert str(para_op) == 'x [Q0 Q1^] ' @@ -112,13 +141,20 @@ def test_para_operators(): def test_eq(): + """ + Description: Test equal + Expectation: + """ a = QubitExcitationOperator('0 1^', 'x') assert a.subs({'x': 1}) == QubitExcitationOperator('0 1^') def test_convert_to_qubit_operator(): - # Check if the qubit excitation operator can correctly convert to - # the qubit operator correctly according to the definition. + """ + Description: Check if the qubit excitation operator can correctly convert to + the qubit operator correctly according to the definition. + Expectation: + """ op = QubitExcitationOperator(((4, 1), (1, 0)), 2.j) qubit_op = QubitOperator("X1 X4", 0.5j) + QubitOperator("X1 Y4", 0.5) + \ QubitOperator("Y1 X4", -0.5) + QubitOperator("Y1 Y4", 0.5j) @@ -127,7 +163,10 @@ def test_convert_to_qubit_operator(): def test_fermion_op(): - # Test the "Fermion excitation version" of a qubit excitation operator + """ + Description: Test the "Fermion excitation version" of a qubit excitation operator + Expectation: + """ op = QubitExcitationOperator(((4, 1), (1, 0)), 2.j) ferm_op = FermionOperator(((4, 1), (1, 0)), 2.j) diff --git a/tests/st/test_ops/test_qubit_ops.py b/tests/st/test_core/test_operators/test_qubit_ops.py similarity index 91% rename from tests/st/test_ops/test_qubit_ops.py rename to tests/st/test_core/test_operators/test_qubit_ops.py index a99c4897d8ba39491c9514fa7ec445aea0ffac43..a7069c53eb294afa70cdc52b1ed4c7d7014901b0 100644 --- a/tests/st/test_ops/test_qubit_ops.py +++ b/tests/st/test_core/test_operators/test_qubit_ops.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (c) 2020 Huawei Technologies Co.,ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,10 +14,14 @@ # limitations under the License. """The test function for QubitOperator.""" -from mindquantum.ops import QubitOperator +from mindquantum.core.operators import QubitOperator def test_qubit_ops_num_coeff(): + """ + Description: Test qubit ops num coeff + Expectation: + """ q1 = QubitOperator('Z1 Z2') + QubitOperator('X1') assert str(q1) == '1.0 [X1] +\n1.0 [Z1 Z2] ' q5 = QubitOperator('X1') * QubitOperator('Y1') @@ -74,6 +79,10 @@ def test_qubit_ops_num_coeff(): def test_qubit_ops_symbol_coeff(): + """ + Description: Test ops symbol coeff + Expectation: + """ q1 = QubitOperator('Z1 Z2', 'a') + QubitOperator('X1', 'b') assert str(q1) == 'b [X1] +\na [Z1 Z2] ' @@ -106,12 +115,20 @@ def test_qubit_ops_symbol_coeff(): def test_qubit_ops_subs(): + """ + Description: Test ops sub + Expectation: + """ q = QubitOperator('X0', 'b') + QubitOperator('X0', 'a') q = q.subs({'a': 1, 'b': 2}) assert str(q) == '3 [X0] ' def test_qubit_ops_sub(): + """ + Description: Test ops sub + Expectation: + """ q1 = QubitOperator('X0') q2 = QubitOperator('Y0') q = QubitOperator('X0') + QubitOperator('Y0', -1) diff --git a/mindquantum/nn/__init__.py b/tests/st/test_core/test_operators/test_time_evolution.py similarity index 53% rename from mindquantum/nn/__init__.py rename to tests/st/test_core/test_operators/test_time_evolution.py index 99a41c2720e2aacf017eb365d01a86c86cc06c4e..eee254e1e34632cafd14b093649f635738c7dc3c 100644 --- a/mindquantum/nn/__init__.py +++ b/tests/st/test_core/test_operators/test_time_evolution.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,18 +13,20 @@ # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================ -"""Quantum neural networks operators and cells.""" +"""Test TimeEvolution.""" -from .pqc import generate_pqc_operator, PQC -from .mindquantum_layer import MindQuantumLayer -from .evolution import generate_evolution_operator, Evolution -from .mindquantum_ansatz_only_layer import MindQuantumAnsatzOnlyLayer -from .mindquantum_ansatz_only_layer import MindQuantumAnsatzOnlyOperator +from mindquantum.core.operators import QubitOperator +from mindquantum import Circuit +from mindquantum import gates as G +from mindquantum.core.operators import TimeEvolution -__all__ = [ - "generate_pqc_operator", "PQC", "MindQuantumLayer", - "generate_evolution_operator", "Evolution", "MindQuantumAnsatzOnlyLayer", - "MindQuantumAnsatzOnlyOperator" -] -__all__.sort() +def test_time_evolution(): + """ + Description: Test TimeEvolution + Expectation: AssertionError + """ + h = QubitOperator('Z0 Z1', 'p') + QubitOperator('X0', 'q') + circ = TimeEvolution(h).circuit + circ_exp = Circuit([G.X.on(1, 0), G.RZ({'p': 2}).on(1), G.X.on(1, 0), G.RX({'q': 2}).on(0)]) + assert circ.__repr__() == circ_exp.__repr__() diff --git a/tests/st/test_parameter_resolver/test_parameter_resolver.py b/tests/st/test_core/test_parameterresolver/test_parameter_resolver.py similarity index 75% rename from tests/st/test_parameter_resolver/test_parameter_resolver.py rename to tests/st/test_core/test_parameterresolver/test_parameter_resolver.py index 7e4087d341853fbc67afcad523e9e8895877e574..09c94e84e427d80ad61df7ae02120682a82921c8 100644 --- a/tests/st/test_parameter_resolver/test_parameter_resolver.py +++ b/tests/st/test_core/test_parameterresolver/test_parameter_resolver.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +19,10 @@ from mindquantum import ParameterResolver as PR def test_parameter_resolve(): - """test parameter resolver.""" + """ + Description: Test parameter resolver + Expectation: + """ pr = PR({'a': 1.0}) pr['b'] = 2.0 pr[['c', 'd']] = [3.0, 4.0] @@ -28,7 +32,7 @@ def test_parameter_resolve(): pr_tmp = PR({'e': 5.0, 'f': 6.0}) pr_tmp.no_grad() pr.update(pr_tmp) - assert pr.para_name == ['a', 'b', 'c', 'd', 'e', 'f'] + assert pr.params_name == ['a', 'b', 'c', 'd', 'e', 'f'] assert pr.para_value == [4.0, 8.0, 12.0, 16.0, 5.0, 6.0] pr.requires_grad_part('e') pr.no_grad_part('b') @@ -36,16 +40,13 @@ def test_parameter_resolve(): assert pr.no_grad_parameters == {'b', 'f'} pr.requires_grad() assert not pr.no_grad_parameters - mindspore_data = pr.mindspore_data() - assert 'gate_params_names' in mindspore_data - assert 'gate_coeff' in mindspore_data - assert 'gate_requires_grad' in mindspore_data - assert sum(mindspore_data['gate_coeff']) == 51.0 - assert sum(mindspore_data['gate_requires_grad']) == 6 - assert ''.join(mindspore_data['gate_params_names']) == 'abcdef' def test_parameter_resolve_combination(): + """ + Description: Test pr combination + Expectation: + """ pr1 = PR({'a': 1}) pr2 = PR({'a': 2, 'b': 3}) assert pr1.combination(pr2) == 2 diff --git a/tests/st/test_engine/test_circuitengine.py b/tests/st/test_engine/test_circuitengine.py index ceeb1eaa6265c493d0b974f3b58a190035b2b15a..b0db1caee3e36dcbdb2fa38e66befac96c1b091a 100755 --- a/tests/st/test_engine/test_circuitengine.py +++ b/tests/st/test_engine/test_circuitengine.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,16 +15,17 @@ # ============================================================================ """Test circuitengine.""" -import mindquantum.gate as G +import mindquantum.core.gates as G from mindquantum.engine.circuitengine import CircuitEngine from mindquantum.engine import circuit_generator from mindquantum import Circuit + def test_allocate_qureg(): """Test allocate qureg.""" eng = CircuitEngine() qubits = eng.allocate_qureg(2) - G.H.__or__((eng.qubits[0],)) + G.H.__or__((eng.qubits[0], )) G.X.__or__((eng.qubits[0], eng.qubits[1])) assert qubits[0].qubit_id == 0 assert qubits[1].qubit_id == 1 @@ -33,8 +35,8 @@ def test_circuit_generator(): """Test circuit generator.""" @circuit_generator(3) def encoder(qubits): - G.H.__or__((qubits[0],)) + G.H.__or__((qubits[0], )) G.X.__or__((qubits[0], qubits[1])) - G.RY('p1').__or__((qubits[2],)) + G.RY('p1').__or__((qubits[2], )) assert isinstance(encoder, Circuit) diff --git a/tests/st/test_framework/test_layer.py b/tests/st/test_framework/test_layer.py new file mode 100644 index 0000000000000000000000000000000000000000..938119ab5dafe99de9b6fa85c08774b0f588bf5f --- /dev/null +++ b/tests/st/test_framework/test_layer.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""Test QNN layers.""" +import numpy as np +import mindspore as ms +from mindquantum import Circuit, Hamiltonian, QubitOperator, Simulator +from mindquantum import MQLayer +from mindquantum.core import gates as G + +ms.context.set_context(mode=ms.context.PYNATIVE_MODE, device_target="CPU") + + +def test_mindquantumlayer(): + """ + Description: Test MQLayer + Expectation: + """ + encoder = Circuit() + ansatz = Circuit() + encoder += G.RX('e1').on(0) + encoder += G.RY('e2').on(1) + ansatz += G.X.on(1, 0) + ansatz += G.RY('p1').on(0) + ham = Hamiltonian(QubitOperator('Z0')) + ms.set_seed(55) + circ = encoder + ansatz + sim = Simulator('projectq', circ.n_qubits) + f_g_ops = sim.get_expectation_with_grad(ham, circ, encoder_params_name=['e1', 'e2'], ansatz_params_name=['p1']) + net = MQLayer(f_g_ops) + encoder_data = ms.Tensor(np.array([[0.1, 0.2]]).astype(np.float32)) + res = net(encoder_data) + assert round(float(res.asnumpy()[0, 0]), 6) == round(float(0.9949919), 6) diff --git a/tests/st/test_framework/test_operations.py b/tests/st/test_framework/test_operations.py new file mode 100644 index 0000000000000000000000000000000000000000..2adbd29802f3253b64f7c36a29d07a2b843e5e5f --- /dev/null +++ b/tests/st/test_framework/test_operations.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""Test QNN operations.""" +import numpy as np +import mindspore as ms +from mindquantum import Circuit, Simulator, Hamiltonian, QubitOperator +from mindquantum import MQAnsatzOnlyOps +from mindquantum.core import gates as G + +ms.context.set_context(mode=ms.context.PYNATIVE_MODE, device_target="CPU") + + +def test_mindquantum_ansatz_only_ops(): + """ + Description: Test MQAnsatzOnlyOps + Expectation: + """ + circ = Circuit(G.RX('a').on(0)) + data = ms.Tensor(np.array([0.5]).astype(np.float32)) + ham = Hamiltonian(QubitOperator('Z0')) + sim = Simulator('projectq', circ.n_qubits) + + evol = MQAnsatzOnlyOps(sim.get_expectation_with_grad(ham, circ)) + output = evol(data) + assert np.allclose(output.asnumpy(), [[8.77582550e-01]]) diff --git a/tests/st/test_io/test_beauty_print.py b/tests/st/test_io/test_beauty_print.py new file mode 100644 index 0000000000000000000000000000000000000000..6bf1bfcc4e247e9aabc88f0fa2e76f05b30a6bcd --- /dev/null +++ b/tests/st/test_io/test_beauty_print.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""Test utils.""" + +from mindquantum.io import bprint + + +def test_beauty_print(): + """ + test + Description: + Expectation: + """ + o = bprint(['Age:17', 'Name:Bob'], title='Info of Bob') + o_exp = ['|===Info of Bob===|', '| Age :17 |', '| Name:Bob |', '==================='] + assert o == o_exp diff --git a/mindquantum/ansatz/__init__.py b/tests/st/test_io/test_qasm/test_hiqasm.py similarity index 56% rename from mindquantum/ansatz/__init__.py rename to tests/st/test_io/test_qasm/test_hiqasm.py index 6a3c7ebed7b1484843535a69a3d99237d215a581..888962b8115d9ed4e33fdfa86e4cf1c8c988554d 100644 --- a/mindquantum/ansatz/__init__.py +++ b/tests/st/test_io/test_qasm/test_hiqasm.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -8,18 +9,25 @@ # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# wITHOUT wARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================ -"""Implementation of different ansatz.""" +"""Test hiqasm.""" -from ._ansatz import Ansatz -from .max_cut import MaxCutAnsatz -from .max_2_sat import Max2SATAnsatz -from .hardware_efficient import HardwareEfficientAnsatz -from .unitary_cc import UCCAnsatz -from .qubit_ucc import QubitUCCAnsatz +from mindquantum.io import random_hiqasm +from mindquantum.io import HiQASM +from mindquantum.core import X -__all__ = ['Ansatz', 'MaxCutAnsatz', 'Max2SATAnsatz', 'HardwareEfficientAnsatz', - "UCCAnsatz", "QubitUCCAnsatz"] +def test_hiqasm(): + """ + test + Description: + Expectation: + """ + qasm = random_hiqasm(3, 10) + assert len(qasm) == 301 + hiqasm = HiQASM() + circ = hiqasm.from_string(qasm) + assert len(circ) == 13 + assert circ[6] == X.on(0, 2) diff --git a/tests/st/test_nn/test_nn.py b/tests/st/test_nn/test_nn.py deleted file mode 100755 index 35bd9fe40cf1ec0fcac3f0955000e6bedf9ef5ee..0000000000000000000000000000000000000000 --- a/tests/st/test_nn/test_nn.py +++ /dev/null @@ -1,147 +0,0 @@ -# Copyright 2021 Huawei Technologies Co., Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ============================================================================ -"""Test nn.""" - -import numpy as np -import mindspore as ms -from mindquantum.ops import QubitOperator -import mindquantum.gate as G -import mindquantum as mq -from mindquantum import Hamiltonian -from mindquantum import Projector -from mindquantum.nn import MindQuantumLayer -from mindquantum.circuit import Circuit -from mindquantum.engine import circuit_generator -from mindquantum.nn import generate_evolution_operator -from mindquantum.nn import MindQuantumAnsatzOnlyOperator -from mindquantum.nn import MindQuantumAnsatzOnlyLayer - - -def test_mindquantumlayer(): - """Test mindquantumlayer forward and backward.""" - encoder = Circuit() - ansatz = Circuit() - encoder += G.RX('e1').on(0) - encoder += G.RY('e2').on(1) - ansatz += G.X.on(1, 0) - ansatz += G.RY('p1').on(0) - ham = Hamiltonian(QubitOperator('Z0')) - ms.set_seed(55) - ms.context.set_context(mode=ms.context.GRAPH_MODE, device_target="CPU") - net = MindQuantumLayer(['e1', 'e2'], ['p1'], encoder + ansatz, ham) - encoder_data = ms.Tensor(np.array([[0.1, 0.2]]).astype(np.float32)) - res = net(encoder_data) - assert round(float(res.asnumpy()[0, 0]), 6) == round(float(0.9949919), 6) - state = net.final_state(encoder_data[0]) - assert np.allclose(state[0], 9.9375761e-01 + 1.2387493e-05j) - - -def test_generate_evolution_operator_state(): - a, b = 0.3, 0.5 - circ = Circuit([G.RX('a').on(0), G.RX('b').on(1)]) - data = ms.Tensor(np.array([a, b]).astype(np.float32)) - evol = generate_evolution_operator(circ) - state = evol(data) - - state_exp = [0.9580325796404553, -0.14479246283091116j, - -0.2446258794777393j, -0.036971585637570345] - assert np.allclose(state, state_exp) - - -def test_generate_pqc_operator(): - """Test generate pqc operator""" - @circuit_generator(2) - def encoder(qubits): - G.RY('a').__or__((qubits[0],)) - G.RY('b').__or__((qubits[1],)) - - @circuit_generator(2) - def ansatz(qubits): - G.X.__or__((qubits[0], qubits[1])) - G.RX('p1').__or__((qubits[0],)) - G.RX('p2').__or__((qubits[1],)) - - ham = mq.Hamiltonian(QubitOperator('Z1')) - encoder_names = ['a', 'b'] - ansatz_names = ['p1', 'p2'] - - ms.context.set_context(mode=ms.context.GRAPH_MODE, device_target="CPU") - - pqc = mq.nn.generate_pqc_operator(encoder_names, ansatz_names, - encoder + ansatz, ham) - encoder_data = ms.Tensor(np.array([[0.1, 0.2]]).astype(np.float32)) - ansatz_data = ms.Tensor(np.array([0.3, 0.4]).astype(np.float32)) - measure_result, encoder_grad, ansatz_grad = pqc(encoder_data, ansatz_data) - assert round(float(measure_result.asnumpy()[0, 0]), - 6) == round(float(0.89819133), 6) - assert round(float(encoder_grad.asnumpy()[0, 0, 0]), - 6) == round(float(-0.09011973), 6) - assert round(float(ansatz_grad.asnumpy()[0, 0, 1]), - 6) == round(float(-3.7974921e-01), 6) - - -def test_generate_evolution_operator(): - circ = Circuit(G.RX('a').on(0)) - data = ms.Tensor(np.array([0.5]).astype(np.float32)) - - evol = generate_evolution_operator(circ, ['a']) - state1 = evol(data) - - evol = generate_evolution_operator(circ) - state2 = evol(data) - - circ = circ.apply_value({'a': 0.5}) - evol = generate_evolution_operator(circ) - state3 = evol() - assert np.allclose(state1, G.RX(0.5).matrix()[:, 0]) - assert np.allclose(state2, G.RX(0.5).matrix()[:, 0]) - assert np.allclose(state3, G.RX(0.5).matrix()[:, 0]) - - -def test_mindquantum_ansatz_only_ops(): - circ = Circuit(G.RX('a').on(0)) - data = ms.Tensor(np.array([0.5]).astype(np.float32)) - ham = Hamiltonian(QubitOperator('Z0')) - evol = MindQuantumAnsatzOnlyOperator(circ.para_name, circ, ham) - output = evol(data) - assert np.allclose(output.asnumpy(), [[8.77582550e-01]]) - - -def test_mindquantum_ansatz_only_layer(): - circuit = Circuit( - [G.H.on(0), - G.RZ(0.4).on(0), - G.RX('a').on(0), - G.RY('b').on(0)]) - ham = Hamiltonian(QubitOperator('Z0')) - init = ms.Tensor(np.array([0, 0]).astype(np.float32)) - net = MindQuantumAnsatzOnlyLayer(circuit.para_name, circuit, ham, init) - opti = ms.nn.Adagrad(net.trainable_params(), learning_rate=0.8) - train_net = ms.nn.TrainOneStepCell(net, opti) - for i in range(1000): - train_net() - assert np.allclose(net().asnumpy(), [-1]) - - -def test_pqc_with_projector(): - circ = Circuit([G.RX('a').on(0), G.RX('b').on(0)]) - proj = Projector('0') - ansatz_data = np.array([0.1]).astype(np.float32) - encoder_data = np.array([[0]]).astype(np.float32) - net = MindQuantumLayer(['a'], ['b'], - circ, - proj, - weight_init=ms.Tensor(ansatz_data)) - assert np.allclose(net(ms.Tensor(encoder_data)).asnumpy(), [[0.99750208]]) diff --git a/tests/st/test_simulator/test_simulator.py b/tests/st/test_simulator/test_simulator.py new file mode 100644 index 0000000000000000000000000000000000000000..e4bdfafeb1f6b0c324a8f803e274551344aeb93c --- /dev/null +++ b/tests/st/test_simulator/test_simulator.py @@ -0,0 +1,222 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# wITHOUT wARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""Test simulator.""" +import numpy as np +import mindspore as ms +import mindquantum.core.operators as ops +from mindquantum.simulator.simulator import Simulator +from mindquantum.core import gates as G +from mindquantum.core.circuit import UN +from mindquantum.algorithm import qft +from mindquantum.framework.layer import MQAnsatzOnlyLayer +from mindquantum import ParameterResolver as PR +from mindquantum import Circuit, Hamiltonian, QubitOperator + + +def _test_init_reset(virtual_qc): + """ + test + Description: + Expectation: + """ + s1 = Simulator(virtual_qc, 2) + circ = Circuit().h(0).h(1) + v1 = s1.get_qs() + s1.apply_circuit(circ) + s1.reset() + v3 = s1.get_qs() + v = np.array([1, 0, 0, 0], dtype=np.complex128) + assert np.allclose(v1, v) + assert np.allclose(v1, v3) + + +def _test_apply_circuit_and_hermitian(virtual_qc): + """ + test + Description: + Expectation: + """ + sv0 = np.array([[1, 0], [0, 0]]) + sv1 = np.array([[0, 0], [0, 1]]) + circ = Circuit() + circ.ry(1.2, 0).ry(3.4, 1) + circ.h(0).h(1) + circ.x(1, 0) + circ.rx('a', 0).ry('b', 1) + circ.zz('c', (0, 1)).z(1, 0) + s1 = Simulator(virtual_qc, circ.n_qubits) + pr = PR({'a': 1, 'b': 3, 'c': 5}) + s1.apply_circuit(circ, pr) + v1 = s1.get_qs() + m = np.kron(G.RY(3.4).matrix(), G.RY(1.2).matrix()) + m = np.kron(G.H.matrix(), G.H.matrix()) @ m + m = (np.kron(G.I.matrix(), sv0) + np.kron(G.X.matrix(), sv1)) @ m + m = np.kron(G.RY(3).matrix(), G.RX(1).matrix()) @ m + m = G.ZZ(5).matrix() @ m + m = (np.kron(G.I.matrix(), sv0) + np.kron(G.Z.matrix(), sv1)) @ m + v = m[:, 0] + assert np.allclose(v, v1) + + circ2 = circ.hermitian() + s1.reset() + s1.apply_circuit(circ2, pr) + m = np.conj(m.T) + v1 = s1.get_qs() + v = m[:, 0] + assert np.allclose(v, v1) + + +def _test_set_and_get(virtual_qc): + """ + test + Description: + Expectation: + """ + sim = Simulator(virtual_qc, 1) + qs1 = sim.get_qs() + assert np.allclose(qs1, np.array([1, 0])) + sim.set_qs(np.array([1, 1])) + qs2 = sim.get_qs() + assert np.allclose(qs2, np.array([1, 1]) / np.sqrt(2)) + + +def _test_non_hermitian_grad_ops(virtual_qc): + """ + test + Description: + Expectation: + """ + sim = Simulator(virtual_qc, 1) + c_r = Circuit().ry('b', 0) + c_l = Circuit().rz('a', 0) + grad_ops = sim.get_expectation_with_grad(ops.Hamiltonian(ops.QubitOperator('')), c_r, c_l) + f, g = grad_ops(np.array([1.2, 2.3])) + f = f[0, 0] + g = g[0, 0] + f_exp = np.exp(-1j * 2.3 / 2) * np.cos(1.2 / 2) + g1 = -0.5 * np.exp(-1j * 2.3 / 2) * np.sin(1.2 / 2) + g2 = -1j / 2 * np.exp(-1j * 2.3 / 2) * np.cos(1.2 / 2) + assert np.allclose(f, f_exp) + assert np.allclose(g, np.array([g1, g2])) + + +def generate_test_circuit(): + c = Circuit() + c += UN(G.H, 3) + c.x(0).y(1).z(2) + c += G.SWAP([0, 2], 1) + c += UN(G.X, 3) + c += G.ISWAP([0, 1], 2) + c.rx(1.2, 0).ry(2.3, 1).rz(3.4, 2) + c.x(0, 1).x(1, 2).x(0, 2) + c += G.PhaseShift(1.3).on(0, [1, 2]) + c += UN(G.H, 3) + c += UN(G.S, 3) + c += qft(range(3)) + c += G.gene_univ_parameterized_gate('fake_x', G.RX(0)._matrix, G.RX(0)._diff_matrix)('a').on(0) + c += G.RX('b').on(1, 2) + c += G.RX('c').on(2, 0) + c += UN(G.H, 3) + c += UN(G.T, 3) + c += G.UnivMathGate('fake_XX', G.XX(1.2).matrix()).on([0, 1]) + c += G.YY(2.3).on([1, 2]) + c += G.ZZ(3.4).on([0, 2]) + c += G.UnivMathGate('myX', G.X.matrix()).on(0) + c += G.Power(G.X, 1.2).on(1) + return c + + +def _test_all_gate_with_simulator(virtual_qc): + """ + test + Description: + Expectation: + """ + c = generate_test_circuit() + qs = c.get_qs(backend=virtual_qc, pr={'a': 1, 'b': 2, 'c': 3}) + qs_exp = np.array([ + 0.09742526 + 0.00536111j, -0.17279339 - 0.32080812j, 0.03473879 - 0.22046017j, -0.0990812 + 0.05735119j, + -0.11858329 - 0.05715877j, 0.37406968 + 0.19326249j, 0.46926914 + 0.52135788j, -0.17429908 + 0.27887826j + ]) + assert np.allclose(qs, qs_exp) + sim = Simulator(virtual_qc, c.n_qubits) + ham = ops.Hamiltonian(ops.QubitOperator('Z0')) + grad_ops = sim.get_expectation_with_grad(ham, c) + p0 = np.array([1, 2, 3]) + f1, g1 = grad_ops(p0) + delta = 0.0001 + p1 = np.array([1 + delta, 2, 3]) + f2, g2 = grad_ops(p1) + g_a = ((f2 - f1) / delta)[0, 0] + g_a_1 = g1[0, 0, 0] + g_a_2 = g2[0, 0, 0] + assert np.allclose(g_a, g_a_1, atol=1e-4) + assert np.allclose(g_a, g_a_2, atol=1e-4) + assert np.allclose(g_a_1, g_a_2, atol=1e-4) + + +def _test_optimization_with_custom_gate(virtual_qc): + """ + test + Description: + Expectation: + """ + def _matrix(theta): + return np.array([[np.cos(theta / 2), -1j * np.sin(theta / 2)], [-1j * np.sin(theta / 2), np.cos(theta / 2)]]) + + def _diff_matrix(theta): + return 0.5 * np.array([[-np.sin(theta / 2), -1j * np.cos(theta / 2)], + [-1j * np.cos(theta / 2), -np.sin(theta / 2)]]) + + h = G.UnivMathGate('H', G.H.matrix()) + rx = G.gene_univ_parameterized_gate('RX', _matrix, _diff_matrix) + + c1 = Circuit() + G.RY(3.4).on(0) + h.on(0) + rx('a').on(0) + c2 = Circuit() + G.RY(3.4).on(0) + G.H.on(0) + G.RX('a').on(0) + + sim = Simulator(virtual_qc, 1) + ham = Hamiltonian(QubitOperator('Z0')) + grad_ops1 = sim.get_expectation_with_grad(ham, c1) + grad_ops2 = sim.get_expectation_with_grad(ham, c2) + ms.context.set_context(mode=ms.context.PYNATIVE_MODE, device_target="CPU") + init_data = ms.Tensor(np.array([1.2]).astype(np.float32)) + net1 = MQAnsatzOnlyLayer(grad_ops1, weight=init_data) + net2 = MQAnsatzOnlyLayer(grad_ops2, weight=init_data) + opti1 = ms.nn.Adam(net1.trainable_params(), learning_rate=0.1) + opti2 = ms.nn.Adam(net2.trainable_params(), learning_rate=0.1) + train1 = ms.nn.TrainOneStepCell(net1, opti1) + train2 = ms.nn.TrainOneStepCell(net2, opti2) + for i in range(10): + train1() + train2() + assert np.allclose(train1().asnumpy(), train2().asnumpy()) + + +def test_virtual_quantum_computer(): + """ + test virtual quantum computer + + Description: Test mindquantum supported virtual quantum computers + Expectation: + """ + vqcs = ['projectq'] + for virtual_qc in vqcs: + _test_init_reset(virtual_qc) + _test_apply_circuit_and_hermitian(virtual_qc) + _test_set_and_get(virtual_qc) + _test_non_hermitian_grad_ops(virtual_qc) + _test_all_gate_with_simulator(virtual_qc) + _test_optimization_with_custom_gate(virtual_qc) diff --git a/tests/st/test_utils/test_utils.py b/tests/st/test_utils/test_f.py similarity index 82% rename from tests/st/test_utils/test_utils.py rename to tests/st/test_utils/test_f.py index 9bbfb105347288c5c984564190bd626a87255502..b4bf0dbe07f1fe5e24825a1f014bf5f562e20d33 100755 --- a/tests/st/test_utils/test_utils.py +++ b/tests/st/test_utils/test_f.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,17 +16,7 @@ """Test utils.""" import numpy as np -from mindquantum.utils import bprint, f - - -def test_beauty_print(): - """Test beauty print""" - o = bprint(['Age:17', 'Name:Bob'], title='Info of Bob') - o_exp = [ - '|===Info of Bob===|', '| Age :17 |', '| Name:Bob |', - '===================' - ] - assert o == o_exp +from mindquantum.utils import f def test_mod(): diff --git a/tests/ut/__init__.py b/tests/ut/__init__.py index 6228b7132697d24157a4052193061e9913f031c4..ed94b4aa0914af596c080cb797ec71c7acaf863d 100644 --- a/tests/ut/__init__.py +++ b/tests/ut/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/ut/runtest.sh b/tests/ut/runtest.sh index 64f81874036c9fb952ab9392fd62564a097971d4..3de02643181a81c950ae78f9fdcfc7c34a21752e 100644 --- a/tests/ut/runtest.sh +++ b/tests/ut/runtest.sh @@ -26,4 +26,4 @@ run_test() { echo "Test all use cases success." } -run_test \ No newline at end of file +run_test diff --git a/tests/ut/test_mindquantum.py b/tests/ut/test_mindquantum.py index b121da4de10d609b57ccc73b5cd9108d115cab43..cd97c492bc9f631ecf6b7b852ba4603d1a8e3f77 100644 --- a/tests/ut/test_mindquantum.py +++ b/tests/ut/test_mindquantum.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/third_party/patch/projectq/projectq.patch001 b/third_party/patch/projectq/projectq.patch001 new file mode 100644 index 0000000000000000000000000000000000000000..89c810c84ce1b80f6c665234dd44fb07221da218 --- /dev/null +++ b/third_party/patch/projectq/projectq.patch001 @@ -0,0 +1,969 @@ +diff --color -aur ProjectQ-0.5.1/projectq/backends/_sim/_cppkernels/intrin/kernel1.hpp ProjectQ-0.5.1_new/projectq/backends/_sim/_cppkernels/intrin/kernel1.hpp +--- ProjectQ-0.5.1/projectq/backends/_sim/_cppkernels/intrin/kernel1.hpp 2020-06-05 21:07:57.000000000 +0800 ++++ ProjectQ-0.5.1_new/projectq/backends/_sim/_cppkernels/intrin/kernel1.hpp 2021-04-19 18:04:05.541802882 +0800 +@@ -17,18 +17,18 @@ + { + __m256d v[2]; + +- v[0] = load2(&psi[I]); +- v[1] = load2(&psi[I + d0]); ++ v[0] = load2(psi + 2 * I); ++ v[1] = load2(psi + 2 * (I + d0)); + +- _mm256_storeu2_m128d((double*)&psi[I + d0], (double*)&psi[I], add(mul(v[0], m[0], mt[0]), mul(v[1], m[1], mt[1]))); ++ _mm256_storeu2_m128d(psi + 2 * (I + d0), psi + 2 * I, add(mul(v[0], m[0], mt[0]), mul(v[1], m[1], mt[1]))); + + } + + // bit indices id[.] are given from high to low (e.g. control first for CNOT) + template +-void kernel(V &psi, unsigned id0, M const& m, std::size_t ctrlmask) ++void kernel(V &psi, unsigned id0, M const& m, std::size_t ctrlmask, unsigned len) + { +- std::size_t n = psi.size(); ++ std::size_t n = len; + std::size_t d0 = 1UL << id0; + + __m256d mm[] = {load(&m[0][0], &m[1][0]), load(&m[0][1], &m[1][1])}; +diff --color -aur ProjectQ-0.5.1/projectq/backends/_sim/_cppkernels/intrin/kernel2.hpp ProjectQ-0.5.1_new/projectq/backends/_sim/_cppkernels/intrin/kernel2.hpp +--- ProjectQ-0.5.1/projectq/backends/_sim/_cppkernels/intrin/kernel2.hpp 2020-06-05 21:07:57.000000000 +0800 ++++ ProjectQ-0.5.1_new/projectq/backends/_sim/_cppkernels/intrin/kernel2.hpp 2021-04-19 18:04:05.541802882 +0800 +@@ -17,21 +17,21 @@ + { + __m256d v[4]; + +- v[0] = load2(&psi[I]); +- v[1] = load2(&psi[I + d0]); +- v[2] = load2(&psi[I + d1]); +- v[3] = load2(&psi[I + d0 + d1]); ++ v[0] = load2(psi + 2 * I); ++ v[1] = load2(psi + 2 * (I + d0)); ++ v[2] = load2(psi + 2 * (I + d1)); ++ v[3] = load2(psi + 2 * (I + d0 + d1)); + +- _mm256_storeu2_m128d((double*)&psi[I + d0], (double*)&psi[I], add(mul(v[0], m[0], mt[0]), add(mul(v[1], m[1], mt[1]), add(mul(v[2], m[2], mt[2]), mul(v[3], m[3], mt[3]))))); +- _mm256_storeu2_m128d((double*)&psi[I + d0 + d1], (double*)&psi[I + d1], add(mul(v[0], m[4], mt[4]), add(mul(v[1], m[5], mt[5]), add(mul(v[2], m[6], mt[6]), mul(v[3], m[7], mt[7]))))); ++ _mm256_storeu2_m128d(psi + 2 * (I + d0), psi + 2 * I, add(mul(v[0], m[0], mt[0]), add(mul(v[1], m[1], mt[1]), add(mul(v[2], m[2], mt[2]), mul(v[3], m[3], mt[3]))))); ++ _mm256_storeu2_m128d(psi + 2 * (I + d0 + d1), psi + 2 * (I + d1), add(mul(v[0], m[4], mt[4]), add(mul(v[1], m[5], mt[5]), add(mul(v[2], m[6], mt[6]), mul(v[3], m[7], mt[7]))))); + + } + + // bit indices id[.] are given from high to low (e.g. control first for CNOT) + template +-void kernel(V &psi, unsigned id1, unsigned id0, M const& m, std::size_t ctrlmask) ++void kernel(V &psi, unsigned id1, unsigned id0, M const& m, std::size_t ctrlmask, unsigned len) + { +- std::size_t n = psi.size(); ++ std::size_t n = len; + std::size_t d0 = 1UL << id0; + std::size_t d1 = 1UL << id1; + +diff --color -aur ProjectQ-0.5.1/projectq/backends/_sim/_cppkernels/intrin/kernel3.hpp ProjectQ-0.5.1_new/projectq/backends/_sim/_cppkernels/intrin/kernel3.hpp +--- ProjectQ-0.5.1/projectq/backends/_sim/_cppkernels/intrin/kernel3.hpp 2020-06-05 21:07:57.000000000 +0800 ++++ ProjectQ-0.5.1_new/projectq/backends/_sim/_cppkernels/intrin/kernel3.hpp 2021-04-19 18:04:05.541802882 +0800 +@@ -17,10 +17,10 @@ + { + __m256d v[4]; + +- v[0] = load2(&psi[I]); +- v[1] = load2(&psi[I + d0]); +- v[2] = load2(&psi[I + d1]); +- v[3] = load2(&psi[I + d0 + d1]); ++ v[0] = load2(psi + 2 * I); ++ v[1] = load2(psi + 2 * (I + d0)); ++ v[2] = load2(psi + 2 * (I + d1)); ++ v[3] = load2(psi + 2 * (I + d0 + d1)); + + __m256d tmp[4]; + +@@ -29,23 +29,23 @@ + tmp[2] = add(mul(v[0], m[8], mt[8]), add(mul(v[1], m[9], mt[9]), add(mul(v[2], m[10], mt[10]), mul(v[3], m[11], mt[11])))); + tmp[3] = add(mul(v[0], m[12], mt[12]), add(mul(v[1], m[13], mt[13]), add(mul(v[2], m[14], mt[14]), mul(v[3], m[15], mt[15])))); + +- v[0] = load2(&psi[I + d2]); +- v[1] = load2(&psi[I + d0 + d2]); +- v[2] = load2(&psi[I + d1 + d2]); +- v[3] = load2(&psi[I + d0 + d1 + d2]); +- +- _mm256_storeu2_m128d((double*)&psi[I + d0], (double*)&psi[I], add(tmp[0], add(mul(v[0], m[16], mt[16]), add(mul(v[1], m[17], mt[17]), add(mul(v[2], m[18], mt[18]), mul(v[3], m[19], mt[19])))))); +- _mm256_storeu2_m128d((double*)&psi[I + d0 + d1], (double*)&psi[I + d1], add(tmp[1], add(mul(v[0], m[20], mt[20]), add(mul(v[1], m[21], mt[21]), add(mul(v[2], m[22], mt[22]), mul(v[3], m[23], mt[23])))))); +- _mm256_storeu2_m128d((double*)&psi[I + d0 + d2], (double*)&psi[I + d2], add(tmp[2], add(mul(v[0], m[24], mt[24]), add(mul(v[1], m[25], mt[25]), add(mul(v[2], m[26], mt[26]), mul(v[3], m[27], mt[27])))))); +- _mm256_storeu2_m128d((double*)&psi[I + d0 + d1 + d2], (double*)&psi[I + d1 + d2], add(tmp[3], add(mul(v[0], m[28], mt[28]), add(mul(v[1], m[29], mt[29]), add(mul(v[2], m[30], mt[30]), mul(v[3], m[31], mt[31])))))); ++ v[0] = load2(psi + 2 * (I + d2)); ++ v[1] = load2(psi + 2 * (I + d0 + d2)); ++ v[2] = load2(psi + 2 * (I + d1 + d2)); ++ v[3] = load2(psi + 2 * (I + d0 + d1 + d2)); ++ ++ _mm256_storeu2_m128d(psi + 2 * (I + d0), psi + 2 * I, add(tmp[0], add(mul(v[0], m[16], mt[16]), add(mul(v[1], m[17], mt[17]), add(mul(v[2], m[18], mt[18]), mul(v[3], m[19], mt[19])))))); ++ _mm256_storeu2_m128d(psi + 2 * (I + d0 + d1), psi + 2 * (I + d1), add(tmp[1], add(mul(v[0], m[20], mt[20]), add(mul(v[1], m[21], mt[21]), add(mul(v[2], m[22], mt[22]), mul(v[3], m[23], mt[23])))))); ++ _mm256_storeu2_m128d(psi + 2 * (I + d0 + d2), psi + 2 * (I + d2), add(tmp[2], add(mul(v[0], m[24], mt[24]), add(mul(v[1], m[25], mt[25]), add(mul(v[2], m[26], mt[26]), mul(v[3], m[27], mt[27])))))); ++ _mm256_storeu2_m128d(psi + 2 * (I + d0 + d1 + d2), psi + 2 * (I + d1 + d2), add(tmp[3], add(mul(v[0], m[28], mt[28]), add(mul(v[1], m[29], mt[29]), add(mul(v[2], m[30], mt[30]), mul(v[3], m[31], mt[31])))))); + + } + + // bit indices id[.] are given from high to low (e.g. control first for CNOT) + template +-void kernel(V &psi, unsigned id2, unsigned id1, unsigned id0, M const& m, std::size_t ctrlmask) ++void kernel(V &psi, unsigned id2, unsigned id1, unsigned id0, M const& m, std::size_t ctrlmask, unsigned len) + { +- std::size_t n = psi.size(); ++ std::size_t n = len; + std::size_t d0 = 1UL << id0; + std::size_t d1 = 1UL << id1; + std::size_t d2 = 1UL << id2; +diff --color -aur ProjectQ-0.5.1/projectq/backends/_sim/_cppkernels/intrin/kernel4.hpp ProjectQ-0.5.1_new/projectq/backends/_sim/_cppkernels/intrin/kernel4.hpp +--- ProjectQ-0.5.1/projectq/backends/_sim/_cppkernels/intrin/kernel4.hpp 2020-06-05 21:07:57.000000000 +0800 ++++ ProjectQ-0.5.1_new/projectq/backends/_sim/_cppkernels/intrin/kernel4.hpp 2021-04-19 18:04:05.541802882 +0800 +@@ -17,10 +17,10 @@ + { + __m256d v[4]; + +- v[0] = load2(&psi[I]); +- v[1] = load2(&psi[I + d0]); +- v[2] = load2(&psi[I + d1]); +- v[3] = load2(&psi[I + d0 + d1]); ++ v[0] = load2(psi + 2 * I); ++ v[1] = load2(psi + 2 * (I + d0)); ++ v[2] = load2(psi + 2 * (I + d1)); ++ v[3] = load2(psi + 2 * (I + d0 + d1)); + + __m256d tmp[8]; + +@@ -33,10 +33,10 @@ + tmp[6] = add(mul(v[0], m[24], mt[24]), add(mul(v[1], m[25], mt[25]), add(mul(v[2], m[26], mt[26]), mul(v[3], m[27], mt[27])))); + tmp[7] = add(mul(v[0], m[28], mt[28]), add(mul(v[1], m[29], mt[29]), add(mul(v[2], m[30], mt[30]), mul(v[3], m[31], mt[31])))); + +- v[0] = load2(&psi[I + d2]); +- v[1] = load2(&psi[I + d0 + d2]); +- v[2] = load2(&psi[I + d1 + d2]); +- v[3] = load2(&psi[I + d0 + d1 + d2]); ++ v[0] = load2(psi + 2 * (I + d2)); ++ v[1] = load2(psi + 2 * (I + d0 + d2)); ++ v[2] = load2(psi + 2 * (I + d1 + d2)); ++ v[3] = load2(psi + 2 * (I + d0 + d1 + d2)); + + tmp[0] = add(tmp[0], add(mul(v[0], m[32], mt[32]), add(mul(v[1], m[33], mt[33]), add(mul(v[2], m[34], mt[34]), mul(v[3], m[35], mt[35]))))); + tmp[1] = add(tmp[1], add(mul(v[0], m[36], mt[36]), add(mul(v[1], m[37], mt[37]), add(mul(v[2], m[38], mt[38]), mul(v[3], m[39], mt[39]))))); +@@ -47,10 +47,10 @@ + tmp[6] = add(tmp[6], add(mul(v[0], m[56], mt[56]), add(mul(v[1], m[57], mt[57]), add(mul(v[2], m[58], mt[58]), mul(v[3], m[59], mt[59]))))); + tmp[7] = add(tmp[7], add(mul(v[0], m[60], mt[60]), add(mul(v[1], m[61], mt[61]), add(mul(v[2], m[62], mt[62]), mul(v[3], m[63], mt[63]))))); + +- v[0] = load2(&psi[I + d3]); +- v[1] = load2(&psi[I + d0 + d3]); +- v[2] = load2(&psi[I + d1 + d3]); +- v[3] = load2(&psi[I + d0 + d1 + d3]); ++ v[0] = load2(psi + 2 * (I + d3)); ++ v[1] = load2(psi + 2 * (I + d0 + d3)); ++ v[2] = load2(psi + 2 * (I + d1 + d3)); ++ v[3] = load2(psi + 2 * (I + d0 + d1 + d3)); + + tmp[0] = add(tmp[0], add(mul(v[0], m[64], mt[64]), add(mul(v[1], m[65], mt[65]), add(mul(v[2], m[66], mt[66]), mul(v[3], m[67], mt[67]))))); + tmp[1] = add(tmp[1], add(mul(v[0], m[68], mt[68]), add(mul(v[1], m[69], mt[69]), add(mul(v[2], m[70], mt[70]), mul(v[3], m[71], mt[71]))))); +@@ -61,27 +61,27 @@ + tmp[6] = add(tmp[6], add(mul(v[0], m[88], mt[88]), add(mul(v[1], m[89], mt[89]), add(mul(v[2], m[90], mt[90]), mul(v[3], m[91], mt[91]))))); + tmp[7] = add(tmp[7], add(mul(v[0], m[92], mt[92]), add(mul(v[1], m[93], mt[93]), add(mul(v[2], m[94], mt[94]), mul(v[3], m[95], mt[95]))))); + +- v[0] = load2(&psi[I + d2 + d3]); +- v[1] = load2(&psi[I + d0 + d2 + d3]); +- v[2] = load2(&psi[I + d1 + d2 + d3]); +- v[3] = load2(&psi[I + d0 + d1 + d2 + d3]); +- +- _mm256_storeu2_m128d((double*)&psi[I + d0], (double*)&psi[I], add(tmp[0], add(mul(v[0], m[96], mt[96]), add(mul(v[1], m[97], mt[97]), add(mul(v[2], m[98], mt[98]), mul(v[3], m[99], mt[99])))))); +- _mm256_storeu2_m128d((double*)&psi[I + d0 + d1], (double*)&psi[I + d1], add(tmp[1], add(mul(v[0], m[100], mt[100]), add(mul(v[1], m[101], mt[101]), add(mul(v[2], m[102], mt[102]), mul(v[3], m[103], mt[103])))))); +- _mm256_storeu2_m128d((double*)&psi[I + d0 + d2], (double*)&psi[I + d2], add(tmp[2], add(mul(v[0], m[104], mt[104]), add(mul(v[1], m[105], mt[105]), add(mul(v[2], m[106], mt[106]), mul(v[3], m[107], mt[107])))))); +- _mm256_storeu2_m128d((double*)&psi[I + d0 + d1 + d2], (double*)&psi[I + d1 + d2], add(tmp[3], add(mul(v[0], m[108], mt[108]), add(mul(v[1], m[109], mt[109]), add(mul(v[2], m[110], mt[110]), mul(v[3], m[111], mt[111])))))); +- _mm256_storeu2_m128d((double*)&psi[I + d0 + d3], (double*)&psi[I + d3], add(tmp[4], add(mul(v[0], m[112], mt[112]), add(mul(v[1], m[113], mt[113]), add(mul(v[2], m[114], mt[114]), mul(v[3], m[115], mt[115])))))); +- _mm256_storeu2_m128d((double*)&psi[I + d0 + d1 + d3], (double*)&psi[I + d1 + d3], add(tmp[5], add(mul(v[0], m[116], mt[116]), add(mul(v[1], m[117], mt[117]), add(mul(v[2], m[118], mt[118]), mul(v[3], m[119], mt[119])))))); +- _mm256_storeu2_m128d((double*)&psi[I + d0 + d2 + d3], (double*)&psi[I + d2 + d3], add(tmp[6], add(mul(v[0], m[120], mt[120]), add(mul(v[1], m[121], mt[121]), add(mul(v[2], m[122], mt[122]), mul(v[3], m[123], mt[123])))))); +- _mm256_storeu2_m128d((double*)&psi[I + d0 + d1 + d2 + d3], (double*)&psi[I + d1 + d2 + d3], add(tmp[7], add(mul(v[0], m[124], mt[124]), add(mul(v[1], m[125], mt[125]), add(mul(v[2], m[126], mt[126]), mul(v[3], m[127], mt[127])))))); ++ v[0] = load2(psi + 2 * (I + d2 + d3)); ++ v[1] = load2(psi + 2 * (I + d0 + d2 + d3)); ++ v[2] = load2(psi + 2 * (I + d1 + d2 + d3)); ++ v[3] = load2(psi + 2 * (I + d0 + d1 + d2 + d3)); ++ ++ _mm256_storeu2_m128d(psi + 2 * (I + d0), psi + 2 * I, add(tmp[0], add(mul(v[0], m[96], mt[96]), add(mul(v[1], m[97], mt[97]), add(mul(v[2], m[98], mt[98]), mul(v[3], m[99], mt[99])))))); ++ _mm256_storeu2_m128d(psi + 2 * (I + d0 + d1), psi + 2 * (I + d1), add(tmp[1], add(mul(v[0], m[100], mt[100]), add(mul(v[1], m[101], mt[101]), add(mul(v[2], m[102], mt[102]), mul(v[3], m[103], mt[103])))))); ++ _mm256_storeu2_m128d(psi + 2 * (I + d0 + d2), psi + 2 * (I + d2), add(tmp[2], add(mul(v[0], m[104], mt[104]), add(mul(v[1], m[105], mt[105]), add(mul(v[2], m[106], mt[106]), mul(v[3], m[107], mt[107])))))); ++ _mm256_storeu2_m128d(psi + 2 * (I + d0 + d1 + d2), psi + 2 * (I + d1 + d2), add(tmp[3], add(mul(v[0], m[108], mt[108]), add(mul(v[1], m[109], mt[109]), add(mul(v[2], m[110], mt[110]), mul(v[3], m[111], mt[111])))))); ++ _mm256_storeu2_m128d(psi + 2 * (I + d0 + d3), psi + 2 * (I + d3), add(tmp[4], add(mul(v[0], m[112], mt[112]), add(mul(v[1], m[113], mt[113]), add(mul(v[2], m[114], mt[114]), mul(v[3], m[115], mt[115])))))); ++ _mm256_storeu2_m128d(psi + 2 * (I + d0 + d1 + d3), psi + 2 * (I + d1 + d3), add(tmp[5], add(mul(v[0], m[116], mt[116]), add(mul(v[1], m[117], mt[117]), add(mul(v[2], m[118], mt[118]), mul(v[3], m[119], mt[119])))))); ++ _mm256_storeu2_m128d(psi + 2 * (I + d0 + d2 + d3), psi + 2 * (I + d2 + d3), add(tmp[6], add(mul(v[0], m[120], mt[120]), add(mul(v[1], m[121], mt[121]), add(mul(v[2], m[122], mt[122]), mul(v[3], m[123], mt[123])))))); ++ _mm256_storeu2_m128d(psi + 2 * (I + d0 + d1 + d2 + d3), psi + 2 * (I + d1 + d2 + d3), add(tmp[7], add(mul(v[0], m[124], mt[124]), add(mul(v[1], m[125], mt[125]), add(mul(v[2], m[126], mt[126]), mul(v[3], m[127], mt[127])))))); + + } + + // bit indices id[.] are given from high to low (e.g. control first for CNOT) + template +-void kernel(V &psi, unsigned id3, unsigned id2, unsigned id1, unsigned id0, M const& m, std::size_t ctrlmask) ++void kernel(V &psi, unsigned id3, unsigned id2, unsigned id1, unsigned id0, M const& m, std::size_t ctrlmask, unsigned len) + { +- std::size_t n = psi.size(); ++ std::size_t n = len; + std::size_t d0 = 1UL << id0; + std::size_t d1 = 1UL << id1; + std::size_t d2 = 1UL << id2; +diff --color -aur ProjectQ-0.5.1/projectq/backends/_sim/_cppkernels/intrin/kernel5.hpp ProjectQ-0.5.1_new/projectq/backends/_sim/_cppkernels/intrin/kernel5.hpp +--- ProjectQ-0.5.1/projectq/backends/_sim/_cppkernels/intrin/kernel5.hpp 2020-06-05 21:07:57.000000000 +0800 ++++ ProjectQ-0.5.1_new/projectq/backends/_sim/_cppkernels/intrin/kernel5.hpp 2021-04-19 18:04:05.541802882 +0800 +@@ -17,10 +17,10 @@ + { + __m256d v[4]; + +- v[0] = load2(&psi[I]); +- v[1] = load2(&psi[I + d0]); +- v[2] = load2(&psi[I + d1]); +- v[3] = load2(&psi[I + d0 + d1]); ++ v[0] = load2(psi + 2 * I); ++ v[1] = load2(psi + 2 * (I + d0)); ++ v[2] = load2(psi + 2 * (I + d1)); ++ v[3] = load2(psi + 2 * (I + d0 + d1)); + + __m256d tmp[16]; + +@@ -41,10 +41,10 @@ + tmp[14] = add(mul(v[0], m[56], mt[56]), add(mul(v[1], m[57], mt[57]), add(mul(v[2], m[58], mt[58]), mul(v[3], m[59], mt[59])))); + tmp[15] = add(mul(v[0], m[60], mt[60]), add(mul(v[1], m[61], mt[61]), add(mul(v[2], m[62], mt[62]), mul(v[3], m[63], mt[63])))); + +- v[0] = load2(&psi[I + d2]); +- v[1] = load2(&psi[I + d0 + d2]); +- v[2] = load2(&psi[I + d1 + d2]); +- v[3] = load2(&psi[I + d0 + d1 + d2]); ++ v[0] = load2(psi + 2 * (I + d2)); ++ v[1] = load2(psi + 2 * (I + d0 + d2)); ++ v[2] = load2(psi + 2 * (I + d1 + d2)); ++ v[3] = load2(psi + 2 * (I + d0 + d1 + d2)); + + tmp[0] = add(tmp[0], add(mul(v[0], m[64], mt[64]), add(mul(v[1], m[65], mt[65]), add(mul(v[2], m[66], mt[66]), mul(v[3], m[67], mt[67]))))); + tmp[1] = add(tmp[1], add(mul(v[0], m[68], mt[68]), add(mul(v[1], m[69], mt[69]), add(mul(v[2], m[70], mt[70]), mul(v[3], m[71], mt[71]))))); +@@ -63,10 +63,10 @@ + tmp[14] = add(tmp[14], add(mul(v[0], m[120], mt[120]), add(mul(v[1], m[121], mt[121]), add(mul(v[2], m[122], mt[122]), mul(v[3], m[123], mt[123]))))); + tmp[15] = add(tmp[15], add(mul(v[0], m[124], mt[124]), add(mul(v[1], m[125], mt[125]), add(mul(v[2], m[126], mt[126]), mul(v[3], m[127], mt[127]))))); + +- v[0] = load2(&psi[I + d3]); +- v[1] = load2(&psi[I + d0 + d3]); +- v[2] = load2(&psi[I + d1 + d3]); +- v[3] = load2(&psi[I + d0 + d1 + d3]); ++ v[0] = load2(psi + 2 * (I + d3)); ++ v[1] = load2(psi + 2 * (I + d0 + d3)); ++ v[2] = load2(psi + 2 * (I + d1 + d3)); ++ v[3] = load2(psi + 2 * (I + d0 + d1 + d3)); + + tmp[0] = add(tmp[0], add(mul(v[0], m[128], mt[128]), add(mul(v[1], m[129], mt[129]), add(mul(v[2], m[130], mt[130]), mul(v[3], m[131], mt[131]))))); + tmp[1] = add(tmp[1], add(mul(v[0], m[132], mt[132]), add(mul(v[1], m[133], mt[133]), add(mul(v[2], m[134], mt[134]), mul(v[3], m[135], mt[135]))))); +@@ -85,10 +85,10 @@ + tmp[14] = add(tmp[14], add(mul(v[0], m[184], mt[184]), add(mul(v[1], m[185], mt[185]), add(mul(v[2], m[186], mt[186]), mul(v[3], m[187], mt[187]))))); + tmp[15] = add(tmp[15], add(mul(v[0], m[188], mt[188]), add(mul(v[1], m[189], mt[189]), add(mul(v[2], m[190], mt[190]), mul(v[3], m[191], mt[191]))))); + +- v[0] = load2(&psi[I + d2 + d3]); +- v[1] = load2(&psi[I + d0 + d2 + d3]); +- v[2] = load2(&psi[I + d1 + d2 + d3]); +- v[3] = load2(&psi[I + d0 + d1 + d2 + d3]); ++ v[0] = load2(psi + 2 * (I + d2 + d3)); ++ v[1] = load2(psi + 2 * (I + d0 + d2 + d3)); ++ v[2] = load2(psi + 2 * (I + d1 + d2 + d3)); ++ v[3] = load2(psi + 2 * (I + d0 + d1 + d2 + d3)); + + tmp[0] = add(tmp[0], add(mul(v[0], m[192], mt[192]), add(mul(v[1], m[193], mt[193]), add(mul(v[2], m[194], mt[194]), mul(v[3], m[195], mt[195]))))); + tmp[1] = add(tmp[1], add(mul(v[0], m[196], mt[196]), add(mul(v[1], m[197], mt[197]), add(mul(v[2], m[198], mt[198]), mul(v[3], m[199], mt[199]))))); +@@ -107,10 +107,10 @@ + tmp[14] = add(tmp[14], add(mul(v[0], m[248], mt[248]), add(mul(v[1], m[249], mt[249]), add(mul(v[2], m[250], mt[250]), mul(v[3], m[251], mt[251]))))); + tmp[15] = add(tmp[15], add(mul(v[0], m[252], mt[252]), add(mul(v[1], m[253], mt[253]), add(mul(v[2], m[254], mt[254]), mul(v[3], m[255], mt[255]))))); + +- v[0] = load2(&psi[I + d4]); +- v[1] = load2(&psi[I + d0 + d4]); +- v[2] = load2(&psi[I + d1 + d4]); +- v[3] = load2(&psi[I + d0 + d1 + d4]); ++ v[0] = load2(psi + 2 * (I + d4)); ++ v[1] = load2(psi + 2 * (I + d0 + d4)); ++ v[2] = load2(psi + 2 * (I + d1 + d4)); ++ v[3] = load2(psi + 2 * (I + d0 + d1 + d4)); + + tmp[0] = add(tmp[0], add(mul(v[0], m[256], mt[256]), add(mul(v[1], m[257], mt[257]), add(mul(v[2], m[258], mt[258]), mul(v[3], m[259], mt[259]))))); + tmp[1] = add(tmp[1], add(mul(v[0], m[260], mt[260]), add(mul(v[1], m[261], mt[261]), add(mul(v[2], m[262], mt[262]), mul(v[3], m[263], mt[263]))))); +@@ -129,10 +129,10 @@ + tmp[14] = add(tmp[14], add(mul(v[0], m[312], mt[312]), add(mul(v[1], m[313], mt[313]), add(mul(v[2], m[314], mt[314]), mul(v[3], m[315], mt[315]))))); + tmp[15] = add(tmp[15], add(mul(v[0], m[316], mt[316]), add(mul(v[1], m[317], mt[317]), add(mul(v[2], m[318], mt[318]), mul(v[3], m[319], mt[319]))))); + +- v[0] = load2(&psi[I + d2 + d4]); +- v[1] = load2(&psi[I + d0 + d2 + d4]); +- v[2] = load2(&psi[I + d1 + d2 + d4]); +- v[3] = load2(&psi[I + d0 + d1 + d2 + d4]); ++ v[0] = load2(psi + 2 * (I + d2 + d4)); ++ v[1] = load2(psi + 2 * (I + d0 + d2 + d4)); ++ v[2] = load2(psi + 2 * (I + d1 + d2 + d4)); ++ v[3] = load2(psi + 2 * (I + d0 + d1 + d2 + d4)); + + tmp[0] = add(tmp[0], add(mul(v[0], m[320], mt[320]), add(mul(v[1], m[321], mt[321]), add(mul(v[2], m[322], mt[322]), mul(v[3], m[323], mt[323]))))); + tmp[1] = add(tmp[1], add(mul(v[0], m[324], mt[324]), add(mul(v[1], m[325], mt[325]), add(mul(v[2], m[326], mt[326]), mul(v[3], m[327], mt[327]))))); +@@ -151,10 +151,10 @@ + tmp[14] = add(tmp[14], add(mul(v[0], m[376], mt[376]), add(mul(v[1], m[377], mt[377]), add(mul(v[2], m[378], mt[378]), mul(v[3], m[379], mt[379]))))); + tmp[15] = add(tmp[15], add(mul(v[0], m[380], mt[380]), add(mul(v[1], m[381], mt[381]), add(mul(v[2], m[382], mt[382]), mul(v[3], m[383], mt[383]))))); + +- v[0] = load2(&psi[I + d3 + d4]); +- v[1] = load2(&psi[I + d0 + d3 + d4]); +- v[2] = load2(&psi[I + d1 + d3 + d4]); +- v[3] = load2(&psi[I + d0 + d1 + d3 + d4]); ++ v[0] = load2(psi + 2 * (I + d3 + d4)); ++ v[1] = load2(psi + 2 * (I + d0 + d3 + d4)); ++ v[2] = load2(psi + 2 * (I + d1 + d3 + d4)); ++ v[3] = load2(psi + 2 * (I + d0 + d1 + d3 + d4)); + + tmp[0] = add(tmp[0], add(mul(v[0], m[384], mt[384]), add(mul(v[1], m[385], mt[385]), add(mul(v[2], m[386], mt[386]), mul(v[3], m[387], mt[387]))))); + tmp[1] = add(tmp[1], add(mul(v[0], m[388], mt[388]), add(mul(v[1], m[389], mt[389]), add(mul(v[2], m[390], mt[390]), mul(v[3], m[391], mt[391]))))); +@@ -173,35 +173,35 @@ + tmp[14] = add(tmp[14], add(mul(v[0], m[440], mt[440]), add(mul(v[1], m[441], mt[441]), add(mul(v[2], m[442], mt[442]), mul(v[3], m[443], mt[443]))))); + tmp[15] = add(tmp[15], add(mul(v[0], m[444], mt[444]), add(mul(v[1], m[445], mt[445]), add(mul(v[2], m[446], mt[446]), mul(v[3], m[447], mt[447]))))); + +- v[0] = load2(&psi[I + d2 + d3 + d4]); +- v[1] = load2(&psi[I + d0 + d2 + d3 + d4]); +- v[2] = load2(&psi[I + d1 + d2 + d3 + d4]); +- v[3] = load2(&psi[I + d0 + d1 + d2 + d3 + d4]); +- +- _mm256_storeu2_m128d((double*)&psi[I + d0], (double*)&psi[I], add(tmp[0], add(mul(v[0], m[448], mt[448]), add(mul(v[1], m[449], mt[449]), add(mul(v[2], m[450], mt[450]), mul(v[3], m[451], mt[451])))))); +- _mm256_storeu2_m128d((double*)&psi[I + d0 + d1], (double*)&psi[I + d1], add(tmp[1], add(mul(v[0], m[452], mt[452]), add(mul(v[1], m[453], mt[453]), add(mul(v[2], m[454], mt[454]), mul(v[3], m[455], mt[455])))))); +- _mm256_storeu2_m128d((double*)&psi[I + d0 + d2], (double*)&psi[I + d2], add(tmp[2], add(mul(v[0], m[456], mt[456]), add(mul(v[1], m[457], mt[457]), add(mul(v[2], m[458], mt[458]), mul(v[3], m[459], mt[459])))))); +- _mm256_storeu2_m128d((double*)&psi[I + d0 + d1 + d2], (double*)&psi[I + d1 + d2], add(tmp[3], add(mul(v[0], m[460], mt[460]), add(mul(v[1], m[461], mt[461]), add(mul(v[2], m[462], mt[462]), mul(v[3], m[463], mt[463])))))); +- _mm256_storeu2_m128d((double*)&psi[I + d0 + d3], (double*)&psi[I + d3], add(tmp[4], add(mul(v[0], m[464], mt[464]), add(mul(v[1], m[465], mt[465]), add(mul(v[2], m[466], mt[466]), mul(v[3], m[467], mt[467])))))); +- _mm256_storeu2_m128d((double*)&psi[I + d0 + d1 + d3], (double*)&psi[I + d1 + d3], add(tmp[5], add(mul(v[0], m[468], mt[468]), add(mul(v[1], m[469], mt[469]), add(mul(v[2], m[470], mt[470]), mul(v[3], m[471], mt[471])))))); +- _mm256_storeu2_m128d((double*)&psi[I + d0 + d2 + d3], (double*)&psi[I + d2 + d3], add(tmp[6], add(mul(v[0], m[472], mt[472]), add(mul(v[1], m[473], mt[473]), add(mul(v[2], m[474], mt[474]), mul(v[3], m[475], mt[475])))))); +- _mm256_storeu2_m128d((double*)&psi[I + d0 + d1 + d2 + d3], (double*)&psi[I + d1 + d2 + d3], add(tmp[7], add(mul(v[0], m[476], mt[476]), add(mul(v[1], m[477], mt[477]), add(mul(v[2], m[478], mt[478]), mul(v[3], m[479], mt[479])))))); +- _mm256_storeu2_m128d((double*)&psi[I + d0 + d4], (double*)&psi[I + d4], add(tmp[8], add(mul(v[0], m[480], mt[480]), add(mul(v[1], m[481], mt[481]), add(mul(v[2], m[482], mt[482]), mul(v[3], m[483], mt[483])))))); +- _mm256_storeu2_m128d((double*)&psi[I + d0 + d1 + d4], (double*)&psi[I + d1 + d4], add(tmp[9], add(mul(v[0], m[484], mt[484]), add(mul(v[1], m[485], mt[485]), add(mul(v[2], m[486], mt[486]), mul(v[3], m[487], mt[487])))))); +- _mm256_storeu2_m128d((double*)&psi[I + d0 + d2 + d4], (double*)&psi[I + d2 + d4], add(tmp[10], add(mul(v[0], m[488], mt[488]), add(mul(v[1], m[489], mt[489]), add(mul(v[2], m[490], mt[490]), mul(v[3], m[491], mt[491])))))); +- _mm256_storeu2_m128d((double*)&psi[I + d0 + d1 + d2 + d4], (double*)&psi[I + d1 + d2 + d4], add(tmp[11], add(mul(v[0], m[492], mt[492]), add(mul(v[1], m[493], mt[493]), add(mul(v[2], m[494], mt[494]), mul(v[3], m[495], mt[495])))))); +- _mm256_storeu2_m128d((double*)&psi[I + d0 + d3 + d4], (double*)&psi[I + d3 + d4], add(tmp[12], add(mul(v[0], m[496], mt[496]), add(mul(v[1], m[497], mt[497]), add(mul(v[2], m[498], mt[498]), mul(v[3], m[499], mt[499])))))); +- _mm256_storeu2_m128d((double*)&psi[I + d0 + d1 + d3 + d4], (double*)&psi[I + d1 + d3 + d4], add(tmp[13], add(mul(v[0], m[500], mt[500]), add(mul(v[1], m[501], mt[501]), add(mul(v[2], m[502], mt[502]), mul(v[3], m[503], mt[503])))))); +- _mm256_storeu2_m128d((double*)&psi[I + d0 + d2 + d3 + d4], (double*)&psi[I + d2 + d3 + d4], add(tmp[14], add(mul(v[0], m[504], mt[504]), add(mul(v[1], m[505], mt[505]), add(mul(v[2], m[506], mt[506]), mul(v[3], m[507], mt[507])))))); +- _mm256_storeu2_m128d((double*)&psi[I + d0 + d1 + d2 + d3 + d4], (double*)&psi[I + d1 + d2 + d3 + d4], add(tmp[15], add(mul(v[0], m[508], mt[508]), add(mul(v[1], m[509], mt[509]), add(mul(v[2], m[510], mt[510]), mul(v[3], m[511], mt[511])))))); ++ v[0] = load2(psi + 2 * (I + d2 + d3 + d4)); ++ v[1] = load2(psi + 2 * (I + d0 + d2 + d3 + d4)); ++ v[2] = load2(psi + 2 * (I + d1 + d2 + d3 + d4)); ++ v[3] = load2(psi + 2 * (I + d0 + d1 + d2 + d3 + d4)); ++ ++ _mm256_storeu2_m128d(psi + 2 * (I + d0), psi + 2 * I, add(tmp[0], add(mul(v[0], m[448], mt[448]), add(mul(v[1], m[449], mt[449]), add(mul(v[2], m[450], mt[450]), mul(v[3], m[451], mt[451])))))); ++ _mm256_storeu2_m128d(psi + 2 * (I + d0 + d1), psi + 2 * (I + d1), add(tmp[1], add(mul(v[0], m[452], mt[452]), add(mul(v[1], m[453], mt[453]), add(mul(v[2], m[454], mt[454]), mul(v[3], m[455], mt[455])))))); ++ _mm256_storeu2_m128d(psi + 2 * (I + d0 + d2), psi + 2 * (I + d2), add(tmp[2], add(mul(v[0], m[456], mt[456]), add(mul(v[1], m[457], mt[457]), add(mul(v[2], m[458], mt[458]), mul(v[3], m[459], mt[459])))))); ++ _mm256_storeu2_m128d(psi + 2 * (I + d0 + d1 + d2), psi + 2 * (I + d1 + d2), add(tmp[3], add(mul(v[0], m[460], mt[460]), add(mul(v[1], m[461], mt[461]), add(mul(v[2], m[462], mt[462]), mul(v[3], m[463], mt[463])))))); ++ _mm256_storeu2_m128d(psi + 2 * (I + d0 + d3), psi + 2 * (I + d3), add(tmp[4], add(mul(v[0], m[464], mt[464]), add(mul(v[1], m[465], mt[465]), add(mul(v[2], m[466], mt[466]), mul(v[3], m[467], mt[467])))))); ++ _mm256_storeu2_m128d(psi + 2 * (I + d0 + d1 + d3), psi + 2 * (I + d1 + d3), add(tmp[5], add(mul(v[0], m[468], mt[468]), add(mul(v[1], m[469], mt[469]), add(mul(v[2], m[470], mt[470]), mul(v[3], m[471], mt[471])))))); ++ _mm256_storeu2_m128d(psi + 2 * (I + d0 + d2 + d3), psi + 2 * (I + d2 + d3), add(tmp[6], add(mul(v[0], m[472], mt[472]), add(mul(v[1], m[473], mt[473]), add(mul(v[2], m[474], mt[474]), mul(v[3], m[475], mt[475])))))); ++ _mm256_storeu2_m128d(psi + 2 * (I + d0 + d1 + d2 + d3), psi + 2 * (I + d1 + d2 + d3), add(tmp[7], add(mul(v[0], m[476], mt[476]), add(mul(v[1], m[477], mt[477]), add(mul(v[2], m[478], mt[478]), mul(v[3], m[479], mt[479])))))); ++ _mm256_storeu2_m128d(psi + 2 * (I + d0 + d4), psi + 2 * (I + d4), add(tmp[8], add(mul(v[0], m[480], mt[480]), add(mul(v[1], m[481], mt[481]), add(mul(v[2], m[482], mt[482]), mul(v[3], m[483], mt[483])))))); ++ _mm256_storeu2_m128d(psi + 2 * (I + d0 + d1 + d4), psi + 2 * (I + d1 + d4), add(tmp[9], add(mul(v[0], m[484], mt[484]), add(mul(v[1], m[485], mt[485]), add(mul(v[2], m[486], mt[486]), mul(v[3], m[487], mt[487])))))); ++ _mm256_storeu2_m128d(psi + 2 * (I + d0 + d2 + d4), psi + 2 * (I + d2 + d4), add(tmp[10], add(mul(v[0], m[488], mt[488]), add(mul(v[1], m[489], mt[489]), add(mul(v[2], m[490], mt[490]), mul(v[3], m[491], mt[491])))))); ++ _mm256_storeu2_m128d(psi + 2 * (I + d0 + d1 + d2 + d4), psi + 2 * (I + d1 + d2 + d4), add(tmp[11], add(mul(v[0], m[492], mt[492]), add(mul(v[1], m[493], mt[493]), add(mul(v[2], m[494], mt[494]), mul(v[3], m[495], mt[495])))))); ++ _mm256_storeu2_m128d(psi + 2 * (I + d0 + d3 + d4), psi + 2 * (I + d3 + d4), add(tmp[12], add(mul(v[0], m[496], mt[496]), add(mul(v[1], m[497], mt[497]), add(mul(v[2], m[498], mt[498]), mul(v[3], m[499], mt[499])))))); ++ _mm256_storeu2_m128d(psi + 2 * (I + d0 + d1 + d3 + d4), psi + 2 * (I + d1 + d3 + d4), add(tmp[13], add(mul(v[0], m[500], mt[500]), add(mul(v[1], m[501], mt[501]), add(mul(v[2], m[502], mt[502]), mul(v[3], m[503], mt[503])))))); ++ _mm256_storeu2_m128d(psi + 2 * (I + d0 + d2 + d3 + d4), psi + 2 * (I + d2 + d3 + d4), add(tmp[14], add(mul(v[0], m[504], mt[504]), add(mul(v[1], m[505], mt[505]), add(mul(v[2], m[506], mt[506]), mul(v[3], m[507], mt[507])))))); ++ _mm256_storeu2_m128d(psi + 2 * (I + d0 + d1 + d2 + d3 + d4), psi + 2 * (I + d1 + d2 + d3 + d4), add(tmp[15], add(mul(v[0], m[508], mt[508]), add(mul(v[1], m[509], mt[509]), add(mul(v[2], m[510], mt[510]), mul(v[3], m[511], mt[511])))))); + + } + + // bit indices id[.] are given from high to low (e.g. control first for CNOT) + template +-void kernel(V &psi, unsigned id4, unsigned id3, unsigned id2, unsigned id1, unsigned id0, M const& m, std::size_t ctrlmask) ++void kernel(V &psi, unsigned id4, unsigned id3, unsigned id2, unsigned id1, unsigned id0, M const& m, std::size_t ctrlmask, unsigned len) + { +- std::size_t n = psi.size(); ++ std::size_t n = len; + std::size_t d0 = 1UL << id0; + std::size_t d1 = 1UL << id1; + std::size_t d2 = 1UL << id2; +diff --color -aur ProjectQ-0.5.1/projectq/backends/_sim/_cppkernels/simulator.hpp ProjectQ-0.5.1_new/projectq/backends/_sim/_cppkernels/simulator.hpp +--- ProjectQ-0.5.1/projectq/backends/_sim/_cppkernels/simulator.hpp 2020-06-05 21:07:57.000000000 +0800 ++++ ProjectQ-0.5.1_new/projectq/backends/_sim/_cppkernels/simulator.hpp 2021-04-20 11:46:27.115554725 +0800 +@@ -18,11 +18,7 @@ + #include + #include + +-#if defined(NOINTRIN) || !defined(INTRIN) +-#include "nointrin/kernels.hpp" +-#else + #include "intrin/kernels.hpp" +-#endif + + #include "intrin/alignedallocator.hpp" + #include "fusion.hpp" +@@ -32,173 +28,29 @@ + #include + #include + #include +- ++#include + + class Simulator{ + public: + using calc_type = double; + using complex_type = std::complex; +- using StateVector = std::vector>; ++ using StateVector = calc_type *; + using Map = std::map; + using RndEngine = std::mt19937; + using Term = std::vector>; + using TermsDict = std::vector>; + using ComplexTermsDict = std::vector>; ++ StateVector vec_; + +- Simulator(unsigned seed = 1) : N_(0), vec_(1,0.), fusion_qubits_min_(4), ++ Simulator(unsigned seed = 1, unsigned N = 0) : N_(N), fusion_qubits_min_(4), + fusion_qubits_max_(5), rnd_eng_(seed) { ++ len_ = 1UL << (N_ + 1); ++ vec_ = (StateVector)calloc(len_, sizeof(calc_type)); + vec_[0]=1.; // all-zero initial state + std::uniform_real_distribution dist(0., 1.); + rng_ = std::bind(dist, std::ref(rnd_eng_)); +- } +- +- void allocate_qubit(unsigned id){ +- if (map_.count(id) == 0){ +- map_[id] = N_++; +- StateVector newvec; // avoid large memory allocations +- if( tmpBuff1_.capacity() >= (1UL << N_) ) +- std::swap(newvec, tmpBuff1_); +- newvec.resize(1UL << N_); +-#pragma omp parallel for schedule(static) +- for (std::size_t i = 0; i < newvec.size(); ++i) +- newvec[i] = (i < vec_.size())?vec_[i]:0.; +- std::swap(vec_, newvec); +- // recycle large memory +- std::swap(tmpBuff1_, newvec); +- if( tmpBuff1_.capacity() < tmpBuff2_.capacity() ) +- std::swap(tmpBuff1_, tmpBuff2_); +- } +- else +- throw(std::runtime_error( +- "AllocateQubit: ID already exists. Qubit IDs should be unique.")); +- } +- +- bool get_classical_value(unsigned id, calc_type tol = 1.e-12){ +- run(); +- unsigned pos = map_[id]; +- std::size_t delta = (1UL << pos); +- +- for (std::size_t i = 0; i < vec_.size(); i += 2*delta){ +- for (std::size_t j = 0; j < delta; ++j){ +- if (std::norm(vec_[i+j]) > tol) +- return false; +- if (std::norm(vec_[i+j+delta]) > tol) +- return true; +- } +- } +- assert(false); // this will never happen +- return false; // suppress 'control reaches end of non-void...' +- } +- +- bool is_classical(unsigned id, calc_type tol = 1.e-12){ +- run(); +- unsigned pos = map_[id]; +- std::size_t delta = (1UL << pos); +- +- short up = 0, down = 0; +- #pragma omp parallel for schedule(static) reduction(|:up,down) +- for (std::size_t i = 0; i < vec_.size(); i += 2*delta){ +- for (std::size_t j = 0; j < delta; ++j){ +- up = up | ((std::norm(vec_[i+j]) > tol)&1); +- down = down | ((std::norm(vec_[i+j+delta]) > tol)&1); +- } +- } +- +- return 1 == (up^down); +- } +- +- void collapse_vector(unsigned id, bool value = false, bool shrink = false){ +- run(); +- unsigned pos = map_[id]; +- std::size_t delta = (1UL << pos); +- +- if (!shrink){ +- #pragma omp parallel for schedule(static) +- for (std::size_t i = 0; i < vec_.size(); i += 2*delta){ +- for (std::size_t j = 0; j < delta; ++j) +- vec_[i+j+static_cast(!value)*delta] = 0.; +- } +- } +- else{ +- StateVector newvec; // avoid costly memory reallocations +- if( tmpBuff1_.capacity() >= (1UL << (N_-1)) ) +- std::swap(tmpBuff1_, newvec); +- newvec.resize((1UL << (N_-1))); +- #pragma omp parallel for schedule(static) if(0) +- for (std::size_t i = 0; i < vec_.size(); i += 2*delta) +- std::copy_n(&vec_[i + static_cast(value)*delta], +- delta, &newvec[i/2]); +- std::swap(vec_, newvec); +- std::swap(tmpBuff1_, newvec); +- if( tmpBuff1_.capacity() < tmpBuff2_.capacity() ) +- std::swap(tmpBuff1_, tmpBuff2_); +- +- for (auto& p : map_){ +- if (p.second > pos) +- p.second--; +- } +- map_.erase(id); +- N_--; +- } +- } +- +- void measure_qubits(std::vector const& ids, std::vector &res){ +- run(); +- +- std::vector positions(ids.size()); +- for (unsigned i = 0; i < ids.size(); ++i) +- positions[i] = map_[ids[i]]; +- +- calc_type P = 0.; +- calc_type rnd = rng_(); +- +- // pick entry at random with probability |entry|^2 +- std::size_t pick = 0; +- while (P < rnd && pick < vec_.size()) +- P += std::norm(vec_[pick++]); +- +- pick--; +- // determine result vector (boolean values for each qubit) +- // and create mask to detect bad entries (i.e., entries that don't agree with measurement) +- res = std::vector(ids.size()); +- std::size_t mask = 0; +- std::size_t val = 0; +- for (unsigned i = 0; i < ids.size(); ++i){ +- bool r = ((pick >> positions[i]) & 1) == 1; +- res[i] = r; +- mask |= (1UL << positions[i]); +- val |= (static_cast(r&1) << positions[i]); +- } +- // set bad entries to 0 +- calc_type N = 0.; +- #pragma omp parallel for reduction(+:N) schedule(static) +- for (std::size_t i = 0; i < vec_.size(); ++i){ +- if ((i & mask) != val) +- vec_[i] = 0.; +- else +- N += std::norm(vec_[i]); +- } +- // re-normalize +- N = 1./std::sqrt(N); +- #pragma omp parallel for schedule(static) +- for (std::size_t i = 0; i < vec_.size(); ++i) +- vec_[i] *= N; +- } +- +- std::vector measure_qubits_return(std::vector const& ids){ +- std::vector ret; +- measure_qubits(ids, ret); +- return ret; +- } +- +- void deallocate_qubit(unsigned id){ +- run(); +- assert(map_.count(id) == 1); +- if (!is_classical(id)) +- throw(std::runtime_error("Error: Qubit has not been measured / uncomputed! There is most likely a bug in your code.")); +- +- bool value = get_classical_value(id); +- collapse_vector(id, value, true); ++ for (unsigned i = 0; i < N_; i++) ++ map_[i] = i; + } + + template +@@ -221,84 +73,13 @@ + fused_gates_ = fused_gates; + } + +- template +- void emulate_math(F const& f, QuReg quregs, const std::vector& ctrl, +- bool parallelize = false){ +- run(); +- auto ctrlmask = get_control_mask(ctrl); +- +- for (unsigned i = 0; i < quregs.size(); ++i) +- for (unsigned j = 0; j < quregs[i].size(); ++j) +- quregs[i][j] = map_[quregs[i][j]]; +- +- StateVector newvec; // avoid costly memory reallocations +- if( tmpBuff1_.capacity() >= vec_.size() ) +- std::swap(newvec, tmpBuff1_); +- newvec.resize(vec_.size()); +-#pragma omp parallel for schedule(static) +- for (std::size_t i = 0; i < vec_.size(); i++) +- newvec[i] = 0; +- +-//#pragma omp parallel reduction(+:newvec[:newvec.size()]) if(parallelize) // requires OpenMP 4.5 +- { +- std::vector res(quregs.size()); +- //#pragma omp for schedule(static) +- for (std::size_t i = 0; i < vec_.size(); ++i){ +- if ((ctrlmask&i) == ctrlmask){ +- for (unsigned qr_i = 0; qr_i < quregs.size(); ++qr_i){ +- res[qr_i] = 0; +- for (unsigned qb_i = 0; qb_i < quregs[qr_i].size(); ++qb_i) +- res[qr_i] |= ((i >> quregs[qr_i][qb_i])&1) << qb_i; +- } +- f(res); +- auto new_i = i; +- for (unsigned qr_i = 0; qr_i < quregs.size(); ++qr_i){ +- for (unsigned qb_i = 0; qb_i < quregs[qr_i].size(); ++qb_i){ +- if (!(((new_i >> quregs[qr_i][qb_i])&1) == ((res[qr_i] >> qb_i)&1))) +- new_i ^= (1UL << quregs[qr_i][qb_i]); +- } +- } +- newvec[new_i] += vec_[i]; +- } +- else +- newvec[i] += vec_[i]; +- } +- } +- std::swap(vec_, newvec); +- std::swap(tmpBuff1_, newvec); +- } +- +- // faster version without calling python +- template +- inline void emulate_math_addConstant(int a, const QuReg& quregs, const std::vector& ctrl) +- { +- emulate_math([a](std::vector &res){for(auto& x: res) x = x + a;}, quregs, ctrl, true); +- } +- +- // faster version without calling python +- template +- inline void emulate_math_addConstantModN(int a, int N, const QuReg& quregs, const std::vector& ctrl) +- { +- emulate_math([a,N](std::vector &res){for(auto& x: res) x = (x + a) % N;}, quregs, ctrl, true); +- } +- +- // faster version without calling python +- template +- inline void emulate_math_multiplyByConstantModN(int a, int N, const QuReg& quregs, const std::vector& ctrl) +- { +- emulate_math([a,N](std::vector &res){for(auto& x: res) x = (x * a) % N;}, quregs, ctrl, true); +- } +- + calc_type get_expectation_value(TermsDict const& td, std::vector const& ids){ + run(); + calc_type expectation = 0.; + +- StateVector current_state; // avoid costly memory reallocations +- if( tmpBuff1_.capacity() >= vec_.size() ) +- std::swap(tmpBuff1_, current_state); +- current_state.resize(vec_.size()); ++ StateVector current_state = (StateVector)malloc(len_ *sizeof(calc_type)); + #pragma omp parallel for schedule(static) +- for (std::size_t i = 0; i < vec_.size(); ++i) ++ for (std::size_t i = 0; i < len_; ++i) + current_state[i] = vec_[i]; + + for (auto const& term : td){ +@@ -306,81 +87,53 @@ + apply_term(term.first, ids, {}); + calc_type delta = 0.; + #pragma omp parallel for reduction(+:delta) schedule(static) +- for (std::size_t i = 0; i < vec_.size(); ++i){ +- auto const a1 = std::real(current_state[i]); +- auto const b1 = -std::imag(current_state[i]); +- auto const a2 = std::real(vec_[i]); +- auto const b2 = std::imag(vec_[i]); ++ for (std::size_t i = 0; i < (len_ >> 1); ++i){ ++ auto const a1 = current_state[2 * i]; ++ auto const b1 = -current_state[2 * i + 1]; ++ auto const a2 = vec_[2 * i]; ++ auto const b2 = vec_[2 * i + 1]; + delta += a1 * a2 - b1 * b2; + // reset vec_ +- vec_[i] = current_state[i]; ++ vec_[2 * i] = current_state[2 * i]; ++ vec_[2 * i + 1] = current_state[2 * i + 1]; + } + expectation += coefficient * delta; + } +- std::swap(current_state, tmpBuff1_); ++ if (NULL != current_state){ ++ free(current_state); ++ current_state = NULL; ++ } + return expectation; + } + + void apply_qubit_operator(ComplexTermsDict const& td, std::vector const& ids){ + run(); +- StateVector new_state, current_state; // avoid costly memory reallocations +- if( tmpBuff1_.capacity() >= vec_.size() ) +- std::swap(tmpBuff1_, new_state); +- if( tmpBuff2_.capacity() >= vec_.size() ) +- std::swap(tmpBuff2_, current_state); +- new_state.resize(vec_.size()); +- current_state.resize(vec_.size()); ++ StateVector new_state = (StateVector)calloc(len_, sizeof(calc_type)); ++ StateVector current_state = (StateVector)malloc(len_ * sizeof(calc_type)); + #pragma omp parallel for schedule(static) +- for (std::size_t i = 0; i < vec_.size(); ++i){ +- new_state[i] = 0; ++ for (std::size_t i = 0; i < len_; ++i){ + current_state[i] = vec_[i]; + } + for (auto const& term : td){ + auto const& coefficient = term.second; + apply_term(term.first, ids, {}); + #pragma omp parallel for schedule(static) +- for (std::size_t i = 0; i < vec_.size(); ++i){ +- new_state[i] += coefficient * vec_[i]; +- vec_[i] = current_state[i]; ++ for (std::size_t i = 0; i < (len_ >> 1); ++i){ ++ new_state[2 * i] += coefficient.real() * vec_[2 * i] - coefficient.imag() * vec_[2 * i + 1]; ++ new_state[2 * i + 1] += coefficient.real() * vec_[2 * i + 1] + coefficient.imag() * vec_[2 * i]; ++ vec_[2 * i] = current_state[2 * i]; ++ vec_[2 * i + 1] = current_state[2 * i + 1]; + } + } +- std::swap(vec_, new_state); +- std::swap(tmpBuff1_, new_state); +- std::swap(tmpBuff2_, current_state); +- } +- +- calc_type get_probability(std::vector const& bit_string, +- std::vector const& ids){ +- run(); +- if (!check_ids(ids)) +- throw(std::runtime_error("get_probability(): Unknown qubit id. Please make sure you have called eng.flush().")); +- std::size_t mask = 0, bit_str = 0; +- for (unsigned i = 0; i < ids.size(); ++i){ +- mask |= 1UL << map_[ids[i]]; +- bit_str |= (bit_string[i]?1UL:0UL) << map_[ids[i]]; +- } +- calc_type probability = 0.; +- #pragma omp parallel for reduction(+:probability) schedule(static) +- for (std::size_t i = 0; i < vec_.size(); ++i) +- if ((i & mask) == bit_str) +- probability += std::norm(vec_[i]); +- return probability; +- } +- +- complex_type const& get_amplitude(std::vector const& bit_string, +- std::vector const& ids){ +- run(); +- std::size_t chk = 0; +- std::size_t index = 0; +- for (unsigned i = 0; i < ids.size(); ++i){ +- if (map_.count(ids[i]) == 0) +- break; +- chk |= 1UL << map_[ids[i]]; +- index |= (bit_string[i]?1UL:0UL) << map_[ids[i]]; ++ if (NULL != vec_) ++ free(vec_); ++ vec_ = new_state; ++ if (NULL != new_state) ++ new_state = NULL; ++ if (NULL != current_state){ ++ free(current_state); ++ current_state = NULL; + } +- if (chk + 1 != vec_.size()) +- throw(std::runtime_error("The second argument to get_amplitude() must be a permutation of all allocated qubits. Please make sure you have called eng.flush().")); +- return vec_[index]; + } + + void emulate_time_evolution(TermsDict const& tdict, calc_type const& time, +@@ -400,87 +153,71 @@ + } + unsigned s = std::abs(time) * op_nrm + 1.; + complex_type correction = std::exp(-time * I * tr / (double)s); +- auto output_state = vec_; ++ auto output_state = copy(vec_, len_); + auto ctrlmask = get_control_mask(ctrl); + for (unsigned i = 0; i < s; ++i){ + calc_type nrm_change = 1.; + for (unsigned k = 0; nrm_change > 1.e-12; ++k){ + auto coeff = (-time * I) / double(s * (k + 1)); +- auto current_state = vec_; +- auto update = StateVector(vec_.size(), 0.); ++ auto current_state = copy(vec_, len_); ++ auto update = (StateVector)calloc(len_, sizeof(calc_type)); + for (auto const& tup : td){ + apply_term(tup.first, ids, {}); + #pragma omp parallel for schedule(static) +- for (std::size_t j = 0; j < vec_.size(); ++j){ ++ for (std::size_t j = 0; j < len_; ++j){ + update[j] += vec_[j] * tup.second; + vec_[j] = current_state[j]; + } + } + nrm_change = 0.; + #pragma omp parallel for reduction(+:nrm_change) schedule(static) +- for (std::size_t j = 0; j < vec_.size(); ++j){ +- update[j] *= coeff; +- vec_[j] = update[j]; ++ for (std::size_t j = 0; j < (len_ >> 1); ++j){ ++ complex_type tmp(update[2 * j], update[2 * j + 1]); ++ tmp *= coeff; ++ update[2 * j] *= std::real(tmp); ++ update[2 * j + 1] *= std::imag(tmp); ++ vec_[2 * j] = update[2 * j]; ++ vec_[2 * j + 1] = update[2 * j + 1]; + if ((j & ctrlmask) == ctrlmask){ +- output_state[j] += update[j]; +- nrm_change += std::norm(update[j]); ++ output_state[2 * j] += update[2 * j]; ++ output_state[2 * j + 1] += update[2 * j + 1]; ++ nrm_change += std::sqrt(update[2 * j] * update[2 * j] + update[2 * j + 1] * update[2 * j + 1]); + } + } + nrm_change = std::sqrt(nrm_change); ++ if (NULL != current_state){ ++ free(current_state); ++ current_state = NULL; ++ } ++ if (NULL != update){ ++ free(update); ++ update = NULL; ++ } + } + #pragma omp parallel for schedule(static) +- for (std::size_t j = 0; j < vec_.size(); ++j){ +- if ((j & ctrlmask) == ctrlmask) +- output_state[j] *= correction; +- vec_[j] = output_state[j]; ++ for (std::size_t j = 0; j < (len_ >>1); ++j){ ++ if ((j & ctrlmask) == ctrlmask){ ++ complex_type tmp(output_state[2 * j], output_state[2 * j + 1]); ++ tmp *= correction; ++ output_state[2 * j] = std::real(tmp); ++ output_state[2 * j + 1] =std::imag(tmp); ++ } ++ vec_[2 * j] = output_state[2 * j]; ++ vec_[2 * j + 1] = output_state[2 * j + 1]; + } + } ++ if (NULL != output_state){ ++ free(output_state); ++ output_state = NULL; ++ } + } + + void set_wavefunction(StateVector const& wavefunction, std::vector const& ordering){ + run(); +- // make sure there are 2^n amplitudes for n qubits +- assert(wavefunction.size() == (1UL << ordering.size())); +- // check that all qubits have been allocated previously +- if (map_.size() != ordering.size() || !check_ids(ordering)) +- throw(std::runtime_error("set_wavefunction(): Invalid mapping provided. Please make sure all qubits have been allocated previously (call eng.flush()).")); +- +- // set mapping and wavefunction +- for (unsigned i = 0; i < ordering.size(); ++i) +- map_[ordering[i]] = i; +- #pragma omp parallel for schedule(static) +- for (std::size_t i = 0; i < wavefunction.size(); ++i) +- vec_[i] = wavefunction[i]; +- } +- +- void collapse_wavefunction(std::vector const& ids, std::vector const& values){ +- run(); +- assert(ids.size() == values.size()); +- if (!check_ids(ids)) +- throw(std::runtime_error("collapse_wavefunction(): Unknown qubit id(s) provided. Try calling eng.flush() before invoking this function.")); +- std::size_t mask = 0, val = 0; +- for (unsigned i = 0; i < ids.size(); ++i){ +- mask |= (1UL << map_[ids[i]]); +- val |= ((values[i]?1UL:0UL) << map_[ids[i]]); +- } +- // set bad entries to 0 and compute probability of outcome to renormalize +- calc_type N = 0.; +- #pragma omp parallel for reduction(+:N) schedule(static) +- for (std::size_t i = 0; i < vec_.size(); ++i){ +- if ((i & mask) == val) +- N += std::norm(vec_[i]); +- } +- if (N < 1.e-12) +- throw(std::runtime_error("collapse_wavefunction(): Invalid collapse! Probability is ~0.")); +- // re-normalize (if possible) +- N = 1./std::sqrt(N); +- #pragma omp parallel for schedule(static) +- for (std::size_t i = 0; i < vec_.size(); ++i){ +- if ((i & mask) != val) +- vec_[i] = 0.; +- else +- vec_[i] *= N; ++ if (NULL != vec_){ ++ free(vec_); + } ++ vec_ = copy(wavefunction, len_); + } + + void run(){ +@@ -500,23 +237,23 @@ + switch (ids.size()){ + case 1: + #pragma omp parallel +- kernel(vec_, ids[0], m, ctrlmask); ++ kernel(vec_, ids[0], m, ctrlmask, len_ >> 1); + break; + case 2: + #pragma omp parallel +- kernel(vec_, ids[1], ids[0], m, ctrlmask); ++ kernel(vec_, ids[1], ids[0], m, ctrlmask, len_ >> 1); + break; + case 3: + #pragma omp parallel +- kernel(vec_, ids[2], ids[1], ids[0], m, ctrlmask); ++ kernel(vec_, ids[2], ids[1], ids[0], m, ctrlmask, len_ >> 1); + break; + case 4: + #pragma omp parallel +- kernel(vec_, ids[3], ids[2], ids[1], ids[0], m, ctrlmask); ++ kernel(vec_, ids[3], ids[2], ids[1], ids[0], m, ctrlmask, len_ >> 1); + break; + case 5: + #pragma omp parallel +- kernel(vec_, ids[4], ids[3], ids[2], ids[1], ids[0], m, ctrlmask); ++ kernel(vec_, ids[4], ids[3], ids[2], ids[1], ids[0], m, ctrlmask, len_ >> 1); + break; + default: + throw std::invalid_argument("Gates with more than 5 qubits are not supported!"); +@@ -525,12 +262,27 @@ + fused_gates_ = Fusion(); + } + +- std::tuple cheat(){ ++ std::vector cheat(){ + run(); +- return make_tuple(map_, std::ref(vec_)); ++ std::vector result; ++ for (unsigned int i = 0; i < (len_ >> 1); i++){ ++ result.push_back({vec_[2 * i], vec_[2 * i + 1]}); ++ } ++ return result; ++ } ++ ++ inline StateVector copy(StateVector source, unsigned len){ ++ StateVector result = (StateVector)malloc(len * sizeof(calc_type)); ++#pragma omp parallel for schedule(static) ++ for (std::size_t i = 0; i < len; ++i) { ++ result[i] = source[i]; + } ++ return result; ++} + + ~Simulator(){ ++ if (NULL != vec_) ++ free(vec_); + } + + private: +@@ -562,18 +314,13 @@ + } + + unsigned N_; // #qubits +- StateVector vec_; ++ unsigned len_; + Map map_; + Fusion fused_gates_; + unsigned fusion_qubits_min_, fusion_qubits_max_; + RndEngine rnd_eng_; + std::function rng_; +- +- // large array buffers to avoid costly reallocations +- static StateVector tmpBuff1_, tmpBuff2_; + }; + +-Simulator::StateVector Simulator::tmpBuff1_; +-Simulator::StateVector Simulator::tmpBuff2_; + + #endif diff --git a/third_party/patch/projectq/projectq.patch002 b/third_party/patch/projectq/projectq.patch002 new file mode 100644 index 0000000000000000000000000000000000000000..95dd0371178f213db7d15b4d8d12bbf3daf444e7 --- /dev/null +++ b/third_party/patch/projectq/projectq.patch002 @@ -0,0 +1,322 @@ +--- projectq-src/projectq/backends/_sim/_cppkernels/intrin/alignedallocator.hpp 2020-06-05 15:07:57.000000000 +0200 ++++ projectq-src-new/projectq/backends/_sim/_cppkernels/intrin/alignedallocator.hpp 2021-10-04 10:44:26.352241980 +0200 +@@ -1,120 +1,231 @@ +-// Copyright (C) 2012 Andreas Hehn . +- +-// Licensed under the Apache License, Version 2.0 (the "License"); +-// you may not use this file except in compliance with the License. +-// You may obtain a copy of the License at ++// Copyright 2021 ++// ++// Licensed under the Apache License, Version 2.0 (the "License"); ++// you may not use this file except in compliance with the License. ++// You may obtain a copy of the License at + // +-// http://www.apache.org/licenses/LICENSE-2.0 ++// http://www.apache.org/licenses/LICENSE-2.0 + // +-// Unless required by applicable law or agreed to in writing, software +-// distributed under the License is distributed on an "AS IS" BASIS, +-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-// See the License for the specific language governing permissions and +-// limitations under the License. ++// Unless required by applicable law or agreed to in writing, software ++// distributed under the License is distributed on an "AS IS" BASIS, ++// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++// See the License for the specific language governing permissions and ++// limitations under the License. + +-#pragma once ++#ifndef ALIGNED_ALLOCATOR_HPP ++#define ALIGNED_ALLOCATOR_HPP + +-#ifdef _WIN32 +-#include +-#else +-#include +-#endif + #include ++#include + #include + #include + +-#if __cplusplus < 201103L +-#define noexcept ++// NOLINTNEXTLINE ++#define DEFAULT_ALIGNMENT 16U ++ ++#ifdef _WIN32 ++# include + #endif + ++#ifndef MALLOC_ALREADY_ALIGNED ++# if defined(__GLIBC__) && ((__GLIBC__ >= 2 && __GLIBC_MINOR__ >= 8) || __GLIBC__ > 2) && defined(__LP64__) \ ++ && !defined(__SANITIZE_ADDRESS__) ++# define GLIBC_MALLOC_ALREADY_ALIGNED 1 // NOLINT ++# else ++# define GLIBC_MALLOC_ALREADY_ALIGNED 0 // NOLINT ++# endif ++ ++// FreeBSD 6 seems to have 16-byte aligned malloc ++// See http://svn.freebsd.org/viewvc/base/stable/6/lib/libc/stdlib/malloc.c?view=markup ++// FreeBSD 7 seems to have 16-byte aligned malloc except on ARM and MIPS architectures ++// See http://svn.freebsd.org/viewvc/base/stable/7/lib/libc/stdlib/malloc.c?view=markup ++# if defined(__FreeBSD__) && !(defined(__arm__) || defined(__mips__) || defined(__mips)) ++# define FREEBSD_MALLOC_ALREADY_ALIGNED 1 // NOLINT ++# else ++# define FREEBSD_MALLOC_ALREADY_ALIGNED 0 // NOLINT ++# endif ++ ++# if (defined(__APPLE__) || defined(_WIN64) || GLIBC_MALLOC_ALREADY_ALIGNED || FREEBSD_MALLOC_ALREADY_ALIGNED) ++# define MALLOC_ALREADY_ALIGNED 1 // NOLINT ++# else ++# define MALLOC_ALREADY_ALIGNED 0 // NOLINT ++# endif ++#endif // !MALLOC_ALREADY_ALIGNED ++ ++#if ((defined __QNXNTO__) || (defined _GNU_SOURCE) || ((defined _XOPEN_SOURCE) && (_XOPEN_SOURCE >= 600))) \ ++ && (defined _POSIX_ADVISORY_INFO) && (_POSIX_ADVISORY_INFO > 0) ++# define HAS_POSIX_MEMALIGN 1 // NOLINT ++#else ++# define HAS_POSIX_MEMALIGN 0 // NOLINT ++#endif ++ ++#if (defined(_M_AMD64) || defined(_M_X64) || defined(__amd64)) && !defined(__x86_64__) ++# define __x86_64__ 1 // NOLINT ++#endif + +-template +-class aligned_allocator ++// Find sse instruction set from compiler macros if SSE_INSTR_SET not defined ++// Note: Not all compilers define these macros automatically ++#ifndef SSE_INSTR_SET ++# if defined(__AVX2__) ++# define SSE_INSTR_SET 8 // NOLINT ++# elif defined(__AVX__) ++# define SSE_INSTR_SET 7 // NOLINT ++# elif defined(__SSE4_2__) ++# define SSE_INSTR_SET 6 // NOLINT ++# elif defined(__SSE4_1__) ++# define SSE_INSTR_SET 5 // NOLINT ++# elif defined(__SSSE3__) ++# define SSE_INSTR_SET 4 // NOLINT ++# elif defined(__SSE3__) ++# define SSE_INSTR_SET 3 // NOLINT ++# elif defined(__SSE2__) || defined(__x86_64__) ++# define SSE_INSTR_SET 2 // NOLINT ++# elif defined(__SSE__) ++# define SSE_INSTR_SET 1 // NOLINT ++# elif defined(_M_IX86_FP) // Defined in MS compiler on 32bits system. 1: SSE, 2: SSE2 ++# define SSE_INSTR_SET _M_IX86_FP ++# else ++# define SSE_INSTR_SET 0 // NOLINT ++# endif // instruction set defines ++#endif // SSE_INSTR_SET ++ ++#if SSE_INSTR_SET > 0 ++# define HAS_MM_MALLOC 1 // NOLINT ++# include ++#else ++# define HAS_MM_MALLOC 0 // NOLINT ++#endif ++ ++#if __cplusplus >= 201703 ++# define HAS_STD_ALIGNED_ALLOC 1 // NOLINT ++#else ++# define HAS_STD_ALIGNED_ALLOC 0 // NOLINT ++#endif ++ ++#if __cplusplus < 201103L ++# error aligned_allocator requires at least C++11 support enabled! ++#endif ++ ++template ++class aligned_allocator : public std::allocator + { +- public: +- typedef T* pointer; +- typedef T const* const_pointer; +- typedef T& reference; +- typedef T const& const_reference; +- typedef T value_type; +- typedef std::size_t size_type; +- typedef std::ptrdiff_t difference_type; ++public: ++ using value_type = T; ++ using reference = T&; ++ using const_reference = const T&; ++ using pointer = T*; ++ using const_pointer = const T*; ++ using size_type = std::size_t; ++ using difference_type = std::ptrdiff_t; ++ ++ static_assert(alignment >= DEFAULT_ALIGNMENT, "Alignment must be equal or greater than default alignment"); + +- template ++ template + struct rebind + { +- typedef aligned_allocator other; ++ using other = aligned_allocator; + }; + +- aligned_allocator() noexcept {} +- aligned_allocator(aligned_allocator const&) noexcept {} +- template +- aligned_allocator(aligned_allocator const&) noexcept +- { +- } ++ aligned_allocator() noexcept = default; ++ aligned_allocator(const aligned_allocator& /* unused */) noexcept = default; ++ aligned_allocator(aligned_allocator&& /* unused */) noexcept = default; ++ ++ template ++ explicit aligned_allocator(const aligned_allocator& /* unused */) noexcept ++ {} ++ ++ ~aligned_allocator() noexcept = default; ++ aligned_allocator& operator=(const aligned_allocator&) noexcept = default; ++ aligned_allocator& operator=(aligned_allocator&&) noexcept = default; ++ ++ // NOLINTNEXTLINE(huawei-force-type-void) ++ auto allocate(size_type n, const void* /* hint */ = nullptr) const; ++ void deallocate(pointer p, size_type /* unused */) const; ++}; + +- pointer allocate(size_type n) ++namespace detail ++{ ++ // NOLINTNEXTLINE(huawei-force-type-void) ++ inline void* _aligned_malloc(size_t size, size_t alignment) + { +- pointer p; +- +- +-#ifdef _WIN32 +- p = reinterpret_cast(_aligned_malloc(n * sizeof(T), Alignment)); +- if (p == 0) throw std::bad_alloc(); +-#else +- if (posix_memalign(reinterpret_cast(&p), Alignment, n * sizeof(T))) +- throw std::bad_alloc(); +-#endif +- return p; ++ void* res = nullptr; ++ void* ptr = malloc(size + alignment); // NOLINT ++ if (ptr != nullptr) { ++ // NOLINTNEXTLINE(performance-no-int-to-ptr) ++ res = reinterpret_cast((reinterpret_cast(ptr) & ~(size_t(alignment - 1))) + alignment); ++ *(reinterpret_cast(res) - 1) = ptr; // NOLINT ++ } ++ return res; + } ++} // namespace detail + +- void deallocate(pointer p, size_type) noexcept +- { +-#ifdef _WIN32 +- _aligned_free(p); ++// NOLINTNEXTLINE(huawei-force-type-void) ++inline void* aligned_malloc(size_t size, size_t alignment) // NOLINT(misc-unused-parameters) ++{ ++#if MALLOC_ALREADY_ALIGNED ++ return malloc(size); // NOLINT ++#elif HAS_MM_MALLOC ++ return _mm_malloc(size, alignment); // NOLINT ++#elif HAS_POSIX_MEMALIGN ++ void* res; ++ const int failed = posix_memalign(&res, alignment, size); ++ if (failed) { ++ res = nullptr; ++ } ++ return res; ++#elif (defined _MSC_VER) ++ return _aligned_malloc(size, alignment); ++#elif HAS_STD_ALIGNED_ALLOC ++ return std::aligned_alloc(alignment, size); + #else +- std::free(p); ++ return detail::_aligned_malloc(size, alignment); + #endif +- } ++} + +- size_type max_size() const noexcept ++namespace detail ++{ ++ // NOLINTNEXTLINE(huawei-force-type-void) ++ inline void _aligned_free(void* ptr) + { +- std::allocator a; +- return a.max_size(); ++ if (ptr != nullptr) { ++ free(*(reinterpret_cast(ptr) - 1)); // NOLINT ++ } + } ++} // namespace detail + +-#if __cplusplus >= 201103L +- template +- void construct(C* c, Args&&... args) +- { +- new ((void*)c) C(std::forward(args)...); +- } ++// NOLINTNEXTLINE(huawei-force-type-void) ++inline void aligned_free(void* ptr) ++{ ++#if MALLOC_ALREADY_ALIGNED ++ free(ptr); // NOLINT ++#elif HAS_MM_MALLOC ++ _mm_free(ptr); // NOLINT ++#elif HAS_POSIX_MEMALIGN ++ free(ptr); // NOLINT ++#elif defined(_MSC_VER) ++ _aligned_free(ptr); // NOLINT ++#elif HAS_STD_ALIGNED_ALLOC ++ std::free(ptr); // NOLINT + #else +- void construct(pointer p, const_reference t) { new ((void*)p) T(t); } ++ detail::_aligned_free(ptr); // NOLINT + #endif ++} + +- template +- void destroy(C* c) +- { +- c->~C(); +- } +- +- bool operator==(aligned_allocator const&) const noexcept { return true; } +- bool operator!=(aligned_allocator const&) const noexcept { return false; } +- template +- bool operator==(aligned_allocator const&) const noexcept +- { +- return false; +- } +- +- template +- bool operator!=(aligned_allocator const&) const noexcept +- { +- return true; ++template ++// NOLINTNEXTLINE(huawei-force-type-void) ++auto aligned_allocator::allocate(size_type n, const void* /* hint */) const ++{ ++ // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) ++ auto res = reinterpret_cast(aligned_malloc(sizeof(T) * n, alignment)); ++ if (res == nullptr) { ++ throw std::bad_alloc(); + } +-}; +- +-#if __cplusplus < 201103L +-#undef noexcept +-#endif ++ return res; ++} + ++template ++void aligned_allocator::deallocate(pointer p, size_type /* unused */) const ++{ ++ aligned_free(p); ++} ++#endif /* ALIGNED_ALLOCATOR_HPP */ diff --git a/third_party/patch/quest/quest.patch001 b/third_party/patch/quest/quest.patch001 new file mode 100644 index 0000000000000000000000000000000000000000..cd4d25ef3f598eefac35d728a43eb05a3bfb5ef3 --- /dev/null +++ b/third_party/patch/quest/quest.patch001 @@ -0,0 +1,211 @@ +diff -aur quest-src-old/QuEST/CMakeLists.txt quest-src/QuEST/CMakeLists.txt +--- quest-src-old/QuEST/CMakeLists.txt 2021-10-05 12:06:40.000000000 +0200 ++++ quest-src/QuEST/CMakeLists.txt 2021-10-05 12:10:45.000000000 +0200 +@@ -29,9 +29,11 @@ + + option(GPUACCELERATED "Whether to program will run on GPU. Set to 1 to enable" 0) + +-set(GPU_COMPUTE_CAPABILITY 30 CACHE STRING "GPU hardware dependent, lookup at https://developer.nvidia.com/cuda-gpus. Write without fullstop") +- +- ++if(NOT DEFINED GPU_COMPUTE_CAPABILITY) ++ set(GPU_COMPUTE_CAPABILITY 30 CACHE STRING "GPU hardware dependent, lookup at https://developer.nvidia.com/cuda-gpus. Write without fullstop") ++else() ++ set(GPU_COMPUTE_CAPABILITY ${GPU_COMPUTE_CAPABILITY} CACHE STRING "GPU hardware dependent, lookup at https://developer.nvidia.com/cuda-gpus. Write without fullstop") ++endif() + + # ***************************************************************************** + # ***** NO CHANGES SHOULD BE REQUIRED FROM THE USER BEYOND THIS POINT ********* +@@ -40,6 +42,9 @@ + # Show the user their settings + message(STATUS "Precision is ${PRECISION}") + message(STATUS "GPU acceleration is ${GPUACCELERATED}") ++if(CUDA_STATIC) ++ message(STATUS "Using static CUDA") ++endif() + message(STATUS "OMP acceleration is ${MULTITHREADED}") + message(STATUS "MPI distribution is ${DISTRIBUTED}") + +@@ -50,7 +55,7 @@ + + # ----- FATAL ----------------------------------------------------------------- + +-if (${DISTRIBUTED} AND ${GPUACCELERATED}) ++if (DISTRIBUTED AND GPUACCELERATED) + message(FATAL_ERROR "DISTRIBUTED=${DISTRIBUTED} and \ + GPUACCELERATED=${GPUACCELERATED} set but \ + distributed GPU acceleration not supported. Aborting") +@@ -65,14 +70,14 @@ + endif() + + if ( (${PRECISION} EQUAL 4) AND +- ${GPUACCELERATED} ) ++ GPUACCELERATED ) + message(FATAL_ERROR "PRECISION=${PRECISION} but quad precision is not \ + supported on GPU. Aborting") + endif() + + # ----- WARNINGS -------------------------------------------------------------- + +-if (${GPUACCELERATED} AND ${MULTITHREADED}) ++if (GPUACCELERATED AND MULTITHREADED) + message(WARNING "MULTITHREADED=${MULTITHREADED} and \ + GPUACCELERATED=${GPUACCELERATED} set but GPU \ + version makes no use of multithreading. Ignoring multithreading settings") +@@ -80,13 +85,13 @@ + + #TODO Add other supported Clang versions if found + if ( ("${CMAKE_C_COMPILER_ID}" STREQUAL "Clang") AND +- ${GPUACCELERATED} AND ++ GPUACCELERATED AND + NOT("${CMAKE_C_COMPILER_VERSION}" STREQUAL "3.7.0") ) + message(WARNING "Some versions of Clang are not NVIDIA-GPU compatible. \ + If compilation fails, try Clang 3.7.") + endif() + +-if ( ${GPUACCELERATED} AND ++if ( GPUACCELERATED AND + ("${CMAKE_C_COMPILER_ID}" STREQUAL "GNU") AND + ("${CMAKE_SYSTEM_NAME}" STREQUAL "DARWIN") ) # DARWIN means Mac OS X + message(WARNING "On some platforms (e.g. OSX), NVIDIA-GPUs are not \ +@@ -109,11 +114,47 @@ + endif() + + if (GPUACCELERATED) +- find_package(CUDA REQUIRED) +- # Stop nvcc sending c compile flags through using -Xcompiler and breaking +- # on compilation of a cpp file receiving -std=c99. In long term should figure +- # out why CMAKE_C_FLAGS and not CMAKE_CXX_FLAGS are being sent through to a cpp file +- set(CUDA_PROPAGATE_HOST_FLAGS FALSE) ++ enable_language(CUDA) ++ if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.17) ++ find_package(CUDAToolkit REQUIRED) ++ else() ++ find_package(CUDA REQUIRED) ++ ++ if(CUDA_LIBRARIES) ++ if(NOT TARGET CUDA::cudart) ++ add_library(CUDA::cudart IMPORTED INTERFACE) ++ target_include_directories(CUDA::cudart SYSTEM INTERFACE "${CUDA_INCLUDE_DIRS}") ++ target_link_libraries(CUDA::cudart INTERFACE "${CUDA_LIBRARIES}") ++ endif() ++ endif() ++ ++ if(CUDA_cudart_static_LIBRARY) ++ if(NOT TARGET CUDA::cudart_static) ++ add_library(CUDA::cudart_static IMPORTED INTERFACE) ++ target_include_directories(CUDA::cudart_static SYSTEM INTERFACE "${CUDA_INCLUDE_DIRS}") ++ target_link_libraries(CUDA::cudart_static INTERFACE "${CUDA_cudart_static_LIBRARY}" Threads::Threads) ++ endif() ++ endif() ++ find_library( ++ CUDA_driver_LIBRARY ++ NAMES cuda_driver cuda ++ HINTS ${CUDA_TOOLKIT_ROOT_DIR} ENV CUDA_PATH ++ PATH_SUFFIXES nvidia/current lib64 lib/x64 lib) ++ if(NOT CUDA_driver_LIBRARY) ++ # Don't try any stub directories until we have exhausted all other search locations. ++ find_library( ++ CUDA_driver_LIBRARY ++ NAMES cuda_driver cuda ++ HINTS ${CUDA_TOOLKIT_ROOT_DIR} ENV CUDA_PATH ++ PATH_SUFFIXES lib64/stubs lib/x64/stubs lib/stubs stubs) ++ endif() ++ mark_as_advanced(CUDA_driver_LIBRARY) ++ if(CUDA_driver_LIBRARY) ++ add_library(CUDA::cuda_driver IMPORTED INTERFACE) ++ target_include_directories(CUDA::cuda_driver SYSTEM INTERFACE "${CUDA_INCLUDE_DIRS}") ++ target_link_libraries(CUDA::cuda_driver INTERFACE "${CUDA_driver_LIBRARY}") ++ endif() ++ endif() + endif() + + +@@ -125,7 +166,7 @@ + + # ----- OPENMP ---------------------------------------------------------------- + +-if (${MULTITHREADED} AND NOT ${GPUACCELERATED}) ++if (MULTITHREADED AND NOT GPUACCELERATED) + find_package(OpenMP) + + # If found, we must also check the version +@@ -149,21 +190,17 @@ + # ----- CUDA FLAGS ------------------------------------------------------------ + + if (GPUACCELERATED) +- set (CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} \ +- -arch=compute_${GPU_COMPUTE_CAPABILITY} -code=sm_${GPU_COMPUTE_CAPABILITY}" +- ) ++ if(CMAKE_VERSION VERSION_LESS 3.18) ++ set(CMAKE_CUDA_FLAGS ++ "${CMAKE_CUDA_FLAGS} -arch=compute_${GPU_COMPUTE_CAPABILITY} -code=sm_${GPU_COMPUTE_CAPABILITY}") ++ endif() ++ + if ("${CMAKE_BUILD_TYPE}" STREQUAL "Release") +- set (CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} \ +- -O2" +- ) ++ set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} -O2") + elseif ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug") +- set (CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} \ +- -G -g -lineinfo" +- ) ++ set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} -G -g -lineinfo") + else() +- set (CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} \ +- -O2" +- ) ++ set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} -O2") + endif() + endif() + +@@ -215,11 +252,6 @@ + + # ----- C++ COMPILER FLAGS -------------------------------------------------- + +-# set C++ flags that are common between compilers and build types +-set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} \ +- -std=c++98 -Wall" +-) +- + # Use -O2 for all but debug mode by default + if (NOT("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")) + set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS} \ +@@ -284,9 +316,13 @@ + add_subdirectory(src) + + if (GPUACCELERATED) +- cuda_add_library(QuEST SHARED ++ add_library(QuEST SHARED + ${QuEST_SRC} +- ) ++ ) ++ ++ if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.18) ++ set_target_properties(QuEST PROPERTIES CUDA_ARCHITECTURES ${GPU_COMPUTE_CAPABILITY}) ++ endif() + elseif (WIN32) + add_library(QuEST STATIC + ${QuEST_SRC} +@@ -300,8 +336,7 @@ + # ----- Location of header files ---------------------------------------------- + + target_include_directories(QuEST +- PRIVATE src +- PUBLIC include ++ PUBLIC src include + ) + + +@@ -328,7 +363,9 @@ + + # ----- GPU ------------------------------------------------------------------- + +-target_link_libraries(QuEST ${CUDA_LIBRARIES}) ++if (GPUACCELERATED) ++ target_link_libraries(QuEST PUBLIC $,CUDA::cudart_static,CUDA::cudart>) ++endif() + + + # ----- Coverage testing with GCC or Clang ------------------------------------ diff --git a/tutorials/0.frequently_asked_questions.ipynb b/tutorials/0.frequently_asked_questions.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..0d13aefad37ff15ffb21f3c37f4d633fa252cba7 --- /dev/null +++ b/tutorials/0.frequently_asked_questions.ipynb @@ -0,0 +1,427 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Frequently Asked Questions (FAQ)\n", + "\n", + "`Linux` `CPU` `全流程` `初级` `中级` `高级`\n", + "\n", + "[![](https://gitee.com/mindspore/docs/raw/master/resource/_static/logo_source.png)](https://gitee.com/mindspore/mindquantum/blob/master/tutorials/0.frequently_asked_questions.ipynb) [![](https://gitee.com/mindspore/mindquantum/raw/master/tutorials/images/view_mindquantum_api.png)](https://mindspore.cn/mindquantum/api/zh-CN/master/index.html)\n", + "\n", + "## 1. 为什么量子模拟器的运算速度很慢?\n", + "\n", + "量子模拟器运算速度很慢的原因可能是设置的并行内核数太低了或者是并行内核数过高,特别是在大型服务器中,如果不设置内核数,默认会占用所有的CPU,速度反而会降低。\n", + "\n", + "在运行代码前,我们需要设置量子模拟器运行时的并行内核数,例如:如果需要设置并行内核数为4,可运行如下代码:\n", + "\n", + "export OMP_NUM_THREADS=4\n", + "\n", + "当发现量子模拟器运算速度很慢的时候,可以适当调整并行内核数。\n", + "\n", + "请根据模型规模合理设置并行内核数以达到最优效果。\n", + "\n", + "## 2. 关于双量子比特门——`CNOT`是如何实现的?\n", + "\n", + "对于`CNOT`门,其本质上是受控`X`门(`Controlled-X` gate),因此在MindQuantum中,如果我们需要执行`CNOT`门,只需设定`X`门的控制比特位和目标比特位即可(实际上,任意的量子门我们都可以设定控制比特位和所需执行量子门操作的目标比特位)。例如运行如下代码:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "X(1 <-: 0)" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import mindquantum as mq\n", + "from mindquantum import X\n", + "\n", + "X.on(1, 0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "运行结果为:X(1 <-: 0) ,其表示第1位量子比特位为目标比特位,第0位量子比特位为控制比特位,第1位量子比特受第0位量子比特控制,若第0位量子比特为1,则对第1位量子比特执行`X`门操作,否则不作任何操作。\n", + "\n", + "为了更加直观,我们将其量子线路打印出来,运行如下代码:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
q0: ──●──\n",
+       "\n",
+       "q1: ──X──\n",
+       "
\n" + ], + "text/plain": [ + "q0: ──●──\n", + " │ \n", + "q1: ──X──" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from mindquantum import Circuit\n", + "\n", + "circuit = Circuit()\n", + "circuit += X.on(1, 0)\n", + "circuit" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "这就是MindQuantum中实现`CNOT`门的语法,大家需要注意。如打印线路错乱,请参考下面第四条,设置浏览器或者终端的等宽字体。\n", + "\n", + "## 3. 关于量子比特的读取顺序?\n", + "\n", + "在MindQuantum中,量子比特的读取顺序都是从右往左的。我们通过一个具体的例子来说明。\n", + "\n", + "首先,运行如下代码,得到一个3量子比特的均匀叠加态:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "√2/4¦000⟩\n", + "√2/4¦001⟩\n", + "√2/4¦010⟩\n", + "√2/4¦011⟩\n", + "√2/4¦100⟩\n", + "√2/4¦101⟩\n", + "√2/4¦110⟩\n", + "√2/4¦111⟩\n" + ] + } + ], + "source": [ + "from mindquantum.simulator import Simulator\n", + "from mindquantum import H, UN\n", + "\n", + "sim = Simulator('projectq', 3)\n", + "\n", + "circuit1 = Circuit()\n", + "circuit1 += UN(H, 3)\n", + "\n", + "sim.apply_circuit(circuit1)\n", + "print(sim.get_qs(True))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "从运行的结果可以看到,我们得到了3量子比特的均匀叠加态,需要说明的是,所呈现的量子态,最右位的表示是第0位量子比特,中间位表示第1位量子比特,最左位表示第2位量子比特。\n", + "\n", + "我们再举一个例子,运行如下代码,打印量子线路和此时的量子态:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "projectq simulator with 2 qubits.\n", + "Current quantum state:\n", + "1¦10⟩\n" + ] + }, + { + "data": { + "text/html": [ + "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
q0: ─────\n",
+       "         \n",
+       "q1: ──X──\n",
+       "
\n" + ], + "text/plain": [ + "q0: ─────\n", + " \n", + "q1: ──X──" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sim1 = Simulator('projectq', 2)\n", + "\n", + "circuit2 = Circuit()\n", + "circuit2 += X.on(1)\n", + "\n", + "sim1.apply_circuit(circuit2)\n", + "\n", + "print(sim1)\n", + "circuit2" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "可以看到此时的量子态为$|10\\rangle$​​态,量子态中的数字1表示的是第1位量子比特为$|1\\rangle$​​,数字0表示的是第0位量子比特为$|0\\rangle$​​。我们简单地验证一下,在第1位量子比特添加测量门,运行如下代码:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
q0: ──────────────\n",
+       "                  \n",
+       "q1: ──X────M(q1)──\n",
+       "
\n" + ], + "text/plain": [ + "q0: ──────────────\n", + " \n", + "q1: ──X────M(q1)──" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from mindquantum import Measure\n", + "\n", + "circuit2 += Measure('q1').on(1)\n", + "circuit2" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "从打印的结果可以看到,我们已经在第1位量子比特上添加了测量门。从理论上讲,第1位的量子比特是$|1\\rangle$​​,所以测量的结果应该也是$|1\\rangle$​​​,我们运行如下代码:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
shots: 100\n",
+       "Keys: q1│0.00     0.2         0.4         0.6         0.8         1.0\n",
+       "────────┼───────────┴───────────┴───────────┴───────────┴───────────┴\n",
+       "       1│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓\n",
+       "\n",
+       "{'1': 100}\n",
+       "
\n" + ], + "text/plain": [ + "shots: 100\n", + "Keys: q1│0.00 0.2 0.4 0.6 0.8 1.0\n", + "────────┼───────────┴───────────┴───────────┴───────────┴───────────┴\n", + " 1│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓\n", + " │ \n", + "{'1': 100}" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sim1.reset()\n", + "\n", + "result = sim1.sampling(circuit2, shots=100)\n", + "result" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "从运行的结果可以看到,对存在测量门的量子比特进行重复采样100次,得到的就是100次1的结果,所以再次验证$|10\\rangle$态中的$|1\\rangle$​是第1位量子比特。\n", + "\n", + "因此,在MindQuantum中,量子比特的读取顺序都是从右往左的。\n", + "\n", + "## 4. 若打印量子线路时出现线路错乱,该如何解决?\n", + "\n", + "我们可以在MindQuantum中搭建各种各样的量子线路,最后我们还可以将搭建好的量子线路打印出来。例如,我们运行如下代码来搭建量子线路:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
q0: ──H────●────RX(π/4)───────●───────M(q0)──\n",
+       "           │                  │              \n",
+       "q1: ───────X───────────────RY(π/2)────M(q1)──\n",
+       "
\n" + ], + "text/plain": [ + "q0: ──H────●────RX(π/4)───────●───────M(q0)──\n", + " │ │ \n", + "q1: ───────X───────────────RY(π/2)────M(q1)──" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import numpy as np\n", + "from mindquantum import RX, RY\n", + "\n", + "circuit3 = Circuit()\n", + "circuit3 += H.on(0)\n", + "circuit3 += X.on(1, 0)\n", + "circuit3 += RX(np.pi/4).on(0)\n", + "circuit3 += RY(np.pi/2).on(1, 0)\n", + "circuit3 += Measure('q0').on(0)\n", + "circuit3 += Measure('q1').on(1)\n", + "circuit3" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "但是,有时会出现如下图所示的线路错乱问题。\n", + "\n", + "![](./images/error_circuit.png)\n", + "\n", + "这个时候,我们只需要打开浏览器的设置,找到“外观”,找到“自定义字体”,然后在“宽度固定的字体”(有的浏览器为“等宽字体”)下,选择“Consolas”字体即可。此外,用户还可以下载并安装开源的[Fira Code](https://github.com/tonsky/FiraCode)字体来获得更优质的输出。当我们设置好等宽字体后,就可以看到最开始打印的量子线路了。(如下网址提供了一些等宽字体供用户自行选择[https://zhuanlan.zhihu.com/p/116230037/](https://zhuanlan.zhihu.com/p/116230037/))\n", + "\n", + "若想查询更多关于MindQuantum的API,请点击:[https://mindspore.cn/mindquantum/](https://mindspore.cn/mindquantum/)。" + ] + } + ], + "metadata": { + "interpreter": { + "hash": "d62cf896b9ca57de08105ce3983377439eacacf6f6599f9150bf400edf4fa4b8" + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.5" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/tutorials/1.parameterized_quantum_circuit.ipynb b/tutorials/1.parameterized_quantum_circuit.ipynb index f62413130d0ec5eb145d3a647a821f70f545ee39..7fe63571e66427314ca33abadb2ac922f560f57d 100644 --- a/tutorials/1.parameterized_quantum_circuit.ipynb +++ b/tutorials/1.parameterized_quantum_circuit.ipynb @@ -8,11 +8,11 @@ "\n", "`Linux` `CPU` `全流程` `初级` `中级` `高级`\n", "\n", - "[![](https://gitee.com/mindspore/docs/raw/master/resource/_static/logo_source.png)](https://gitee.com/mindspore/docs/blob/master/docs/mindquantum/docs/source_zh_cn/parameterized_quantum_circuit.ipynb) [![](https://gitee.com/mindspore/mindquantum/raw/master/tutorials/images/view_mindquantum_api.png)](https://mindspore.cn/mindquantum/api/zh-CN/master/index.html)\n", + "[![](https://gitee.com/mindspore/docs/raw/master/resource/_static/logo_source.png)](https://gitee.com/mindspore/mindquantum/blob/dev/tutorials/1.parameterized_quantum_circuit.ipynb) [![](https://gitee.com/mindspore/mindquantum/raw/master/tutorials/images/view_mindquantum_api.png)](https://mindspore.cn/mindquantum/api/zh-CN/master/index.html)\n", "\n", "## 1. 概述\n", "\n", - "参数化量子线路(Parameterized Quantum Circuit, PQC)即由含参数的量子门组成的量子线路,是进行量子机器学习的途径之一。量子-经典混合计算架构MindQuantum能够处理此类参数化量子线路,并利用量子神经网络的可逆性对该线路进行自动微分,最后通过测量得到的观测值,即可计算出观测值对于各参数的导数。\n", + "参数化量子线路(Parameterized Quantum Circuit, PQC)即由含参数的量子门组成的量子线路,是进行量子机器学习的途径之一。在很多情况下,为了能与经典机器学习中神经网络进行类比,我们也经常会把参数化量子线路称为量子神经网络。量子-经典混合计算架构MindQuantum能够处理此类参数化量子线路,并利用量子神经网络的可逆性对该线路进行自动微分,最后通过测量得到的观测值,即可计算出观测值对于各参数的导数。\n", "\n", "构建PQC并用PQC模拟器算子对量子线路进行演化的大致流程如下:\n", "\n", @@ -33,10 +33,12 @@ "source": [ "import numpy as np #导入numpy库并简写为np\n", "import mindquantum as mq #导入mindquantum库并简写为mq\n", - "from mindquantum.gate import X, Y, Z, H, RX, RY, RZ #从mindquantum.gate模块中导入量子门H, X, Y, Z, RX, RY, RZ" + "from mindquantum.core import X, Y, Z, H, RX, RY, RZ #导入量子门H, X, Y, Z, RX, RY, RZ" ] }, { + "cell_type": "markdown", + "metadata": {}, "source": [ "说明:\n", "\n", @@ -44,7 +46,7 @@ "\n", "(2)mindquantum是量子-经典混合计算框架,支持多种量子神经网络的训练和推理;\n", "\n", - "(3)搭建的量子线路中所需执行的量子门需要从mindquantum.gate模块中导入。\n", + "(3)搭建的量子线路中所需执行的量子门需要从mindquantum.core模块中导入;\n", "\n", "## 3. 量子门\n", "\n", @@ -55,9 +57,7 @@ "$$\\text{X}=\\begin{pmatrix}0&1\\\\\\\\1&0\\end{pmatrix},\\text{Y}=\\begin{pmatrix}0&-i\\\\\\\\i&0\\end{pmatrix},\\text{Z}=\\begin{pmatrix}1&0\\\\\\\\0&-1\\end{pmatrix},\\text{H}=\\frac{1}{\\sqrt{2}}\\begin{pmatrix}1&1\\\\\\\\1&-1\\end{pmatrix},\\text{CNOT}=\\begin{pmatrix}1&0&0&0\\\\\\\\0&1&0&0\\\\\\\\0&0&0&1\\\\\\\\0&0&1&0\\end{pmatrix}.$$\n", "\n", "分别打印上述量子门的矩阵形式,可以得到:\n" - ], - "cell_type": "markdown", - "metadata": {} + ] }, { "cell_type": "code", @@ -67,17 +67,22 @@ }, "outputs": [ { - "output_type": "stream", "name": "stdout", - "text": "Gate name: X\n" + "output_type": "stream", + "text": [ + "Gate name: X\n" + ] }, { - "output_type": "execute_result", "data": { - "text/plain": "array([[0, 1],\n [1, 0]])" + "text/plain": [ + "array([[0, 1],\n", + " [1, 0]])" + ] }, + "execution_count": 2, "metadata": {}, - "execution_count": 2 + "output_type": "execute_result" } ], "source": [ @@ -93,17 +98,22 @@ }, "outputs": [ { - "output_type": "stream", "name": "stdout", - "text": "Gate name: Y\n" + "output_type": "stream", + "text": [ + "Gate name: Y\n" + ] }, { - "output_type": "execute_result", "data": { - "text/plain": "array([[ 0.+0.j, -0.-1.j],\n [ 0.+1.j, 0.+0.j]])" + "text/plain": [ + "array([[ 0.+0.j, -0.-1.j],\n", + " [ 0.+1.j, 0.+0.j]])" + ] }, + "execution_count": 3, "metadata": {}, - "execution_count": 3 + "output_type": "execute_result" } ], "source": [ @@ -112,11 +122,11 @@ ] }, { + "cell_type": "markdown", + "metadata": {}, "source": [ "说明:矩阵里的每一项,左边的“0.”表示小数形式(浮点数)的实部(若实部为负,则在小数前显示“-”,否则默认为非负),右边的“0.”表示小数形式(浮点数)的虚部(若虚部为负,则在小数前会显示“-”,否则显示“+”),j表示虚数单位$i$)。" - ], - "cell_type": "markdown", - "metadata": {} + ] }, { "cell_type": "code", @@ -126,17 +136,22 @@ }, "outputs": [ { - "output_type": "stream", "name": "stdout", - "text": "Gate name: Z\n" + "output_type": "stream", + "text": [ + "Gate name: Z\n" + ] }, { - "output_type": "execute_result", "data": { - "text/plain": "array([[ 1, 0],\n [ 0, -1]])" + "text/plain": [ + "array([[ 1, 0],\n", + " [ 0, -1]])" + ] }, + "execution_count": 4, "metadata": {}, - "execution_count": 4 + "output_type": "execute_result" } ], "source": [ @@ -152,17 +167,22 @@ }, "outputs": [ { - "output_type": "stream", "name": "stdout", - "text": "Gate name: H\n" + "output_type": "stream", + "text": [ + "Gate name: H\n" + ] }, { - "output_type": "execute_result", "data": { - "text/plain": "array([[ 0.70710678, 0.70710678],\n [ 0.70710678, -0.70710678]])" + "text/plain": [ + "array([[ 0.70710678, 0.70710678],\n", + " [ 0.70710678, -0.70710678]])" + ] }, + "execution_count": 5, "metadata": {}, - "execution_count": 5 + "output_type": "execute_result" } ], "source": [ @@ -171,11 +191,11 @@ ] }, { + "cell_type": "markdown", + "metadata": {}, "source": [ "对于`CNOT`门,其本质上是受控`X`门(`Controlled-X` gate),因此在MindQuantum中,如果我们需要执行`CNOT`门,只需设定`X`门的控制比特位和目标比特位即可(实际上,任意的量子门我们都可以设定控制比特位和所需执行量子门操作的目标比特位)。例如:" - ], - "cell_type": "markdown", - "metadata": {} + ] }, { "cell_type": "code", @@ -185,9 +205,11 @@ }, "outputs": [ { - "output_type": "stream", "name": "stdout", - "text": "X(0 <-: 1)\n" + "output_type": "stream", + "text": [ + "X(0 <-: 1)\n" + ] } ], "source": [ @@ -196,8 +218,12 @@ ] }, { + "cell_type": "markdown", + "metadata": {}, "source": [ - "说明:X(0 <-: 1) ,表示第0位量子比特位为目标比特位,第1位量子比特位为控制比特位,第0位量子比特受第1位量子比特控制,若第1位量子比特为1,则对第0位量子比特执行`X`门操作,否则不作任何操作。\n", + "说明:\n", + "\n", + "(1)X(0 <-: 1) ,表示第0位量子比特位为目标比特位,第1位量子比特位为控制比特位,第0位量子比特受第1位量子比特控制,若第1位量子比特为1,则对第0位量子比特执行`X`门操作,否则不作任何操作;\n", "\n", "上面介绍了一些常用的不含参量子门,接下来,我们将介绍一些含参量子门(如旋转门`RX`门、`RY`门和`RZ`门),通过赋予旋转角度$\\theta$某些确定的值,可以得到作用不同的旋转门。另外,这些含参量子门是后续搭建量子神经网络的重要组成单元。\n", "\n", @@ -210,9 +236,7 @@ "$$\\text{RZ}(\\theta)= e^{-\\frac{i\\theta Z}{2}}=\\cos\\left(\\frac{\\theta}{2}\\right)\\cdot I-i\\sin\\left(\\frac{\\theta}{2}\\right)\\cdot Z=\\begin{pmatrix}e^{-\\frac{i\\theta}{2}}&0\\\\\\\\0&e^{\\frac{i\\theta}{2}}\\end{pmatrix}.$$\n", "\n", "我们令$\\theta$分别为$0、\\frac{\\pi}{2}$和$\\pi$,然后打印$\\text{RX}(0)$门、$\\text{RY}(\\frac{\\pi}{2}$)门和$\\text{RZ}(\\pi)$门的矩阵形式,可以得到:" - ], - "cell_type": "markdown", - "metadata": {} + ] }, { "cell_type": "code", @@ -222,17 +246,22 @@ }, "outputs": [ { - "output_type": "stream", "name": "stdout", - "text": "Gate name: RX(theta)\n" + "output_type": "stream", + "text": [ + "Gate name: RX(theta)\n" + ] }, { - "output_type": "execute_result", "data": { - "text/plain": "array([[1.+0.j, 0.-0.j],\n [0.-0.j, 1.+0.j]])" + "text/plain": [ + "array([[1.+0.j, 0.-0.j],\n", + " [0.-0.j, 1.+0.j]])" + ] }, + "execution_count": 7, "metadata": {}, - "execution_count": 7 + "output_type": "execute_result" } ], "source": [ @@ -242,11 +271,11 @@ ] }, { + "cell_type": "markdown", + "metadata": {}, "source": [ "当$\\theta=0$时,此时$\\text{RX}(0)$门就是我们熟悉的`I`门。" - ], - "cell_type": "markdown", - "metadata": {} + ] }, { "cell_type": "code", @@ -256,17 +285,22 @@ }, "outputs": [ { - "output_type": "stream", "name": "stdout", - "text": "Gate name: RY(theta)\n" + "output_type": "stream", + "text": [ + "Gate name: RY(theta)\n" + ] }, { - "output_type": "execute_result", "data": { - "text/plain": "array([[ 0.70710678, -0.70710678],\n [ 0.70710678, 0.70710678]])" + "text/plain": [ + "array([[ 0.70710678, -0.70710678],\n", + " [ 0.70710678, 0.70710678]])" + ] }, + "execution_count": 8, "metadata": {}, - "execution_count": 8 + "output_type": "execute_result" } ], "source": [ @@ -276,11 +310,11 @@ ] }, { + "cell_type": "markdown", + "metadata": {}, "source": [ "当$\\theta=\\frac{\\pi}{2}$时,此时$\\text{RY}(\\frac{\\pi}{2})$门就是我们熟悉的`H`门。" - ], - "cell_type": "markdown", - "metadata": {} + ] }, { "cell_type": "code", @@ -290,17 +324,22 @@ }, "outputs": [ { - "output_type": "stream", "name": "stdout", - "text": "Gate name: RZ(theta)\n" + "output_type": "stream", + "text": [ + "Gate name: RZ(theta)\n" + ] }, { - "output_type": "execute_result", "data": { - "text/plain": "array([[0.-1.j, 0.+0.j],\n [0.+0.j, 0.+1.j]])" + "text/plain": [ + "array([[0.-1.j, 0.+0.j],\n", + " [0.+0.j, 0.+1.j]])" + ] }, + "execution_count": 9, "metadata": {}, - "execution_count": 9 + "output_type": "execute_result" } ], "source": [ @@ -310,6 +349,8 @@ ] }, { + "cell_type": "markdown", + "metadata": {}, "source": [ "当$\\theta=\\pi$时,此时$\\text{RZ}(\\pi)$门就是我们熟悉的`Z`门。(相差一个全局相位$-i$)\n", "\n", @@ -320,9 +361,7 @@ "![quantum circuit](https://gitee.com/mindspore/docs/raw/master/docs/mindquantum/docs/source_zh_cn/images/quantum_circuit.png)\n", "\n", "通过在量子线路中添加作用在不同量子比特位上的量子门即可快速完成对量子线路的搭建。" - ], - "cell_type": "markdown", - "metadata": {} + ] }, { "cell_type": "code", @@ -332,26 +371,40 @@ }, "outputs": [ { - "output_type": "stream", "name": "stdout", - "text": "H(0)\nX(1 <-: 0)\nRY(theta|2)\n=========Circuit Summary=========\n|Total number of gates : 3. |\n|Parameter gates : 1. |\n|with 1 parameters are : theta.|\n|Number qubit of circuit: 3 |\n=================================\n" + "output_type": "stream", + "text": [ + "q0: ──────H────────●──\n", + " │ \n", + "q1: ───────────────X──\n", + " \n", + "q2: ──RY(theta)───────\n", + "=========Circuit Summary=========\n", + "|Total number of gates : 3. |\n", + "|Parameter gates : 1. |\n", + "|with 1 parameters are : theta.|\n", + "|Number qubit of circuit: 3 |\n", + "=================================\n" + ] } ], "source": [ - "from mindquantum import Circuit #需要从mindquantum导入Circuit模块才可以搭建量子线路\n", + "from mindquantum.core import Circuit #导入Circuit模块,用于搭建量子线路\n", + "\n", "encoder = Circuit() #初始化量子线路\n", "encoder += H.on(0) #H门作用在第0位量子比特\n", "encoder += X.on(1,0) #X门作用在第1位量子比特且受第0位量子比特控制\n", "encoder += RY('theta').on(2) #RY(theta)门作用在第2位量子比特\n", - "print(encoder) #打印encoder\n", - "encoder.summary() #总结encoder量子线路" + "\n", + "print(encoder) #打印Encoder\n", + "encoder.summary() #总结Encoder量子线路" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "从对encoder的Summary中可以看到,该量子线路由3个量子门组成,其中有1个含参量子门且参数为theta,该量子线路调控的量子比特数为3。\n", + "从对Encoder的Summary中可以看到,该量子线路由3个量子门组成,其中有1个含参量子门且参数为theta,该量子线路调控的量子比特数为3。\n", "\n", "因此,我们可以根据自身所需求解的问题,搭建对应的量子线路。赶紧动手搭建属于你的第一个量子线路吧!\n", "\n", @@ -361,9 +414,9 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3.7.5 64-bit", + "display_name": "Python 3", "language": "python", - "name": "python37564bit6afae4a42a5941c0967cdcfc2650559a" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -375,9 +428,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.5-final" + "version": "3.8.5" } }, "nbformat": 4, "nbformat_minor": 4 -} \ No newline at end of file +} diff --git a/tutorials/2.initial_experience_of_quantum_neural_network.ipynb b/tutorials/2.initial_experience_of_quantum_neural_network.ipynb index 636395b3bb85df3afd3ed67c04c41ef7e8a9c2f5..5a66cde3d261dc905a9e62f374d7442144fcfb11 100644 --- a/tutorials/2.initial_experience_of_quantum_neural_network.ipynb +++ b/tutorials/2.initial_experience_of_quantum_neural_network.ipynb @@ -1,33 +1,14 @@ { - "metadata": { - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.5-final" - }, - "orig_nbformat": 2, - "kernelspec": { - "name": "python37564bit6afae4a42a5941c0967cdcfc2650559a", - "display_name": "Python 3.7.5 64-bit" - } - }, - "nbformat": 4, - "nbformat_minor": 2, "cells": [ { + "cell_type": "markdown", + "metadata": {}, "source": [ "# 量子神经网络初体验\n", "\n", "`Linux` `CPU` `全流程` `初级` `中级` `高级`\n", "\n", - "[![](https://gitee.com/mindspore/docs/raw/master/resource/_static/logo_source.png)](https://gitee.com/mindspore/docs/blob/master/docs/mindquantum/docs/source_zh_cn/parameterized_quantum_circuit.ipynb) [![](https://gitee.com/mindspore/mindquantum/raw/master/tutorials/images/view_mindquantum_api.png)](https://mindspore.cn/mindquantum/api/zh-CN/master/index.html)\n", + "[![](https://gitee.com/mindspore/docs/raw/master/resource/_static/logo_source.png)](https://gitee.com/mindspore/mindquantum/blob/dev/tutorials/2.initial_experience_of_quantum_neural_network.ipynb) [![](https://gitee.com/mindspore/mindquantum/raw/master/tutorials/images/view_mindquantum_api.png)](https://mindspore.cn/mindquantum/api/zh-CN/master/index.html)\n", "\n", "## 1. 量子神经网络的结构\n", "\n", @@ -66,12 +47,10 @@ "\n", "解决方案:通过训练Ansatz中的参数,希望测量值接近于目标期望值,换句话说,我们只需让测量值尽可能接近于$|0\\rangle$态关于泡利`Z`算符对应的期望值,那么此时的状态就是$|0\\rangle$,即Ansatz抵消了Encoder对初始量子态产生的误差。\n", "\n", - "#### 2.1 环境准备\n", + "## 3. 环境准备\n", "\n", "导入本教程所依赖的模块" - ], - "cell_type": "markdown", - "metadata": {} + ] }, { "cell_type": "code", @@ -81,18 +60,18 @@ "source": [ "import numpy as np #导入numpy库并简写为np\n", "import mindquantum as mq #导入mindquantum库并简写为mq\n", - "from mindquantum import Circuit #从mindquantum库导入Circuit模块,搭建量子线路\n", - "from mindquantum.gate import H, RX, RY, RZ #从mindquantum.gate模块中导入量子门H, RX, RY, RZ" + "from mindquantum.core import Circuit #导入Circuit模块,用于搭建量子线路\n", + "from mindquantum.core import H, RX, RY, RZ #导入量子门H, RX, RY, RZ" ] }, { + "cell_type": "markdown", + "metadata": {}, "source": [ - "#### 2.2 搭建Encoder\n", + "## 4. 搭建Encoder\n", "\n", "根据图示的量子线路图,我们可以在MindQuantum中搭建Encoder。" - ], - "cell_type": "markdown", - "metadata": {} + ] }, { "cell_type": "code", @@ -102,9 +81,40 @@ }, "outputs": [ { - "output_type": "stream", "name": "stdout", - "text": "==================Circuit Summary==================\n|Total number of gates : 4. |\n|Parameter gates : 3. |\n|with 3 parameters are : alpha0, alpha1, alpha2. |\n|Number qubit of circuit: 1 |\n===================================================\n" + "output_type": "stream", + "text": [ + "==================Circuit Summary==================\n", + "|Total number of gates : 4. |\n", + "|Parameter gates : 3. |\n", + "|with 3 parameters are : alpha0, alpha1, alpha2. |\n", + "|Number qubit of circuit: 1 |\n", + "===================================================\n" + ] + }, + { + "data": { + "text/html": [ + "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
q0: ──H────RX(alpha0)────RY(alpha1)────RZ(alpha2)──\n",
+       "
\n" + ], + "text/plain": [ + "q0: ──H────RX(alpha0)────RY(alpha1)────RZ(alpha2)──" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ @@ -114,18 +124,19 @@ "encoder += RY(f'alpha{1}').on(0) #RY(alpha_1)门作用在第0位量子比特\n", "encoder += RZ(f'alpha{2}').on(0) #RZ(alpha_2)门作用在第0位量子比特\n", "encoder = encoder.no_grad() #Encoder作为整个量子神经网络的第一层,不用对编码线路中的梯度求导数,因此加入no_grad()\n", - "encoder.summary() #总结Encoder" + "encoder.summary() #总结Encoder\n", + "encoder" ] }, { + "cell_type": "markdown", + "metadata": {}, "source": [ "从对Encoder的Summary中可以看到,该量子线路由4个量子门组成,其中有3个含参量子门且参数为$\\alpha_0,\\alpha_1,\\alpha_2$​​​​,该量子线路调控的量子比特数为1。\n", "\n", "\n", "然后,我们需要对Encoder中的参数进行赋值。由于Encoder中的参数$\\alpha_0, \\alpha_1$​和$\\alpha_2$​分别为已知值0.2, 0.3和0.4,因此可以直接对参数进行赋值,并打印此时的状态。" - ], - "cell_type": "markdown", - "metadata": {} + ] }, { "cell_type": "code", @@ -135,36 +146,37 @@ }, "outputs": [ { - "output_type": "stream", "name": "stdout", - "text": "(0.566990315914154-0.17539066076278687j)¦0⟩\n(0.8008146286010742+0.08034947514533997j)¦1⟩\n" + "output_type": "stream", + "text": [ + "(0.5669903122552596-0.1753906567580312j)¦0⟩\n", + "(0.800814626197614+0.08034947292077024j)¦1⟩\n" + ] } ], "source": [ - "from mindquantum.circuit import StateEvolution #从mindquantum.circuit模块中导入StateEvolution模块,演化量子线路,计算末态\n", "\n", "alpha0, alpha1, alpha2 = 0.2, 0.3, 0.4 #alpha0, alpha1, alpha2为已知的固定值,分别赋值0.2, 0.3 和0.4\n", - "\n", - "state = (StateEvolution(encoder)).final_state({'alpha0': alpha0, 'alpha1': alpha1,'alpha2': alpha2}, ket=True) #量子线路中的参数必须赋值,ket=True表示是否展示为量子态 \n", + "state = encoder.get_qs(pr={'alpha0': alpha0, 'alpha1': alpha1, 'alpha2': alpha2}, ket=True)\n", "print(state)" ] }, { + "cell_type": "markdown", + "metadata": {}, "source": [ "上述步骤为了展示MindQuantum可以演化量子线路(若量子线路中的量子门带参数,则需要对参数赋值)并得到演化后的末态。从上述打印可以看到,演化后得到的末态为$|0\\rangle$​​​和$|1\\rangle$​​​组成的叠加态,各项对应的振幅为上述打印的状态左边对应的数值。 \n", "\n", "说明:\n", "\n", - "(1)StateEvolution模块用于演化量子线路,计算末态,一般格式如下:StateEvolution(circuit),括号中的circuit就是我们搭建的需要演化的量子线路,在上面的例子中,此时的Encoder我们上述的量子线路;\n", + "(1)通过调用量子线路的`get_qs`函数,我们能够得到该量子线路在全零态基础上演化出来的量子态。\n", "\n", - "(2)final_state模块用于展现初态经过给定的量子线路(即明确参数)后的末态,一般格式如下:final_state(param=None, ket=False),线路中的参数param必须要明确给出,ket=True表示是否展示量子态;\n", + "(2)`get_qs`的`pr`参数代表参数化量子线路中的参数值,`ket`表示是否将量子态输出为右矢形式。\n", "\n", - "#### 2.3 搭建Ansatz\n", + "## 5. 搭建Ansatz\n", "\n", "同样地,我们也可以在MindQuantum中搭建Ansatz。" - ], - "cell_type": "markdown", - "metadata": {} + ] }, { "cell_type": "code", @@ -174,26 +186,45 @@ }, "outputs": [ { - "output_type": "stream", - "name": "stdout", - "text": "==============Circuit Summary==============\n|Total number of gates : 2. |\n|Parameter gates : 2. |\n|with 2 parameters are : theta1, theta0. |\n|Number qubit of circuit: 1 |\n===========================================\n" + "data": { + "text/html": [ + "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
q0: ──RX(theta0)────RY(theta1)──\n",
+       "
\n" + ], + "text/plain": [ + "q0: ──RX(theta0)────RY(theta1)──" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ "ansatz = Circuit() #初始化量子线路\n", "ansatz += RX(f'theta{0}').on(0) #RX(theta_0)门作用在第0位量子比特\n", "ansatz += RY(f'theta{1}').on(0) #RY(theta_1)门作用在第0位量子比特\n", - "ansatz.summary() #总结Ansatz量子线路" + "ansatz #打印量子线路" ] }, { + "cell_type": "markdown", + "metadata": {}, "source": [ "从对Ansatz的Summary中可以看到,该量子线路由2个量子门组成,其中有2个含参量子门且参数为$\\theta_0,\\theta_1$​​,该量子线路调控的量子比特数为1。\n", "\n", - "然后,对Ansatz中的参数进行赋值。由于Ansatz为需要训练的量子线路,因此Ansatz中的参数$\\theta_0$​​和$\\theta_1$​​可以随机设定,通常默认设为初始值0。我们同样可以打印此时的量子态,不过这并不是必要的步骤,只是为了再次熟悉一下如何使用StateEvolution模块中的final_state。" - ], - "cell_type": "markdown", - "metadata": {} + "然后,对Ansatz中的参数进行赋值。由于Ansatz为需要训练的量子线路,因此Ansatz中的参数$\\theta_0$​​和$\\theta_1$​​可以随机设定,通常默认设为初始值0。我们同样可以打印此时的量子态,不过这并不是必要的步骤,只是为了再次熟悉一下`get_qs`函数。" + ] }, { "cell_type": "code", @@ -203,26 +234,27 @@ }, "outputs": [ { - "output_type": "stream", "name": "stdout", - "text": "1.0¦0⟩\n" + "output_type": "stream", + "text": [ + "1¦0⟩\n" + ] } ], "source": [ - "theta0, theta1 = 0, 0 #对theta0, theta1进行赋值,设为初始值0 , 0\n", - "\n", - "state = (StateEvolution(ansatz)).final_state({'theta0': theta0, 'theta1': theta1}, ket = True) \n", + "theta0, theta1 = 0, 0 #对theta0, theta1进行赋值,设为初始值0, 0\n", + "state = ansatz.get_qs(pr=dict(zip(ansatz.params_name, [theta0, theta1])), ket=True)\n", "print(state)" ] }, { + "cell_type": "markdown", + "metadata": {}, "source": [ "从上述打印可以看到,此时的状态为$|0\\rangle$​​且振幅为1。这是因为对于Ansatz来说,默认的输入量子态为$|0\\rangle$​​,而且其中的参数$\\theta_0$​​和$\\theta_1$​​都为0,此时的`RX(0)`门和`RY(0)`门都相当于`I`门,因此整个线路演化的过程就是$|0\\rangle$​​经过$I\\cdot I$,那么最后输出的态当然就是$|0\\rangle$​​​了。\n", "\n", "那么完整的量子线路就是Encoder加上Ansatz。" - ], - "cell_type": "markdown", - "metadata": {} + ] }, { "cell_type": "code", @@ -232,26 +264,45 @@ }, "outputs": [ { - "output_type": "stream", - "name": "stdout", - "text": "==========================Circuit Summary==========================\n|Total number of gates : 6. |\n|Parameter gates : 5. |\n|with 5 parameters are : alpha1, theta1, alpha0, alpha2, theta0. |\n|Number qubit of circuit: 1 |\n===================================================================\n" + "data": { + "text/html": [ + "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
q0: ──H────RX(alpha0)────RY(alpha1)────RZ(alpha2)────RX(theta0)────RY(theta1)──\n",
+       "
\n" + ], + "text/plain": [ + "q0: ──H────RX(alpha0)────RY(alpha1)────RZ(alpha2)────RX(theta0)────RY(theta1)──" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ "circuit = encoder + ansatz #完整的量子线路由Encoder和Ansatz组成\n", - "circuit.summary()" + "circuit" ] }, { + "cell_type": "markdown", + "metadata": {}, "source": [ "从对完整的量子线路的Summary中可以看到,该量子线路由6个量子门组成,其中有5个含参量子门且参数为$\\alpha_0,\\alpha_1,\\alpha_2,\\theta_0,\\theta_1$​​​,该量子线路调控的量子比特数为1。\n", "\n", - "#### 2.4 构建哈密顿量\n", + "## 6. 构建哈密顿量\n", "\n", "我们对第0位量子比特执行泡利`Z`算符测量,构建对应的哈密顿量。" - ], - "cell_type": "markdown", - "metadata": {} + ] }, { "cell_type": "code", @@ -261,20 +312,24 @@ }, "outputs": [ { - "output_type": "stream", "name": "stdout", - "text": "-1 [Z0] \n" + "output_type": "stream", + "text": [ + "-1 [Z0] \n" + ] } ], "source": [ - "from mindquantum.ops import QubitOperator #从mindquantum.ops模块导入QubitOperator模块,构造泡利算符\n", - "from mindquantum.gate import Hamiltonian #从mindquantum.gate模块导入Hamiltonian模块,构建哈密顿量\n", + "from mindquantum.core import QubitOperator #导入QubitOperator模块,用于构造泡利算符\n", + "from mindquantum.core import Hamiltonian #导入Hamiltonian模块,用于构建哈密顿量\n", "\n", - "ham = Hamiltonian(QubitOperator('Z0',-1)) #对第0位量子比特执行泡利Z算符测量,且将系数设置为-1,构建对应的哈密顿量\n", + "ham = Hamiltonian(QubitOperator('Z0', -1)) #对第0位量子比特执行泡利Z算符测量,且将系数设置为-1,构建对应的哈密顿量\n", "print(ham)" ] }, { + "cell_type": "markdown", + "metadata": {}, "source": [ "从上述打印可以看到,此时构建的哈密顿量为对第0位量子比特执行泡利`Z`算符测量,且系数为-1。之所以将系数设为-1,是因为在量子神经网络的训练中,Ansatz中的参数的梯度会一直下降,同时测量值也会一直减少。如果最后收敛于-1,那么此时对应的量子态是$|1\\rangle$而不是$|0\\rangle$​,如下所示\n", "$$\n", @@ -292,16 +347,14 @@ "\n", "(2)Hamiltonian是哈密顿量包装器,主要用于构建哈密顿量,一般格式如下:Hamiltonian(QubitOperator('X0 Y2', 0.5)),X0和Y2表示泡利`X`算符作用在第0位量子比特,泡利`Y`算符作用在第2位量子比特,系数为0.5;\n", "\n", - "#### 2.5 生成参数化量子线路模拟算子\n", + "## 7. 生成参数化量子线路模拟算子\n", "\n", "对于上述搭建的量子线路,我们可以在MindQuantum生成一个参数化量子线路模拟算子对其进行模拟。\n", "\n", "\n", "\n", "首先,为了方便,我们对Encoder和Ansatz中的参数数组分别命名为encoder_names和ansatz_names。" - ], - "cell_type": "markdown", - "metadata": {} + ] }, { "cell_type": "code", @@ -311,26 +364,30 @@ }, "outputs": [ { - "output_type": "stream", "name": "stdout", - "text": "encoder_names = ['alpha0', 'alpha1', 'alpha2'] \nansatz_names = ['theta0', 'theta1']\n" + "output_type": "stream", + "text": [ + "encoder_names = ['alpha0', 'alpha1', 'alpha2'] \n", + "ansatz_names = ['theta0', 'theta1']\n" + ] } ], "source": [ - "encoder_names = encoder.para_name #Encoder中所有参数组成的数组,encoder.para_name系统会自动生成\n", - "ansatz_names = ansatz.para_name #Ansatz中所有参数组成的数组,ansatz.para_name系统会自动生成\n", - "print('encoder_names = ', encoder.para_name, '\\nansatz_names =', ansatz.para_name)" + "encoder_names = encoder.params_name # Encoder中所有参数组成的数组,encoder.para_name系统会自动生成\n", + "ansatz_names = ansatz.params_name # Ansatz中所有参数组成的数组,ansatz.para_name系统会自动生成\n", + "\n", + "print('encoder_names = ', encoder.params_name, '\\nansatz_names =', ansatz.params_name)" ] }, { + "cell_type": "markdown", + "metadata": {}, "source": [ - "从上述打印可以看到,encoder_names为encoder中所有参数$\\alpha_0, \\alpha_1, \\alpha_2$​组成的数组,ansatz_names为ansatz中所有参数$\\theta_0,\\theta_1$​组成的数组,这两个数组会在生成参数化量子线路模拟算子时用到。\n", + "从上述打印可以看到,encoder_names为Encoder中所有参数$\\alpha_0, \\alpha_1, \\alpha_2$​组成的数组,ansatz_names为Ansatz中所有参数$\\theta_0,\\theta_1$​组成的数组,这两个数组会在生成参数化量子线路模拟算子时用到。\n", "\n", "\n", - "然后,我们通过模块generate_pqc_operator生成一个参数化量子线路模拟算子。" - ], - "cell_type": "markdown", - "metadata": {} + "然后,我们通过`Simulator`模块得到参数化量子线路演化和梯度求解的算子。" + ] }, { "cell_type": "code", @@ -340,53 +397,76 @@ }, "outputs": [ { - "output_type": "stream", "name": "stdout", - "text": "Measurement result: [[0.29552022]]\nGradient of encoder parameters: [[[0. 0. 0.]]]\nGradient of ansatz parameters: [[[-0.37202555 0.87992316]]]\n" + "output_type": "stream", + "text": [ + "Measurement result: [[0.29552022+0.j]]\n", + "Gradient of encoder parameters: [[[0.+0.j 0.+0.j 0.+0.j]]]\n", + "Gradient of ansatz parameters: [[[-0.37202556+0.j 0.87992317+0.j]]]\n" + ] } ], "source": [ - "from mindquantum.nn import generate_pqc_operator #从mindquantum.nn模块中(nn表示神经网络)导入generate_pqc_operator模块\n", + "# 导入Simulator模块\n", + "from mindquantum.simulator import Simulator\n", "\n", - "pqc = generate_pqc_operator(encoder_names, ansatz_names, circuit, ham) #模块generate_pqc_operator可生成参数化量子线路模拟算子\n", + "# 生成一个基于projectq后端的模拟器,并设置模拟器的比特数为量子线路的比特数。\n", + "sim = Simulator('projectq', circuit.n_qubits)\n", "\n", - "import mindspore as ms #导入mindspore库并简写为ms\n", - "from mindspore import Tensor #从mindspore库导入导入Tensor模块,用于数据储存\n", - "from mindspore import context #从mindspore库导入导入context模块,用于配置当前运行环境\n", - "context.set_context(mode=context.GRAPH_MODE, device_target=\"CPU\") #模式:搭建静态训练图;要运行的目标设备:CPU(目前量子-经典混合神经网络只支持CPU的运行模式)\n", + "# 获取模拟器基于当前量子态的量子线路演化以及期望、梯度求解算子\n", + "grad_ops = sim.get_expectation_with_grad(ham,\n", + " circuit,\n", + " encoder_params_name=encoder_names,\n", + " ansatz_params_name=ansatz_names)\n", "\n", - "encoder_data = Tensor(np.array([[alpha0,alpha1,alpha2]]).astype(np.float32))#Encoder中的alpha0, alpha1, alpha2这三个参数组成的数组,将其数据类型转换为float32,并利用Tensor储存在encoder_data中\n", - " #MindQuantum支持多样本的batch训练,Encoder数组是两个维度,第一个维度为样本,第二个维度为特征(即参数)\n", + "# Encoder中的alpha0, alpha1, alpha2这三个参数组成的数组,\n", + "# 将其数据类型转换为float32,并储存在encoder_data中。\n", + "# MindQuantum支持多样本的batch训练,Encoder数组是两个维度,\n", + "# 第一个维度为样本,第二个维度为特征(即参数)\n", + "encoder_data = np.array([[alpha0, alpha1, alpha2]]).astype(np.float32)\n", "\n", - "ansatz_data = Tensor(np.array([theta0,theta1]).astype(np.float32)) #Ansatz中的theta0, theta1这两个参数组成的数组,将其数据类型转换为float32,并利用Tensor储存在ansatzr_data中,Ansatz数据只有一个维度,特征(即参数)\n", + "# Ansatz中的theta0, theta1这两个参数组成的数组,将其数据类型转换为float32,\n", + "# 并储存在ansatzr_data中,Ansatz数据只有一个维度,特征(即参数)\n", + "ansatz_data = np.array([theta0, theta1]).astype(np.float32)\n", "\n", - "measure_result, encoder_grad, ansatz_grad = pqc(encoder_data, ansatz_data) #根据Encoder和Ansatz的数据,输出参数化量子线路的测量值,Encoder中的参数的导数和Ansatz中的参数的导数\n", + "# 根据Encoder和Ansatz的数据,输出参数化量子线路的测量值,Encoder中的参数的导数和Ansatz中的参数的导数\n", + "measure_result, encoder_grad, ansatz_grad = grad_ops(encoder_data, ansatz_data)\n", "\n", - "print('Measurement result: ', measure_result.asnumpy())\n", - "print('Gradient of encoder parameters: ', encoder_grad.asnumpy())\n", - "print('Gradient of ansatz parameters: ', ansatz_grad.asnumpy())" + "print('Measurement result: ', measure_result)\n", + "print('Gradient of encoder parameters: ', encoder_grad)\n", + "print('Gradient of ansatz parameters: ', ansatz_grad)" ] }, { + "cell_type": "markdown", + "metadata": {}, "source": [ - "从上述打印可以看到,测量结果(期望值)为2.9552022,Encoder中的3个参数的导数为0,0,0(因为我们对Encoder设置了no_grad()),Ansatz中的2个参数的导数为-0.37202555,-0.87992316。\n", + "从上述打印可以看到,测量结果(期望值)为0.29552022,Encoder中的3个参数的导数为0,0,0(因为我们对Encoder设置了no_grad()),Ansatz中的2个参数的导数为-0.37202555,-0.87992316。\n", "\n", - "这里通过generate_pqc_operator产生的只是一个算子,还不能进行训练,要把它放到量子神经网络里面才能进行训练。通过训练Ansatz中的参数,可以使得Ansatz中的参数的导数一直下降并接近于0,那么测量值也就会接近于-1。\n", + "这里通过`get_expectation_with_grad`产生的只是一个算子,还不能进行训练,要把它放到量子神经网络里面才能进行训练。通过训练Ansatz中的参数,可以使得Ansatz中的参数的导数一直下降并接近于0,那么测量值也就会接近于-1。\n", "\n", "说明:\n", "\n", - "(1)generate_pqc_operator模块用于生成参数化量子线路来模拟算子,一般格式如下:mindquantum.nn.generate_pqc_operator(encoder_params_names, ansatz_params_names, circuit, measurements, n_threads=1),通常circuit为我们搭建的Encoder和Ansatz,measurements为我们构建的哈密顿量ham,n_threads为用于数据并行的线程数,默认值:1;\n", + "(1)`Simulator`的`get_expectation_with_grad`用于生成参数化量子线路来模拟算子,一般格式如下:\n", "\n", - "(2)mindspore是一个全场景深度学习框架,旨在实现易开发、高效执行、全场景覆盖三大目标,提供支持异构加速的张量可微编程能力,支持云、服务器、边和端多种硬件平台;\n", + "```python\n", + "Simulator.get_expectation_with_grad(ham,\n", + " circ_right,\n", + " circ_left,\n", + " encoder_params_name,\n", + " ansatz_params_name,\n", + " parallel_worker=1)\n", + "```\n", + "此函数适用于计算如下模型:\n", "\n", - "(3)Tensor模块主要用于数据储存;\n", + "$$E=\\left<0\\right|U^\\dagger_l(\\theta) H U_r(\\theta)\\left|0\\right>$$\n", "\n", - "(4)context模块用于配置当前运行环境;\n", + "其中`circ_right`是$U_r$,`circ_left`是$U_l$,当不提供时,默认跟`circ_right`是相同的线路,`encoder_params_name`指定整个体系中哪些参数是属于编码器中的参数,编码器可以将经典数据通过feature mapping映射到高位希尔伯特空间中,`ansatz_params_name`指定整个体系中哪些参数是属于待训练线路中的参数,`parallel_worker`指定并行数,当需要编码的经典数据是一个batch时,合理设置此参数可以提高计算效率。\n", "\n", - "#### 2.6 搭建量子神经网络" - ], - "cell_type": "markdown", - "metadata": {} + "(2)mindspore是一个全场景深度学习框架,旨在实现易开发、高效执行、全场景覆盖三大目标,提供支持异构加速的张量可微编程能力,支持云、服务器、边和端多种硬件平台;\n", + "\n", + "## 8. 搭建量子神经网络" + ] }, { "cell_type": "code", @@ -394,74 +474,71 @@ "metadata": {}, "outputs": [ { - "output_type": "execute_result", "data": { - "text/plain": "MindQuantumLayer<>" + "text/plain": [ + "MQLayer<\n", + " (evolution): MQOps<1 qubit projectq VQA Operator>\n", + " >" + ] }, + "execution_count": 10, "metadata": {}, - "execution_count": 10 + "output_type": "execute_result" } ], "source": [ - "from mindquantum.nn import MindQuantumLayer #从mindquantum.nn模块中(nn表示神经网络)导入MindQuantumLayer\n", + "from mindquantum.framework import MQLayer # 导入MQLayer\n", + "import mindspore as ms # 导入mindspore\n", + "\n", "ms.set_seed(1) #设置生成随机数的种子\n", - "Quantumnet = MindQuantumLayer(encoder_names, ansatz_names, circuit, ham, n_threads=1)\n", - "Quantumnet" + "ms.context.set_context(mode=ms.context.PYNATIVE_MODE, device_target=\"CPU\")\n", + "\n", + "QuantumNet = MQLayer(grad_ops)\n", + "QuantumNet" ] }, { + "cell_type": "markdown", + "metadata": {}, "source": [ "上述打印可以看到,我们已经成功搭建了量子机器学习层,其可以无缝地跟MindSpore中其它的算子构成一张更大的机器学习网络。\n", "\n", "说明:\n", "\n", - "(1)MindQuantumLayer模块可以生成可训练的MindQuantum量子机器学习层,一般格式如下:MindQuantumLayer(encoder_params_names, ansatz_params_names, circuit, measurements, weight_init=\"normal\", n_threads=1);\n", + "(1)MindQuantum中的量子线路梯度计算算子都是在`PYNATIVE_MODE`下的,因此需要设置MindSpore的运行模式。\n", "\n", - "(2)我们也可以通过如下代码方式搭建量子机器学习层,只是在MindQuantum中,已经将下述过程封装打包,这样我们就可以直接利用MindQuantumLayer模块搭建量子机器学习层。对于更复杂的量子-经典混合神经网络,如下搭建方式会展示它的优势(将在以后的tutorials中介绍);" - ], - "cell_type": "markdown", - "metadata": {} + "(2)我们也可以通过如下代码方式搭建量子机器学习层,只是在MindQuantum中,已经将下述过程封装打包,这样我们就可以直接利用MQLayer模块搭建量子机器学习层。对于更复杂的量子-经典混合神经网络,如下搭建方式会展示它的优势(将在以后的tutorials中介绍);" + ] }, { + "cell_type": "markdown", + "metadata": {}, "source": [ "```python\n", - "class MindQuantumLayer(nn.Cell):\n", - " def __init__(self,\n", - " encoder_params_names,\n", - " ansatz_params_names,\n", - " circuit,\n", - " measurements,\n", - " weight_init='normal',\n", - " n_threads=1):\n", - " super(MindQuantumLayer, self).__init__()\n", - " self.circuit = circuit\n", - " self.measurements = measurements\n", - " self.encoder_params_names = encoder_params_names\n", - " self.ansatz_params_names = ansatz_params_names\n", - " self.pqc = generate_pqc_operator(encoder_params_names,\n", - " ansatz_params_names,\n", - " circuit,\n", - " measurements,\n", - " n_threads=n_threads)\n", - " self.weight = Parameter(initializer(weight_init,\n", - " len(ansatz_params_names)),\n", - " name=\"weight\")\n", + "class MQLayer(nn.Cell):\n", + " def __init__(self, expectation_with_grad, weight='normal'):\n", + " super(MQLayer, self).__init__()\n", + " self.evolution = MQOps(expectation_with_grad)\n", + " weight_size = len(\n", + " self.evolution.expectation_with_grad.ansatz_params_name)\n", + " self.weight = Parameter(initializer(weight,\n", + " weight_size,\n", + " dtype=ms.float32),\n", + " name='ansatz_weight')\n", + "\n", " def construct(self, x):\n", - " x, _, _ = self.pqc(x, self.weight)\n", - " return x\n", + " return self.evolution(x, self.weight)\n", "```" - ], - "cell_type": "markdown", - "metadata": {} + ] }, { + "cell_type": "markdown", + "metadata": {}, "source": [ - "#### 2.7 训练\n", + "## 9. 训练\n", "\n", "我们采用Adam优化器优化Ansatz中的参数。" - ], - "cell_type": "markdown", - "metadata": {} + ] }, { "cell_type": "code", @@ -471,40 +548,61 @@ }, "outputs": [ { + "name": "stdout", "output_type": "stream", - "name": "stderr", - "text": "[WARNING] DEBUG(6099,python):2021-08-30-08:09:15.881.718 [mindspore/ccsrc/debug/debugger/debugger.cc:79] Debugger] Not enabling debugger. Debugger does not support CPU.\n0 : [[0.2837115]]\n10 : [[-0.8851233]]\n20 : [[-0.97001773]]\n30 : [[-0.9929431]]\n40 : [[-0.9939507]]\n50 : [[-0.9967014]]\n60 : [[-0.99878186]]\n70 : [[-0.9995535]]\n80 : [[-0.9999011]]\n90 : [[-0.99998033]]\n100 : [[-0.9999989]]\n110 : [[-0.99999785]]\n120 : [[-0.999997]]\n130 : [[-0.9999987]]\n140 : [[-0.9999998]]\n150 : [[-1.]]\n160 : [[-0.99999994]]\n170 : [[-1.]]\n180 : [[-1.]]\n190 : [[-1.]]\n" + "text": [ + "0 : [[0.2837115]]\n", + "10 : [[-0.8851233]]\n", + "20 : [[-0.97001773]]\n", + "30 : [[-0.9929431]]\n", + "40 : [[-0.9939507]]\n", + "50 : [[-0.9967015]]\n", + "60 : [[-0.99878186]]\n", + "70 : [[-0.9995535]]\n", + "80 : [[-0.9999011]]\n", + "90 : [[-0.99998033]]\n", + "100 : [[-0.9999989]]\n", + "110 : [[-0.99999785]]\n", + "120 : [[-0.999997]]\n", + "130 : [[-0.9999987]]\n", + "140 : [[-0.9999998]]\n", + "150 : [[-1.]]\n", + "160 : [[-0.99999994]]\n", + "170 : [[-1.]]\n", + "180 : [[-1.]]\n", + "190 : [[-1.]]\n" + ] } ], "source": [ - "from mindspore import nn #从mindspore库导入nn模块,nn即经典神经网络\n", - "from mindspore.nn import Adam, TrainOneStepCell #从mindspore.nn模块导入Adam模块和TrainOneStepCell模块\n", + "from mindspore import nn #导入nn模块,nn即经典神经网络\n", + "from mindspore.nn import Adam, TrainOneStepCell #导入Adam模块和TrainOneStepCell模块\n", "\n", - "opti = Adam(Quantumnet.trainable_params(), learning_rate = 0.5) #需要优化的是Quantumnet中可训练的参数,学习率设为0.5 \n", - "net = TrainOneStepCell(Quantumnet, opti)\n", + "opti = Adam(QuantumNet.trainable_params(), learning_rate=0.5) #需要优化的是Quantumnet中可训练的参数,学习率设为0.5 \n", + "net = TrainOneStepCell(QuantumNet, opti)\n", "\n", "for i in range(200):\n", - " res = net(Tensor(encoder_data))\n", + " res = net(ms.Tensor(encoder_data))\n", " if i % 10 == 0:\n", " print(i, ': ', res)" ] }, { + "cell_type": "markdown", + "metadata": {}, "source": [ "从上述打印可以看到,最后测量值收敛于-1。\n", "\n", "说明:\n", "\n", - "(1)Adam模块通过自适应矩估计算法更新梯度,可以优化Ansazt中的参数,输入的是神经网络中可训练的参数;一般格式如下:nn.Adam(net.trainable_params(), learning_rate = 0.5)\n", + "(1)Adam模块通过自适应矩估计算法更新梯度,可以优化Ansazt中的参数,输入的是神经网络中可训练的参数;一般格式如下:nn.Adam(net.trainable_params(), learning_rate=0.5);\n", "\n", "(2)TrainOneStepCell模块为网络训练包类,用优化器包装网络。生成的单元格使用输入“inputs”进行训练,将在构造函数中创建反向图,以更新参数,有不同的并行模式可用于训练。一般格式如下:nn.TrainOneStepCell(network, optimizer, sens=1.0);\n", "\n", - "#### 2.8 结果呈现\n", + "## 10. 结果呈现\n", "\n", "由于测量值已经收敛于-1,所以我们可以打印此时Ansatz中的参数。" - ], - "cell_type": "markdown", - "metadata": {} + ] }, { "cell_type": "code", @@ -514,24 +612,27 @@ }, "outputs": [ { - "output_type": "stream", "name": "stdout", - "text": "[ 2.2420275 -1.0756909]\n" + "output_type": "stream", + "text": [ + "[ 2.2420275 -1.0756909]\n" + ] } ], "source": [ - "theta0, theta1 = Quantumnet.weight.asnumpy()\n", - "print(Quantumnet.weight.asnumpy())" + "theta0, theta1 = QuantumNet.weight.asnumpy()\n", + "\n", + "print(QuantumNet.weight.asnumpy())" ] }, { + "cell_type": "markdown", + "metadata": {}, "source": [ "从上述打印可以看到,此时Ansatz中的参数$\\theta_1, \\theta_2$分别为2.2420275和-1.0756909。\n", "\n", - "通过StateEvolution模块的final_state,可以输出量子线路在最优参数时的量子态。" - ], - "cell_type": "markdown", - "metadata": {} + "通过get_qs,可以输出量子线路在最优参数时的量子态。" + ] }, { "cell_type": "code", @@ -541,23 +642,27 @@ }, "outputs": [ { - "output_type": "stream", "name": "stdout", - "text": "(0.37129759788513184-0.9285139441490173j)¦0⟩\n(1.4565440324076917e-05+6.52097298825538e-07j)¦1⟩\n" + "output_type": "stream", + "text": [ + "(0.37129760050057437-0.9285139157007681j)¦0⟩\n", + "(1.4564552975271372e-05+6.455516706194153e-07j)¦1⟩\n" + ] } ], "source": [ - "pr = {'alpha0': alpha0, 'alpha1': alpha1,'alpha2': alpha2,'theta0': theta0, 'theta1': theta1}\n", - "state = StateEvolution(circuit).final_state(pr, ket = True)\n", + "pr = {'alpha0': alpha0, 'alpha1': alpha1, 'alpha2': alpha2, 'theta0': theta0, 'theta1': theta1}\n", + "state = circuit.get_qs(pr=pr, ket=True)\n", + "\n", "print(state)" ] }, { + "cell_type": "markdown", + "metadata": {}, "source": [ "从上述打印可以看到,这就是量子线路在最优参数时的量子态。从其数值表示可以看到,这是一个接近于目标态$|0\\rangle$​​​的态。最后,我们计算一下此量子态与目标态$|0\\rangle$​​​​​的保真度(用于验证两个量子态的相似程度),并将保真度打印。" - ], - "cell_type": "markdown", - "metadata": {} + ] }, { "cell_type": "code", @@ -567,18 +672,23 @@ }, "outputs": [ { - "output_type": "stream", "name": "stdout", - "text": "1.0000000506744333\n" + "output_type": "stream", + "text": [ + "0.9999999997874571\n" + ] } ], "source": [ - "state = StateEvolution(circuit).final_state(pr)\n", + "state = circuit.get_qs(pr=pr)\n", "fid = np.abs(np.vdot(state, [1, 0]))**2 #保真度fidelity为向量内积的绝对值的模平方,即计算此时量子态对应的向量与|0>态对应的向量[1,0]的内积的模平方\n", + "\n", "print(fid)" ] }, { + "cell_type": "markdown", + "metadata": {}, "source": [ "\n", "可以看到,此时的保真度为100.00%,也就是说,该状态与目标态$|0\\rangle$​​的相似程度为100.00%。\n", @@ -588,9 +698,28 @@ "至此,我们通过MindQuantum完成了对量子神经网络的初体验!赶紧动手体验一下量子编程的乐趣吧!\n", "\n", "若想查询更多关于MindQuantum的API,请点击:[https://mindspore.cn/mindquantum/](https://mindspore.cn/mindquantum/)。" - ], - "cell_type": "markdown", - "metadata": {} + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.5" } - ] -} \ No newline at end of file + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tutorials/3.classification_of_iris_by_qnn.ipynb b/tutorials/3.classification_of_iris_by_qnn.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..610d74a96d066c0c9f52d560849f65c773f7f5be --- /dev/null +++ b/tutorials/3.classification_of_iris_by_qnn.ipynb @@ -0,0 +1,867 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 通过量子神经网络对鸢尾花进行分类\n", + "\n", + "`Linux` `CPU` `全流程` `初级` `中级` `高级`\n", + "\n", + "[![](https://gitee.com/mindspore/docs/raw/master/resource/_static/logo_source.png)](https://gitee.com/mindspore/mindquantum/blob/dev/tutorials/3.classification_of_iris_by_qnn.ipynb) [![](https://gitee.com/mindspore/mindquantum/raw/master/tutorials/images/view_mindquantum_api.png)](https://mindspore.cn/mindquantum/api/zh-CN/master/index.html)\n", + "\n", + "## 1. 概述\n", + "\n", + "在之前的案例中,我们介绍了什么是参数化量子线路并通过一个简单的例子体验了如何搭建量子神经网络来解决一个小问题。在本教案中,我们将体验升级,将会介绍如何通过搭建量子神经网络来解决经典机器学习中的问题。我们选取的问题是:监督学习中的鸢尾花分类问题。\n", + "\n", + "问题描述:鸢尾花(iris)数据集是经典机器学习中常用的数据集,该数据集总共包含150个样本(分为3种不同的亚属:山鸢尾(setosa)、杂色鸢尾(versicolor)和维吉尼亚鸢尾(virginica),每个亚属各有50个样本),每个样本包含4个特征,分别为花萼长度(sepal length)、花萼宽度(sepal width)和花瓣长度(petal length)、花瓣宽度(petal width)。\n", + "\n", + "我们选取前100个样本(山鸢尾(setosa)和杂色鸢尾(versicolor)),并随机抽取80个样本作为训练集,通过搭建量子神经网络对量子分类器(Ansatz)进行训练,学习完成后,对剩余的20个样本进行分类测试,期望预测的准确率尽可能高。\n", + "\n", + "思路:我们需要将100个样本进行划分,分成80个训练样本和20个测试样本,根据训练样本的经典数据计算搭建Encoder所需的参数,然后,搭建Encoder,将训练样本的经典数据编码到量子态上,接着,搭建Ansatz,通过搭建的量子神经网络层和MindSpore的算子对Ansatz中的参数进行训练,进而得到最终的分类器,最后,对剩余的20个测试样本进行分类测试,得到预测的准确率。\n", + "\n", + "## 2. 环境准备\n", + "\n", + "首先,我们需要导入鸢尾花的数据集,而在导入该数据集前,我们需要使用sklearn库中的datasets模块,因此读者需要检查是否安装了sklearn库,可执行如下代码检查。" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Name: scikit-learn\n", + "Version: 0.24.1\n", + "Summary: A set of python modules for machine learning and data mining\n", + "Home-page: http://scikit-learn.org\n", + "Author: None\n", + "Author-email: None\n", + "License: new BSD\n", + "Location: /home/xuxs/anaconda3/lib/python3.8/site-packages\n", + "Requires: threadpoolctl, scipy, joblib, numpy\n", + "Required-by: qiskit-aqua\n", + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "pip show scikit-learn" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "若无报错,则表明已安装。简单说明一下,sklearn是scikit-learn的简称,是一个基于Python的第三方模块。sklearn库集成了一些常用的机器学习方法,在进行机器学习任务时,并不需要实现算法,只需要简单的调用sklearn库中提供的模块就能完成大多数的机器学习任务。\n", + "\n", + "若未安装sklearn库,则可通过运行如下代码来安装。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```python\n", + "!pip install scikit-learn\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "然后,我们设置本教程所需的线程数。" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import os #导入os库\n", + "os.environ['OMP_NUM_THREADS'] = '2' #通过os.environ将量子线路模拟器的线程数设置为2" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "说明:\n", + "\n", + "(1)os是一个标准库,里面包含许多操作文件和目录的函数;\n", + "\n", + "(2)os.environ()模块,可以获取并修改环境变量;一般来说,我们需要在一开始设置线程数;\n", + "\n", + "## 3. 导入鸢尾花数据集\n", + "\n", + "有了上述准备,现在我们就可以导入鸢尾花的数据集了。" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(150, 4)\n", + "['sepal length (cm)', 'sepal width (cm)', 'petal length (cm)', 'petal width (cm)']\n", + "['setosa' 'versicolor' 'virginica']\n", + "[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 0 0 0 0 0 0 0 0\n", + " 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1\n", + " 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2\n", + " 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2\n", + " 2 2]\n", + "(150,)\n" + ] + } + ], + "source": [ + "import numpy as np #导入numpy库并简写为np\n", + "from sklearn import datasets #导入datasets模块,用于加载鸢尾花的数据集\n", + "\n", + "iris_dataset = datasets.load_iris() #加载鸢尾花的数据集,并存在iris_dataset\n", + "\n", + "print(iris_dataset.data.shape) #打印iris_dataset的样本的数据维度\n", + "print(iris_dataset.feature_names) #打印iris_dataset的样本的特征名称\n", + "print(iris_dataset.target_names) #打印iris_dataset的样本包含的亚属名称\n", + "print(iris_dataset.target) #打印iris_dataset的样本的标签的数组\n", + "print(iris_dataset.target.shape) #打印iris_dataset的样本的标签的数据维度" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "从上述打印可以看到,该数据集共有150个样本,每个样本均有4个特征,分别为花萼长度(sepal length)、花萼宽度(sepal width)和花瓣长度(petal length)、花瓣宽度(petal width)。同时样本包含3种不同的亚属:山鸢尾(setosa)、杂色鸢尾(versicolor)和维吉尼亚鸢尾(virginica),每个样本有对应的分类编号,0表示样本属于setosa,1表示样本属于versicolor,2表示样本属于virginica,因此有一个由150个数字组成的数组来表示样本的亚属类型。\n", + "\n", + "由于我们只选取前100个样本,因此执行如下命令。" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(100, 4)\n", + "['sepal length (cm)', 'sepal width (cm)', 'petal length (cm)', 'petal width (cm)']\n", + "['setosa' 'versicolor']\n", + "[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 0 0 0 0 0 0 0 0\n", + " 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1\n", + " 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1]\n", + "(100,)\n" + ] + } + ], + "source": [ + "X = iris_dataset.data[:100, :].astype(np.float32) #选取iris_dataset的data的前100个数据,将其数据类型转换为float32,并储存在X中\n", + "X_feature_names = iris_dataset.feature_names #将iris_dataset的特征名称储存在X_feature_names中\n", + "y = iris_dataset.target[:100].astype(int) #选取iris_dataset的target的前100个数据,将其数据类型转换为int,并储存在y中\n", + "y_target_names = iris_dataset.target_names[:2] #选取iris_dataset的target_names的前2个数据,并储存在y_target_names中\n", + "\n", + "print(X.shape) #打印样本的数据维度\n", + "print(X_feature_names) #打印样本的特征名称\n", + "print(y_target_names) #打印样本包含的亚属名称\n", + "print(y) #打印样本的标签的数组\n", + "print(y.shape) #打印样本的标签的数据维度" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "从上述打印可以看到,此时的数据集`X`中只有100个样本,每个样本依然有4个特征,仍为花萼长度(sepal length)、花萼宽度(sepal width)和花瓣长度(petal length)、花瓣宽度(petal width)。此时只有2种不同的亚属:山鸢尾(setosa)和杂色鸢尾(versicolor),并且每一个样本有对应的分类编号,0表示它属于setosa,1表示它属于versicolor,因此有一个由100个数字组成的数组来表示样本的亚属类型。\n", + "\n", + "## 4. 数据图像化\n", + "\n", + "为了更加直观地了解这100个样本组成的数据集,我们画出所有样本不同特征之间组成的散点图,执行如下命令。" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt #导入matplotlib.pyplot模块并简写为plt\n", + "\n", + "feature_name = {0: 'sepal length', 1: 'sepal width', 2: 'petal length', 3: 'petal width'} #将不同的特征名称分别标记为0,1,2,3\n", + "axes = plt.figure(figsize=(23, 23)).subplots(4, 4) #画出一个大小为23*23的图,包含4*4=16个子图\n", + "\n", + "colormap = {0: 'r', 1: 'g'} #将标签为0的样本设为红色,标签为1的样本设为绿色\n", + "cvalue = [colormap[i] for i in y] #将100个样本对应的标签设置相应的颜色\n", + "\n", + "for i in range(4):\n", + " for j in range(4):\n", + " if i!= j:\n", + " ax = axes[i][j] #在[i][j]的子图上开始画图\n", + " ax.scatter(X[:, i], X[:, j], c=cvalue) #画出第[i]个特征和第[j]个特征组成的散点图\n", + " ax.set_xlabel(feature_name[i], fontsize=22) #设置X轴的名称为第[i]个特征名称,字体大小为22\n", + " ax.set_ylabel(feature_name[j], fontsize=22) #设置Y轴的名称为第[j]个特征名称,字体大小为22\n", + "plt.show() #渲染图像,即呈现图像" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "从上述呈现的图像可以看到,红色的点表示标签为“0”的样本,绿色的点表示标签为“1”的样本,另外,我们发现,这两类样本的不同特征还是比较容易区分的。\n", + "\n", + "## 5. 数据预处理\n", + "\n", + "接下来,我们需要计算生成搭建Encoder时所要用到的参数,然后将数据集划分为训练集和测试集,执行如下命令。" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "scrolled": true, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(100, 7)\n" + ] + } + ], + "source": [ + "alpha = X[:, :3] * X[:, 1:] #每一个样本中,利用相邻两个特征值计算出一个参数,即每一个样本会多出3个参数(因为有4个特征值),并储存在alpha中\n", + "X = np.append(X, alpha, axis=1) #在axis=1的维度上,将alpha的数据值添加到X的特征值中\n", + "\n", + "print(X.shape) #打印此时X的样本的数据维度" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "从上述打印可以看到,此时的数据集`X`中仍有100个样本,但此时每个样本却有7个特征,前4个特征值就是原来的特征值,后3个特征值就是通过上述预处理计算得到的特征值,其具体计算公式如下:\n", + "$$\n", + "X_{i+4}^{j} = X_{i}^{j} * X_{i+1}^{j}, i=0,1,2,j=1,2,...,100.\n", + "$$\n", + "最后,我们将此时的数据集分为训练集和测试集,执行如下命令。" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(80, 7)\n", + "(20, 7)\n" + ] + } + ], + "source": [ + "from sklearn.model_selection import train_test_split #导入train_test_split函数,用于对数据集进行划分\n", + "\n", + "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0, shuffle=True) #将数据集划分为训练集和测试集\n", + " \n", + "print(X_train.shape) #打印训练集中样本的数据类型\n", + "print(X_test.shape) #打印测试集中样本的数据类型" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "从上述打印可以看到,此时的训练集有80个样本,测试集有20个样本,每个样本均有7个特征。\n", + "\n", + "说明:\n", + "\n", + "(1)append主要用于为原始数组添加一些值,一般格式如下:np.append(arr, values, axis=None),arr就是需要被添加值的数组,values就是添加到数组arr中的值,axis表示沿着那个方向;\n", + "\n", + "(2)shuffle=True表示将数据集打乱,每次都会以不同的顺序返回, shuffle就是为了避免数据投入的顺序对网络训练造成影响。增加随机性,提高网络的泛化性能,避免因为有规律的数据出现,导致权重更新时的梯度过于极端,避免最终模型过拟合或欠拟合。\n", + "\n", + "(3)train_test_split是交叉验证中常用的函数,主要用于是从样本中随机的按比例选取train data和test data,一般格式如下:\n", + "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size, random_state, shuffle=True),其中test_size表示测试样本的比例,random_state表示产生随机数的种子,shuffle=True表示将数据集打乱;\n", + "\n", + "## 6. 搭建Encoder\n", + "\n", + "根据图示的量子线路图,我们可以在MindQuantum中搭建Encoder,将经典数据编码到量子态上。\n", + "\n", + "![](https://gitee.com/mindspore/mindquantum/raw/master/tutorials/images/encoder_classification_of_iris_by_qnn.png)\n", + "\n", + "在这里,我们采用的编码方式是IQP编码(Instantaneous Quantum Polynomial encoding),一般来说Encoder的编码方式不固定,可根据问题需要选择不同的编码方式,有时也会根据最后的性能对Encoder进行调整。\n", + "\n", + "Encoder中的参数$\\alpha_0,\\alpha_1,...,\\alpha_6$​​的值,就是用上述数据预处理中得到的7个特征值代入。​" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "==================================Circuit Summary==================================\n", + "|Total number of gates : 17. |\n", + "|Parameter gates : 7. |\n", + "|with 7 parameters are : alpha0, alpha1, alpha2, alpha3, alpha4, alpha5, alpha6. |\n", + "|Number qubit of circuit: 4 |\n", + "===================================================================================\n" + ] + }, + { + "data": { + "text/html": [ + "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
q0: ──H────RZ(alpha0)────●──────────────────●──────────────────────────────────────────────────\n",
+       "                         │                  │                                                  \n",
+       "q1: ──H────RZ(alpha1)────X────RZ(alpha4)────X────●──────────────────●──────────────────────────\n",
+       "                                                 │                  │                          \n",
+       "q2: ──H────RZ(alpha2)────────────────────────────X────RZ(alpha5)────X────●──────────────────●──\n",
+       "                                                                         │                  │  \n",
+       "q3: ──H────RZ(alpha3)────────────────────────────────────────────────────X────RZ(alpha6)────X──\n",
+       "
\n" + ], + "text/plain": [ + "q0: ──H────RZ(alpha0)────●──────────────────●──────────────────────────────────────────────────\n", + " │ │ \n", + "q1: ──H────RZ(alpha1)────X────RZ(alpha4)────X────●──────────────────●──────────────────────────\n", + " │ │ \n", + "q2: ──H────RZ(alpha2)────────────────────────────X────RZ(alpha5)────X────●──────────────────●──\n", + " │ │ \n", + "q3: ──H────RZ(alpha3)────────────────────────────────────────────────────X────RZ(alpha6)────X──" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import mindquantum as mq #导入mindquantum库并简写为mq\n", + "from mindquantum.core import Circuit #导入Circuit模块,用于搭建量子线路\n", + "from mindquantum.core import UN #导入UN模块\n", + "from mindquantum.core import H, X, RZ #导入量子门H, X, RZ\n", + "\n", + "encoder = Circuit() #初始化量子线路\n", + "\n", + "encoder += UN(H, 4) #H门作用在每1位量子比特 \n", + "for i in range(4): #i = 0, 1, 2, 3 \n", + " encoder += RZ(f'alpha{i}').on(i) #RZ(alpha_i)门作用在第i位量子比特\n", + "for j in range(3): #j = 0, 1, 2\n", + " encoder += X.on(j+1, j) #X门作用在第j+1位量子比特,受第j位量子比特控制\n", + " encoder += RZ(f'alpha{j+4}').on(j+1) #RZ(alpha_{j+4})门作用在第0位量子比特 \n", + " encoder += X.on(j+1, j) #X门作用在第j+1位量子比特,受第j位量子比特控制\n", + " \n", + "encoder = encoder.no_grad() #Encoder作为整个量子神经网络的第一层,不用对编码线路中的梯度求导数,因此加入no_grad()\n", + "encoder.summary() #总结Encoder\n", + "encoder" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "从对Encoder的Summary中可以看到,该量子线路由17个量子门组成,其中有7个含参量子门且参数为$\\alpha_0,\\alpha_1,...,\\alpha_6$,该量子线路调控的量子比特数为4。\n", + "\n", + "说明:\n", + "\n", + "(1)UN模块用于将量子门映射到不同的目标量子比特和控制量子比特,一般格式如下:mindquantum.circuit.UN(gate, maps_obj, maps_ctrl=None),括号中的gate就是我们需要执行的量子门,maps_obj就是需要执行该量子门的目标量子比特,maps_ctrl就是控制量子比特,若为None即无控制量子位。若每个量子比特位执行同一个非参数量子门,则可以直接写出UN(gate, N),N表示量子比特个数;\n", + "\n", + "## 7. 搭建Ansatz\n", + "\n", + "根据图示的量子线路图,我们可以在MindQuantum中搭建Ansatz。\n", + "\n", + "![](https://gitee.com/mindspore/mindquantum/raw/master/tutorials/images/ansatz_classification_of_iris_by_qnn.png)\n", + "\n", + "与Encoder一样,Ansatz的编码方式也不固定,我们可以尝试不同的编码方式来测试最后的结果。\n", + "\n", + "在这里,我们采用的是HardwareEfficientAnsatz,即上述量子线路图所示的编码方式。" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====================================================Circuit Summary====================================================\n", + "|Total number of gates : 25. |\n", + "|Parameter gates : 16. |\n", + "|with 16 parameters are : d0_n0_0, d0_n1_0, d0_n2_0, d0_n3_0, d1_n0_0, d1_n1_0, d1_n2_0, d1_n3_0, d2_n0_0, d2_n1_0... |\n", + "|Number qubit of circuit: 4 |\n", + "=======================================================================================================================\n" + ] + }, + { + "data": { + "text/html": [ + "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
q0: ──RY(d0_n0_0)────●────RY(d1_n0_0)────────────────────────●─────────RY(d2_n0_0)────────────────────────●─────────RY(d3_n0_0)────────────────────────────────\n",
+       "                     │                                       │                                            │                                                    \n",
+       "q1: ──RY(d0_n1_0)────X─────────●─────────RY(d1_n1_0)─────────X──────────────●─────────RY(d2_n1_0)─────────X──────────────●─────────RY(d3_n1_0)─────────────────\n",
+       "                               │                                            │                                            │                                     \n",
+       "q2: ──RY(d0_n2_0)──────────────X──────────────●─────────RY(d1_n2_0)─────────X──────────────●─────────RY(d2_n2_0)─────────X──────────────●─────────RY(d3_n2_0)──\n",
+       "                                              │                                            │                                            │                      \n",
+       "q3: ──RY(d0_n3_0)─────────────────────────────X─────────RY(d1_n3_0)────────────────────────X─────────RY(d2_n3_0)────────────────────────X─────────RY(d3_n3_0)──\n",
+       "
\n" + ], + "text/plain": [ + "q0: ──RY(d0_n0_0)────●────RY(d1_n0_0)────────────────────────●─────────RY(d2_n0_0)────────────────────────●─────────RY(d3_n0_0)────────────────────────────────\n", + " │ │ │ \n", + "q1: ──RY(d0_n1_0)────X─────────●─────────RY(d1_n1_0)─────────X──────────────●─────────RY(d2_n1_0)─────────X──────────────●─────────RY(d3_n1_0)─────────────────\n", + " │ │ │ \n", + "q2: ──RY(d0_n2_0)──────────────X──────────────●─────────RY(d1_n2_0)─────────X──────────────●─────────RY(d2_n2_0)─────────X──────────────●─────────RY(d3_n2_0)──\n", + " │ │ │ \n", + "q3: ──RY(d0_n3_0)─────────────────────────────X─────────RY(d1_n3_0)────────────────────────X─────────RY(d2_n3_0)────────────────────────X─────────RY(d3_n3_0)──" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from mindquantum.algorithm import HardwareEfficientAnsatz # 导入HardwareEfficientAnsatz\n", + "from mindquantum.core import RY # 导入量子门RY\n", + "\n", + "ansatz = HardwareEfficientAnsatz(4, single_rot_gate_seq=[RY], entangle_gate=X, depth=3).circuit # 通过HardwareEfficientAnsatz搭建Ansatz\n", + "ansatz.summary() # 总结Ansatz\n", + "ansatz" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "从对Ansatz的Summary中可以看到,该量子线路由25个量子门组成,其中有16个含参量子门且参数为d2_n3_0, d1_n1_0, d0_n2_0, d1_n0_0, d3_n2_0, d2_n2_0, d0_n1_0, d3_n1_0, d2_n0_0, d3_n0_0...,该量子线路调控的量子比特数为4。\n", + "\n", + "说明:\n", + "\n", + "(1)HardwareEfficientAnsatz是一种容易在量子芯片上实现的Ansatz,其量子线路图由红色虚线框内的量子门组成,一般格式如下:mindquantum.ansatz.HardwareEfficientAnsatz(n_qubits, single_rot_gate_seq, entangle_gate=X, entangle_mapping=\"linear\", depth=1),括号中的n_qubits表示ansatz需要作用的量子比特总数,single_rot_gate_seq表示一开始每一位量子比特执行的参数门,同时后面需要执行的参数门也固定了,只是参数不同,entangle_gate=X表示执行的纠缠门为X,entangle_mapping=\"linear\"表示纠缠门将作用于每对相邻量子比特,depth表示黑色虚线框内的量子门需要重复的次数;\n", + "\n", + "那么完整的量子线路就是Encoder加上Ansatz。" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "================================================Circuit Summary================================================\n", + "|Total number of gates : 42. |\n", + "|Parameter gates : 23. |\n", + "|with 23 parameters are : alpha0, alpha1, alpha2, alpha3, alpha4, alpha5, alpha6, d0_n0_0, d0_n1_0, d0_n2_0...|\n", + "|Number qubit of circuit: 4 |\n", + "===============================================================================================================\n" + ] + }, + { + "data": { + "text/html": [ + "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
q0: ──H────RZ(alpha0)────●──────────────────●────RY(d0_n0_0)──────────────────────────────────────────●─────────RY(d1_n0_0)────────────────────────────────────────────●─────────RY(d2_n0_0)────────────────────────●─────────RY(d3_n0_0)────────────────────────────────\n",
+       "                         │                  │                                                         │                                                                │                                            │                                                    \n",
+       "q1: ──H────RZ(alpha1)────X────RZ(alpha4)────X─────────●───────────────────────●────RY(d0_n1_0)────────X───────────────────────────────────────●────RY(d1_n1_0)─────────X──────────────●─────────RY(d2_n1_0)─────────X──────────────●─────────RY(d3_n1_0)─────────────────\n",
+       "                                                      │                       │                                                               │                                       │                                            │                                     \n",
+       "q2: ──H────RZ(alpha2)─────────────────────────────────X─────────RZ(alpha5)────X─────────●────────────────────────────●─────────RY(d0_n2_0)────X─────────●─────────RY(d1_n2_0)─────────X──────────────●─────────RY(d2_n2_0)─────────X──────────────●─────────RY(d3_n2_0)──\n",
+       "                                                                                        │                            │                                  │                                            │                                            │                      \n",
+       "q3: ──H────RZ(alpha3)───────────────────────────────────────────────────────────────────X─────────RZ(alpha6)─────────X─────────RY(d0_n3_0)──────────────X─────────RY(d1_n3_0)────────────────────────X─────────RY(d2_n3_0)────────────────────────X─────────RY(d3_n3_0)──\n",
+       "
\n" + ], + "text/plain": [ + "q0: ──H────RZ(alpha0)────●──────────────────●────RY(d0_n0_0)──────────────────────────────────────────●─────────RY(d1_n0_0)────────────────────────────────────────────●─────────RY(d2_n0_0)────────────────────────●─────────RY(d3_n0_0)────────────────────────────────\n", + " │ │ │ │ │ \n", + "q1: ──H────RZ(alpha1)────X────RZ(alpha4)────X─────────●───────────────────────●────RY(d0_n1_0)────────X───────────────────────────────────────●────RY(d1_n1_0)─────────X──────────────●─────────RY(d2_n1_0)─────────X──────────────●─────────RY(d3_n1_0)─────────────────\n", + " │ │ │ │ │ \n", + "q2: ──H────RZ(alpha2)─────────────────────────────────X─────────RZ(alpha5)────X─────────●────────────────────────────●─────────RY(d0_n2_0)────X─────────●─────────RY(d1_n2_0)─────────X──────────────●─────────RY(d2_n2_0)─────────X──────────────●─────────RY(d3_n2_0)──\n", + " │ │ │ │ │ \n", + "q3: ──H────RZ(alpha3)───────────────────────────────────────────────────────────────────X─────────RZ(alpha6)─────────X─────────RY(d0_n3_0)──────────────X─────────RY(d1_n3_0)────────────────────────X─────────RY(d2_n3_0)────────────────────────X─────────RY(d3_n3_0)──" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "circuit = encoder + ansatz #完整的量子线路由Encoder和Ansatz组成\n", + "circuit.summary()\n", + "circuit" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "从对完整的量子线路的Summary中可以看到,该量子线路由42个量子门组成,其中有23个含参量子门且参数为$\\alpha_0,\\alpha_1,...,\\alpha_6$和d2_n3_0, d1_n1_0, d0_n2_0, d1_n0_0, d3_n2_0, d2_n2_0, d0_n1_0, d3_n1_0, d2_n0_0, d3_n0_0...,该量子线路调控的量子比特数为4。\n", + "\n", + "## 8. 构建哈密顿量\n", + "\n", + "我们分别对第2位和第3位量子比特执行泡利`Z`算符测量,构建对应的哈密顿量。" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[1.0 [Z2] , 1.0 [Z3] ]\n" + ] + } + ], + "source": [ + "from mindquantum.core import QubitOperator # 导入QubitOperator模块,用于构造泡利算符\n", + "from mindquantum.core import Hamiltonian # 导入Hamiltonian模块,用于构建哈密顿量\n", + "\n", + "hams = [Hamiltonian(QubitOperator(f'Z{i}')) for i in [2, 3]] # 分别对第2位和第3位量子比特执行泡利Z算符测量,且将系数都设为1,构建对应的哈密顿量\n", + "print(hams)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "从上述打印可以看到,此时构建的哈密顿量有2个,分别为对第2位和第3位量子比特执行泡利`Z`算符,且将系数都设为1。通过泡利`Z`算符测量,我们可以得到2个哈密顿量测量值,若第1个测量值更大,则会将此样本归类到标签为“0”的类,同理,若第2个测量值更大,则会将此样本归类到标签为“1”的类。通过神经网络的训练,期望训练样本中标签为“0”的样本的第1个测量值更大,而标签为“1”的样本的第2个测量值更大,最后应用此模型来预测新样本的分类。\n", + "\n", + "## 9. 搭建量子神经网络" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "MQLayer<\n", + " (evolution): MQOps<4 qubits projectq VQA Operator>\n", + " >" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import mindspore as ms # 导入mindspore库并简写为ms\n", + "from mindquantum.framework import MQLayer # 导入MQLayer\n", + "from mindquantum.simulator import Simulator\n", + "\n", + "ms.context.set_context(mode=ms.context.PYNATIVE_MODE, device_target=\"CPU\")\n", + "ms.set_seed(1) # 设置生成随机数的种子\n", + "sim = Simulator('projectq', circuit.n_qubits)\n", + "grad_ops = sim.get_expectation_with_grad(hams,\n", + " circuit,\n", + " None,\n", + " encoder.params_name,\n", + " ansatz.params_name,\n", + " parallel_worker=5)\n", + "QuantumNet = MQLayer(grad_ops) # 搭建量子神经网络\n", + "QuantumNet" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "从上述打印可以看到,我们已经成功搭建了量子机器学习层,其可以无缝地跟MindSpore中其它的算子构成一张更大的机器学习网络。\n", + "\n", + "说明:\n", + "\n", + "(1)mindspore是一个全场景深度学习框架,旨在实现易开发、高效执行、全场景覆盖三大目标,提供支持异构加速的张量可微编程能力,支持云、服务器、边和端多种硬件平台;\n", + "\n", + "\n", + "## 10. 训练\n", + "\n", + "接下来,我们需要定义损失函数,设定需要优化的参数,然后将搭建好的量子机器学习层和MindSpore的算子组合,构成一张更大的机器学习网络,最后对该模型进行训练。" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "epoch: 1 step: 16, loss is 0.6600145\n", + "epoch: 2 step: 16, loss is 0.4009103\n", + "epoch: 3 step: 16, loss is 0.39099234\n", + "epoch: 4 step: 16, loss is 0.3733629\n", + "epoch: 5 step: 16, loss is 0.3705962\n", + "epoch: 6 step: 16, loss is 0.37426245\n", + "epoch: 7 step: 16, loss is 0.37181872\n", + "epoch: 8 step: 16, loss is 0.37131247\n", + "epoch: 9 step: 16, loss is 0.37142643\n", + "epoch: 10 step: 16, loss is 0.37067422\n", + "epoch: 11 step: 16, loss is 0.3701976\n", + "epoch: 12 step: 16, loss is 0.36975253\n", + "epoch: 13 step: 16, loss is 0.36923727\n", + "epoch: 14 step: 16, loss is 0.3688001\n", + "epoch: 15 step: 16, loss is 0.3684062\n", + "epoch: 16 step: 16, loss is 0.36804128\n", + "epoch: 17 step: 16, loss is 0.36773998\n", + "epoch: 18 step: 16, loss is 0.36747772\n", + "epoch: 19 step: 16, loss is 0.36726192\n", + "epoch: 20 step: 16, loss is 0.36708587\n" + ] + } + ], + "source": [ + "from mindspore.nn import SoftmaxCrossEntropyWithLogits #导入SoftmaxCrossEntropyWithLogits模块,用于定义损失函数\n", + "from mindspore.nn import Adam, Accuracy #导入Adam模块和Accuracy模块,分别用于定义优化参数,评估预测准确率\n", + "from mindspore import Model #导入Model模块,用于建立模型\n", + "from mindspore.dataset import NumpySlicesDataset #导入NumpySlicesDataset模块,用于创建模型可以识别的数据集\n", + "from mindspore.train.callback import Callback, LossMonitor #导入Callback模块和LossMonitor模块,分别用于定义回调函数和监控损失\n", + "\n", + "loss = SoftmaxCrossEntropyWithLogits(sparse=True, reduction='mean') #通过SoftmaxCrossEntropyWithLogits定义损失函数,sparse=True表示指定标签使用稀疏格式,reduction='mean'表示损失函数的降维方法为求平均值\n", + "opti = Adam(QuantumNet.trainable_params(), learning_rate=0.1) #通过Adam优化器优化Ansatz中的参数,需要优化的是Quantumnet中可训练的参数,学习率设为0.1 \n", + " \n", + "model = Model(QuantumNet, loss, opti, metrics={'Acc': Accuracy()}) #建立模型:将MindQuantum构建的量子机器学习层和MindSpore的算子组合,构成一张更大的机器学习网络\n", + " \n", + "train_loader = NumpySlicesDataset({'features': X_train, 'labels': y_train}, shuffle=False).batch(5) #通过NumpySlicesDataset创建训练样本的数据集,shuffle=False表示不打乱数据,batch(5)表示训练集每批次样本点有5个\n", + "test_loader = NumpySlicesDataset({'features': X_test, 'labels': y_test}).batch(5) #通过NumpySlicesDataset创建测试样本的数据集,batch(5)表示测试集每批次样本点有5个\n", + "\n", + "class StepAcc(Callback): #定义一个关于每一步准确率的回调函数\n", + " def __init__(self, model, test_loader):\n", + " self.model = model\n", + " self.test_loader = test_loader\n", + " self.acc = []\n", + "\n", + " def step_end(self, run_context):\n", + " self.acc.append(self.model.eval(self.test_loader, dataset_sink_mode=False)['Acc']) \n", + "\n", + "monitor = LossMonitor(16) #监控训练中的损失,每16步打印一次损失值\n", + "\n", + "acc = StepAcc(model, test_loader) #使用建立的模型和测试样本计算预测的准确率\n", + "\n", + "model.train(20, train_loader, callbacks=[monitor, acc], dataset_sink_mode=False)#将上述建立好的模型训练20次" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "从上述打印可以看到,20次迭代后,损失值不断下降并趋于稳定,最后收敛于约0.367。\n", + "\n", + "\n", + "\n", + "说明:\n", + "\n", + "(1)nn.SoftmaxCrossEntropyWithLogits可以计算数据和标签之间的softmax交叉熵。使用交叉熵损失测量输入(使用softmax函数计算)的概率和目标之间的分布误差,其中类是互斥的(只有一个类是正的),一般格式如下:mindspore.nn.SoftmaxCrossEntropyWithLogits(sparse=False, reduction=\"none\"),sparse=False表示指定标签是否使用稀疏格式,默认值:False;reduction=\"none\"表示适用于损失的减少类型。可选值为mean、sum和none。如果为none,则不执行减少,默认值:“没有”。\n", + "\n", + "(2)Adam模块通过自适应矩估计算法更新梯度,可以优化Ansazt中的参数,输入的是神经网络中可训练的参数;一般格式如下:nn.Adam(net.trainable_params(), learning_rate=0.1),学习率可以自己调节;\n", + "\n", + "(3)mindspore.Model是用于训练或测试的高级API,模型将层分组到具有训练和推理特征的对象中,一般格式如下:mindspore.Model(network, loss_fn=None, optimizer=None, metrics=None, eval_network=None, eval_indexes=None, amp_level=\"O0\", acc_level=\"O0\"),其中network就是我们要训练的网络即Quantumnet;loss_fn即目标函数,在这里就是定义的loss函数;optimizer即优化器,用于更新权重,在这里就是定义的opti;metrics就是模型在训练和测试期间需要评估的字典或一组度量,在这里就是评估准确率;\n", + "\n", + "(4)Accuracy用于计算分类和多标签数据的准确率,一般格式如下:mindspore.nn.Accuracy(eval_type=\"classification\"),用于分类(单标签)和多标签(多标签分类))的数据集上计算准确率的度量,默认值:“分类”;\n", + "\n", + "(5)NumpySlicesDataset使用给定的数据切片创建数据集,主要用于将Python数据加载到数据集中,一般格式如下:mindspore.dataset.NumpySlicesDataset(data, column_names=None, num_samples=None, num_parallel_workers=1, shuffle=None, sampler=None, num_shards=None, shard_id=None);\n", + "\n", + "(6)Callback用于构建回调类的抽象基类,回调是上下文管理器,在传递到模型时将输入和输出。你可以使用此机制自动初始化和释放资源。回调函数将执行当前步骤或数据轮回中的一些操作;\n", + "\n", + "(7)LossMonitor主要用于监控训练中的损失,如果损失是NAN或INF,它将终止训练,一般格式如下:mindspore.train.callback.LossMonitor(per_print_times=1),per_print_times=1表示每秒钟打印一次损失,默认值:1;\n", + "\n", + "(8)train模块用于训练模型,其中迭代由python前端控制;当设置pynative模式或CPU时,训练过程将在数据集不接收的情况下执行,一般格式如下:train(epoch, train_dataset, callbacks=None, dataset_sink_mode=True, sink_size=-1),其中epoch表示在数据上的总迭代次数;train_dataset就是我们定义的train_loader;callbacks就是我们需要回调的损失值和准确率;dataset_sink_mode表示确定是否通过数据集通道传递数据,教案中为否;\n", + "\n", + "## 11. 训练过程中的准确率\n", + "\n", + "我们已经看到损失值趋于稳定,那么我们还可以将模型在训练过程中的预测的准确率呈现出来,执行如下代码。" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0, 0.5, 'Accuracy')" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "plt.plot(acc.acc)\n", + "plt.title('Statistics of accuracy', fontsize=20)\n", + "plt.xlabel('Steps', fontsize=20)\n", + "plt.ylabel('Accuracy', fontsize=20)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "从上述打印的图像可以看到,在大约50步后,预测的准确率收敛于1,也就是说预测的准确率已经可以达到100%。\n", + "\n", + "## 12. 预测\n", + "\n", + "最后,我们测试一下训练好的模型,将其应用在测试集上。" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "预测分类结果: [0 1 0 1 1 1 0 1 1 1 1 1 1 0 0 0 0 0 0 0]\n", + "实际分类结果: [0 1 0 1 1 1 0 1 1 1 1 1 1 0 0 0 0 0 0 0]\n", + "{'Acc': 1.0}\n" + ] + } + ], + "source": [ + "from mindspore import ops, Tensor #导入ops模块和Tensor模块\n", + "\n", + "predict = np.argmax(ops.Softmax()(model.predict(Tensor(X_test))), axis=1) #使用建立的模型和测试样本,得到测试样本预测的分类\n", + "correct = model.eval(test_loader, dataset_sink_mode=False) #计算测试样本应用训练好的模型的预测准确率\n", + "\n", + "print(\"预测分类结果:\", predict) #对于测试样本,打印预测分类结果\n", + "print(\"实际分类结果:\", y_test) #对于测试样本,打印实际分类结果\n", + "\n", + "print(correct) #打印模型预测的准确率" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "从上述打印的可以看到,预测分类结果和实际分类结果完全一致,模型预测的准确率达到了100%。\n", + "\n", + "至此,我们体验了如何通过搭建量子神经网络来解决经典机器学习中的经典问题——鸢尾花分类问题。相信大家也对使用MindQuantum有了更进一步的了解!期待大家挖掘更多的问题,充分发挥MindQuantum强大的功能!\n", + "\n", + "若想查询更多关于MindQuantum的API,请点击:[https://mindspore.cn/mindquantum/](https://mindspore.cn/mindquantum/)。" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.8" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tutorials/4.quantum_phase_estimation.ipynb b/tutorials/4.quantum_phase_estimation.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..ac8f344890c7f8faacbe0745890faa1802f1015e --- /dev/null +++ b/tutorials/4.quantum_phase_estimation.ipynb @@ -0,0 +1,434 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 量子相位估计算法\n", + "\n", + "\n", + "`Linux` `CPU` `全流程` `初级` `中级` `高级`\n", + "\n", + "[![](https://gitee.com/mindspore/docs/raw/master/resource/_static/logo_source.png)](https://gitee.com/mindspore/mindquantum/blob/master/tutorials/4.quantum_phase_estimation.ipynb) [![](https://gitee.com/mindspore/mindquantum/raw/master/tutorials/images/view_mindquantum_api.png)](https://mindspore.cn/mindquantum/api/zh-CN/master/index.html)\n", + "\n", + "## 概述\n", + "\n", + "量子相位估计算法(Quantum hase Estimation Algorithm,简称QPE),是很多量子算法的关键。假设一个幺正算符 $U$,这个幺正算符作用在其本征态 $|u\\rangle$ 上会出现一个相位 $e^{2\\pi i \\varphi}$,现在我们假设 $U$ 算符的本征值未知,也就是 $\\varphi$ 未知,但是 $U$ 算符和本征态 $|u\\rangle$ 已知,相位估计算法的作用就是对这个相位 $\\varphi$ 进行估计。 " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![QPE](./images/quantum_phase_estimation.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 算法解析\n", + "\n", + "相位估计算法的实现需要两个寄存器(register),第一个寄存器包含t个初始在的 $|0\\rangle$ 的量子比特,比特数和最后相位估计的结果的精度和算法的成功概率相关;第二个寄存器初始化在幺正算符 $U$ 的而本征态 $|u\\rangle$ 上。相位估计算法主要分为三步:\n", + "\n", + "1. 对第一个寄存器的所有量子比特进行``Hadamard``门操作,对第二寄存器连续进行``控制U``门操作,其中U门的幂次依次为 $2^0, 2^1,...,2^{t-1}$,控制比特依次为 $q_{t-1}, q_{t-2},..., q_{1}, q_{0}$。这时第一寄存器中的态就会变为\n", + "\n", + "$$\n", + "|\\psi_1\\rangle=\\frac{1}{2^{t/2}}\\left(|0\\rangle+e^{i2\\pi 2^{t-1}\\varphi}|1\\rangle\\right)\\left(|0\\rangle+e^{i2\\pi2^{t-2}\\varphi}|1\\rangle\\right)...\\left(|0\\rangle+e^{i2\\pi 2^{0}\\varphi}|1\\rangle\\right) = \\frac{1}{2^{t/2}}\\sum_{k=0}^{2^t-1}e^{i2\\pi\\varphi k}|k\\rangle\n", + "$$\n", + "\n", + "其中k为直积态的十进制表示,比如 $k=0$ 表示第一寄存器中t个比特全部在基态 $|00...00\\rangle$, $k=2$ 表示 $|00...10\\rangle$,以此类推。\n", + "\n", + "2. 对第一寄存器的进行量子傅里叶变换的逆变换(Inverse Quantum Fourier Transform),在线路中表示成 $QFT^\\dagger$, 对 $|\\psi_1\\rangle$ 进行逆量子傅里叶变换可得 $|\\psi_2\\rangle$\n", + "\n", + "$$\n", + "|\\psi_2\\rangle=QFT^\\dagger|\\psi_1\\rangle =\\frac{1}{2^t}\\sum_{x=0}^{2^t-1}a_x|x\\rangle\n", + "$$\n", + "其中\n", + "$$\n", + "a_x=\\sum_{k=0}^{2^t-1}e^{2\\pi i k(\\varphi-x/2^t)}\n", + "$$\n", + "为本征基矢 $|x\\rangle$ ($x=0.1,...,2^t$) 对应的概率幅 。由上式可得,当 $2^t\\varphi$ 为整数,且满足 $x=2^t\\varphi$ 时,概率幅取最大值1,此时第一寄存器的末态可以精确反映 $\\varphi$;当 $2^t\\varphi$ 不是整数时,$x$ 为 $\\varphi$ 的估计,且t越大,估计精度越高。\n", + "\n", + "3. 对第一寄存器的量子比特进行测量,得到第一寄存器的末态 $f=\\sum_{x}^{2^t-1}a_x|x\\rangle$, $x=0,1,...,2^t$;从中找到最大的概率幅 $a_{max}$,其对应的本征基矢 $|x\\rangle$ 中的 $x$ 在除以 $2^t$ 即为相位的估计值。\n", + "\n", + "\n", + "\n", + "## QPE代码实现\n", + "\n", + "下面用一个实例来演示如何在mindquantum实现相位估计算法,选择 ``T``门作为进行估计的幺正算符,由定义\n", + "\n", + "$$\n", + "T|1\\rangle=e^{i\\pi/4}|1\\rangle\n", + "$$\n", + "可知需要估计的相位角为 $\\varphi=\\frac{1}{8}$。\n", + "\n", + "首先导入相关依赖" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from mindquantum import Circuit\n", + "from mindquantum import Simulator\n", + "from mindquantum import UN, PhaseShift, qft, H, X, BARRIER\n", + "import numpy as np" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "``Mindquantum.UN`` 可以指定量子门,目标比特和控制比特,从而在线路中搭建门操作。因为我们已知 $\\varphi=1/8$,当 $t=3$ 时可令 $2^t\\varphi$ 为整数,所以第一寄存器只需要3个比特即可准确估计;又已知 ``T`` 门的本征态为 $|1\\rangle$,所以第二寄存器选择一个比特,即:我们需要搭建4比特线路,前 $q_0, q_1, q_2$ 比特用于估计,属于第一寄存器;$q_3$ 属于第二寄存器用于传入 $T$ 算符的本征态。\n", + "\n", + "利用 ``UN`` 对 $q_0, q_1, q_2$ 进行 ``Hadamard`` 门操作, 用 ``X`` 门对 $q_3$ 进行翻转,得到 ``T`` 门的本征态 $|1\\rangle$。" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
q0: ──H──\n",
+       "         \n",
+       "q1: ──H──\n",
+       "         \n",
+       "q2: ──H──\n",
+       "         \n",
+       "q3: ──X──\n",
+       "
\n" + ], + "text/plain": [ + "q0: ──H──\n", + " \n", + "q1: ──H──\n", + " \n", + "q2: ──H──\n", + " \n", + "q3: ──X──" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "n = 3\n", + "c = Circuit()\n", + "c += UN(H, n)\n", + "c += X.on(n)\n", + "c" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "以 $q_3$ 为目标比特,添加 ``控制PhaseShift``门" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
q0: ──H────────────────────────────────●──────\n",
+       "\n",
+       "q1: ──H───────────────────●────────────┼──────\n",
+       "                          │            │      \n",
+       "q2: ──H───────●───────────┼────────────┼──────\n",
+       "              │           │            │      \n",
+       "q3: ──X────PS(phi)────PS(2*phi)────PS(4*phi)──\n",
+       "
\n" + ], + "text/plain": [ + "q0: ──H────────────────────────────────●──────\n", + " │ \n", + "q1: ──H───────────────────●────────────┼──────\n", + " │ │ \n", + "q2: ──H───────●───────────┼────────────┼──────\n", + " │ │ │ \n", + "q3: ──X────PS(phi)────PS(2*phi)────PS(4*phi)──" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "for i in range(n):\n", + " c += PhaseShift({'phi': 2**i}).on(n, n-i-1)\n", + "c" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "对第一寄存器比特进行逆傅里叶变换" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
q0: ──H────────────────────────────────●────────@──────────────────────────PS(-π/4)────PS(-π/2)────H──\n",
+       "                                       │        │                             │           │           \n",
+       "q1: ──H───────────────────●────────────┼────────┼─────────PS(-π/2)────H───────┼───────────●───────────\n",
+       "                          │            │        │            │                │                       \n",
+       "q2: ──H───────●───────────┼────────────┼────────@────H───────●────────────────●───────────────────────\n",
+       "              │           │            │                                                              \n",
+       "q3: ──X────PS(phi)────PS(2*phi)────PS(4*phi)──────────────────────────────────────────────────────────\n",
+       "
\n" + ], + "text/plain": [ + "q0: ──H────────────────────────────────●────────@──────────────────────────PS(-π/4)────PS(-π/2)────H──\n", + " │ │ │ │ \n", + "q1: ──H───────────────────●────────────┼────────┼─────────PS(-π/2)────H───────┼───────────●───────────\n", + " │ │ │ │ │ \n", + "q2: ──H───────●───────────┼────────────┼────────@────H───────●────────────────●───────────────────────\n", + " │ │ │ \n", + "q3: ──X────PS(phi)────PS(2*phi)────PS(4*phi)──────────────────────────────────────────────────────────" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "c += BARRIER\n", + "c += qft(range(n)).hermitian()\n", + "c" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "选择后端、传入总比特数创建模拟器,将 $\\varphi$ 值传入并进行演化,得到末态" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1¦1100⟩\n" + ] + }, + { + "data": { + "text/html": [ + "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
shots: 100\n",
+       "Keys: q3 q2 q1 q0│0.00     0.2         0.4         0.6         0.8         1.0\n",
+       "─────────────────┼───────────┴───────────┴───────────┴───────────┴───────────┴\n",
+       "             1100│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓\n",
+       "\n",
+       "{'1100': 100}\n",
+       "
\n" + ], + "text/plain": [ + "shots: 100\n", + "Keys: q3 q2 q1 q0│0.00 0.2 0.4 0.6 0.8 1.0\n", + "─────────────────┼───────────┴───────────┴───────────┴───────────┴───────────┴\n", + " 1100│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓\n", + " │ \n", + "{'1100': 100}" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from mindquantum import Measure\n", + "sim = Simulator('projectq', c.n_qubits)\n", + "phi = 0.125\n", + "sim.apply_circuit(c,{'phi': 2*np.pi*phi})\n", + "qs = sim.get_qs()\n", + "print(sim.get_qs(ket=True))\n", + "res = sim.sampling(UN(Measure(), c.n_qubits), shots=100)\n", + "res" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "找出概率幅最大值的位置" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "12\n" + ] + } + ], + "source": [ + "index = np.argmax(np.abs(qs))\n", + "print(index)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "注意此时的 ``index`` 对应的 $x$ 并不是真正的估计值,被 $2^t$ 除之后也不是,因为测量结果中包括第二寄存器中的辅助比特,需要将``index``转成二进制后将辅助位剔除" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "100\n" + ] + } + ], + "source": [ + "bit_string = bin(index)[2:].zfill(c.n_qubits)[1:]\n", + "print(bit_string)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "在将二进制转回十进制,得到我们最终的估计值" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0.125" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "theta_exp = int(bit_string[::-1], 2) / 2**n\n", + "theta_exp" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "可见得到的估计相位和 $\\varphi$ 近似相等。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 参考文献\n", + "\n", + "[1] Michael A. Nielsen and Isaac L. Chuang. [Quantum computation and quantum information](www.cambridge.org/9781107002173)" + ] + } + ], + "metadata": { + "interpreter": { + "hash": "6cd6e2203b621035efd3b4ac9716079b52ce7fc5622f6651a3ae71459e0d54ce" + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.5" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/tutorials/5.quantum_approximate_optimization_algorithm.ipynb b/tutorials/5.quantum_approximate_optimization_algorithm.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..f78bce04f1e64876b0e8892423652abea76e795b --- /dev/null +++ b/tutorials/5.quantum_approximate_optimization_algorithm.ipynb @@ -0,0 +1,481 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 量子近似优化算法\n", + "\n", + "\n", + "`Linux` `CPU` `全流程` `初级` `中级` `高级`\n", + "\n", + "[![](https://gitee.com/mindspore/docs/raw/master/resource/_static/logo_source.png)](https://gitee.com/mindspore/mindquantum/blob/master/tutorials/5.quantum_approximate_optimization_algorithm.ipynb) [![](https://gitee.com/mindspore/mindquantum/raw/master/tutorials/images/view_mindquantum_api.png)](https://mindspore.cn/mindquantum/api/zh-CN/master/index.html)\n", + "\n", + "## 概述\n", + "\n", + "量子近似优化算法(Quantum Approximate Optimization Algorithm,QAOA)是利用量子计算机来近似解决组合优化问题的量子算法,最早由Farhi等人于2014年提出。在本教程里,我们将利用QAOA算法来解决最大割问题(Max-Cut),来熟悉MindQuantum中量子线路的搭建和训练。\n", + "\n", + "> 本文档适用于CPU环境。 \n", + "> 你可以在这里找到完整的可运行的样例代码:。\n", + "\n", + "## 环境准备\n", + "\n", + "本教程所需要的额外库:\n", + "\n", + "- networkx\n", + "\n", + "> `networkx`是创建、操作和研究复杂网络的结构、动态和功能库。可通过`pip3 install networkx`来进行安装。\n", + "\n", + "## Max-Cut问题描述\n", + "\n", + "Max-Cut问题是图论中的一个NP-complete问题,它需要将一个图中的顶点分成两部分,并使得两部分被切割的边最多。如下图(a),一个图由五个顶点构成,相互连接的边为```(0, 1), (0, 2), (1, 2), (2, 3), (3, 4), (0, 4)```。为了使得被切割的边最多,我们尝试通过(b)图的分割,将1、2、4分为一组,0、3分成另一组,因此可得到被切割的边有5条。当图中顶点增多时,我们很难找到有效的经典算法来解决Max-Cut问题。下面,我们介绍怎么将Max-Cut问题转化为一个哈密顿量的基态能力求解问题。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![max cut](./images/Max_Cut.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Max-Cut问题量子化\n", + "\n", + "这里我们将图中的每个顶点赋予一个量子比特,当顶点被分到左边时,我们将该顶点上的量子比特设置为$\\left|0\\right>$态,同理,右边为$\\left|1\\right>$态,当两个顶点被分到不同的集合中时,这两个顶点上的比特将处于不同的量子态。例如对于第0个顶点和第1个顶点,当其连线被切割是,两个顶点上的比特对应的量子态可以为$\\left|\\psi\\right>=\\left|0_11_0\\right>$或$\\left|\\psi\\right>=\\left|1_10_0\\right>$,其中下角标表示顶点的序号。此时,我们选择哈密顿量$H=(Z_1Z_0-1)/2$,这里$Z$为泡利$Z$算符。不难发现:\n", + "$$\\left<\\psi\\right|H\\left|\\psi\\right>=-1$$\n", + "而当顶点被分到同一集合中是,不难验证此时:\n", + "$$\\left<\\psi\\right|H\\left|\\psi\\right>=0$$\n", + "因此,我们只用按照上面的规则,写出图对应的哈密顿量$H$,利用量子计算机求得$H$的基态能量与基态,我们就可以得到该图的Max-Cut切割方案与最大切割边数。我们记所有边的集合为$C$,所有边个数为$c$,则哈密顿量可写为:\n", + "$$H=\\sum_{(i,j)\\in C}(Z_iZ_j-1)/2$$\n", + "\n", + "## 导入相关依赖" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from mindquantum.core import Circuit, Hamiltonian, UN, H, ZZ, RX, QubitOperator\n", + "from mindquantum.framework import MQAnsatzOnlyLayer\n", + "from mindquantum.simulator import Simulator\n", + "import networkx as nx\n", + "import mindspore.nn as nn\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 搭建所需求解的图\n", + "\n", + "通过`add_path`可在图中添加边。最后画出图的结构。" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "g = nx.Graph()\n", + "nx.add_path(g, [0,1])\n", + "nx.add_path(g, [1,2])\n", + "nx.add_path(g, [2,3])\n", + "nx.add_path(g, [3,4])\n", + "nx.add_path(g, [0,4])\n", + "nx.add_path(g, [0,2])\n", + "nx.draw(g,with_labels=True, font_weight='bold')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "如上如,我们得到一个由5个节点和6条边构成的图结构。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 搭建QAOA量子线路\n", + "\n", + "### 线路搭建\n", + "\n", + "这里我们采用量子绝热近似算法,经过演化将量子态从$X^{\\otimes n}$的本征态演化到图多应哈密的量的基态。\n", + "\n", + "搭建图对应哈密顿量的含时演化线路:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "def build_hc(g,para):\n", + " hc = Circuit()\n", + " for i in g.edges:\n", + " hc += ZZ(para).on(i)\n", + " return hc" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "搭建$X^{\\otimes n}$含时演化的量子线路:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "def build_hb(g, para):\n", + " hc = Circuit()\n", + " for i in g.nodes:\n", + " hc += RX(para).on(i)\n", + " return hc" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "为了使得最后优化的结果足够准确,我们需要将量子线路重复多次,因此我们通过如下函数搭建多层的训练网络:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "def build_ansatz(g, p):\n", + " c = Circuit()\n", + " for i in range(p):\n", + " c += build_hc(g,f'g{i}')\n", + " c += build_hb(g,f'b{i}')\n", + " return c" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "构建图对应的哈密顿量:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "def build_ham(g):\n", + " hc = QubitOperator()\n", + " for i in g.edges:\n", + " hc += QubitOperator(f'Z{i[0]} Z{i[1]}')\n", + " return hc" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 生成完整的量子线路和图所对应的哈密顿量\n", + "\n", + "这里我们选择`p = 4`,表示选用4曾的QAOA量子线路,`ansatz`是求解该问题的量子线路,`init_state_circ`是将量子态制备到均匀叠加态上的量子线路。" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "p = 4\n", + "ham = Hamiltonian(build_ham(g))\n", + "ansatz = build_ansatz(g, p)\n", + "init_state_circ = UN(H, g.nodes)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 搭建待训练量子神经网络\n", + "\n", + "由于该问题不需要编码层量子线路,我们这里使用`MQAnsatzOnlyLayer`作为待训练的量子神经网络,并采用`Adam`优化器。" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "import mindspore as ms\n", + "ms.context.set_context(mode=ms.context.PYNATIVE_MODE, device_target=\"CPU\")\n", + "\n", + "circ = init_state_circ + ansatz\n", + "sim = Simulator('projectq', circ.n_qubits)\n", + "grad_ops = sim.get_expectation_with_grad(ham, circ)\n", + "net = MQAnsatzOnlyLayer(grad_ops)\n", + "opti = nn.Adam(net.trainable_params(), learning_rate=0.05)\n", + "train_net = nn.TrainOneStepCell(net, opti)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 训练并展示结果" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "train step: 0 , cut: [3.0000384]\n", + "train step: 10 , cut: [3.0740116]\n", + "train step: 20 , cut: [3.291378]\n", + "train step: 30 , cut: [3.5130358]\n", + "train step: 40 , cut: [3.7116692]\n", + "train step: 50 , cut: [3.9258695]\n", + "train step: 60 , cut: [4.06885]\n", + "train step: 70 , cut: [4.1482286]\n", + "train step: 80 , cut: [4.2075667]\n", + "train step: 90 , cut: [4.2216697]\n", + "train step: 100 , cut: [4.219716]\n", + "train step: 110 , cut: [4.2484426]\n", + "train step: 120 , cut: [4.310321]\n", + "train step: 130 , cut: [4.3869066]\n", + "train step: 140 , cut: [4.468603]\n", + "train step: 150 , cut: [4.5552807]\n", + "train step: 160 , cut: [4.6389103]\n", + "train step: 170 , cut: [4.7030687]\n", + "train step: 180 , cut: [4.7325706]\n", + "train step: 190 , cut: [4.720634]\n", + "train step: 200 , cut: [4.6801863]\n", + "train step: 210 , cut: [4.641413]\n", + "train step: 220 , cut: [4.630719]\n", + "train step: 230 , cut: [4.6555276]\n", + "train step: 240 , cut: [4.7030406]\n", + "train step: 250 , cut: [4.7517853]\n", + "train step: 260 , cut: [4.7854385]\n", + "train step: 270 , cut: [4.8003826]\n", + "train step: 280 , cut: [4.8025503]\n", + "train step: 290 , cut: [4.800086]\n", + "train step: 300 , cut: [4.7993703]\n", + "train step: 310 , cut: [4.8025393]\n", + "train step: 320 , cut: [4.8066764]\n", + "train step: 330 , cut: [4.8070974]\n", + "train step: 340 , cut: [4.8019547]\n", + "train step: 350 , cut: [4.7941465]\n", + "train step: 360 , cut: [4.7895412]\n", + "train step: 370 , cut: [4.7927914]\n", + "train step: 380 , cut: [4.8037663]\n", + "train step: 390 , cut: [4.817476]\n", + "train step: 400 , cut: [4.8274126]\n", + "train step: 410 , cut: [4.830078]\n", + "train step: 420 , cut: [4.8275557]\n", + "train step: 430 , cut: [4.82526]\n", + "train step: 440 , cut: [4.826516]\n", + "train step: 450 , cut: [4.829952]\n", + "train step: 460 , cut: [4.8320074]\n", + "train step: 470 , cut: [4.831057]\n", + "train step: 480 , cut: [4.8289337]\n", + "train step: 490 , cut: [4.828836]\n", + "train step: 500 , cut: [4.832123]\n", + "train step: 510 , cut: [4.8371778]\n", + "train step: 520 , cut: [4.8411226]\n", + "train step: 530 , cut: [4.842478]\n", + "train step: 540 , cut: [4.8421907]\n", + "train step: 550 , cut: [4.8421745]\n", + "train step: 560 , cut: [4.8431606]\n", + "train step: 570 , cut: [4.844285]\n", + "train step: 580 , cut: [4.8445735]\n", + "train step: 590 , cut: [4.844259]\n" + ] + } + ], + "source": [ + "for i in range(600):\n", + " if i%10 == 0:\n", + " print(\"train step:\", i, \", cut:\", (len(g.edges)-train_net())/2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "根据上面的训练结果我们发现,该问题哈密顿量的基态能量对应的边切割数趋近与5。\n", + "\n", + "### 量子态展示\n", + "\n", + "前面我们通过训练得到了量子线路中参数的最优值,下面,我们通过`StateEvolution`类的`final_state`来输出量子线路在最优参数时的量子态,其中`ket`参数表示是否将最终量子态表示为右矢形式。" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(0.02090837256298316-0.013896610875073252j)¦00000⟩\n", + "(0.012865120061967678+0.0036417563002864367j)¦00001⟩\n", + "(0.024461651769855132+0.016082588541572487j)¦00010⟩\n", + "(0.02665900288209095+0.028667026525713252j)¦00011⟩\n", + "(0.012865120061967661+0.003641756300286507j)¦00100⟩\n", + "(-0.13261220694607562+0.12131816696926725j)¦00101⟩\n", + "(0.02665900288209106+0.028667026525713284j)¦00110⟩\n", + "(0.0077605605960298545+0.013307014150508825j)¦00111⟩\n", + "(-0.005108830641820924-0.016855682940484437j)¦01000⟩\n", + "(0.2893552119928307+0.36375187206353543j)¦01001⟩\n", + "(-0.05369567695690487+0.10854655574235807j)¦01010⟩\n", + "(0.2893552119928306+0.36375187206353565j)¦01011⟩\n", + "(0.012083509070472419+0.010521810302265303j)¦01100⟩\n", + "(-0.05369567695690495+0.10854655574235811j)¦01101⟩\n", + "(0.012083509070472474+0.010521810302265332j)¦01110⟩\n", + "(-0.005108830641820898-0.016855682940484465j)¦01111⟩\n", + "(-0.005108830641820921-0.016855682940484402j)¦10000⟩\n", + "(0.012083509070472466+0.010521810302265334j)¦10001⟩\n", + "(-0.05369567695690492+0.1085465557423581j)¦10010⟩\n", + "(0.012083509070472504+0.01052181030226525j)¦10011⟩\n", + "(0.2893552119928306+0.3637518720635356j)¦10100⟩\n", + "(-0.053695676956904935+0.10854655574235816j)¦10101⟩\n", + "(0.2893552119928306+0.3637518720635357j)¦10110⟩\n", + "(-0.005108830641820899-0.016855682940484448j)¦10111⟩\n", + "(0.007760560596029854+0.01330701415050879j)¦11000⟩\n", + "(0.02665900288209102+0.028667026525713263j)¦11001⟩\n", + "(-0.13261220694607564+0.1213181669692672j)¦11010⟩\n", + "(0.012865120061967626+0.0036417563002865416j)¦11011⟩\n", + "(0.02665900288209097+0.028667026525713308j)¦11100⟩\n", + "(0.02446165176985506+0.01608258854157249j)¦11101⟩\n", + "(0.012865120061967642+0.003641756300286466j)¦11110⟩\n", + "(0.020908372562983193-0.013896610875073252j)¦11111⟩\n" + ] + } + ], + "source": [ + "pr = dict(zip(ansatz.params_name, net.weight.asnumpy()))\n", + "print(circ.get_qs(pr=pr, ket=True))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 概率图\n", + "\n", + "我们画出最终量子态在计算基矢下的概率分布" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "def show_amp(state):\n", + " amp = np.abs(state)**2\n", + " n_qubits = int(np.log2(len(amp)))\n", + " labels = [bin(i)[2:].zfill(n_qubits) for i in range(len(amp))]\n", + " plt.bar(labels, amp)\n", + " plt.xticks(rotation=45)\n", + " plt.show()\n", + "state = circ.get_qs(pr=pr)\n", + "show_amp(state)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "根据概率分布图我们发现,该Max-Cut问题具有四个简并解,每个解对应的概率大概为25%。\n", + "\n", + "## 总结\n", + "\n", + "这里我们通过量子近似优化算法来解决了Max-Cut问题,并得到了案例中的图对应的最大切割方案。" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 参考文献\n", + "\n", + "[1] Edward Farhi, Jeffrey Goldstone, and Sam Gutmann. [A Quantum Approximate Optimization Algorithm](https://arxiv.org/pdf/1411.4028.pdf)" + ] + } + ], + "metadata": { + "interpreter": { + "hash": "5545a57ef4a1ac7dca167cae0bf17fda051fcd0639773c034bd7ce77ffd97d30" + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.5" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/tutorials/qnn_for_nlp.ipynb b/tutorials/6.qnn_for_nlp.ipynb similarity index 33% rename from tutorials/qnn_for_nlp.ipynb rename to tutorials/6.qnn_for_nlp.ipynb index d1d45f6b97d589585f638b44aef4d6f437c40534..600750822dd0b08ff9012b381b3d45f0f87575fa 100644 --- a/tutorials/qnn_for_nlp.ipynb +++ b/tutorials/6.qnn_for_nlp.ipynb @@ -9,7 +9,7 @@ "\n", "`Linux` `CPU` `全流程` `初级` `中级` `高级`\n", "\n", - "[![](https://gitee.com/mindspore/docs/raw/master/resource/_static/logo_source.png)](https://gitee.com/mindspore/docs/blob/master/docs/mindquantum/docs/source_zh_cn/qnn_for_nlp.ipynb)\n", + "[![](https://gitee.com/mindspore/docs/raw/master/resource/_static/logo_source.png)](https://gitee.com/mindspore/mindquantum/blob/master/tutorials/6.qnn_for_nlp.ipynb) [![](https://gitee.com/mindspore/mindquantum/raw/master/tutorials/images/view_mindquantum_api.png)](https://mindspore.cn/mindquantum/api/zh-CN/master/index.html)\n", "\n", "## 概述\n", "\n", @@ -29,13 +29,13 @@ "source": [ "import numpy as np\n", "import time\n", - "from mindquantum.ops import QubitOperator\n", + "from mindquantum.core import QubitOperator\n", "import mindspore.ops as ops\n", "import mindspore.dataset as ds\n", "from mindspore import nn\n", "from mindspore.train.callback import LossMonitor\n", "from mindspore import Model\n", - "from mindquantum.nn import MindQuantumLayer\n", + "from mindquantum.framework import MQLayer\n", "from mindquantum import Hamiltonian, Circuit, RX, RY, X, H, UN" ] }, @@ -141,10 +141,30 @@ "outputs": [ { "data": { + "text/html": [ + "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
q0: ──RX(e_0)──\n",
+       "               \n",
+       "q1: ──RX(e_1)──\n",
+       "               \n",
+       "q2: ──RX(e_2)──\n",
+       "
\n" + ], "text/plain": [ - "RX(e_0|0)\n", - "RX(e_1|1)\n", - "RX(e_2|2)" + "q0: ──RX(e_0)──\n", + " \n", + "q1: ──RX(e_1)──\n", + " \n", + "q2: ──RX(e_2)──" ] }, "execution_count": 5, @@ -166,7 +186,7 @@ "\n", "对于$n$比特的量子态,其将处于$2^n$维的希尔伯特空间中。对于上面由5个词构成的词典,我们只需要$\\lceil \\log_2 5 \\rceil=3$个量子比特即可完成编码,这也体现出量子计算的优越性。\n", "\n", - "例如对于上面词典中的\"love\",其对应的标签为2,2的二进制表示为`010`,我们只需将编码线路中的`e_0`、`e_1`和`e_2`分别设为$0$、$\\pi$和$0$即可。下面我们通过`Evolution`算子来验证以下。" + "例如对于上面词典中的\"love\",其对应的标签为2,2的二进制表示为`010`,我们只需将编码线路中的`e_0`、`e_1`和`e_2`分别设为$0$、$\\pi$和$0$即可。下面来验证一下。" ] }, { @@ -183,9 +203,11 @@ "Parameters of encoder is: \n", " [0. 3.14159 0. ]\n", "Encoder circuit is: \n", - " RX(e_0|0)\n", - "RX(e_1|1)\n", - "RX(e_2|2)\n", + " q0: ──RX(e_0)──\n", + " \n", + "q1: ──RX(e_1)──\n", + " \n", + "q2: ──RX(e_2)──\n", "Encoder parameter names are: \n", " ['e_0', 'e_1', 'e_2']\n", "Amplitude of quantum state is: \n", @@ -195,7 +217,7 @@ } ], "source": [ - "from mindquantum.nn import generate_evolution_operator\n", + "from mindquantum.simulator import Simulator\n", "from mindspore import context\n", "from mindspore import Tensor\n", "\n", @@ -204,18 +226,16 @@ "label_bin = bin(label)[-1:1:-1].ljust(n_qubits,'0') # binary form of label\n", "label_array = np.array([int(i)*np.pi for i in label_bin]).astype(np.float32) # parameter value of encoder\n", "encoder = GenerateEncoderCircuit(n_qubits, prefix='e') # encoder circuit\n", - "encoder_para_names = encoder.para_name # parameter names of encoder\n", + "encoder_params_names = encoder.params_name # parameter names of encoder\n", "\n", "print(\"Label is: \", label)\n", "print(\"Binary label is: \", label_bin)\n", "print(\"Parameters of encoder is: \\n\", np.round(label_array, 5))\n", "print(\"Encoder circuit is: \\n\", encoder)\n", - "print(\"Encoder parameter names are: \\n\", encoder_para_names)\n", + "print(\"Encoder parameter names are: \\n\", encoder_params_names)\n", "\n", - "context.set_context(mode=context.GRAPH_MODE, device_target=\"CPU\")\n", "# quantum state evolution operator\n", - "evol = generate_evolution_operator(param_names=encoder_para_names, circuit=encoder)\n", - "state = evol(Tensor(label_array))\n", + "state = encoder.get_qs(pr=dict(zip(encoder_params_names, label_array)))\n", "amp = np.round(np.abs(state)**2, 3)\n", "\n", "print(\"Amplitude of quantum state is: \\n\", amp)\n", @@ -314,21 +334,38 @@ "outputs": [ { "data": { + "text/html": [ + "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
q0: ──RY(a_0_0)────────●────────RY(a_1_0)───────\n",
+       "\n",
+       "q1: ──RY(a_0_1)────────X────────RY(a_1_1)────●──\n",
+       "\n",
+       "q2: ──RY(a_0_2)────────●────────RY(a_1_2)────X──\n",
+       "\n",
+       "q3: ──RY(a_0_3)────────X────────RY(a_1_3)────●──\n",
+       "\n",
+       "q4: ──RY(a_0_4)────RY(a_1_4)─────────────────X──\n",
+       "
\n" + ], "text/plain": [ - "RY(a_0_0|0)\n", - "RY(a_0_1|1)\n", - "RY(a_0_2|2)\n", - "RY(a_0_3|3)\n", - "RY(a_0_4|4)\n", - "X(1 <-: 0)\n", - "X(3 <-: 2)\n", - "RY(a_1_0|0)\n", - "RY(a_1_1|1)\n", - "RY(a_1_2|2)\n", - "RY(a_1_3|3)\n", - "RY(a_1_4|4)\n", - "X(2 <-: 1)\n", - "X(4 <-: 3)" + "q0: ──RY(a_0_0)────────●────────RY(a_1_0)───────\n", + " │ \n", + "q1: ──RY(a_0_1)────────X────────RY(a_1_1)────●──\n", + " │ \n", + "q2: ──RY(a_0_2)────────●────────RY(a_1_2)────X──\n", + " │ \n", + "q3: ──RY(a_0_3)────────X────────RY(a_1_3)────●──\n", + " │ \n", + "q4: ──RY(a_0_4)────RY(a_1_4)─────────────────X──" ] }, "execution_count": 10, @@ -379,7 +416,7 @@ { "data": { "text/plain": [ - "[1.0 Z0, 1.0 Z1, 1.0 Z0 Z1, 1.0 Z2, 1.0 Z0 Z2]" + "[1.0 [Z0] , 1.0 [Z1] , 1.0 [Z0 Z1] , 1.0 [Z2] , 1.0 [Z0 Z2] ]" ] }, "execution_count": 12, @@ -421,14 +458,15 @@ " encoder.no_grad()\n", " circ += encoder\n", " circ += ansatz\n", - " encoder_param_name.extend(encoder.para_name)\n", - " ansatz_param_name.extend(ansatz.para_name)\n", - " net = MindQuantumLayer(encoder_param_name,\n", - " ansatz_param_name,\n", - " circ,\n", - " hams,\n", - " n_threads=n_threads)\n", - " return net" + " encoder_param_name.extend(encoder.params_name)\n", + " ansatz_param_name.extend(ansatz.params_name)\n", + " grad_ops = Simulator('projectq', circ.n_qubits).get_expectation_with_grad(hams,\n", + " circ,\n", + " None,\n", + " encoder_param_name,\n", + " ansatz_param_name,\n", + " n_threads)\n", + " return MQLayer(grad_ops)" ] }, { @@ -536,21 +574,21 @@ "name": "stdout", "output_type": "stream", "text": [ - "epoch: 25 step: 20 time: 0.592, loss is 3.154\n", - "epoch: 50 step: 20 time: 0.614, loss is 2.944\n", - "epoch: 75 step: 20 time: 0.572, loss is 0.224\n", - "epoch: 100 step: 20 time: 0.562, loss is 0.015\n", - "epoch: 125 step: 20 time: 0.545, loss is 0.009\n", - "epoch: 150 step: 20 time: 0.599, loss is 0.003\n", - "epoch: 175 step: 20 time: 0.586, loss is 0.002\n", - "epoch: 200 step: 20 time: 0.552, loss is 0.045\n", - "epoch: 225 step: 20 time: 0.590, loss is 0.001\n", - "epoch: 250 step: 20 time: 0.643, loss is 0.001\n", - "epoch: 275 step: 20 time: 0.562, loss is 0.001\n", - "epoch: 300 step: 20 time: 0.584, loss is 0.001\n", - "epoch: 325 step: 20 time: 0.566, loss is 0.000\n", - "epoch: 350 step: 20 time: 0.578, loss is 0.000\n", - "Total time used: 206.29734826087952\n" + "epoch: 25 step: 20 time: 0.336, loss is 3.154\n", + "epoch: 50 step: 20 time: 0.449, loss is 2.945\n", + "epoch: 75 step: 20 time: 0.325, loss is 0.226\n", + "epoch: 100 step: 20 time: 0.370, loss is 0.016\n", + "epoch: 125 step: 20 time: 0.377, loss is 0.002\n", + "epoch: 150 step: 20 time: 0.399, loss is 0.006\n", + "epoch: 175 step: 20 time: 0.370, loss is 0.166\n", + "epoch: 200 step: 20 time: 0.345, loss is 0.139\n", + "epoch: 225 step: 20 time: 0.350, loss is 3.355\n", + "epoch: 250 step: 20 time: 0.334, loss is 1.059\n", + "epoch: 275 step: 20 time: 0.339, loss is 0.035\n", + "epoch: 300 step: 20 time: 0.334, loss is 0.024\n", + "epoch: 325 step: 20 time: 0.344, loss is 0.010\n", + "epoch: 350 step: 20 time: 0.344, loss is 0.009\n", + "Total time used: 126.26282787322998\n" ] } ], @@ -558,7 +596,7 @@ "import mindspore as ms\n", "from mindspore import context\n", "from mindspore import Tensor\n", - "context.set_context(mode=context.GRAPH_MODE, device_target=\"CPU\")\n", + "context.set_context(mode=context.PYNATIVE_MODE, device_target=\"CPU\")\n", "corpus = \"\"\"We are about to study the idea of a computational process.\n", "Computational processes are abstract beings that inhabit computers.\n", "As they evolve, processes manipulate other abstract things called data.\n", @@ -594,11 +632,24 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "metadata": { "scrolled": true }, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], "source": [ "import matplotlib.pyplot as plt\n", "\n", @@ -612,16 +663,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "得到收敛图为\n", - "\n", - "![nlp loss](images/nlp_loss.png)\n", - "\n", "通过如下方法打印量子嵌入层的量子线路中的参数:" ] }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 18, "metadata": { "scrolled": true }, @@ -629,28 +676,28 @@ { "data": { "text/plain": [ - "array([ 1.52044818e-01, 1.71521559e-01, 2.35021308e-01, -3.95286232e-01,\n", - " -3.71680595e-03, 7.96886325e-01, -4.04954888e-02, 1.55393332e-01,\n", - " 4.11805660e-02, 7.79824018e-01, 2.96543002e-01, -2.21819162e-01,\n", - " -4.67430688e-02, 4.66759771e-01, 2.75283188e-01, 1.35858059e-01,\n", - " -3.23841363e-01, -2.31937021e-01, -4.68942285e-01, -1.96520030e-01,\n", - " 2.16065589e-02, 1.23866223e-01, -9.68078300e-02, 1.69127151e-01,\n", - " -8.90062153e-01, 2.56734312e-01, 8.37369189e-02, -1.15734830e-01,\n", - " -1.34410933e-01, -3.12207133e-01, -8.90189946e-01, 1.97006428e+00,\n", - " -2.49193460e-02, 2.25960299e-01, -3.90179232e-02, -3.03875893e-01,\n", - " 2.02030335e-02, -7.07065910e-02, -4.81521547e-01, 5.04257262e-01,\n", - " -1.32081115e+00, 2.83502758e-01, 2.80248702e-01, 1.63375765e-01,\n", - " -6.91465080e-01, 6.82975233e-01, -2.67829001e-01, 2.29658693e-01,\n", - " 2.78859794e-01, -1.04206935e-01, -5.57148576e-01, 4.41706657e-01,\n", - " -6.76973104e-01, 2.47751385e-01, -2.96468334e-03, -1.66827604e-01,\n", - " -3.47717047e-01, -9.04396921e-03, -7.69433856e-01, 4.33617719e-02,\n", - " -2.09145937e-02, -1.55236557e-01, -2.16777384e-01, -2.26556376e-01,\n", - " -6.16374731e-01, 2.05871137e-03, -3.08128931e-02, -1.63372140e-02,\n", - " 1.46710426e-01, 2.31793106e-01, 4.16066934e-04, -9.28813033e-03],\n", + "array([ 1.5175925e+00, -5.5282825e-01, 1.9824509e-01, -2.3327057e+00,\n", + " 8.4526891e-01, -1.3019586e+00, 9.3813318e-01, -1.0318477e-01,\n", + " 4.4351882e-01, 1.8607093e+00, 6.0036021e-01, -3.0638957e-01,\n", + " 9.3188483e-01, 6.0410827e-01, -1.8905094e-01, 6.5970606e-01,\n", + " -1.2129487e+00, -3.1650740e-01, -2.5501034e+00, 3.6324959e-02,\n", + " 4.0066850e-01, 7.5752664e-01, -5.6982380e-01, -5.6846058e-01,\n", + " -9.0591955e-01, 3.3477244e-01, -6.1832809e-01, 2.1618415e-01,\n", + " 1.0225463e-01, 4.0966314e-01, -9.0604734e-01, 1.3528558e+00,\n", + " -5.3387892e-01, -3.2625124e-02, 6.8196923e-02, 4.1799426e-01,\n", + " 2.6094767e-01, -3.3765252e+00, -1.9021339e+00, -1.1502613e+00,\n", + " -2.0344164e+00, 8.0160522e-01, -2.8717926e-01, 3.3720109e-01,\n", + " -2.1616800e+00, 1.1822585e+00, -7.0481867e-01, 4.0014455e-01,\n", + " -2.8856799e-01, 8.4199363e-01, -5.8137196e-01, -1.9842222e+00,\n", + " 1.7555025e-01, 4.1823694e-01, -3.1270559e+00, 2.6714945e+00,\n", + " 2.3251233e+00, 3.0707479e-01, -5.3547442e-01, 3.0258337e-01,\n", + " -1.5764916e+00, 3.0099937e-01, -2.9257689e+00, -1.1786047e+00,\n", + " -5.7270378e-01, 2.0587114e-03, -1.5863895e+00, -2.1442556e+00,\n", + " -1.7923084e-01, -1.2772868e+00, 4.1606693e-04, -9.2881303e-03],\n", " dtype=float32)" ] }, - "execution_count": 17, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } @@ -672,7 +719,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 19, "metadata": {}, "outputs": [], "source": [ @@ -704,7 +751,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 20, "metadata": {}, "outputs": [ { @@ -740,32 +787,34 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 21, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "epoch: 25 step: 20 time: 0.008, loss is 3.155\n", - "epoch: 50 step: 20 time: 0.026, loss is 3.027\n", - "epoch: 75 step: 20 time: 0.010, loss is 3.010\n", - "epoch: 100 step: 20 time: 0.009, loss is 2.955\n", - "epoch: 125 step: 20 time: 0.008, loss is 0.630\n", - "epoch: 150 step: 20 time: 0.008, loss is 0.059\n", - "epoch: 175 step: 20 time: 0.009, loss is 0.008\n", - "epoch: 200 step: 20 time: 0.008, loss is 0.003\n", - "epoch: 225 step: 20 time: 0.017, loss is 0.001\n", - "epoch: 250 step: 20 time: 0.008, loss is 0.001\n", - "epoch: 275 step: 20 time: 0.016, loss is 0.000\n", - "epoch: 300 step: 20 time: 0.008, loss is 0.000\n", - "epoch: 325 step: 20 time: 0.016, loss is 0.000\n", - "epoch: 350 step: 20 time: 0.008, loss is 0.000\n", - "Total time used: 5.06074857711792\n" + "epoch: 25 step: 20 time: 0.031, loss is 3.155\n", + "epoch: 50 step: 20 time: 0.033, loss is 3.027\n", + "epoch: 75 step: 20 time: 0.033, loss is 3.010\n", + "epoch: 100 step: 20 time: 0.033, loss is 2.955\n", + "epoch: 125 step: 20 time: 0.032, loss is 0.630\n", + "epoch: 150 step: 20 time: 0.034, loss is 0.059\n", + "epoch: 175 step: 20 time: 0.033, loss is 0.008\n", + "epoch: 200 step: 20 time: 0.031, loss is 0.003\n", + "epoch: 225 step: 20 time: 0.032, loss is 0.001\n", + "epoch: 250 step: 20 time: 0.030, loss is 0.001\n", + "epoch: 275 step: 20 time: 0.032, loss is 0.000\n", + "epoch: 300 step: 20 time: 0.030, loss is 0.000\n", + "epoch: 325 step: 20 time: 0.030, loss is 0.000\n", + "epoch: 350 step: 20 time: 0.029, loss is 0.000\n", + "Total time used: 11.819875240325928\n" ] } ], "source": [ + "context.set_context(mode=context.GRAPH_MODE, device_target=\"CPU\")\n", + "\n", "train_loader = ds.NumpySlicesDataset({\n", " \"around\": train_x,\n", " \"center\": train_y\n", @@ -787,9 +836,22 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 22, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], "source": [ "import matplotlib.pyplot as plt\n", "\n", @@ -803,10 +865,6 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "得到收敛图为\n", - "\n", - "![classical nlp loss](images/classical_nlp_loss.png)\n", - "\n", "由上可知,通过量子模拟得到的量子版词嵌入模型也能很好的完成嵌入任务。当数据集大到经典计算机算力难以承受时,量子计算机将能够轻松处理这类问题。" ] }, diff --git a/tutorials/vqe_for_quantum_chemistry.ipynb b/tutorials/7.vqe_for_quantum_chemistry.ipynb similarity index 78% rename from tutorials/vqe_for_quantum_chemistry.ipynb rename to tutorials/7.vqe_for_quantum_chemistry.ipynb index 1c7db5c952953cbb45fb79077b6c3a3c7dfb5316..567676db2273222277c03b6d2a19ca5df049cc78 100644 --- a/tutorials/vqe_for_quantum_chemistry.ipynb +++ b/tutorials/7.vqe_for_quantum_chemistry.ipynb @@ -2,7 +2,6 @@ "cells": [ { "cell_type": "markdown", - "id": "grand-contest", "metadata": {}, "source": [ "# 在量子化学计算中应用量子变分求解器\n", @@ -10,7 +9,7 @@ "\n", "`Linux` `CPU` `全流程` `初级` `中级` `高级`\n", "\n", - "[![](https://gitee.com/mindspore/docs/raw/master/tutorials/training/source_zh_cn/_static/logo_source.png)](https://gitee.com/mindspore/mindquantum/blob/master/tutorials/vqe_for_quantum_chemistry.ipynb)\n", + "[![](https://gitee.com/mindspore/docs/raw/master/resource/_static/logo_source.png)](https://gitee.com/mindspore/mindquantum/blob/master/tutorials/7.vqe_for_quantum_chemistry.ipynb) [![](https://gitee.com/mindspore/mindquantum/raw/master/tutorials/images/view_mindquantum_api.png)](https://mindspore.cn/mindquantum/api/zh-CN/master/index.html)\n", "\n", "## 概述\n", "\n", @@ -25,7 +24,7 @@ "3. 使用MindQuantum实现高效自动求导的VQE模拟。\n", "\n", "> 本文档适用于CPU环境。 \n", - "> 你可以在这里找到完整的可运行的样例代码:。\n", + "> 你可以在这里找到完整的可运行的样例代码:。\n", "\n", "## 环境准备\n", "\n", @@ -49,30 +48,27 @@ { "cell_type": "code", "execution_count": 1, - "id": "comic-pointer", "metadata": { "scrolled": true }, + "outputs": [], "source": [ "import numpy as np\n", "from openfermion.chem import MolecularData\n", "from openfermionpyscf import run_pyscf\n", "import mindquantum as mq\n", - "from mindquantum import Circuit, X, RX, Hamiltonian\n", - "from mindquantum.circuit import generate_uccsd\n", - "from mindquantum.nn import generate_pqc_operator\n", + "from mindquantum import Circuit, X, RX, Hamiltonian, Simulator\n", + "from mindquantum.algorithm import generate_uccsd\n", "import mindspore as ms\n", "import mindspore.context as context\n", "from mindspore.common.parameter import Parameter\n", "from mindspore.common.initializer import initializer\n", "\n", - "context.set_context(mode=context.GRAPH_MODE, device_target=\"CPU\")" - ], - "outputs":[] + "context.set_context(mode=context.PYNATIVE_MODE, device_target=\"CPU\")" + ] }, { "cell_type": "markdown", - "id": "seasonal-newman", "metadata": {}, "source": [ "## 量子化学计算方法\n", @@ -97,7 +93,6 @@ { "cell_type": "code", "execution_count": 2, - "id": "frequent-framing", "metadata": {}, "outputs": [ { @@ -122,7 +117,6 @@ }, { "cell_type": "markdown", - "id": "alpha-shoot", "metadata": {}, "source": [ "上面的代码定义了一个Li-H键长为1.5Å分子。使用STO-3G基组进行计算。接下来使用openfermionpyscf,调用PySCF进行HF、CCSD和FCI计算。这三种方法属于波函数方法,开始计算之前,先对这些方法作一个简单的介绍。\n", @@ -208,16 +202,15 @@ { "cell_type": "code", "execution_count": 3, - "id": "horizontal-johns", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Hartree-Fock energy: -7.8633576215351200 Ha\n", - "CCSD energy: -7.8823529091527051 Ha\n", - "FCI energy: -7.8823622867987249 Ha\n" + "Hartree-Fock energy: -7.8633576215351164 Ha\n", + "CCSD energy: -7.8823529091527007 Ha\n", + "FCI energy: -7.8823622867987213 Ha\n" ] } ], @@ -241,7 +234,6 @@ }, { "cell_type": "markdown", - "id": "advisory-milton", "metadata": {}, "source": [ "在上面的例子中,我们运行了Hartree-Fock(HF)、CCSD、FCI进行总能量的计算。若对运行时间进行统计,会发现$T_{HF}E_{CCSD}>E_{FCI}$。计算完成后,我们将结果保存到`molecule_file`文件(即`molecule_of.filename`)中:" @@ -250,14 +242,13 @@ { "cell_type": "code", "execution_count": 4, - "id": "overall-puzzle", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "/home/xuxs/anaconda3/envs/p37/lib/python3.7/site-packages/openfermion/testing/data/H1-Li1_sto3g_singlet\n" + "/home/xuxs/anaconda3/lib/python3.8/site-packages/openfermion/testing/data/H1-Li1_sto3g_singlet\n" ] } ], @@ -269,7 +260,6 @@ }, { "cell_type": "markdown", - "id": "cross-wrong", "metadata": {}, "source": [ "量子化学计算的一大阻碍是计算量。随着体系大小(电子数、原子数)的增加,求解FCI波函数和基态能量的时间消耗大约以$2^{N}$增长,即使是较小的分子如乙烯分子等,进行FCI计算也并不容易。量子计算机的出现为此提供了一条可能的解决途径,已有的研究表明,量子计算机可以多项式的时间复杂度模拟哈密顿量的含时演化,在量子处理器上进行化学模拟相较于经典计算机有指数级的加速。本教程将介绍其中一类量子算法:量子变分求解器。\n", @@ -304,17 +294,19 @@ { "cell_type": "code", "execution_count": 5, - "id": "handmade-headline", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "X(0)\n", - "X(1)\n", - "X(2)\n", - "X(3)\n" + "q0: ──X──\n", + " \n", + "q1: ──X──\n", + " \n", + "q2: ──X──\n", + " \n", + "q3: ──X──\n" ] } ], @@ -325,7 +317,6 @@ }, { "cell_type": "markdown", - "id": "promotional-excitement", "metadata": {}, "source": [ "基于此,我们可以构造如下形式的试探波函数:\n", @@ -348,7 +339,6 @@ }, { "cell_type": "markdown", - "id": "brave-discharge", "metadata": {}, "source": [ "使用mindquantum的circuit模块中的`generate_uccsd`函数可读取先前保存在`molecule_file`的计算结果,“一键”构造UCCSD波函数拟设,以及其对应的量子线路:" @@ -357,15 +347,14 @@ { "cell_type": "code", "execution_count": 6, - "id": "hundred-omaha", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "ccsd:-7.882352909152705.\n", - "fci:-7.882362286798725.\n" + "ccsd:-7.882352909152701.\n", + "fci:-7.882362286798721.\n" ] } ], @@ -379,7 +368,6 @@ }, { "cell_type": "markdown", - "id": "specified-hello", "metadata": {}, "source": [ "`generate_uccsd`将幺正耦合簇相关的函数打包了起来,包括导出分子哈密度量、构造幺正耦合簇拟设算符、提取CCSD计算的耦合簇系数等多个步骤。该函数通过输入分子的文件路径来读取该分子,参数`th`是表示量子线路中哪些参数需要更新梯度的阈值。在[分步构造幺正耦合簇拟设](#step-by-step)章节,我们会演示如何使用mindquantum的相关接口分步完成其中包含的步骤。完整的量子线路包含HF初态+UCCSD拟设,如下代码所示:" @@ -388,19 +376,18 @@ { "cell_type": "code", "execution_count": 7, - "id": "celtic-citation", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "==============================Circuit Summary==============================\n", - "|Total number of gates : 12612. |\n", - "|Parameter gates : 640. |\n", - "|with 44 parameters are : p40, p9, p8, p3, p32, p28, p15, p4, p18, p22... |\n", - "|Number qubit of circuit: 12 |\n", - "===========================================================================\n", + "============================Circuit Summary============================\n", + "|Total number of gates : 15172. |\n", + "|Parameter gates : 640. |\n", + "|with 44 parameters are : p0, p8, p1, p9, p2, p10, p3, p11, p4, p12...|\n", + "|Number qubit of circuit: 12 |\n", + "=======================================================================\n", "Number of parameters: 44\n" ] } @@ -413,7 +400,6 @@ }, { "cell_type": "markdown", - "id": "municipal-bolivia", "metadata": {}, "source": [ "对于LiH分子而言,其UCCSD波函数拟设中包含44个变分参数。该线路总共的量子比特门数量为12612,总共需要12个量子比特进行模拟。" @@ -421,7 +407,6 @@ }, { "cell_type": "markdown", - "id": "accurate-incentive", "metadata": {}, "source": [ "### VQE的一般流程\n", @@ -444,76 +429,55 @@ { "cell_type": "code", "execution_count": 8, - "id": "featured-chocolate", "metadata": {}, "outputs": [], "source": [ - "molecule_pqc = generate_pqc_operator(\n", - " [\"null\"], ansatz_parameter_names, \n", - " RX(\"null\").on(0) + total_circuit, \n", - " Hamiltonian(hamiltonian_QubitOp))" + "sim = Simulator('projectq', total_circuit.n_qubits)\n", + "molecule_pqc = sim.get_expectation_with_grad(Hamiltonian(hamiltonian_QubitOp), total_circuit)" ] }, { "cell_type": "markdown", - "id": "aerial-department", "metadata": {}, "source": [ - "由于mindquantum需要提供两套线路(以及参数)分别作为Encoding circuit和Ansatz circuit,此处我们使用`RX(\"null\")`作为一个Encoding circuit,在之后令参数`null`等于0将其无效化。通过将参数的具体数值传入`molecule_pqc`,即可得到对应于此变分参数的能量$E(\\theta)=\\langle \\Psi_{UCC}(\\theta) | \\hat{H} | \\Psi_{UCC}(\\theta) \\rangle$以及关于每个变分参数的导数。" + "通过将参数的具体数值传入`molecule_pqc`,即可得到对应于此变分参数的能量$E(\\theta)=\\langle \\Psi_{UCC}(\\theta) | \\hat{H} | \\Psi_{UCC}(\\theta) \\rangle$以及关于每个变分参数的导数。" ] }, { "cell_type": "markdown", - "id": "waiting-committee", "metadata": {}, "source": [ - "接下来需要进行VQE优化的(5)~(7)步,即对参数化量子线路进行优化。我们可以借助MindSpore框架,使用参数化量子线路算子`molecule_pqc`构造一个神经网络模型,然后通过类似于训练神经网络的方法来优化变分参数:" + "接下来需要进行VQE优化的(5)~(7)步,即对参数化量子线路进行优化。我们可以借助MindQuantum框架,使用参数化量子线路算子`molecule_pqc`构造一个神经网络模型,然后通过类似于训练神经网络的方法来优化变分参数:" ] }, { "cell_type": "code", "execution_count": 9, - "id": "biological-mouse", "metadata": {}, "outputs": [], "source": [ - "class PQCNet(ms.nn.Cell):\n", - " def __init__(self, pqc):\n", - " super(PQCNet, self).__init__()\n", - " self.pqc = pqc\n", - " self.weight = Parameter(initializer(\"Zeros\",\n", - " len(self.pqc.ansatz_params_names)),\n", - " name=\"weight\")\n", - " self.encoder_data_dummy = ms.Tensor([[0]], \n", - " self.weight.dtype)\n", + "from mindquantum.framework import MQAnsatzOnlyLayer\n", "\n", - " def construct(self):\n", - " energy, _, grads = self.pqc(self.encoder_data_dummy, self.weight)\n", - " return energy\n", - "\n", - "molecule_pqcnet = PQCNet(molecule_pqc)" + "molecule_pqcnet = MQAnsatzOnlyLayer(molecule_pqc, 'Zeros')" ] }, { "cell_type": "markdown", - "id": "preceding-outline", "metadata": {}, "source": [ - "此处我们手动构造了一个基本的`PQCNet`作为模型示例,该模型可以和常规的机器学习模型类似使用,比如优化权重、计算导数等。更好的选择是使用mindquantum中封装的`MindQuantumAnsatzOnlyLayer`,将会在后文中进行演示。" + "此处我们构造了一个基本的`MQAnsatzOnlyLayer`作为模型示例,该模型可以和常规的机器学习模型类似使用,比如优化权重、计算导数等" ] }, { "cell_type": "markdown", - "id": "proved-stress", "metadata": {}, "source": [ - "构造的`PQCNet`使用`\"Zeros\"`关键字,将所有的变分参数初始化为0。使用CCSD(耦合簇理论)或者MP2(二阶多体微扰论)的计算结果也可以作为幺正耦合簇变分参数的初始值。此时有$E(\\vec{0})=\\langle \\Psi_{UCC}(\\vec{0}) | \\hat{H} | \\Psi_{UCC}(\\vec{0}) \\rangle = E_{HF}$:" + "构造的`MQAnsatzOnlyLayer`使用`\"Zeros\"`关键字,将所有的变分参数初始化为0。使用CCSD(耦合簇理论)或者MP2(二阶多体微扰论)的计算结果也可以作为幺正耦合簇变分参数的初始值。此时有$E(\\vec{0})=\\langle \\Psi_{UCC}(\\vec{0}) | \\hat{H} | \\Psi_{UCC}(\\vec{0}) \\rangle = E_{HF}$:" ] }, { "cell_type": "code", "execution_count": 10, - "id": "secret-dubai", "metadata": {}, "outputs": [ { @@ -531,7 +495,6 @@ }, { "cell_type": "markdown", - "id": "charming-argument", "metadata": {}, "source": [ "最后使用mindspore的Adam优化器进行优化,学习率设置为$1\\times 10^{-2}$,优化终止标准设置为$\\left.|\\epsilon|\\right. = \\left.|E^{k+1} - E^{k}|\\right. \\le 1\\times 10^{-8}$" @@ -540,7 +503,6 @@ { "cell_type": "code", "execution_count": 11, - "id": "pediatric-budapest", "metadata": {}, "outputs": [ { @@ -553,28 +515,27 @@ "Step 15 energy -7.8822836875915527\n", "Step 20 energy -7.8823199272155762\n", "Step 25 energy -7.8823370933532715\n", - "Step 30 energy -7.8823437690734863\n", - "Step 35 energy -7.8618836402893066\n", - "Step 40 energy -7.8671770095825195\n", - "Step 45 energy -7.8751692771911621\n", - "Step 50 energy -7.8822755813598633\n", - "Step 55 energy -7.8812966346740723\n", - "Step 60 energy -7.8823189735412598\n", - "Step 65 energy -7.8823523521423340\n", - "Optimization completed at step 67\n", - "Optimized energy: -7.8823528289794922\n", + "Step 30 energy -7.8815641403198242\n", + "Step 35 energy -7.8786268234252930\n", + "Step 40 energy -7.8778734207153320\n", + "Step 45 energy -7.8808088302612305\n", + "Step 50 energy -7.8819031715393066\n", + "Step 55 energy -7.8821558952331543\n", + "Step 60 energy -7.8823504447937012\n", + "Optimization completed at step 63\n", + "Optimized energy: -7.8823523521423340\n", "Optimized amplitudes: \n", - " [ 2.3980068e-04 1.8912849e-03 3.5044324e-02 1.6005965e-02\n", - " -1.9985158e-07 9.0940151e-04 1.6222824e-05 1.4160988e-02\n", - " -1.1072063e-07 9.0867787e-04 1.3825165e-05 1.4166672e-02\n", - " -5.4699212e-04 4.2679289e-04 2.8641545e-03 5.3817011e-02\n", - " 2.3320253e-04 1.7034533e-07 6.6684343e-08 -2.7686235e-07\n", - " 7.2332718e-08 1.2834757e-05 -1.0439425e-04 7.1826143e-08\n", - " 3.6483241e-06 6.1677817e-08 3.1003920e-06 7.9770159e-04\n", - " -5.4951470e-02 3.0904056e-03 -4.4321241e-05 8.5840838e-07\n", - " -1.9589644e-08 -4.9430941e-08 8.6163556e-07 -2.5008637e-07\n", - " 2.1493735e-08 -4.6331229e-06 3.0904033e-03 9.5311613e-08\n", - " -4.8755901e-08 2.0483398e-08 -3.9453280e-06 3.7235476e-04]\n" + " [ 2.40339446e-04 1.89154677e-03 3.49554531e-02 1.59917790e-02\n", + " 2.33248898e-07 9.09393420e-04 -1.79268172e-05 1.41595434e-02\n", + " 6.28582342e-08 9.08669957e-04 -1.49387897e-05 1.41652254e-02\n", + " -5.46666037e-04 4.26779327e-04 2.86067789e-03 5.38198128e-02\n", + " 2.32545775e-04 -2.78862785e-07 -7.10907813e-08 -7.98562283e-08\n", + " 7.00364581e-07 -9.21200325e-08 -6.73263187e-08 1.26236855e-05\n", + " -1.04519488e-04 7.97090179e-04 -4.01437364e-06 -3.34858555e-06\n", + " -5.49289174e-02 3.09006264e-03 7.01365061e-05 -1.36400865e-06\n", + " -1.35536197e-06 4.63907739e-08 5.32547162e-08 -2.34681625e-08\n", + " 3.92657455e-07 5.11744884e-06 3.09006032e-03 -2.05122589e-07\n", + " 5.91138871e-08 -2.44064164e-08 4.26194856e-06 3.72134935e-04]\n" ] } ], @@ -601,7 +562,6 @@ }, { "cell_type": "markdown", - "id": "advised-infrared", "metadata": {}, "source": [ "可以看到,幺正耦合簇给出的计算结果和FCI非常接近,具有良好的精度。" @@ -609,7 +569,6 @@ }, { "cell_type": "markdown", - "id": "honey-budapest", "metadata": {}, "source": [ "## 分步构造幺正耦合簇拟设\n", @@ -619,7 +578,6 @@ }, { "cell_type": "markdown", - "id": "color-indian", "metadata": {}, "source": [ "在上文中,我们使用了`generate_uccsd`一步构造出了幺正耦合簇拟设所需要的所有内容,此处我们将步骤拆分,分别得到我们需要的耦合簇算符、对应的量子线路以及取自于经典CCSD计算结果的变分参数初猜值。\n", @@ -629,20 +587,18 @@ { "cell_type": "code", "execution_count": 12, - "id": "addressed-cathedral", "metadata": {}, "outputs": [], "source": [ - "from mindquantum.hiqfermion.transforms import Transform\n", - "from mindquantum.hiqfermion.ucc import get_qubit_hamiltonian\n", - "from mindquantum.hiqfermion.ucc import uccsd_singlet_generator, uccsd_singlet_get_packed_amplitudes\n", - "from mindquantum.circuit import TimeEvolution\n", - "from mindquantum.nn.mindquantum_ansatz_only_layer import MindQuantumAnsatzOnlyLayer" + "from mindquantum.algorithm.nisq.chem import Transform\n", + "from mindquantum.algorithm.nisq.chem import get_qubit_hamiltonian\n", + "from mindquantum.algorithm.nisq.chem import uccsd_singlet_generator, uccsd_singlet_get_packed_amplitudes\n", + "from mindquantum.core.operators import TimeEvolution\n", + "from mindquantum.framework import MQAnsatzOnlyLayer" ] }, { "cell_type": "markdown", - "id": "strategic-nature", "metadata": {}, "source": [ "分子哈密顿量使用`get_qubit_hamiltonian`,读取之前的计算结果得到:" @@ -651,7 +607,6 @@ { "cell_type": "code", "execution_count": 13, - "id": "vocational-literature", "metadata": {}, "outputs": [], "source": [ @@ -660,7 +615,6 @@ }, { "cell_type": "markdown", - "id": "coordinate-vampire", "metadata": {}, "source": [ "对于幺正耦合簇算符$ \\hat{T} - \\hat{T}^{\\dagger} $,可以使用`uccsd_singlet_generator`进行构造。提供总量子比特数(总自旋轨道数)和总电子数,并设置参数`anti_hermitian=True`:" @@ -669,7 +623,6 @@ { "cell_type": "code", "execution_count": 14, - "id": "sound-course", "metadata": {}, "outputs": [], "source": [ @@ -679,7 +632,6 @@ }, { "cell_type": "markdown", - "id": "serial-possibility", "metadata": {}, "source": [ "上一步构造的`ucc_fermion_ops`是参数化的。使用Jordan-Wigner变换将费米子激发算符映射为Pauli算符:" @@ -688,7 +640,6 @@ { "cell_type": "code", "execution_count": 15, - "id": "silver-following", "metadata": {}, "outputs": [], "source": [ @@ -697,7 +648,6 @@ }, { "cell_type": "markdown", - "id": "miniature-airplane", "metadata": {}, "source": [ "接下来,我们需要得到幺正算符 $ \\exp{(\\hat{T} - \\hat{T}^{\\dagger})} $ 所对应的量子线路。`TimeEvolution`可生成$ \\exp{(-i\\hat{H}t)} $所对应的线路,其中$ \\hat{H} $是一个厄米算符,$t$是实数。需要注意的是,使用`TimeEvolution`时,`ucc_qubit_ops`中已经包含了复数因子$i$,所以我们需要将`ucc_qubit_ops`除以$i$,或者提取其虚部:" @@ -706,17 +656,15 @@ { "cell_type": "code", "execution_count": 16, - "id": "olive-blade", "metadata": {}, "outputs": [], "source": [ "ansatz_circuit = TimeEvolution(ucc_qubit_ops.imag, 1.0).circuit\n", - "ansatz_parameter_names = ansatz_circuit.para_name" + "ansatz_parameter_names = ansatz_circuit.params_name" ] }, { "cell_type": "markdown", - "id": "improved-canvas", "metadata": {}, "source": [ "我们使用`ansatz_parameter_names`记录该线路中的参数名。到目前为止,我们已经得到了VQE量子线路所需要内容,包括哈密顿量`hamiltonian_QubitOp`、参数化的波函数拟设线路`ansatz_circuit`,故可仿照前文,得到完整的态制备线路。其中Hartree-Fock参考态复用之前的`hartreefock_wfn_circuit`:" @@ -725,19 +673,18 @@ { "cell_type": "code", "execution_count": 17, - "id": "former-support", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "======================================Circuit Summary======================================\n", - "|Total number of gates : 12612. |\n", - "|Parameter gates : 640. |\n", - "|with 44 parameters are : d1_3, d2_26, d2_6, d2_1, d2_2, d2_14, d1_1, s_1, d2_16, d2_11...|\n", - "|Number qubit of circuit: 12 |\n", - "===========================================================================================\n" + "==================================Circuit Summary==================================\n", + "|Total number of gates : 15172. |\n", + "|Parameter gates : 640. |\n", + "|with 44 parameters are : s_0, d1_0, s_1, d1_1, s_2, d1_2, s_3, d1_3, s_4, d1_4...|\n", + "|Number qubit of circuit: 12 |\n", + "===================================================================================\n" ] } ], @@ -748,7 +695,6 @@ }, { "cell_type": "markdown", - "id": "sought-intellectual", "metadata": {}, "source": [ "下一步,需要为变分参数提供一个合理的初始值。前文构造的`PQCNet`默认使用0作为初猜,在大多数情况下是可行的。不过,使用CCSD的计算数据作为UCC的出发点,可能会有更好的结果。使用`uccsd_singlet_get_packed_amplitudes`函数从`molecule_of`提取CCSD的参数:" @@ -757,7 +703,6 @@ { "cell_type": "code", "execution_count": 18, - "id": "indie-politics", "metadata": {}, "outputs": [], "source": [ @@ -768,26 +713,26 @@ }, { "cell_type": "markdown", - "id": "active-space", "metadata": {}, "source": [ - "使用`MindQuantumAnsatzOnlyLayer`可以方便地由参数、量子线路获得以参数化量子线路为基础的机器学习模型:" + "使用`MQAnsatzOnlyLayer`可以方便地由参数、量子线路获得以参数化量子线路为基础的机器学习模型:" ] }, { "cell_type": "code", "execution_count": 19, - "id": "rental-pierce", "metadata": {}, "outputs": [], "source": [ - "molecule_pqcnet = MindQuantumAnsatzOnlyLayer(\n", - " ansatz_parameter_names, total_circuit, Hamiltonian(hamiltonian_QubitOp.real))" + "grad_ops = Simulator('projectq', total_circuit.n_qubits).get_expectation_with_grad(\n", + " Hamiltonian(hamiltonian_QubitOp.real),\n", + " total_circuit)\n", + "\n", + "molecule_pqcnet = MQAnsatzOnlyLayer(grad_ops)" ] }, { "cell_type": "markdown", - "id": "ready-trick", "metadata": {}, "source": [ "使用`init_amplitudes_ccsd`(即CCSD计算的耦合簇系数)作为初始变分参数:" @@ -796,7 +741,6 @@ { "cell_type": "code", "execution_count": 20, - "id": "passive-border", "metadata": {}, "outputs": [ { @@ -815,7 +759,6 @@ }, { "cell_type": "markdown", - "id": "behavioral-microphone", "metadata": {}, "source": [ "在这个例子中,CCSD初猜并没有提供一个更好的起点。读者可以对更多的分子、更多种类的初始值(如随机数初猜)等进行测试和探究。最后进行VQE的优化步骤,优化器依然使用Adam,收敛标准不变。优化所用的代码与前文基本一致,注意更新相应的变量即可:" @@ -824,7 +767,6 @@ { "cell_type": "code", "execution_count": 21, - "id": "million-twelve", "metadata": {}, "outputs": [ { @@ -833,32 +775,32 @@ "text": [ "eps: 1e-08\n", "Step 0 energy -7.8173098564147949\n", - "Step 5 energy -7.8740763664245605\n", + "Step 5 energy -7.8740758895874023\n", "Step 10 energy -7.8818783760070801\n", "Step 15 energy -7.8821649551391602\n", "Step 20 energy -7.8822622299194336\n", - "Step 25 energy -7.8823084831237793\n", - "Step 30 energy -7.8823180198669434\n", - "Step 35 energy -7.8737111091613770\n", - "Step 40 energy -7.8724455833435059\n", - "Step 45 energy -7.8801403045654297\n", - "Step 50 energy -7.8821926116943359\n", - "Step 55 energy -7.8818311691284180\n", - "Step 60 energy -7.8823456764221191\n", - "Optimization completed at step 64\n", + "Step 25 energy -7.8823080062866211\n", + "Step 30 energy -7.8822288513183594\n", + "Step 35 energy -7.8758554458618164\n", + "Step 40 energy -7.8761253356933594\n", + "Step 45 energy -7.8807921409606934\n", + "Step 50 energy -7.8818383216857910\n", + "Step 55 energy -7.8821811676025391\n", + "Step 60 energy -7.8823504447937012\n", + "Optimization completed at step 63\n", "Optimized energy: -7.8823523521423340\n", "Optimized amplitudes: \n", - " [-2.4216002e-04 1.8924323e-03 -3.4653045e-02 1.5943546e-02\n", - " 3.6362690e-07 9.0936717e-04 -1.7181528e-05 1.4154296e-02\n", - " -4.4650793e-08 9.0864423e-04 -2.6399141e-06 1.4159971e-02\n", - " 5.4558384e-04 4.2672374e-04 -2.8494308e-03 5.3833455e-02\n", - " 2.3033506e-04 1.2578158e-06 3.3855862e-08 7.3955505e-08\n", - " -5.2005623e-07 2.9746575e-08 1.2325607e-08 1.1919828e-05\n", - " -1.0492613e-04 7.9503102e-04 3.8478893e-06 5.9738107e-07\n", - " -5.4855812e-02 3.0889052e-03 7.9252044e-05 -1.5384763e-06\n", - " -1.5373821e-06 -3.0784176e-07 -3.5303248e-08 1.7360321e-08\n", - " 4.4359115e-07 -4.9067144e-06 3.0889027e-03 1.3888703e-07\n", - " -1.6715177e-08 6.3234533e-09 -7.5149819e-07 3.7140178e-04]\n" + " [-2.42472161e-04 1.89258391e-03 -3.46013680e-02 1.59353409e-02\n", + " -8.40432079e-08 9.09362687e-04 2.01798011e-05 1.41534032e-02\n", + " -2.81526667e-07 9.08639806e-04 2.00776722e-05 1.41590768e-02\n", + " 5.45396935e-04 4.26715094e-04 -2.84755090e-03 5.38354851e-02\n", + " 2.29954778e-04 9.55212727e-07 -1.24844689e-07 -9.20767249e-08\n", + " -4.53033465e-07 -7.44455733e-08 -8.83169875e-08 1.17984437e-05\n", + " -1.04996754e-04 7.94677646e-04 -4.50417019e-06 -4.49753043e-06\n", + " -5.48430867e-02 3.08870710e-03 -6.50319926e-05 1.26427835e-06\n", + " 1.25660222e-06 -2.82077963e-07 7.96948143e-08 -3.28978906e-08\n", + " -3.63568660e-07 5.76087541e-06 3.08870478e-03 8.82309266e-08\n", + " 5.73797401e-08 -2.53652850e-08 5.72846511e-06 3.71275470e-04]\n" ] } ], @@ -885,7 +827,6 @@ }, { "cell_type": "markdown", - "id": "changing-coach", "metadata": {}, "source": [ "## 总结\n", @@ -910,7 +851,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.5" + "version": "3.8.5" } }, "nbformat": 4, diff --git a/tutorials/8.grover_search_algorithm_based_on_mindquantum.ipynb b/tutorials/8.grover_search_algorithm_based_on_mindquantum.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..6d8671ccab574a0928b034ecf3211b69b6741f45 --- /dev/null +++ b/tutorials/8.grover_search_algorithm_based_on_mindquantum.ipynb @@ -0,0 +1,1069 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 基于MindQuantum的Grover搜索算法\n", + "\n", + "`Linux` `CPU` `全流程` `初级` `中级` `高级`\n", + "\n", + "[![](https://gitee.com/mindspore/docs/raw/master/resource/_static/logo_source.png)](https://gitee.com/mindspore/mindquantum/blob/master/tutorials/8.grover_search_algorithm_based_on_mindquantum.ipynb) [![](https://gitee.com/mindspore/mindquantum/raw/master/tutorials/images/view_mindquantum_api.png)](https://mindspore.cn/mindquantum/api/zh-CN/master/index.html)\n", + "\n", + "## 1. 概述\n", + "\n", + "如果你听过量子计算,那么你一定听说过Grover搜索算法。1996年,Lov Grover \\[1\\] 提出了Grover搜索算法,它是一种利用量子状态的叠加性进行并行计算并实现加速的算法。Grover搜索算法被公认为是继Shor算法后的第二大量子算法,也是第一个被完整的实验实现的量子算法,它解决的是无序数据库搜索问题。1997年,Bennett \\[2\\] 等人证明,对于非结构化的量子搜索问题,至少需要$\\Omega(\\sqrt{N})$​次量子查询,因此Grover搜索算法对于该问题是渐进意义下的最优算法。\n", + "\n", + "无序数据库搜索问题(Unordered Database Search problem)就是从一个海量元素的无序数据库中,找到某些满足要求的元素。由于数据库中元素的数量是巨大的且这些元素是无序排列的,所以,要验证给定的元素是否满足要求很容易,但反过来,要找到这些元素却不是一件容易的事。\n", + "\n", + "求解无序数据库搜索问题(不妨假设只有一个目标搜索数据),经典算法所需的时间复杂度为$\\mathcal{O}(N)$,而Grover搜索算法所需的时间复杂度仅为$\\mathcal{O}(\\sqrt{N})$,相比经典算法具有平方加速,展示了量子计算的强大性能。此外,Grover搜索算法中用到的振幅扩大技巧,对许多启发式的经典搜索算法可以实现加速,因而具有广泛的应用。\n", + "\n", + "本教案将会介绍Grover搜索算法的基本原理,以及通过两个具体的小例子来展示如何利用MindQuantum实现该算法。\n", + "\n", + "## 2. 问题描述\n", + "\n", + "我们需要在一组无序的$N$元素集合(数据库)中进行搜索。将数据库中的元素与索引(从$0$到$N-1$之间的整数)建立一一对应,我们关注于搜索这些元素的索引。考虑将该搜索问题表示为一个关于输入$x$的函数$f(x)$,其中$x$为$0$到$N-1$之间的整数。那么,函数$f$定义为:\n", + "$$\n", + "\\begin{equation}\n", + "f(x)=\\begin{cases}0,x\\neq x_{target}\\\\\\\\\n", + "1,x=x_{target}\n", + "\\end{cases}\n", + "\\end{equation}.\n", + "$$\n", + "不是一般性,假设$N=2^n$​,那么在量子系统中,索引以量子态$|0\\rangle,|1\\rangle,...,|N-1\\rangle$​(或$|00...0\\rangle,|00...1\\rangle,...,|11...1\\rangle$​)表示,也即我们可以使用$n$​个量子比特存储这些索引。\n", + "\n", + "同时假设搜索问题只有一个目标态$|\\omega\\rangle$。Grover搜索算法的目标就是以极大的概率将$|\\omega\\rangle$搜索出来。\n", + "\n", + "## 3. Grover搜索算法的基本原理\n", + "\n", + "Grover搜索算法的基本原理:首先通过`Hadamard`门产生均匀叠加态,然后反复调用Grover迭代(或称为$G$算子),以放大目标项的概率振幅同时抑制非目标项的概率振幅(该方法称之为振幅放大),最后对末态进行测量,那么就能以极大的概率得到目标态$|\\omega\\rangle$​​。\n", + "\n", + "Grover搜索算法主要包括以下步骤:\n", + "\n", + "- Step 1:数据库初始化\n", + "\n", + " 对$|0\\rangle^{\\otimes n}$​​​​执行$H^{\\otimes n}$​​​​​操作,使得数据库被初始为一个均匀叠加态,即\n", + " $$\n", + " |\\psi_0\\rangle=H^{\\otimes n}|0\\rangle^{\\otimes n}=\\frac{1}{\\sqrt{N}}\\sum_{i=0}^{N-1}|i\\rangle.\n", + " $$\n", + "\n", + "- Step 2:Grover迭代\n", + "\n", + " Grover迭代又可以分解为四步:\n", + "\n", + " - Substep 1:执行Oracle算子$U_{\\omega}$​,翻转目标态$|\\omega \\rangle$​​​​​的相位\n", + "\n", + " 为了将需要寻找的数据和其它的数据区别开,最简单的方法就是翻转目标态的相位(增加一个负号),此时我们需要构造一个Oracle算子$U_{\\omega}$,其作用如下:\n", + " $$\n", + " \\begin{equation}\n", + " U_{\\omega}|x\\rangle=\\begin{cases}\n", + " &|x\\rangle,x\\neq \\omega&\\\\\\\\\n", + " -&|x\\rangle,x=\\omega&\n", + " \\end{cases}\n", + " \\end{equation}.\n", + " $$\n", + " 由于当$x=\\omega$​时,$f(\\omega)=1$​,那么$U_{\\omega}$​​的作用还可以表示成:\n", + " $$\n", + " U_{\\omega}|x\\rangle=(-1)^{f(x)}|x\\rangle,\n", + " $$\n", + " 其矩阵表达式为\n", + " $$\n", + " \\begin{equation}\n", + " U_{\\omega}=\n", + " \\left[\n", + " \\begin{array}{ccc}\n", + " (-1)^{f(0)} & 0 & \\dots & 0 \\\\\\\\\n", + " 0 & (-1)^{f(1)} & \\dots & 0 \\\\\\\\\n", + " \\vdots & \\vdots & \\ddots & \\vdots \\\\\\\\\n", + " 0 & 0 & \\dots & (-1)^{f(N-1)}\n", + " \\end{array}\n", + " \\right] \n", + " \\end{equation}.\n", + " $$\n", + "\n", + " - Substep 2:执行$H^{\\otimes n}$操作\n", + "\n", + " 对$n$位量子比特执行$H^{\\otimes n}$操作。\n", + "\n", + " - Substep 3:执行条件相移算子$P$\n", + "\n", + " 条件相移算子$P$能使$|0\\rangle$​态以外的每个态的相位都翻转,其作用如下:\n", + " $$\n", + " \\begin{equation}\n", + " P|x\\rangle=\\begin{cases}&|0\\rangle,x= 0&\\\\\\\\\n", + " -&|x\\rangle,x\\neq0&\n", + " \\end{cases}\n", + " \\end{equation}.\n", + " $$\n", + " 其矩阵表达式为\n", + " $$\n", + " \\begin{equation}\n", + " P = 2(|0\\rangle\\langle0|)^{\\otimes n} - I_n =\n", + " \\left[\n", + " \\begin{array}{ccc}\n", + " 1 & 0 & \\dots & 0 \\\\\\\\\n", + " 0 & -1 & \\dots & 0 \\\\\\\\\n", + " \\vdots & \\vdots & \\ddots & \\vdots \\\\\\\\\n", + " 0 & 0 & \\dots & -1\n", + " \\end{array}\n", + " \\right]\n", + " \\end{equation}.\n", + " $$\n", + "\n", + " - Substep 4:再次执行$H^{\\otimes n}$操作\n", + "\n", + " 至此,完整的$G$算子可以表示为\n", + " $$\n", + " G = H^{\\otimes n} [2(|0\\rangle\\langle0|)^{\\otimes n} - I_n] H^{\\otimes n} U_{\\omega}.\n", + " $$\n", + "\n", + " 注意:$G$算子需要迭代的次数为\n", + " $$\n", + " r = \\left[ \\frac{\\pi}{4} \\sqrt{\\frac{N}{M}} \\right] \\sim O(\\sqrt{N}),\n", + " $$\n", + "\n", + " 其中,M表示目标态的个数。\n", + "\n", + "- Step 3:测量\n", + "\n", + " 对末态进行$\\{|0\\rangle,|1\\rangle\\}$基测量,就能以极大的概率得到目标态$|\\omega \\rangle$。\n", + "\n", + "Grover搜索算法的完整量子线路模型如下所示:\n", + "\n", + "[![](./images/grover_algorithm_circuit.png)](https://gitee.com/mindspore/docs/blob/master/docs/mindquantum/docs/source_zh_cn/grover_algorithm_circuit.ipynb) \n", + "\n", + "## 4. 构造翻转量子比特相位的酉算子\n", + "\n", + "通过上述介绍,我们发现,Grover搜索算法中最关键的部分就是存在可以翻转量子比特相位的酉算子,Oracle算子$U_{\\omega}$可以翻转目标态的相位,条件相移算子$P$可以翻转$|0\\rangle$态以外的每个态的相位。\n", + "\n", + "接下来,我们将构造可以翻转某一位量子比特相位的酉算子,定义如下:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "def bitphaseflip_operator(phase_inversion_qubit, n_qubits): #定义可以翻转某一位量子比特相位的函数\n", + " s = [1 for i in range(1 << n_qubits)]\n", + " for i in phase_inversion_qubit:\n", + " s[i] = -1\n", + " if s[0] == -1:\n", + " for i in range(len(s)):\n", + " s[i] = -1 * s[i]\n", + " circuit = Circuit()\n", + " length = len(s)\n", + " cz = []\n", + " for i in range(length):\n", + " if s[i] == -1:\n", + " cz.append([])\n", + " current = i\n", + " t = 0\n", + " while current != 0:\n", + " if (current & 1) == 1:\n", + " cz[-1].append(t)\n", + " t += 1\n", + " current = current >> 1\n", + " for j in range(i + 1, length):\n", + " if i & j == i:\n", + " s[j] = -1 * s[j]\n", + " for i in cz:\n", + " if i:\n", + " if len(i) > 1:\n", + " circuit += Z.on(i[-1], i[:-1])\n", + " else:\n", + " circuit += Z.on(i[0])\n", + "\n", + " return circuit" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "现在,`bitphaseflip_operator()`函数就可以实现翻转某一位量子比特的相位,只需要输入需要翻转相位的目标量子态和量子比特总数即可。\n", + "\n", + "举个例子,我们现在生成3​​量子比特的均匀叠加态,运行如下代码:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
q0: ──H──\n",
+       "         \n",
+       "q1: ──H──\n",
+       "         \n",
+       "q2: ──H──\n",
+       "
\n" + ], + "text/plain": [ + "q0: ──H──\n", + " \n", + "q1: ──H──\n", + " \n", + "q2: ──H──" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import mindquantum as mq\n", + "from mindquantum import Circuit, UN, H, Z\n", + "from mindquantum.simulator import Simulator\n", + "\n", + "n_qubits = 3 #设定量子比特数为3\n", + "sim = Simulator('projectq', n_qubits) #使用projectq模拟器,命名为sim\n", + "\n", + "circuit = Circuit() #初始化量子线路,命名为circuit\n", + "circuit += UN(H, n_qubits) #每位量子比特上执行H门操作\n", + "\n", + "sim.apply_circuit(circuit) #通过模拟器sim运行搭建好的量子线路circuit\n", + "\n", + "circuit #打印此时的量子线路circuit" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "√2/4¦000⟩\n", + "√2/4¦001⟩\n", + "√2/4¦010⟩\n", + "√2/4¦011⟩\n", + "√2/4¦100⟩\n", + "√2/4¦101⟩\n", + "√2/4¦110⟩\n", + "√2/4¦111⟩\n" + ] + } + ], + "source": [ + "print(sim.get_qs(True)) #打印模拟器sim中运行量子线路circuit后的末态" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "从运行的结果看到此时的量子线路,以及我们成功生成了3量子比特的均匀叠加态。\n", + "\n", + "假设我们需要翻转$|4\\rangle$态的相位,只需调用我们定义好的`bitphaseflip_operator()`函数即可,运行如下代码:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
q0: ──H─────────●─────────●──\n",
+       "                │         │  \n",
+       "q1: ──H─────────┼────●────●──\n",
+       "                │    │    │  \n",
+       "q2: ──H────Z────Z────Z────Z──\n",
+       "
\n" + ], + "text/plain": [ + "q0: ──H─────────●─────────●──\n", + " │ │ \n", + "q1: ──H─────────┼────●────●──\n", + " │ │ │ \n", + "q2: ──H────Z────Z────Z────Z──" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sim.reset() #重置模拟器sim维护好的量子态,使得初始化的量子态为|000>\n", + "\n", + "phase_inversion_qubit = [4] #翻转|4>态的相位\n", + "operator = bitphaseflip_operator(phase_inversion_qubit, n_qubits)#调用我们定义好的bitphaseflip_operator()函数\n", + "\n", + "circuit += operator #在量子线路circuit中添加翻转|4>态的相位所需的量子门\n", + "\n", + "sim.apply_circuit(circuit) #通过模拟器sim再次运行搭建好的量子线路circuit\n", + "\n", + "circuit #打印此时的量子线路circuit" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "√2/4¦000⟩\n", + "√2/4¦001⟩\n", + "√2/4¦010⟩\n", + "√2/4¦011⟩\n", + "-√2/4¦100⟩\n", + "√2/4¦101⟩\n", + "√2/4¦110⟩\n", + "√2/4¦111⟩\n" + ] + } + ], + "source": [ + "print(sim.get_qs(True)) #打印模拟器sim中运行量子线路circuit后的末态" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "从运行的结果看到此时的量子线路,以及$|100\\rangle$​​的相位翻转为-1,运行如下代码:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "4\n" + ] + } + ], + "source": [ + "print(int('100', 2))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "从运行的结果看到,发生相位翻转的$|100\\rangle$态即为我们希望相位翻转的$|4\\rangle$态。\n", + "\n", + "假设我们需要翻转除$|0\\rangle$态以外的每个态的相位,运行如下代码:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
q0: ──H────Z────●────●─────────●──\n",
+       "                │    │         │  \n",
+       "q1: ──H────Z────Z────┼────●────●──\n",
+       "                     │    │    │  \n",
+       "q2: ──H────Z─────────Z────Z────Z──\n",
+       "
\n" + ], + "text/plain": [ + "q0: ──H────Z────●────●─────────●──\n", + " │ │ │ \n", + "q1: ──H────Z────Z────┼────●────●──\n", + " │ │ │ \n", + "q2: ──H────Z─────────Z────Z────Z──" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "n_qubits = 3 #设定量子比特数为3\n", + "sim1 = Simulator('projectq', n_qubits) #使用projectq模拟器,命名为sim1\n", + "\n", + "operator1 = bitphaseflip_operator([i for i in range(1, pow(2, n_qubits))], n_qubits) #调用我们定义好的bitphaseflip_operator()函数,翻转除|0>态以外的每个态的相位,命名为operator1\n", + " \n", + "circuit1 = Circuit() #初始化量子线路,命名为circuit1\n", + "circuit1 += UN(H, n_qubits) #每位量子比特上执行H门操作\n", + "circuit1 += operator1 #在量子线路circuit1中添加翻转除|0>态以外的每个态的相位所需的量子门\n", + "\n", + "sim1.apply_circuit(circuit1) #通过模拟器sim1运行搭建好的量子线路circuit1\n", + "\n", + "circuit1 #打印此时的量子线路circuit1" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "√2/4¦000⟩\n", + "-√2/4¦001⟩\n", + "-√2/4¦010⟩\n", + "-√2/4¦011⟩\n", + "-√2/4¦100⟩\n", + "-√2/4¦101⟩\n", + "-√2/4¦110⟩\n", + "-√2/4¦111⟩\n" + ] + } + ], + "source": [ + "print(sim1.get_qs(True)) #打印模拟器sim1中运行量子线路circuit1后的末态" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "从运行的结果看到此时的量子线路,以及我们成功翻转除$|0\\rangle$态以外的每个态的相位。\n", + "\n", + "也就是说,我们定义的函数`bitphaseflip_operator()`可以实现Grover搜素算法中的Oracle算子$U_{\\omega}$和条件相移算子$P$。\n", + "\n", + "## 5. 利用MindQuantum实现Grover搜素算法实例\n", + "\n", + "### 5.1 实例1:$n=3$​,$|\\omega\\rangle=|2\\rangle$(单目标)​\n", + "\n", + "首先,我们需要定义$G$算子,运行如下代码:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "def G(phase_inversion_qubit, n_qubits): #定义Grover搜索算法中的G算子 \n", + " operator = bitphaseflip_operator(phase_inversion_qubit, n_qubits) \n", + " operator += UN(H, n_qubits) \n", + " operator += bitphaseflip_operator([i for i in range(1, pow(2, n_qubits))], n_qubits) \n", + " operator += UN(H, n_qubits) \n", + " return operator" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "然后,我们根据Grover搜索算法的量子线路模型在MindQuantum中搭建对应的量子线路:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
q0: ──H─────────●─────────●────H────Z────●────●─────────●────H─────────●─────────●────H────Z────●────●─────────●────H──\n",
+       "                │         │              │    │         │              │         │              │    │         │       \n",
+       "q1: ──H────Z────Z────●────●────H────Z────Z────┼────●────●────H────Z────Z────●────●────H────Z────Z────┼────●────●────H──\n",
+       "                     │    │                   │    │    │                   │    │                   │    │    │       \n",
+       "q2: ──H──────────────Z────Z────H────Z─────────Z────Z────Z────H──────────────Z────Z────H────Z─────────Z────Z────Z────H──\n",
+       "
\n" + ], + "text/plain": [ + "q0: ──H─────────●─────────●────H────Z────●────●─────────●────H─────────●─────────●────H────Z────●────●─────────●────H──\n", + " │ │ │ │ │ │ │ │ │ │ \n", + "q1: ──H────Z────Z────●────●────H────Z────Z────┼────●────●────H────Z────Z────●────●────H────Z────Z────┼────●────●────H──\n", + " │ │ │ │ │ │ │ │ │ │ \n", + "q2: ──H──────────────Z────Z────H────Z─────────Z────Z────Z────H──────────────Z────Z────H────Z─────────Z────Z────Z────H──" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import numpy as np\n", + "from numpy import pi, sqrt\n", + "\n", + "n_qubits = 3 #设定量子比特数为3\n", + "phase_inversion_qubit = [2] #设定需要翻转相位的目标态,在这里翻转|2>态的相位\n", + "\n", + "N = 2 ** (n_qubits) #计算出数据库中元素的总个数\n", + "M = len(phase_inversion_qubit) #计算出目标态的总个数\n", + "\n", + "r = int(pi / 4 * sqrt(N / M)) #设定G算子迭代次数为r\n", + "\n", + "sim2 = Simulator('projectq', n_qubits) #使用projectq模拟器,命名为sim2\n", + "\n", + "circuit2 = Circuit() #初始化量子线路,命名为circuit2\n", + "circuit2 += UN(H, n_qubits) #每位量子比特上执行H门操作\n", + "\n", + "for i in range(r): #循环执行G算子r次\n", + " circuit2 += G(phase_inversion_qubit, n_qubits)\n", + "\n", + "sim2.apply_circuit(circuit2) #通过模拟器sim2运行搭建好的量子线路circuit2\n", + "\n", + "circuit2 #打印此时的量子线路circuit2" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "-√2/16¦000⟩\n", + "-√2/16¦001⟩\n", + "0.9722718241315036¦010⟩\n", + "-√2/16¦011⟩\n", + "-√2/16¦100⟩\n", + "-√2/16¦101⟩\n", + "-√2/16¦110⟩\n", + "-√2/16¦111⟩\n" + ] + } + ], + "source": [ + "print(sim2.get_qs(True)) #打印模拟器sim2中运行量子线路circuit2后的末态" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "从运行的结果看到,$|010\\rangle$态的振幅为0.9722718241315036,相比于其它的量子态,这是极大的振幅,也就是说,若我们测量此时的状态,将会以极大的概率得到目标态$|010\\rangle$​,运行如下代码进行测量:" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
shots: 1000\n",
+       "Keys: q2 q1 q0│0.00     0.2         0.4         0.6         0.8         1.0\n",
+       "──────────────┼───────────┴───────────┴───────────┴───────────┴───────────┴\n",
+       "           000│▒\n",
+       "\n",
+       "           001│▒\n",
+       "\n",
+       "           010│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓\n",
+       "\n",
+       "           011│▒\n",
+       "\n",
+       "           100│▒\n",
+       "\n",
+       "           101│▒\n",
+       "\n",
+       "           110│▒\n",
+       "\n",
+       "           111│▒\n",
+       "\n",
+       "{'000': 9, '001': 10, '010': 947, '011': 4, '100': 8, '101': 8, '110': 8, '111': 6}\n",
+       "
\n" + ], + "text/plain": [ + "shots: 1000\n", + "Keys: q2 q1 q0│0.00 0.2 0.4 0.6 0.8 1.0\n", + "──────────────┼───────────┴───────────┴───────────┴───────────┴───────────┴\n", + " 000│▒\n", + " │ \n", + " 001│▒\n", + " │ \n", + " 010│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓\n", + " │ \n", + " 011│▒\n", + " │ \n", + " 100│▒\n", + " │ \n", + " 101│▒\n", + " │ \n", + " 110│▒\n", + " │ \n", + " 111│▒\n", + " │ \n", + "{'000': 9, '001': 10, '010': 947, '011': 4, '100': 8, '101': 8, '110': 8, '111': 6}" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from mindquantum import Measure\n", + "\n", + "sim2.reset() #重置模拟器sim2维护好的量子态,使得初始化的量子态为|000>\n", + "\n", + "circuit2 += UN(Measure(), circuit2.n_qubits) #对量子线路circuit2中的每一位量子比特添加测量门\n", + "\n", + "result = sim2.sampling(circuit2, shots=1000) #通过模拟器sim2对量子线路circuit2进行1000次的采样\n", + "result #打印采样结果" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "从运行的结果看到,1000次采样中有947次的采样结果为`010`,将其转化为10进制数,运行如下代码:" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2\n" + ] + } + ], + "source": [ + "print(int('010', 2))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "从运行的结果看到,我们成功地搜索出$|2\\rangle$态。\n", + "\n", + "### 5.2 实例2:$n=5$,$|\\omega\\rangle=|5\\rangle$和$|11\\rangle$(多目标)\n", + "\n", + "实例1中实现的是单目标搜索,现在我们尝试实现多目标搜索。首先,$G$算子已经定义好了,我们只需设定量子比特数和需要翻转相位的目标态,然后搭建对应的量子线路即可,运行如下代码:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
q0: ──H────●────●────●────●────●────●────●────●────H────Z────●────●─────────●────●─────────●─────────●─────────●────●─────────●─────────●─────────●─────────●─────────●─────────●─────────●────H────●────●────●────●────●────●────●────●────H────Z────●────●─────────●────●─────────●─────────●─────────●────●─────────●─────────●─────────●─────────●─────────●─────────●─────────●────H────●────●────●────●────●────●────●────●────H────Z────●────●─────────●────●─────────●─────────●─────────●────●─────────●─────────●─────────●─────────●─────────●─────────●─────────●────H──\n",
+       "           │    │    │    │    │    │    │    │              │    │         │    │         │         │         │    │         │         │         │         │         │         │         │         │    │    │    │    │    │    │    │              │    │         │    │         │         │         │    │         │         │         │         │         │         │         │         │    │    │    │    │    │    │    │              │    │         │    │         │         │         │    │         │         │         │         │         │         │         │       \n",
+       "q1: ──H────┼────●────●────┼────┼────●────●────┼────H────Z────Z────┼────●────●────┼────●────●─────────┼────●────●────┼────●────●─────────┼────●────●─────────┼────●────●─────────┼────●────●────H────┼────●────●────┼────┼────●────●────┼────H────Z────Z────┼────●────●────┼────●────●─────────┼────●────●────┼────●────●─────────┼────●────●─────────┼────●────●─────────┼────●────●────H────┼────●────●────┼────┼────●────●────┼────H────Z────Z────┼────●────●────┼────●────●─────────┼────●────●────┼────●────●─────────┼────●────●─────────┼────●────●─────────┼────●────●────H──\n",
+       "           │    │    │    │    │    │    │    │                   │    │    │    │    │    │         │    │    │    │    │    │         │    │    │         │    │    │         │    │    │         │    │    │    │    │    │    │    │                   │    │    │    │    │    │         │    │    │    │    │    │         │    │    │         │    │    │         │    │    │         │    │    │    │    │    │    │    │                   │    │    │    │    │    │         │    │    │    │    │    │         │    │    │         │    │    │         │    │    │       \n",
+       "q2: ──H────Z────Z────┼────●────●────●────┼────●────H────Z─────────Z────Z────Z────┼────┼────┼────●────●────●────●────┼────┼────┼────●────●────●────●─────────┼────┼────┼────●────●────●────●────H────Z────Z────┼────●────●────●────┼────●────H────Z─────────Z────Z────Z────┼────┼────┼────●────●────●────●────┼────┼────┼────●────●────●────●─────────┼────┼────┼────●────●────●────●────H────Z────Z────┼────●────●────●────┼────●────H────Z─────────Z────Z────Z────┼────┼────┼────●────●────●────●────┼────┼────┼────●────●────●────●─────────┼────┼────┼────●────●────●────●────H──\n",
+       "                     │    │    │    │    │    │                                  │    │    │    │    │    │    │    │    │    │    │    │    │    │         │    │    │    │    │    │    │                   │    │    │    │    │    │                                  │    │    │    │    │    │    │    │    │    │    │    │    │    │         │    │    │    │    │    │    │                   │    │    │    │    │    │                                  │    │    │    │    │    │    │    │    │    │    │    │    │    │         │    │    │    │    │    │    │       \n",
+       "q3: ──H──────────────Z────Z────┼────┼────●────●────H────Z────────────────────────Z────Z────Z────Z────Z────Z────Z────┼────┼────┼────┼────┼────┼────┼────●────●────●────●────●────●────●────●────H──────────────Z────Z────┼────┼────●────●────H────Z────────────────────────Z────Z────Z────Z────Z────Z────Z────┼────┼────┼────┼────┼────┼────┼────●────●────●────●────●────●────●────●────H──────────────Z────Z────┼────┼────●────●────H────Z────────────────────────Z────Z────Z────Z────Z────Z────Z────┼────┼────┼────┼────┼────┼────┼────●────●────●────●────●────●────●────●────H──\n",
+       "                               │    │    │    │                                                                     │    │    │    │    │    │    │    │    │    │    │    │    │    │    │                             │    │    │    │                                                                     │    │    │    │    │    │    │    │    │    │    │    │    │    │    │                             │    │    │    │                                                                     │    │    │    │    │    │    │    │    │    │    │    │    │    │    │       \n",
+       "q4: ──H────────────────────────Z────Z────Z────Z────H────Z───────────────────────────────────────────────────────────Z────Z────Z────Z────Z────Z────Z────Z────Z────Z────Z────Z────Z────Z────Z────H────────────────────────Z────Z────Z────Z────H────Z───────────────────────────────────────────────────────────Z────Z────Z────Z────Z────Z────Z────Z────Z────Z────Z────Z────Z────Z────Z────H────────────────────────Z────Z────Z────Z────H────Z───────────────────────────────────────────────────────────Z────Z────Z────Z────Z────Z────Z────Z────Z────Z────Z────Z────Z────Z────Z────H──\n",
+       "
\n" + ], + "text/plain": [ + "q0: ──H────●────●────●────●────●────●────●────●────H────Z────●────●─────────●────●─────────●─────────●─────────●────●─────────●─────────●─────────●─────────●─────────●─────────●─────────●────H────●────●────●────●────●────●────●────●────H────Z────●────●─────────●────●─────────●─────────●─────────●────●─────────●─────────●─────────●─────────●─────────●─────────●─────────●────H────●────●────●────●────●────●────●────●────H────Z────●────●─────────●────●─────────●─────────●─────────●────●─────────●─────────●─────────●─────────●─────────●─────────●─────────●────H──\n", + " │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ \n", + "q1: ──H────┼────●────●────┼────┼────●────●────┼────H────Z────Z────┼────●────●────┼────●────●─────────┼────●────●────┼────●────●─────────┼────●────●─────────┼────●────●─────────┼────●────●────H────┼────●────●────┼────┼────●────●────┼────H────Z────Z────┼────●────●────┼────●────●─────────┼────●────●────┼────●────●─────────┼────●────●─────────┼────●────●─────────┼────●────●────H────┼────●────●────┼────┼────●────●────┼────H────Z────Z────┼────●────●────┼────●────●─────────┼────●────●────┼────●────●─────────┼────●────●─────────┼────●────●─────────┼────●────●────H──\n", + " │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ \n", + "q2: ──H────Z────Z────┼────●────●────●────┼────●────H────Z─────────Z────Z────Z────┼────┼────┼────●────●────●────●────┼────┼────┼────●────●────●────●─────────┼────┼────┼────●────●────●────●────H────Z────Z────┼────●────●────●────┼────●────H────Z─────────Z────Z────Z────┼────┼────┼────●────●────●────●────┼────┼────┼────●────●────●────●─────────┼────┼────┼────●────●────●────●────H────Z────Z────┼────●────●────●────┼────●────H────Z─────────Z────Z────Z────┼────┼────┼────●────●────●────●────┼────┼────┼────●────●────●────●─────────┼────┼────┼────●────●────●────●────H──\n", + " │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ \n", + "q3: ──H──────────────Z────Z────┼────┼────●────●────H────Z────────────────────────Z────Z────Z────Z────Z────Z────Z────┼────┼────┼────┼────┼────┼────┼────●────●────●────●────●────●────●────●────H──────────────Z────Z────┼────┼────●────●────H────Z────────────────────────Z────Z────Z────Z────Z────Z────Z────┼────┼────┼────┼────┼────┼────┼────●────●────●────●────●────●────●────●────H──────────────Z────Z────┼────┼────●────●────H────Z────────────────────────Z────Z────Z────Z────Z────Z────Z────┼────┼────┼────┼────┼────┼────┼────●────●────●────●────●────●────●────●────H──\n", + " │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ \n", + "q4: ──H────────────────────────Z────Z────Z────Z────H────Z───────────────────────────────────────────────────────────Z────Z────Z────Z────Z────Z────Z────Z────Z────Z────Z────Z────Z────Z────Z────H────────────────────────Z────Z────Z────Z────H────Z───────────────────────────────────────────────────────────Z────Z────Z────Z────Z────Z────Z────Z────Z────Z────Z────Z────Z────Z────Z────H────────────────────────Z────Z────Z────Z────H────Z───────────────────────────────────────────────────────────Z────Z────Z────Z────Z────Z────Z────Z────Z────Z────Z────Z────Z────Z────Z────H──" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "n_qubits = 5 #设定量子比特数为5\n", + "phase_inversion_qubit = [5, 11] #设定需要翻转相位的目标态,在这里翻转|5>态和|11>态的相位\n", + "\n", + "N = 2 ** (n_qubits) #计算出数据库中元素的总个数\n", + "M = len(phase_inversion_qubit) #计算出目标态的总个数\n", + "\n", + "r = int(pi / 4 * sqrt(N / M)) #设定G算子迭代次数为r\n", + "\n", + "sim3 = Simulator('projectq', n_qubits) #使用projectq模拟器,命名为sim3\n", + "\n", + "circuit3 = Circuit() #初始化量子线路,命名为circuit3\n", + "circuit3 += UN(H, n_qubits) #每位量子比特上执行H门操作\n", + "\n", + "for i in range(r): #循环执行G算子r次\n", + " circuit3 += G(phase_inversion_qubit, n_qubits)\n", + "\n", + "sim3.apply_circuit(circuit3) #通过模拟器sim3运行搭建好的量子线路circuit3\n", + "\n", + "circuit3 #打印此时的量子线路circuit3" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "-0.03590776623212942¦00000⟩\n", + "-0.03590776623212939¦00001⟩\n", + "-0.03590776623212932¦00010⟩\n", + "-0.03590776623212946¦00011⟩\n", + "-0.03590776623212935¦00100⟩\n", + "0.6932961018664991¦00101⟩\n", + "-0.035907766232129434¦00110⟩\n", + "-0.03590776623212945¦00111⟩\n", + "-0.035907766232129434¦01000⟩\n", + "-0.03590776623212945¦01001⟩\n", + "-0.03590776623212935¦01010⟩\n", + "0.6932961018664991¦01011⟩\n", + "-0.03590776623212932¦01100⟩\n", + "-0.03590776623212946¦01101⟩\n", + "-0.03590776623212939¦01110⟩\n", + "-0.03590776623212939¦01111⟩\n", + "-0.0359077662321294¦10000⟩\n", + "-0.03590776623212941¦10001⟩\n", + "-0.035907766232129414¦10010⟩\n", + "-0.035907766232129434¦10011⟩\n", + "-0.03590776623212944¦10100⟩\n", + "-0.035907766232129434¦10101⟩\n", + "-0.03590776623212944¦10110⟩\n", + "-0.035907766232129434¦10111⟩\n", + "-0.03590776623212943¦11000⟩\n", + "-0.035907766232129434¦11001⟩\n", + "-0.03590776623212943¦11010⟩\n", + "-0.035907766232129434¦11011⟩\n", + "-0.0359077662321294¦11100⟩\n", + "-0.035907766232129434¦11101⟩\n", + "-0.035907766232129414¦11110⟩\n", + "-0.03590776623212941¦11111⟩\n" + ] + } + ], + "source": [ + "print(sim3.get_qs(True)) #打印模拟器sim3中运行量子线路circuit3后的末态" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "从运行的结果看到,$|00101\\rangle$​​和$|01011\\rangle$​​态的振幅均为0.6932961018664989,相比于其它的量子态,这是极大的振幅,也就是说,若我们测量此时的状态,将会以极大的概率得到目标态$|00101\\rangle$​​和$|01011\\rangle$​​态,运行如下代码进行测量:" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n"
+      ],
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/html": [
+       "
shots: 1000\n",
+       "Keys: q4 q3 q2 q1 q0│0.00   0.126       0.251       0.377       0.503       0.629\n",
+       "────────────────────┼───────────┴───────────┴───────────┴───────────┴───────────┴\n",
+       "               00000│▒\n",
+       "\n",
+       "               00001│▒\n",
+       "\n",
+       "               00100│▒\n",
+       "\n",
+       "               00101│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒\n",
+       "\n",
+       "               00110│▒\n",
+       "\n",
+       "               00111│▒\n",
+       "\n",
+       "               01000│▒\n",
+       "\n",
+       "               01001│▒\n",
+       "\n",
+       "               01010│▒\n",
+       "\n",
+       "               01011│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓\n",
+       "\n",
+       "               01100│▒\n",
+       "\n",
+       "               01101│▒\n",
+       "\n",
+       "               01111│▒\n",
+       "\n",
+       "               10000│▒\n",
+       "\n",
+       "               10001│▒\n",
+       "\n",
+       "               10011│▒\n",
+       "\n",
+       "               10100│▒\n",
+       "\n",
+       "               10101│▒\n",
+       "\n",
+       "               10111│▒\n",
+       "\n",
+       "               11000│▒\n",
+       "\n",
+       "               11011│▒\n",
+       "\n",
+       "               11100│▒\n",
+       "\n",
+       "               11101│▒\n",
+       "\n",
+       "               11111│▒\n",
+       "\n",
+       "{'00000': 2, '00001': 2, '00100': 1, '00101': 463, '00110': 2, '00111': 3, '01000': 1, \n",
+       "'01001': 1, '01010': 1, '01011': 503, '01100': 1, '01101': 1, '01111': 1, '10000': 1, \n",
+       "'10001': 1, '10011': 2, '10100': 2, '10101': 2, '10111': 1, '11000': 3, '11011': 1, '11100': \n",
+       "2, '11101': 2, '11111': 1}\n",
+       "
\n" + ], + "text/plain": [ + "shots: 1000\n", + "Keys: q4 q3 q2 q1 q0│0.00 0.126 0.251 0.377 0.503 0.629\n", + "────────────────────┼───────────┴───────────┴───────────┴───────────┴───────────┴\n", + " 00000│▒\n", + " │ \n", + " 00001│▒\n", + " │ \n", + " 00100│▒\n", + " │ \n", + " 00101│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒\n", + " │ \n", + " 00110│▒\n", + " │ \n", + " 00111│▒\n", + " │ \n", + " 01000│▒\n", + " │ \n", + " 01001│▒\n", + " │ \n", + " 01010│▒\n", + " │ \n", + " 01011│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓\n", + " │ \n", + " 01100│▒\n", + " │ \n", + " 01101│▒\n", + " │ \n", + " 01111│▒\n", + " │ \n", + " 10000│▒\n", + " │ \n", + " 10001│▒\n", + " │ \n", + " 10011│▒\n", + " │ \n", + " 10100│▒\n", + " │ \n", + " 10101│▒\n", + " │ \n", + " 10111│▒\n", + " │ \n", + " 11000│▒\n", + " │ \n", + " 11011│▒\n", + " │ \n", + " 11100│▒\n", + " │ \n", + " 11101│▒\n", + " │ \n", + " 11111│▒\n", + " │ \n", + "{'00000': 2, '00001': 2, '00100': 1, '00101': 463, '00110': 2, '00111': 3, '01000': 1, '01001': 1, '01010': 1, '01011': 503, '01100': 1, '01101': 1, '01111': 1, '10000': 1, '10001': 1, '10011': 2, '10100': 2, '10101': 2, '10111': 1, '11000': 3, '11011': 1, '11100': 2, '11101': 2, '11111': 1}" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sim3.reset() #重置模拟器sim3维护好的量子态,使得初始化的量子态为|00000>\n", + "\n", + "circuit3 += UN(Measure(), circuit3.n_qubits) #对量子线路circuit3中的每一位量子比特添加测量门\n", + "\n", + "result1 = sim3.sampling(circuit3, shots=1000) #通过模拟器sim3对量子线路circuit3进行1000次的采样\n", + "result1 #打印采样结果" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "从运行的结果看到,1000次采样中有463次的采样结果为`00101`和503次的采样结果为`01011`,将其转化为10进制数,运行如下代码:" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "5\n", + "11\n" + ] + } + ], + "source": [ + "print(int('00101', 2))\n", + "print(int('01011', 2))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "从运行的结果看到,我们成功地搜索出$|5\\rangle$​​和$|11\\rangle$​​​​​态。\n", + "\n", + "至此,我们介绍了Grover搜索算法的基本原理,以及通过两个具体的小例子来展示如何利用MindQuantum实现该算法!赶紧动手体验一下量子编程的乐趣吧!\n", + "\n", + "若想查询更多关于MindQuantum的API,请点击:https://mindspore.cn/mindquantum/。\n", + "\n", + "\n", + "### **参考文献:**\n", + "\n", + "\\[1\\] L. K. Grover, A fast quantum mechanical algorithm for database search\\[C\\]// Proceedings of the twenty-eighth annual ACM symposium on Theory of computing. ACM, 1996: 212-219.\n", + "\n", + "\\[2\\] G. Brassard, P. Hoyer, M. Mosca, et al. Quantum amplitude amplification and estimation\\[J\\]. Contemporary Mathematics, 2002, 305: 53-74." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tutorials/9.shor's_algorithm.ipynb b/tutorials/9.shor's_algorithm.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..fd2acd4972809554ddcf9cc3968d8d5da65685b5 --- /dev/null +++ b/tutorials/9.shor's_algorithm.ipynb @@ -0,0 +1,523 @@ +{ + "metadata": { + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.5-final" + }, + "orig_nbformat": 2, + "kernelspec": { + "name": "python37564bitf15e711fd06842a7ae337b5cf8b0b378", + "display_name": "Python 3.7.5 64-bit" + } + }, + "nbformat": 4, + "nbformat_minor": 2, + "cells": [ + { + "source": [ + "# 基于MindQuantum的Shor算法\n", + "\n", + "`Linux` `CPU` `全流程` `初级` `中级` `高级`\n", + "\n", + "[![](https://gitee.com/mindspore/docs/raw/master/resource/_static/logo_source.png)](https://gitee.com/mindspore/docs/blob/master/docs/mindquantum/docs/source_zh_cn/9.shor's_algorithm.ipynb) [![](https://gitee.com/mindspore/mindquantum/raw/master/tutorials/images/view_mindquantum_api.png)](https://mindspore.cn/mindquantum/api/zh-CN/master/index.html)\n", + "\n", + "## 1. Shor算法简介\n", + "\n", + "Shor算法在量子计算机上分解整数N的时间复杂度为$logN$,几乎是对已知最有效的经典因数分解算法的$e$指数级加速,这种加速有可能在量子计算机上中断如RSA的现代加密机制。\n", + "\n", + "## 2. Shor算法基本思路\n", + "\n", + "Shor算法要解决的主要问题是:给定一个整数$N$,找出它的质因数。即对一个给定的较大数$N$在多项式时间内确定两个素因子 $p1$和$p2$满足$p1\\cdot p2=N$。 在介绍Shor算法步骤之前,先介绍一些数论知识。\n", + "\n", + "因子分解涉及到数论里的一些知识,可以将因子分解问题归结为函数\n", + "$$\n", + "f(x)=a^x\\ mod\\ N\n", + "$$\n", + "对于$a$的周期查找($a$和$N$互质,否则通过调用$gcd(a,N)$就可以马上得到一个因子)。 由于函数$f(x)$存在周期$r$满足$f(x)=f(x+r)$。在这种情形下,就可得\n", + "$$\n", + "a^x=a^{x+r}\\ mod\\ N\\ \\ \\forall x\n", + "$$\n", + "令$x=0$,得到$a^r=1+qN$,其中$q$为某一整数,即\n", + "$$\n", + "a^r-1=(a^{r/2}-1)(a^{r/2}+1)=qN\n", + "$$\n", + "这也表明对于$N$使用$gcd$就可以找到其因子。\n", + "\n", + "因此,Shor算法的核心在于,将大数分解的问题转化为找周期的问题。由于量子计算可以利用叠加态进行并行计算,因此通过量子算法我们可以很快地找到函数$f(x)$的周期$r$(具体的原理和步骤请参考本教程中的[3.2 周期查找算法](shor's_algorithm.ipynb###3.2))。总的来说,我们需要在量子线路中实现$f(|x\\rangle)=a^{|x\\rangle}\\ mod\\ N$的函数运算,可以构造一个酉矩阵$U_{a,N}$使得$U_{a,N}|x\\rangle |y\\rangle \\rightarrow |x\\rangle |y \\oplus f(x) \\rangle$,然后利用量子傅立叶变换我们就可以找到周期$r$满足$a^r\\equiv 1(\\ mod\\ N)$。\n", + "\n", + "下面以 $N=15$为例,介绍shor算法在因子分解的步骤:\n", + "\n", + "1. 选择一个任意的数字,比如$a=2(<15)$\n", + "\n", + "2. $gcd(a,N)=gcd(2,15)=1$\n", + "\n", + "3. 找函数$f(x)=a^x\\ mod\\ N$的周期,使得$f(x+r)=f(x)$\n", + "\n", + "4. 通过量子电路图运算得到$r=4$\n", + "\n", + "5. $\\gcd(a^{r/2}+1,N)=\\gcd(5,15)=5$\n", + "\n", + "6. $\\gcd(a^{r/2}-1,N)=\\gcd(3,15)=3$\n", + "\n", + "7. $N=15$分解得到的质数结果为3和5,分解完成。\n", + "\n", + "Shor算法的量子电路如下图所示:\n", + "\n", + "![](./images/shor's_algorithm_circuit.png) \n", + "\n", + "\n", + "## 3. 通过MindQuantum实现Shor算法\n", + "\n", + "首先,导入需要用到的模块。" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from fractions import Fraction\n", + "import mindquantum as mq\n", + "from mindquantum.core.gates import X, Z, H, UnivMathGate, Measure\n", + "from mindquantum.core.circuit import Circuit, controlled, UN\n", + "from mindquantum.algorithm.library import qft\n", + "from mindquantum.simulator import Simulator" + ] + }, + { + "source": [ + "从shor算法的基本思路我们可以看出,shor算法最核心的部分就在于由量子计算机处理的周期查找算法,而周期查找算法中最困难的地方就是将态$|x\\rangle |y\\rangle$变为$|x\\rangle |y \\oplus f(x) \\rangle$的算子$U$,这个算子的量子线路构造较为复杂,因此以下我们先通过经典计算机算出算子$U$并当作一个Oracle,以便本教程可以整体而直观地演示出Shor算法。\n", + "\n", + "### 3.1 构造Oracle\n", + "该Oracle的构造方法原本十分简单,只需3步:\n", + "\n", + "1. 将变换前所有可能的$x$进行穷举(从$0$到$N-1$共有$N$个数),并一一算出对应的$f(x)=a^x\\ mod\\ N$。\n", + "\n", + "2. 对每一个$x$,我们都可以写出变换前的态$|x\\rangle |0\\rangle$和变换后的态$|x\\rangle |f(x)\\rangle$的矩阵表示,将它们进行外乘即可得到每一个$x$对应的变换矩阵,然后将所有矩阵求和即得到算子$U$的矩阵表示,即\n", + "$$\n", + "U=\\sum_{x=0}^{N-1} |x\\rangle |0\\rangle \\langle x|\\langle f(x)|\n", + "$$\n", + "\n", + "3. 用矩阵$U$生成自定义门。\n", + "\n", + "举例:$N=15,a=2$的情况,我们可以得到$x$与$f(x)$:" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": "x: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]\nf(x): [1, 2, 4, 8, 1, 2, 4, 8, 1, 2, 4, 8, 1, 2, 4, 8]\n" + } + ], + "source": [ + "q = 4 # 比特数\n", + "N = 15\n", + "a = 2\n", + "x = []\n", + "f = []\n", + "for i in range(2**q):\n", + " x.append(i)\n", + " f.append(a**i % N) \n", + "print('x: ', x)\n", + "print('f(x): ', f)" + ] + }, + { + "source": [ + "然后计算$|0\\rangle |0\\rangle \\langle 0| \\langle 1|+|1\\rangle |0\\rangle \\langle 1| \\langle 2|+|2\\rangle |0\\rangle \\langle 2|\\langle 4|+...$即得到变换$U$的矩阵表示。其中$|0\\rangle |0\\rangle$、$|0\\rangle |1\\rangle$、$|0\\rangle |2\\rangle$...可以表示为相互正交的、含有256个元素的列向量,其中只有一个元素是1,其余为0。例如$|0\\rangle |0\\rangle$是第一个元素是1,$|0\\rangle |1\\rangle$是第二个元素是1,$|1\\rangle |0\\rangle$是第17个元素为1,以此类推。" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "source": [ + "但是,由于MindQuantum当前版本的Simulator对自定义门的比特数做出了限制(不能大于5比特),而即使分解最小的非偶质因数整数15=3*5也要至少8个比特,因此构造此Oracle使用了妥协而复杂得多的办法,即寄存器1(4个比特)作为控制比特,在寄存器2(4个比特)作用每一个$x$对应的变换$T_x$:\n", + "$$\n", + "T_x|x\\rangle \\rightarrow |a^x\\ mod\\ N\\rangle\n", + "$$\n", + "每一个$T_x$都受寄存器1控制,只当寄存器1中存储的数为$x$时才作用,最后一共有$N$个门,包括这$N$个门的整个线路对应于算子$U$。需要注意的是,$T_x$自身不是一个门,因为它不是一个可逆操作。尽管变换$T_x$是非酉的,但当其连接上控制比特时,控制门作为一个整体则重新变得可逆,即整个控制门对应的矩阵是酉矩阵。\n", + "\n", + "下面是妥协后的Oracle构造方法:" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "def U_operator(N, a, register1, register2):\n", + " Q = 2**len(register1)\n", + " x = []\n", + " f = []\n", + " for i in range(Q):\n", + " x.append(i)\n", + " f.append(a**i % N) # 计算f(x)\n", + "\n", + " # 创建量子态|register2>的矩阵表示\n", + " vector = np.zeros((Q, Q))\n", + " for i in range(Q):\n", + " vector[i, i] = 1\n", + "\n", + " T = []\n", + " for i in range(Q):\n", + " matrix = np.outer(vector[f[i]], vector[0]) # 计算映射Tx的矩阵\n", + " T.append(UnivMathGate(f'f({i})', matrix)) # 用变换矩阵构造Tx“门”\n", + "\n", + " # 创建控制线路,得到算子U。对于每个Tx“门”,都受寄存器1中所有比特控制,其对应x的二进制中比特位是1的是正常控制节点,比特位是0的则要在控制节点两侧作用X门,翻转控制位\n", + " circuit = Circuit()\n", + " for i in range(Q):\n", + " bin_x = bin(x[i])[2:] # 将x转换为二进制\n", + " flip_control_qubit = list(range(len(register1))) # 初始化需要作用X门的比特的list\n", + "\n", + " for j in range(len(bin_x)):\n", + " if bin_x[len(bin_x) - j - 1] == '1': # 获得x的二进制中是‘1’的比特\n", + " flip_control_qubit.remove(j) # 从list中删除不需要作用X门的控制比特\n", + "\n", + " circuit.barrier() # 添加barrier\n", + " circuit += UN(X, flip_control_qubit) # 在控制节点前作用X门\n", + " circuit += T[x[i]].on(register2, list(register1)) # 给Tx“门”接上控制比特\n", + " circuit += UN(X, flip_control_qubit) # 在控制节点后作用X门\n", + "\n", + " return circuit" + ] + }, + { + "source": [ + "现在,U_operator()函数就可以对寄存器1中的量子态$|x\\rangle$进行指数模运算,并将得到的结果$a^{|x\\rangle}\\ mod\\ N$存入寄存器2。\n", + "\n", + "举例:$N=15,a=2$的情况,我们构造出对应的$U$算子,并验证它是否能正确得到结果(寄存器1和寄存器2各需要4个比特来存储$x$和$f(x)$):" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": "1¦00010100⟩\n" + }, + { + "output_type": "display_data", + "data": { + "text/plain": "", + "text/html": "
\n"
+     },
+     "metadata": {}
+    },
+    {
+     "output_type": "execute_result",
+     "data": {
+      "text/plain": "q0: ─────‖──X─────●──────X──‖────────●─────────‖──X─────●──────X──‖────────●─────────‖──X─────●──────X──‖────────●─────────‖──X─────●──────X──‖────────●─────────‖──X─────●──────X──‖────────●─────────‖──X──────●──────X──‖─────────●─────────‖──X──────●──────X──‖─────────●─────────‖──X──────●──────X──‖────●────\n         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖         │         ‖         │         ‖         │         ‖         │         ‖         │         ‖    │\nq1: ─────‖──X─────●──────X──‖──X─────●──────X──‖────────●─────────‖────────●─────────‖──X─────●──────X──‖──X─────●──────X──‖────────●─────────‖────────●─────────‖──X─────●──────X──‖──X─────●──────X──‖─────────●─────────‖─────────●─────────‖──X──────●──────X──‖──X──────●──────X──‖─────────●─────────‖────●────\n         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖         │         ‖         │         ‖         │         ‖         │         ‖         │         ‖    │\nq2: ──X──‖──X─────●──────X──‖──X─────●──────X──‖──X─────●──────X──‖──X─────●──────X──‖────────●─────────‖────────●─────────‖────────●─────────‖────────●─────────‖──X─────●──────X──‖──X─────●──────X──‖──X──────●──────X──‖──X──────●──────X──‖─────────●─────────‖─────────●─────────‖─────────●─────────‖────●────\n         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖         │         ‖         │         ‖         │         ‖         │         ‖         │         ‖    │\nq3: ─────‖──X─────●──────X──‖──X─────●──────X──‖──X─────●──────X──‖──X─────●──────X──‖──X─────●──────X──‖──X─────●──────X──‖──X─────●──────X──‖──X─────●──────X──‖────────●─────────‖────────●─────────‖─────────●─────────‖─────────●─────────‖─────────●─────────‖─────────●─────────‖─────────●─────────‖────●────\n         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖         │         ‖         │         ‖         │         ‖         │         ‖         │         ‖    │\nq4: ─────‖───────f(0)───────‖───────f(1)───────‖───────f(2)───────‖───────f(3)───────‖───────f(4)───────‖───────f(5)───────‖───────f(6)───────‖───────f(7)───────‖───────f(8)───────‖───────f(9)───────‖───────f(10)───────‖───────f(11)───────‖───────f(12)───────‖───────f(13)───────‖───────f(14)───────‖──f(15)──\n         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖         │         ‖         │         ‖         │         ‖         │         ‖         │         ‖    │\nq5: ─────‖───────f(0)───────‖───────f(1)───────‖───────f(2)───────‖───────f(3)───────‖───────f(4)───────‖───────f(5)───────‖───────f(6)───────‖───────f(7)───────‖───────f(8)───────‖───────f(9)───────‖───────f(10)───────‖───────f(11)───────‖───────f(12)───────‖───────f(13)───────‖───────f(14)───────‖──f(15)──\n         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖         │         ‖         │         ‖         │         ‖         │         ‖         │         ‖    │\nq6: ─────‖───────f(0)───────‖───────f(1)───────‖───────f(2)───────‖───────f(3)───────‖───────f(4)───────‖───────f(5)───────‖───────f(6)───────‖───────f(7)───────‖───────f(8)───────‖───────f(9)───────‖───────f(10)───────‖───────f(11)───────‖───────f(12)───────‖───────f(13)───────‖───────f(14)───────‖──f(15)──\n         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖         │         ‖         │         ‖         │         ‖         │         ‖         │         ‖    │\nq7: ─────‖───────f(0)───────‖───────f(1)───────‖───────f(2)───────‖───────f(3)───────‖───────f(4)───────‖───────f(5)───────‖───────f(6)───────‖───────f(7)───────‖───────f(8)───────‖───────f(9)───────‖───────f(10)───────‖───────f(11)───────‖───────f(12)───────‖───────f(13)───────‖───────f(14)───────‖──f(15)──",
+      "text/html": "
q0: ─────‖──X─────●──────X──‖────────●─────────‖──X─────●──────X──‖────────●─────────‖──X─────●──────X──‖────────●─────────‖──X─────●──────X──‖────────●─────────‖──X─────●──────X──‖────────●─────────‖──X──────●──────X──‖─────────●─────────‖──X──────●──────X──‖─────────●─────────‖──X──────●──────X──‖────●────\n         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖         │         ‖         │         ‖         │         ‖         │         ‖         │         ‖    │\nq1: ─────‖──X─────●──────X──‖──X─────●──────X──‖────────●─────────‖────────●─────────‖──X─────●──────X──‖──X─────●──────X──‖────────●─────────‖────────●─────────‖──X─────●──────X──‖──X─────●──────X──‖─────────●─────────‖─────────●─────────‖──X──────●──────X──‖──X──────●──────X──‖─────────●─────────‖────●────\n         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖         │         ‖         │         ‖         │         ‖         │         ‖         │         ‖    │\nq2: ──X──‖──X─────●──────X──‖──X─────●──────X──‖──X─────●──────X──‖──X─────●──────X──‖────────●─────────‖────────●─────────‖────────●─────────‖────────●─────────‖──X─────●──────X──‖──X─────●──────X──‖──X──────●──────X──‖──X──────●──────X──‖─────────●─────────‖─────────●─────────‖─────────●─────────‖────●────\n         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖         │         ‖         │         ‖         │         ‖         │         ‖         │         ‖    │\nq3: ─────‖──X─────●──────X──‖──X─────●──────X──‖──X─────●──────X──‖──X─────●──────X──‖──X─────●──────X──‖──X─────●──────X──‖──X─────●──────X──‖──X─────●──────X──‖────────●─────────‖────────●─────────‖─────────●─────────‖─────────●─────────‖─────────●─────────‖─────────●─────────‖─────────●─────────‖────●────\n         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖         │         ‖         │         ‖         │         ‖         │         ‖         │         ‖    │\nq4: ─────‖───────f(0)───────‖───────f(1)───────‖───────f(2)───────‖───────f(3)───────‖───────f(4)───────‖───────f(5)───────‖───────f(6)───────‖───────f(7)───────‖───────f(8)───────‖───────f(9)───────‖───────f(10)───────‖───────f(11)───────‖───────f(12)───────‖───────f(13)───────‖───────f(14)───────‖──f(15)──\n         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖         │         ‖         │         ‖         │         ‖         │         ‖         │         ‖    │\nq5: ─────‖───────f(0)───────‖───────f(1)───────‖───────f(2)───────‖───────f(3)───────‖───────f(4)───────‖───────f(5)───────‖───────f(6)───────‖───────f(7)───────‖───────f(8)───────‖───────f(9)───────‖───────f(10)───────‖───────f(11)───────‖───────f(12)───────‖───────f(13)───────‖───────f(14)───────‖──f(15)──\n         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖         │         ‖         │         ‖         │         ‖         │         ‖         │         ‖    │\nq6: ─────‖───────f(0)───────‖───────f(1)───────‖───────f(2)───────‖───────f(3)───────‖───────f(4)───────‖───────f(5)───────‖───────f(6)───────‖───────f(7)───────‖───────f(8)───────‖───────f(9)───────‖───────f(10)───────‖───────f(11)───────‖───────f(12)───────‖───────f(13)───────‖───────f(14)───────‖──f(15)──\n         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖         │         ‖         │         ‖         │         ‖         │         ‖         │         ‖    │\nq7: ─────‖───────f(0)───────‖───────f(1)───────‖───────f(2)───────‖───────f(3)───────‖───────f(4)───────‖───────f(5)───────‖───────f(6)───────‖───────f(7)───────‖───────f(8)───────‖───────f(9)───────‖───────f(10)───────‖───────f(11)───────‖───────f(12)───────‖───────f(13)───────‖───────f(14)───────‖──f(15)──\n
\n" + }, + "metadata": {}, + "execution_count": 4 + } + ], + "source": [ + "register1 = range(4)\n", + "register2 = range(4, 8)\n", + "circuit = Circuit(X.on(2)) # 创建线路,使输入态为|0100⟩|0000⟩,即x=8,|8⟩|0⟩\n", + "circuit += U_operator(15, 2, register1, register2) # 作用U算子\n", + "\n", + "print(circuit.get_qs(ket=True)) # 打印末态\n", + "circuit #打印线路" + ] + }, + { + "source": [ + "寄存器1中结果为0100,寄存器2中结果为0001,先前我们已经算出了$f(8)=2^8\\ mod\\ 15=1$,因此输出结果正确。线路虽然看起来较为复杂,实际上就是16个受控4比特门依次作用而已,前四个比特上的`X`门用途为翻转控制位。\n", + "\n", + "接下来我们需要实现周期查找算法。" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "source": [ + "### 3.2 周期查找算法\n", + "1. 在寄存器1中我们需要$q>log_2 N$个比特来记录自变量$x \\in [0,N-1]$的二进制数,寄存器2中同样需要$q$个比特来记录$f(x)=a^x\\ mod\\ N\\ \\in [0,N-1]$的二进制数。此时寄存器1和寄存器2分别能记录$[0,Q-1]$的整数,其中$Q=2^q>N$。\n", + "\n", + "2. 对寄存器1中的所有比特作用`Hadamard`门,此时寄存器1中的比特处于$[0,Q-1]$中所有整数的均匀叠加态\n", + "$$\n", + "|\\psi\\rangle=\\sum_{x=0}^{Q-1}|x\\rangle\n", + "$$\n", + "\n", + "3. 对寄存器1存储的态$|\\psi\\rangle$做函数运算$a^{|\\psi\\rangle}\\ mod\\ N$,并将结果存入寄存器2,此步骤由先前构造的U_operator完成。由于直接对叠加态$|\\psi\\rangle$进行运算,此步骤只需一步完成,体现了量子计算的优势————并行计算。此时线路中存储的态是纠缠态,可以表示为\n", + "$$\n", + "\\sum_{x=0}^{Q-1}|x\\rangle|f(x)\\rangle=\\sum_{i=0}^{r-1}(|i\\rangle+|i+r\\rangle+|i+2r\\rangle+...)\\ |f(i)\\rangle\n", + "$$\n", + "4. 对寄存器1做傅立叶逆变换,此变换使用一个$Q$次单位根$\\omega^{2\\pi i/Q}$,会将任意给定态$|x\\rangle$的振幅平均分布在$Q$个$|y\\rangle$态上。而如步骤3中显示的,寄存器1中$|i\\rangle$与$|i+r\\rangle$等态均与寄存器2中同一个态$|f(i)\\rangle$相纠缠,因此会发生量子干涉,最终使得当单位矢量$\\omega^{2\\pi iry/Q}$越接近1(指向正实数轴)时,测量得到态$|y\\rangle$的概率越大。换句话说,我们测得的态$|y\\rangle$,有很大概率使得$\\frac{ry}{Q}$接近某一整数$c$。更详尽的数学描述可以参考链接:https://zh.wikipedia.org/wiki/秀爾演算法 中的‘量子部分:周期查找子程序’。\n", + "\n", + "5. 测量寄存器1,得到二进制串。将二进制数转化为十进制数$y$,此时$\\frac{y}{Q}\\sim\\frac{c}{r}$,其中$c$是未知整数。通过连分数分解法计算$\\frac{y}{Q}$逼近的不可约分数(分母不大于$N$),取其分母即得到周期$r$。但是,在分母小于$N$的不可约分数中可能存在比$\\frac{c}{r}$更逼近$\\frac{y}{Q}$的分数,或是$c$与$r$存在公因数,则得到的$r$会是真正函数周期的因数,此时计算失败,重新计算。" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "source": [ + "举例:还是用$N=15,a=2$的例子,在3.1中我们把每一个$f(x)$都算了出来,从中可以直接看出函数周期为4。现在我们可以搭建对应的周期查找线路,并进行100次模拟,看看会得到哪些结果。" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": "", + "text/html": "
\n"
+     },
+     "metadata": {}
+    },
+    {
+     "output_type": "execute_result",
+     "data": {
+      "text/plain": "q0: ──H──‖──X─────●──────X──‖────────●─────────‖──X─────●──────X──‖────────●─────────‖──X─────●──────X──‖────────●─────────‖──X─────●──────X──‖────────●─────────‖──X─────●──────X──‖────────●─────────‖──X──────●──────X──‖─────────●─────────‖──X──────●──────X──‖─────────●─────────‖──X──────●──────X──‖────●────‖───────@────H───────●────────────────●────────────────────────────●───────────────────────────────────‖──M(q0)──\n         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖         │         ‖         │         ‖         │         ‖         │         ‖         │         ‖    │    ‖       │            │                │                            │                                   ‖\nq1: ──H──‖──X─────●──────X──‖──X─────●──────X──‖────────●─────────‖────────●─────────‖──X─────●──────X──‖──X─────●──────X──‖────────●─────────‖────────●─────────‖──X─────●──────X──‖──X─────●──────X──‖─────────●─────────‖─────────●─────────‖──X──────●──────X──‖──X──────●──────X──‖─────────●─────────‖────●────‖──@────┼─────────PS(-π/2)────H───────┼───────────●────────────────┼───────────●───────────────────────‖──M(q1)──\n         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖         │         ‖         │         ‖         │         ‖         │         ‖         │         ‖    │    ‖  │    │                             │           │                │           │                       ‖\nq2: ──H──‖──X─────●──────X──‖──X─────●──────X──‖──X─────●──────X──‖──X─────●──────X──‖────────●─────────‖────────●─────────‖────────●─────────‖────────●─────────‖──X─────●──────X──‖──X─────●──────X──‖──X──────●──────X──‖──X──────●──────X──‖─────────●─────────‖─────────●─────────‖─────────●─────────‖────●────‖──@────┼──────────────────────────PS(-π/4)────PS(-π/2)────H───────┼───────────┼───────────●───────────‖──M(q2)──\n         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖         │         ‖         │         ‖         │         ‖         │         ‖         │         ‖    │    ‖       │                                                          │           │           │           ‖\nq3: ──H──‖──X─────●──────X──‖──X─────●──────X──‖──X─────●──────X──‖──X─────●──────X──‖──X─────●──────X──‖──X─────●──────X──‖──X─────●──────X──‖──X─────●──────X──‖────────●─────────‖────────●─────────‖─────────●─────────‖─────────●─────────‖─────────●─────────‖─────────●─────────‖─────────●─────────‖────●────‖───────@───────────────────────────────────────────────────────PS(-π/8)────PS(-π/4)────PS(-π/2)────H──‖──M(q3)──\n         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖         │         ‖         │         ‖         │         ‖         │         ‖         │         ‖    │    ‖                                                                                                      ‖\nq4: ─────‖───────f(0)───────‖───────f(1)───────‖───────f(2)───────‖───────f(3)───────‖───────f(4)───────‖───────f(5)───────‖───────f(6)───────‖───────f(7)───────‖───────f(8)───────‖───────f(9)───────‖───────f(10)───────‖───────f(11)───────‖───────f(12)───────‖───────f(13)───────‖───────f(14)───────‖──f(15)──‖──────────────────────────────────────────────────────────────────────────────────────────────────────‖─────────\n         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖         │         ‖         │         ‖         │         ‖         │         ‖         │         ‖    │    ‖                                                                                                      ‖\nq5: ─────‖───────f(0)───────‖───────f(1)───────‖───────f(2)───────‖───────f(3)───────‖───────f(4)───────‖───────f(5)───────‖───────f(6)───────‖───────f(7)───────‖───────f(8)───────‖───────f(9)───────‖───────f(10)───────‖───────f(11)───────‖───────f(12)───────‖───────f(13)───────‖───────f(14)───────‖──f(15)──‖──────────────────────────────────────────────────────────────────────────────────────────────────────‖─────────\n         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖         │         ‖         │         ‖         │         ‖         │         ‖         │         ‖    │    ‖                                                                                                      ‖\nq6: ─────‖───────f(0)───────‖───────f(1)───────‖───────f(2)───────‖───────f(3)───────‖───────f(4)───────‖───────f(5)───────‖───────f(6)───────‖───────f(7)───────‖───────f(8)───────‖───────f(9)───────‖───────f(10)───────‖───────f(11)───────‖───────f(12)───────‖───────f(13)───────‖───────f(14)───────‖──f(15)──‖──────────────────────────────────────────────────────────────────────────────────────────────────────‖─────────\n         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖         │         ‖         │         ‖         │         ‖         │         ‖         │         ‖    │    ‖                                                                                                      ‖\nq7: ─────‖───────f(0)───────‖───────f(1)───────‖───────f(2)───────‖───────f(3)───────‖───────f(4)───────‖───────f(5)───────‖───────f(6)───────‖───────f(7)───────‖───────f(8)───────‖───────f(9)───────‖───────f(10)───────‖───────f(11)───────‖───────f(12)───────‖───────f(13)───────‖───────f(14)───────‖──f(15)──‖──────────────────────────────────────────────────────────────────────────────────────────────────────‖─────────",
+      "text/html": "
q0: ──H──‖──X─────●──────X──‖────────●─────────‖──X─────●──────X──‖────────●─────────‖──X─────●──────X──‖────────●─────────‖──X─────●──────X──‖────────●─────────‖──X─────●──────X──‖────────●─────────‖──X──────●──────X──‖─────────●─────────‖──X──────●──────X──‖─────────●─────────‖──X──────●──────X──‖────●────‖───────@────H───────●────────────────●────────────────────────────●───────────────────────────────────‖──M(q0)──\n         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖         │         ‖         │         ‖         │         ‖         │         ‖         │         ‖    │    ‖       │            │                │                            │                                   ‖\nq1: ──H──‖──X─────●──────X──‖──X─────●──────X──‖────────●─────────‖────────●─────────‖──X─────●──────X──‖──X─────●──────X──‖────────●─────────‖────────●─────────‖──X─────●──────X──‖──X─────●──────X──‖─────────●─────────‖─────────●─────────‖──X──────●──────X──‖──X──────●──────X──‖─────────●─────────‖────●────‖──@────┼─────────PS(-π/2)────H───────┼───────────●────────────────┼───────────●───────────────────────‖──M(q1)──\n         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖         │         ‖         │         ‖         │         ‖         │         ‖         │         ‖    │    ‖  │    │                             │           │                │           │                       ‖\nq2: ──H──‖──X─────●──────X──‖──X─────●──────X──‖──X─────●──────X──‖──X─────●──────X──‖────────●─────────‖────────●─────────‖────────●─────────‖────────●─────────‖──X─────●──────X──‖──X─────●──────X──‖──X──────●──────X──‖──X──────●──────X──‖─────────●─────────‖─────────●─────────‖─────────●─────────‖────●────‖──@────┼──────────────────────────PS(-π/4)────PS(-π/2)────H───────┼───────────┼───────────●───────────‖──M(q2)──\n         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖         │         ‖         │         ‖         │         ‖         │         ‖         │         ‖    │    ‖       │                                                          │           │           │           ‖\nq3: ──H──‖──X─────●──────X──‖──X─────●──────X──‖──X─────●──────X──‖──X─────●──────X──‖──X─────●──────X──‖──X─────●──────X──‖──X─────●──────X──‖──X─────●──────X──‖────────●─────────‖────────●─────────‖─────────●─────────‖─────────●─────────‖─────────●─────────‖─────────●─────────‖─────────●─────────‖────●────‖───────@───────────────────────────────────────────────────────PS(-π/8)────PS(-π/4)────PS(-π/2)────H──‖──M(q3)──\n         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖         │         ‖         │         ‖         │         ‖         │         ‖         │         ‖    │    ‖                                                                                                      ‖\nq4: ─────‖───────f(0)───────‖───────f(1)───────‖───────f(2)───────‖───────f(3)───────‖───────f(4)───────‖───────f(5)───────‖───────f(6)───────‖───────f(7)───────‖───────f(8)───────‖───────f(9)───────‖───────f(10)───────‖───────f(11)───────‖───────f(12)───────‖───────f(13)───────‖───────f(14)───────‖──f(15)──‖──────────────────────────────────────────────────────────────────────────────────────────────────────‖─────────\n         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖         │         ‖         │         ‖         │         ‖         │         ‖         │         ‖    │    ‖                                                                                                      ‖\nq5: ─────‖───────f(0)───────‖───────f(1)───────‖───────f(2)───────‖───────f(3)───────‖───────f(4)───────‖───────f(5)───────‖───────f(6)───────‖───────f(7)───────‖───────f(8)───────‖───────f(9)───────‖───────f(10)───────‖───────f(11)───────‖───────f(12)───────‖───────f(13)───────‖───────f(14)───────‖──f(15)──‖──────────────────────────────────────────────────────────────────────────────────────────────────────‖─────────\n         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖         │         ‖         │         ‖         │         ‖         │         ‖         │         ‖    │    ‖                                                                                                      ‖\nq6: ─────‖───────f(0)───────‖───────f(1)───────‖───────f(2)───────‖───────f(3)───────‖───────f(4)───────‖───────f(5)───────‖───────f(6)───────‖───────f(7)───────‖───────f(8)───────‖───────f(9)───────‖───────f(10)───────‖───────f(11)───────‖───────f(12)───────‖───────f(13)───────‖───────f(14)───────‖──f(15)──‖──────────────────────────────────────────────────────────────────────────────────────────────────────‖─────────\n         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖        │         ‖         │         ‖         │         ‖         │         ‖         │         ‖         │         ‖    │    ‖                                                                                                      ‖\nq7: ─────‖───────f(0)───────‖───────f(1)───────‖───────f(2)───────‖───────f(3)───────‖───────f(4)───────‖───────f(5)───────‖───────f(6)───────‖───────f(7)───────‖───────f(8)───────‖───────f(9)───────‖───────f(10)───────‖───────f(11)───────‖───────f(12)───────‖───────f(13)───────‖───────f(14)───────‖──f(15)──‖──────────────────────────────────────────────────────────────────────────────────────────────────────‖─────────\n
\n" + }, + "metadata": {}, + "execution_count": 5 + } + ], + "source": [ + "circuit = Circuit() # 创建量子线路\n", + "register1 = range(4) # 设置前4个比特为寄存器1\n", + "register2 = range(4, 8) # 设置后4个比特为寄存器2\n", + "\n", + "circuit += UN(H, register1) # 对寄存器1中的所有比特作用H门\n", + "\n", + "# 对寄存器1做模乘运算,并将结果存入寄存器2,该操作由一个大的U门完成\n", + "circuit += U_operator(15, 2, register1, register2)\n", + "\n", + "circuit.barrier() # 添加barrier\n", + "circuit += qft(register1[::-1]).hermitian() # 对寄存器1做傅立叶逆变换,须注意傅立叶变换作用的比特顺序,在这里需要反序\n", + "circuit.barrier() # 添加barrier\n", + "circuit += UN(Measure(), register1) # 测量寄存器1\n", + "\n", + "circuit # 画出线路图" + ] + }, + { + "source": [ + "从线路图我们可以很直观地看到,整个周期查找线路由四部分组成:产生叠加态$\\rightarrow$函数运算$\\rightarrow$傅立叶逆变换$\\rightarrow$测量。\n", + "\n", + "接下来运行该线路100次,观察测量结果。" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": "", + "text/html": "
\n"
+     },
+     "metadata": {}
+    },
+    {
+     "output_type": "execute_result",
+     "data": {
+      "text/plain": "shots: 100\nKeys: q3 q2 q1 q0│0.00   0.077       0.155       0.232        0.31       0.387\n─────────────────┼───────────┴───────────┴───────────┴───────────┴───────────┴\n             0000│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒\n                 │\n             0100│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓\n                 │\n             1000│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒\n                 │\n             1100│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒\n                 │\n{'0000': 18, '0100': 31, '1000': 29, '1100': 22}",
+      "text/html": "
shots: 100\nKeys: q3 q2 q1 q0│0.00   0.077       0.155       0.232        0.31       0.387\n─────────────────┼───────────┴───────────┴───────────┴───────────┴───────────┴\n             0000│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒\n\n             0100│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓\n\n             1000│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒\n\n             1100│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒\n\n{'0000': 18, '0100': 31, '1000': 29, '1100': 22}\n
\n" + }, + "metadata": {}, + "execution_count": 6 + } + ], + "source": [ + "sim = Simulator('projectq', circuit.n_qubits) # 创建量子线路模拟器\n", + "\n", + "# 模拟线路100次,打印测量结果,随机种子seed设为100内的随机整数\n", + "result = sim.sampling(circuit, shots=100, seed=np.random.randint(100))\n", + "\n", + "result" + ] + }, + { + "source": [ + "从统计结果可以看出,最后寄存器1中只可能测出4个态,分别是$y=[0,4,8,12]$,这是由于$\\omega^{2\\pi iry/Q}\\ (Q=16)$当$y$取这四个值时恰好为1,而其它的态由于量子干涉导致概率幅抵消为零。把测量结果代入$\\frac{y}{Q}\\sim\\frac{c}{r}$,可以看出该式确实成立,我们有约50%的概率得到正确的周期$r$,但有约25%概率得到$r$的因数,还有25%概率得到0态,后两种情况需要重新计算。\n", + "\n", + "接下来构造的是通用的周期查找算法。" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "def period_finder(N, a, q):\n", + " circuit = Circuit() # 创建量子线路\n", + " register1 = range(q) # 设置前q个比特为寄存器1\n", + " register2 = range(q, 2 * q) # 设置后q个比特为寄存器2\n", + "\n", + " circuit += UN(H, register1) # 对寄存器1中的所有比特作用H门\n", + "\n", + " # 对寄存器1做模乘运算,并将结果存入寄存器2,该操作由一个大的U门完成\n", + " circuit += U_operator(N, a, register1, register2)\n", + "\n", + " circuit += qft(register1[::-1]).hermitian() # 对寄存器1做傅立叶逆变换,须注意傅立叶变换作用的比特顺序,在这里需要反序\n", + " circuit.barrier() # 添加barrier\n", + " circuit += UN(Measure(), register1) # 测量寄存器1\n", + "\n", + " sim = Simulator('projectq', circuit.n_qubits) # 创建量子线路模拟器\n", + "\n", + " # 模拟线路,收集测量结果,随机种子seed设为100内的随机整数\n", + " result = sim.sampling(circuit, seed=np.random.randint(100))\n", + "\n", + " # result.data是一个字典,key是测量结果,value是出现频数,我们只做了一次采样,因此只有一个key, value必定为1\n", + " result = list(result.data.keys())[0] # 将key取出\n", + " result = int(result, 2) # 将结果从二进制转化为十进制\n", + "\n", + " # 通过连分数分解法计算result/2**q逼近的不可约分数,分母不能大于N\n", + " eigenphase = float(result / 2**q)\n", + " f = Fraction.from_float(eigenphase).limit_denominator(N)\n", + " r = f.denominator # 取f的分母,得到周期r\n", + "\n", + " # r有可能是周期的因数,因此需要验证,当且仅当r是函数周期本身时返回r,否则返回None\n", + " if a**r % N == 1:\n", + " return r\n", + " return None" + ] + }, + { + "source": [ + "### 3.3 经典计算机部分\n", + "经典计算机部分负责将因数分解问题转化成寻找函数周期的问题,具体步骤如下:\n", + "\n", + "1. 随机取一个小于$N$的整数$a$,用gcd算法验证$a$与$N$是否互质,若$a$与$N$存在公因数,则直接得到$N$的一个因数,输出结果。\n", + "\n", + "2. 计算需要$q$个比特来存储$N$的二进制数。\n", + "\n", + "3. 用周期查找算法得到函数$f(x)=a^x\\ mod\\ N$的周期$r$。\n", + "\n", + "4. 判断$r$是否为偶数,若不是则回到第一步。\n", + "\n", + "5. 计算$a^{r/2}+1$和$a^{r/2}-1$,它们当中必有其一与$N$存在非1公因数。但是,$a^{r/2}+1$有可能可以整除$N$,因此最后输出结果仍有可能是$N$本身。" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "def shor(N):\n", + " while True:\n", + " a = np.random.randint(N - 2) + 2 # 获得区间[2,N-1]内的随机整数a\n", + " b = np.gcd(a, N) # 得到a与N的最大公因数b\n", + " if b != 1:\n", + " return b, int(N / b) # 如果b不等于1,则b是N的质因数,返回分解结果\n", + "\n", + " # 获得足够表示N的二进制的比特数q\n", + " q = 0\n", + " while True:\n", + " Q = 2**q\n", + " if Q >= N:\n", + " break\n", + " q += 1\n", + "\n", + " r = period_finder(N, a, q) # 使用周期查找算法得到r\n", + "\n", + " # 判断r是否为偶数,若是则跳出循环,若不是则重新选择随机整数a\n", + " if r != None and r % 2 == 0:\n", + " break\n", + "\n", + " # 计算a**(r/2)+1和a**(r/2)-1,并验证它们是否与N有公约数,若有则输出结果\n", + " c = np.gcd(a**(int(r / 2)) + 1, N)\n", + " d = np.gcd(a**(int(r / 2)) - 1, N)\n", + " if c != 1 and N % c == 0:\n", + " return c, int(N / c)\n", + " else:\n", + " return d, int(N / d)\n", + " " + ] + }, + { + "source": [ + "由于经典计算机模拟量子算法需要大量的内存,以及先前提到的MindQuantum中的模拟器暂时无法运行超过5比特的自定义门,因此我们暂时无法利用Shor算法计算$N>21$的情况。最后让我们试着用写好的Shor算法分解$N=15$。" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": "Factoring N = p * q = 15\np = 5\nq = 3\n" + } + ], + "source": [ + "N = 15\n", + "print(\"Factoring N = p * q =\", N)\n", + "\n", + "p, q = shor(N)\n", + "print(\"p =\", p)\n", + "print(\"q =\", q)" + ] + }, + { + "source": [ + "从运行结果可以看到,我们成功的分解出15的两个质因数:3和5。\n", + "\n", + "至此,我们成功的使用MindQuantum实现了Shor算法。\n", + "\n", + "若想查询更多关于MindQuantum的API,请点击:https://mindspore.cn/mindquantum/" + ], + "cell_type": "markdown", + "metadata": {} + } + ] +} \ No newline at end of file diff --git a/tutorials/README.md b/tutorials/README.md new file mode 100644 index 0000000000000000000000000000000000000000..8417204ca72ff9d40ac1076ced2ae0ecab019f55 --- /dev/null +++ b/tutorials/README.md @@ -0,0 +1 @@ +# MindQuantum Tutorials diff --git a/tutorials/benchmarks/README.md b/tutorials/benchmarks/README.md index d6e05a9bb42a3b8adb9642f928cae5c4ba2cab83..3ffe8ec203143141d63bc11368658ec180716f9e 100644 --- a/tutorials/benchmarks/README.md +++ b/tutorials/benchmarks/README.md @@ -8,4 +8,4 @@ ## Content Description -These scripts are going to test the performance between MindQuantum and other frameworks. Please read the material in each folder for detail description. \ No newline at end of file +These scripts are going to test the performance between MindQuantum and other frameworks. Please read the material in each folder for detail description. diff --git a/tutorials/benchmarks/grad/_parse_args.py b/tutorials/benchmarks/grad/_parse_args.py index b0b5f53685ad1d9799238b12a7a20f200f734a21..722c4e43dc9dd34a1abb169130cd1a72a1399aa2 100644 --- a/tutorials/benchmarks/grad/_parse_args.py +++ b/tutorials/benchmarks/grad/_parse_args.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tutorials/benchmarks/grad/mindquantum_grad.py b/tutorials/benchmarks/grad/mindquantum_grad.py index 9946d8a7a155ba4ad169400fc72084f92bf3e910..8609983d3db26c010371c38b30575b8372dec90a 100644 --- a/tutorials/benchmarks/grad/mindquantum_grad.py +++ b/tutorials/benchmarks/grad/mindquantum_grad.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,17 +17,17 @@ import time import os from _parse_args import parser + args = parser.parse_args() os.environ['OMP_NUM_THREADS'] = str(args.omp_num_threads) import numpy as np -from mindquantum.ops import QubitOperator -from mindquantum import Circuit, X, H, XX, ZZ, RX, Hamiltonian -from mindquantum.nn import generate_pqc_operator +from mindquantum.core import QubitOperator +from mindquantum.core import Circuit, X, H, XX, ZZ, Hamiltonian +from mindquantum.simulator import Simulator import mindspore.context as context -from mindspore import Tensor import tqdm -context.set_context(mode=context.GRAPH_MODE, device_target="CPU") +context.set_context(mode=context.PYNATIVE_MODE, device_target="CPU") class CircuitLayerBuilder(): @@ -42,7 +43,12 @@ class CircuitLayerBuilder(): def convert_to_circuit(image, data_qubits=None): - """convert_to_circuit""" + """ + convert_to_circuit + + Returns: + Circuit + """ values = np.ndarray.flatten(image) if data_qubits is None: data_qubits = range(len(values)) @@ -55,7 +61,12 @@ def convert_to_circuit(image, data_qubits=None): def create_quantum_model(n_qubits): - """Create QNN.""" + """ + Create QNN. + + Returns: + tuple + """ data_qubits = range(1, n_qubits) readout = 0 c = Circuit() @@ -75,18 +86,15 @@ x_train_bin, y_train_nocon, x_test_bin, y_test_nocon = data['arr_0'], data[ x_train_circ = [convert_to_circuit(x, range(1, n_qubits)) for x in x_train_bin] ansatz, ham = create_quantum_model(n_qubits) -model_para_names = ansatz.para_name -ops = generate_pqc_operator(model_para_names, ['null'], - RX('null').on(0) + ansatz, - ham, - n_threads=args.parallel_worker) +model_params_names = ansatz.params_name +ops = Simulator('projectq', ansatz.n_qubits).get_expectation_with_grad( + ham, ansatz, parallel_worker=args.parallel_worker) t0 = time.time() eval_time = [] for x in tqdm.tqdm(x_train_circ[:args.num_sampling]): eval_time.append(time.time()) - ops(Tensor(np.random.normal(size=(1, 32)).astype(np.float32)), - Tensor(np.array([0]).astype(np.float32))) + ops(np.random.normal(size=32)) eval_time[-1] = time.time() - eval_time[-1] eval_time = np.sort(eval_time[1:]) t1 = time.time() diff --git a/tutorials/benchmarks/grad/tensorflow_quantum_grad.py b/tutorials/benchmarks/grad/tensorflow_quantum_grad.py index d5c4617d7a5d877280b5e59bbfab24df8754fcbe..fe8a12df434331e76da626ea7ce09b2af2d7a42e 100644 --- a/tutorials/benchmarks/grad/tensorflow_quantum_grad.py +++ b/tutorials/benchmarks/grad/tensorflow_quantum_grad.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tutorials/benchmarks/mnist/README.md b/tutorials/benchmarks/mnist/README.md index 424ba01fc293605692d8f318101b314a677c96bc..17568a9304e87517f4cdba4a9c832fecd6e4ad48 100644 --- a/tutorials/benchmarks/mnist/README.md +++ b/tutorials/benchmarks/mnist/README.md @@ -2,7 +2,7 @@ These scripts are going to test the performance between MindQuantum and TensorFlow Quantum on mnist dataset. -Please install TensorFlow Quantum before runing these scripts. +Please install TensorFlow Quantum before running these scripts. ## MindQuantum diff --git a/tutorials/benchmarks/mnist/_parse_args.py b/tutorials/benchmarks/mnist/_parse_args.py index b0b5f53685ad1d9799238b12a7a20f200f734a21..722c4e43dc9dd34a1abb169130cd1a72a1399aa2 100644 --- a/tutorials/benchmarks/mnist/_parse_args.py +++ b/tutorials/benchmarks/mnist/_parse_args.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tutorials/benchmarks/mnist/mnist.py b/tutorials/benchmarks/mnist/mnist.py index 23e7312eaebc8453c344feab1f3dfd083bd95f20..22521b74b4979713fdb5881935c8f67ea41e5bdb 100644 --- a/tutorials/benchmarks/mnist/mnist.py +++ b/tutorials/benchmarks/mnist/mnist.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,9 +19,11 @@ import time import os import numpy as np from _parse_args import parser + args = parser.parse_args() os.environ['OMP_NUM_THREADS'] = str(args.omp_num_threads) +import mindspore as ms import mindspore.context as context import mindspore.dataset as ds from mindspore.ops import operations as P @@ -28,9 +31,10 @@ from mindspore import Tensor from mindspore import nn from mindspore import Model from mindspore.train.callback import Callback -from mindquantum import Circuit, X, Z, H, XX, ZZ, Hamiltonian, RX -from mindquantum.nn import MindQuantumLayer -from mindquantum.ops import QubitOperator +from mindquantum.core import Circuit, X, Z, H, XX, ZZ, Hamiltonian, RX, QubitOperator +from mindquantum.framework import MQLayer +from mindquantum.simulator import Simulator +ms.context.set_context(mode=ms.context.PYNATIVE_MODE, device_target="CPU") class FPSMonitor(Callback): @@ -81,7 +85,12 @@ class MnistNet(nn.Cell): def encoder_circuit_builder(n_qubits_range, prefix='encoder'): - """RX encoder circuit.""" + """ + RX encoder circuit. + + Returns: + Circuit + """ c = Circuit() for i in n_qubits_range: c += RX('{}_{}'.format(prefix, i)).on(i) @@ -102,7 +111,12 @@ class CircuitLayerBuilder(): def create_quantum_model(n_qubits): - """Create QNN.""" + """ + Create QNN. + + Returns: + tuple + """ data_qubits = range(1, n_qubits) readout = 0 c = Circuit() @@ -116,7 +130,12 @@ def create_quantum_model(n_qubits): def binary_encoder(image, n_qubits=None): - """Input a binary image into data supported by RX encoder.""" + """ + Input a binary image into data supported by RX encoder. + + Returns: + numbers.Number + """ values = np.ndarray.flatten(image) if n_qubits is None: n_qubits = len(values) @@ -125,7 +144,12 @@ def binary_encoder(image, n_qubits=None): def generate_dataset(data_file_path, n_qubits, sampling_num, batch_num, eval_size_num): - """Generate train and test dataset.""" + """ + Generate train and test dataset. + + Returns: + Dataset + """ data = np.load(data_file_path) x_train_bin, y_train_nocon, x_test_bin, y_test_nocon = data['arr_0'], data[ 'arr_1'], data['arr_2'], data['arr_3'] @@ -156,7 +180,6 @@ def generate_dataset(data_file_path, n_qubits, sampling_num, batch_num, if __name__ == '__main__': - context.set_context(mode=context.GRAPH_MODE, device_target="CPU") n = 17 num_sampling = args.num_sampling eval_size = 100 @@ -169,16 +192,16 @@ if __name__ == '__main__': ansatz, read_out = create_quantum_model(n) encoder_circuit = encoder_circuit_builder(range(1, n)) encoder_circuit.no_grad() - encoder_names = encoder_circuit.para_name - ansatz_names = ansatz.para_name + encoder_names = encoder_circuit.params_name + ansatz_names = ansatz.params_name ham = Hamiltonian(QubitOperator('Z0')) - mql = MindQuantumLayer(encoder_names, - ansatz_names, - encoder_circuit + ansatz, - ham, - weight_init='normal', - n_threads=parallel_worker) + circ = encoder_circuit + ansatz + sim = Simulator('projectq', circ.n_qubits) + grad_ops = sim.get_expectation_with_grad(ham, circ, None, encoder_names, + ansatz_names, parallel_worker) + mql = MQLayer(grad_ops, 'normal') + mnist_net = MnistNet(mql) net_loss = Hinge() net_opt = nn.Adam(mnist_net.trainable_params()) diff --git a/tutorials/benchmarks/mnist/mnist_tf.py b/tutorials/benchmarks/mnist/mnist_tf.py index 5ad53981c79bde7fdd3064cd72a417c048cb7ac3..c3cf0931f9f26d42ea65d06aa4b6d53218f8de89 100644 --- a/tutorials/benchmarks/mnist/mnist_tf.py +++ b/tutorials/benchmarks/mnist/mnist_tf.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tutorials/benchmarks/qaoa/_parse_args.py b/tutorials/benchmarks/qaoa/_parse_args.py index b0b5f53685ad1d9799238b12a7a20f200f734a21..722c4e43dc9dd34a1abb169130cd1a72a1399aa2 100644 --- a/tutorials/benchmarks/qaoa/_parse_args.py +++ b/tutorials/benchmarks/qaoa/_parse_args.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tutorials/benchmarks/qaoa/qaoa_mindquantum.py b/tutorials/benchmarks/qaoa/qaoa_mindquantum.py index 8f521f4b5776abc4621728f01a6eaa5b25dcd327..648c035b415557db58ba77664d427ce7e0a6bbeb 100644 --- a/tutorials/benchmarks/qaoa/qaoa_mindquantum.py +++ b/tutorials/benchmarks/qaoa/qaoa_mindquantum.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,20 +17,21 @@ import time import os from _parse_args import parser + args = parser.parse_args() os.environ['OMP_NUM_THREADS'] = str(args.omp_num_threads) import numpy as np -from mindquantum.ops import QubitOperator import mindspore.context as context import mindspore.dataset as ds import mindspore.nn as nn -from mindspore import Model -from mindspore.train.callback import LossMonitor -from mindquantum import Hamiltonian -from mindquantum import Circuit -from mindquantum import RX, X, RZ, H -from mindquantum.circuit import UN -from mindquantum.nn import MindQuantumLayer +from mindquantum.core import QubitOperator +from mindquantum.core import Hamiltonian +from mindquantum.core import Circuit +from mindquantum.core import RX, X, RZ, H +from mindquantum.core import UN +from mindquantum.simulator import Simulator +from mindquantum.framework import MQAnsatzOnlyLayer +context.set_context(mode=context.PYNATIVE_MODE, device_target="CPU") def circuit_qaoa(p): @@ -59,25 +61,18 @@ for (v, u) in E: ham = Hamiltonian(ham) circ = circuit_qaoa(p) -ansatz_name = circ.para_name -net = MindQuantumLayer(['null'], ansatz_name, RX('null').on(0) + circ, ham) +ansatz_name = circ.params_name +net = MQAnsatzOnlyLayer( + Simulator('projectq', circ.n_qubits).get_expectation_with_grad(ham, circ)) train_loader = ds.NumpySlicesDataset({ 'x': np.array([[0]]).astype(np.float32), 'y': np.array([0]).astype(np.float32) }).batch(1) - -class Loss(nn.MSELoss): - """Loss""" - def construct(self, base, target): - return self.get_loss(-base) - - -context.set_context(mode=context.GRAPH_MODE, device_target="CPU") -net_loss = Loss() net_opt = nn.Adam(net.trainable_params(), learning_rate=LR) -model = Model(net, net_loss, net_opt) +train_net = nn.TrainOneStepCell(net, net_opt) t0 = time.time() -model.train(ITR, train_loader, callbacks=[LossMonitor()]) +for i in range(ITR): + train_net() t1 = time.time() print('Total time for mindquantum :{}'.format(t1 - t0)) diff --git a/tutorials/benchmarks/qaoa/qaoa_paddle.py b/tutorials/benchmarks/qaoa/qaoa_paddle.py index bc20378ca3f4affa29b4d4897ef343776abbf7d6..4f7292b4cbd9b0a16f87f2ff7923a2f76f67a29d 100644 --- a/tutorials/benchmarks/qaoa/qaoa_paddle.py +++ b/tutorials/benchmarks/qaoa/qaoa_paddle.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright 2021 Huawei Technologies Co., Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tutorials/images/ansatz_classification_of_iris_by_qnn.png b/tutorials/images/ansatz_classification_of_iris_by_qnn.png new file mode 100644 index 0000000000000000000000000000000000000000..70e9f78c8e6e505f096daf27873c3c11cfe19d5b Binary files /dev/null and b/tutorials/images/ansatz_classification_of_iris_by_qnn.png differ diff --git a/tutorials/images/encoder_classification_of_iris_by_qnn.png b/tutorials/images/encoder_classification_of_iris_by_qnn.png new file mode 100644 index 0000000000000000000000000000000000000000..869cc8db507fc20d6c4afa05ac7e7bf393542de1 Binary files /dev/null and b/tutorials/images/encoder_classification_of_iris_by_qnn.png differ diff --git a/tutorials/images/error_circuit.png b/tutorials/images/error_circuit.png new file mode 100644 index 0000000000000000000000000000000000000000..f7664d7a5d40f8c1aff18646d01627b57f2a881f Binary files /dev/null and b/tutorials/images/error_circuit.png differ diff --git a/tutorials/images/faq_circuit1.jpg b/tutorials/images/faq_circuit1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tutorials/images/faq_circuit2.jpg b/tutorials/images/faq_circuit2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tutorials/images/faq_circuit3.jpg b/tutorials/images/faq_circuit3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tutorials/images/faq_circuit4.jpg b/tutorials/images/faq_circuit4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tutorials/images/faq_circuit5.jpg b/tutorials/images/faq_circuit5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tutorials/images/grover_algorithm_circuit.png b/tutorials/images/grover_algorithm_circuit.png new file mode 100644 index 0000000000000000000000000000000000000000..de831ffb981326cefd4558d58a8ec0d78755b4f0 Binary files /dev/null and b/tutorials/images/grover_algorithm_circuit.png differ diff --git a/tutorials/images/quantum_circuit.png b/tutorials/images/quantum_circuit.png index 5c566a46942448d0d600a1207376d880a44087e1..ac3e9299d5b3bff438e981126ce0e6603f166e64 100644 Binary files a/tutorials/images/quantum_circuit.png and b/tutorials/images/quantum_circuit.png differ diff --git a/tutorials/images/quantum_phase_estimation.png b/tutorials/images/quantum_phase_estimation.png new file mode 100644 index 0000000000000000000000000000000000000000..e2d4d4bd0a5002b817c22c498a29e0f88db815c6 Binary files /dev/null and b/tutorials/images/quantum_phase_estimation.png differ diff --git a/tutorials/images/shor's_algorithm_circuit.png b/tutorials/images/shor's_algorithm_circuit.png new file mode 100644 index 0000000000000000000000000000000000000000..83f92cf1978fc08ca332bedd981687e633dd2b60 Binary files /dev/null and b/tutorials/images/shor's_algorithm_circuit.png differ diff --git a/tutorials/iris_classification.ipynb b/tutorials/iris_classification.ipynb deleted file mode 100644 index a54a7eed96377b6f15de02a31e4d014f6d22e428..0000000000000000000000000000000000000000 --- a/tutorials/iris_classification.ipynb +++ /dev/null @@ -1,424 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 利用量子神经网络对鸢尾花进行分类\n", - "\n", - "\n", - "`Linux` `CPU` `全流程` `初级` `中级` `高级`\n", - "\n", - "## 概述\n", - "\n", - "在本次案例中,我们将利用量子神经网络对鸢尾花进行分类。鸢尾花数据集包含150个样本的4个特征数据,分别为花萼长度、宽度和花瓣长度、宽度,这也样本分别属于鸢尾花的三个不同亚属。这里选择数据集中的前100个样本(包含2个鸢尾花品种),并对样本进行训练和预测。\n", - "\n", - "\n", - "## 导入鸢尾花数据集" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "train sample and feature shape: (80, 4)\n" - ] - } - ], - "source": [ - "import os\n", - "os.environ['OMP_NUM_THREADS']='2'\n", - "from sklearn import datasets\n", - "from sklearn import preprocessing\n", - "from sklearn.model_selection import train_test_split\n", - "import numpy as np\n", - "def generate_train_and_test(split=0.8,shuffle=True):\n", - " iris=datasets.load_iris()\n", - " data=iris.data[:100,:].astype(np.float32)\n", - " data=preprocessing.minmax_scale(data)*2-1\n", - " label=np.zeros(100).astype(int)\n", - " label[50:]=1\n", - " return train_test_split(data,label,train_size=split,shuffle=True)\n", - "train_x,test_x,train_y,test_y=generate_train_and_test()\n", - "print('train sample and feature shape: ',train_x.shape)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "初步探索探索数据集,我们画出各个特征以及种类的散点图。" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "import matplotlib.pyplot as plt\n", - "feature_name={0:'speal length',1:'speal width',2:'petal length',3:'petal width'}\n", - "axs=plt.figure(figsize=(18,18)).subplots(4,4)\n", - "for i in range(4):\n", - " for j in range(4):\n", - " if i!=j:\n", - " ax=axs[i][j]\n", - " ax.scatter(train_x[:,i],train_x[:,j],c=train_y)\n", - " ax.set_xlabel(feature_name[i])\n", - " ax.set_ylabel(feature_name[j])\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 编码线路\n", - "我们搭建如下的量子神经网络来对将经典数据编码到量子态上\n", - "\n", - "![encoder](./images/iris_encoder.png)" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/xuxs/gitee/mindquantum/mindquantum/__init__.py:49: UserWarning: [NOTE] Current simulator thread is 2. If your simulation is slow, set OMP_NUM_THREADS to a appropriate number according to your model.\n", - " omp_num_threads))\n" - ] - }, - { - "data": { - "text/plain": [ - "H(0)\n", - "H(1)\n", - "H(2)\n", - "H(3)\n", - "RZ(x0|0)\n", - "RZ(x1|1)\n", - "RZ(x2|2)\n", - "RZ(x3|3)\n", - "X(1 <-: 0)\n", - "RZ(x0,1|1)\n", - "X(1 <-: 0)\n", - "X(2 <-: 1)\n", - "RZ(x1,2|2)\n", - "X(2 <-: 1)\n", - "X(3 <-: 2)\n", - "RZ(x2,3|3)\n", - "X(3 <-: 2)" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from mindquantum import H, RZ, RX, X, Circuit\n", - "def encoder(n):\n", - " c = Circuit([H.on(i) for i in range(n)])\n", - " for i in range(n):\n", - " c += RZ(f'x{i}').on(i)\n", - " for i in range(n - 1):\n", - " c += X.on(i + 1, i)\n", - " c += RZ(f'x{i},{i+1}').on(i + 1)\n", - " c += X.on(i + 1, i)\n", - " return c\n", - "enc=encoder(4).no_grad()\n", - "enc" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 待训练线路\n", - "待训练线路的搭建如下\n", - "\n", - "![ansatz](./images/iris_ansatz.png)" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "====================================================Circuit Summary====================================================\n", - "|Total number of gates : 25. |\n", - "|Parameter gates : 16. |\n", - "|with 16 parameters are : d3_n2_0, d0_n1_0, d3_n0_0, d0_n2_0, d2_n3_0, d2_n0_0, d0_n3_0, d3_n1_0, d3_n3_0, d1_n3_0... |\n", - "|Number qubit of circuit: 4 |\n", - "=======================================================================================================================\n" - ] - } - ], - "source": [ - "from mindquantum.ansatz import HardwareEfficientAnsatz\n", - "from mindquantum import X\n", - "\n", - "ans=HardwareEfficientAnsatz(4, single_rot_gate_seq=[RX], entangle_gate=X, depth=3).circuit\n", - "ans.summary()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 测量哈密顿量\n", - "我们将量子线路的前两个比特上对$Z$算符的测量值作为神经网络输出,因此构建如下的哈密顿量。" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[1.0 [Z0] , 1.0 [Z1] ]" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from mindquantum.ops import QubitOperator\n", - "from mindquantum import Hamiltonian\n", - "\n", - "hams=[Hamiltonian(QubitOperator(f'Z{i}')) for i in [0, 1]]\n", - "hams" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 搭建量子神经网络" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "MindQuantumLayer<>" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from mindquantum.nn import MindQuantumLayer\n", - "\n", - "pqc = MindQuantumLayer(enc.para_name,ans.para_name,enc+ans,hams,n_threads=5)\n", - "pqc" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 训练" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "epoch: 1 step: 16, loss is 0.48822236\n", - "epoch: 2 step: 16, loss is 0.4617273\n", - "epoch: 3 step: 16, loss is 0.4521536\n", - "epoch: 4 step: 16, loss is 0.45006198\n", - "epoch: 5 step: 16, loss is 0.45019674\n", - "epoch: 6 step: 16, loss is 0.4823143\n", - "epoch: 7 step: 16, loss is 0.48250145\n", - "epoch: 8 step: 16, loss is 0.48271722\n", - "epoch: 9 step: 16, loss is 0.48295054\n", - "epoch: 10 step: 16, loss is 0.4832099\n" - ] - } - ], - "source": [ - "import mindspore.dataset as ds\n", - "import mindspore.nn as nn\n", - "import mindspore as ms\n", - "from mindspore.train.callback import Callback\n", - "\n", - "class StepAcc(Callback):\n", - " def __init__(self,model, test_loader):\n", - " self.model = model\n", - " self.test_loader = test_loader\n", - " self.acc = []\n", - "\n", - " def step_end(self, run_context):\n", - " self.acc.append(self.model.eval(self.test_loader, dataset_sink_mode=False)['Acc'])\n", - "\n", - "\n", - "class DataPrep(ms.nn.Cell):\n", - " def __init__(self):\n", - " super(DataPrep, self).__init__()\n", - " self.concat = ms.ops.Concat(axis=1)\n", - " self.pi = np.pi\n", - "\n", - " def construct(self, x):\n", - " y = (self.pi - x[:, :-1]) * (self.pi - x[:, 1:])\n", - " y = self.concat((x, y))\n", - " return y\n", - "\n", - "class QuantumNet(ms.nn.Cell):\n", - " def __init__(self, pqc):\n", - " super(QuantumNet, self).__init__()\n", - " self.dp = DataPrep()\n", - " self.pqc = pqc\n", - "\n", - " def construct(self, x):\n", - " x = self.dp(x)\n", - " x = self.pqc(x)\n", - " return x\n", - "\n", - "batch=5\n", - "train_loader=ds.NumpySlicesDataset({'feats':train_x,'labs':train_y},shuffle=False).batch(batch)\n", - "test_loader=ds.NumpySlicesDataset({'feats':test_x,'labs':test_y}).batch(batch)\n", - "net = QuantumNet(pqc)\n", - "loss = ms.nn.SoftmaxCrossEntropyWithLogits(sparse=True, reduction='mean')\n", - "opti = ms.nn.Adam(net.trainable_params(), learning_rate=1e-1)\n", - "monitor = ms.train.callback.LossMonitor(16)\n", - "model = ms.Model(net, loss, opti, metrics={'Acc': ms.nn.Accuracy()})\n", - "acc=StepAcc(model, test_loader)\n", - "model.train(10, train_loader, callbacks=[monitor, acc], dataset_sink_mode=False)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 画出训练过程中的准确度" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Text(0, 0.5, 'acc')" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAY4AAAEWCAYAAABxMXBSAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/Z1A+gAAAACXBIWXMAAAsTAAALEwEAmpwYAAAtWElEQVR4nO3de5xkZX3n8c+3q6sv3AdmIDjDMGPA6EQS0BFxUaMYdWSNoDER1kTJGtlccBMvSWA1yLL6iiZGYyIxwQTvgoQYnFUSogJmYxAYZLgNGRxQYQaMg4AS6J6uy2//OKeqT1VXd9XpmlNVwPf9evWrq86tnzozfX79PL/noojAzMysV2PDLoCZmT2+OHCYmVkuDhxmZpaLA4eZmeXiwGFmZrk4cJiZWS4OHGZmlosDh5mZ5eLAYWZmuThwmBVI0tmS7pL0iKRtkl6d2fdmSXdk9j0r3X6EpC9I2i3ph5I+MrxPYLbQ+LALYPYEdxfwAuD7wC8Bn5F0FPB84DzgVGAL8JNARVIJ+BJwFfCrQA3YOPBSmy1BnqvKbHAkbQXeDfwWcEVEfLht//OAzcDhEVEdfAnNunNTlVmBJL1B0lZJD0t6GHgmsBI4gqQ20u4I4HsOGjbK3FRlVhBJRwIfA14CXBsRtbTGIeBekuapdvcCayWNO3jYqHKNw6w4+wIB7AaQ9GskNQ6AvwHeIenZShyVBprrgfuB90naV9KUpBOHUXizxThwmBUkIrYBfwpcC/wHcAzwjXTf3wHvBT4HPAJcDhwcETXgF4CjgHuAncDrBl12s6U4OW5mZrm4xmFmZrk4cJiZWS4OHGZmlosDh5mZ5fKkGMexcuXKWLdu3bCLYWb2uHLjjTc+EBGr2rc/KQLHunXr2LJly7CLYWb2uCLpe522u6nKzMxyceAwM7NcHDjMzCwXBw4zM8vFgcPMzHIpNHBI2iRpu6Qdks7usP9ISV+TdIukayStyeyrpesYbJW0ObN9vaTr0mt+XtJEkZ/BzMxaFRY40iUwLwBeAWwATpe0oe2wDwCfioifAc4H/iizbyYijk2/XpXZ/n7gQxFxFPAQ8KaiPoOZmS1U5DiO44EdEXE3gKRLgFOAbZljNgBvS19fTTK19KIkCTgJ+G/ppk+SrNv80b1V6Lwigk/+23d58NG55rYDpsv82onrKY1pIGXY8YP/ZPPN90EEx68/hOcfvbLrORHB3924k1OOfQqT46Xm9h/8eJaLr7+XWr1eZJHNbEDe+F/Wcch+k3v1mkUGjtUkq5k17ASe23bMzcBrgA8Drwb2l3RIRPwQmJK0BagC74uIy4FDgIczK6PtTH/OApLOBM4EWLt27V75QJ3c++AM5/3fbenPhMYs9Sc89RCeufrAwn5u1t/+63e4+Pp7APipw/6DK9/6wq7n3H7fj/n9y25hxT4TvHTDYc3tl2/dxYe+eieQfB4ze3x71bGrH1eBoxfvAD4i6QzgX4BdQC3dd2RE7JL0VOAqSbcCP+r1whFxIXAhwMaNGwtbdGRPNSnuX5x+HL/ws0/hGzse4PV/cx2PzdW6nLn3PDZX5chD9uFZa1ew5XsP9nTOf+6pNs/NenRPUu7v/NHJyJHDzDooMnDsAo7IvF+TbmuKiPtIahxI2g/4xYh4ON23K/1+t6RrgOOAvwcOyqzHvOCag1apJTGpXEoeslPlpNlnpjK4wDEzV2O6XGKqXGJmrrcmpkb5ZtoC3GylxlR5zEHDzBZVZK+qG4Cj015QE8BpwObsAZJWSmqU4RzgonT7CkmTjWOAE4FtkSxXeDXw2vScNwJfLPAzdFVNcwGlseRjTDcCxwBrHDOVGlPlEtPlErM9BqzZtHztAW6mUmt+BjOzTgoLHGmN4CzgSuAO4NKIuF3S+ZIavaReBGyXdCdwGMkazADPALZIupkkULwvXb8Z4A+At0naQZLz+NuiPkMvGjWO8bTGMT2RPHR7fYDvDbPpw356YoyZSo1elgNu1jjaA8ecA4eZLa3QHEdEXAFc0bbt3Mzry4DLOpz3b8Axi1zzbpIeWyOhWktqHOX2Gscgm6oqNQ7dv8x0uUStHlRqwcT40k1NjfLNzi2scUxNOHCY2eI8crxP1XpbjWMYTVWZHAf0FrRmFmmqmnVTlZl14cDRp0qjxtFIjk8kt3SQNY7ZSj3JceRoJptdrKnKgcPMunDg6FO1keNIm6omSmOMabA5jplKjemJsVy1nfleVa29sGbmas0AZGbWiQNHnxq9qhpNVZKYLpeG0lSVJ78yW6mn39trHPVmk5eZWScOHH2aH8cxfyunJ0oDa6qKiGbzUiOp3VOOY5GmKuc4zKwbB44+NWscmXmppsql5l/0RdtTTX7O1MR8jaOnHEdaI2o/1oHDzLpx4OhTxxpHjoF4/Wr8nGxTVS8/e9FxHBXnOMxsaQ4cfaq2DQCEwTZVzWQDR6OpqodpRxabcmRmruYch5ktyYGjT/NNVfO3cmp8cMnxxs+ZKpeYGs8/jiNbO6nXgz3VOlNl/7cws8X5CdGn9kkOIck3DLrGMVUu5RpD0mkcx2x1vvZiZrYYB44+NaYcGW/JcYwNPseRTY7nGscxf2zjtXMcZrYUB44+NaccyfSqmi4PsMaR5jNyTznSmKsq0/srW3sxM1uMA0ef5qccaRvHMagcRyY5Xi6NUS6pxxxHUu65Wr1Za8r20DIzW4wDR5+qtUCiZX3xqUHWOJpNVWPzP7uHoLWnJbeRBI5s7cXMbDEOHH2q1qM5pXrDQMdxzLU2L/X6s2cqNQ6YSmbVb58p1zkOM1uKA0efqrV6yxgOSB7elVo0m7GKNNPWvNTLGJJKrU61Hhy87wSwsIeVcxxmtpRCA4ekTZK2S9oh6ewO+4+U9DVJt0i6RtKadPuxkq6VdHu673WZcz4h6TuStqZfxxb5Gbqp1qMlMQ6DXQWwvZbQywSLjXNWpIGjvYeVm6rMbCmFBQ5JJeAC4BXABuB0SRvaDvsA8KmI+BngfOCP0u2PAW+IiJ8GNgF/JumgzHm/FxHHpl9bi/oMvajU6i2JcSBX76Z+NQcApoP/esmvNJq3Dt5nouUas26qMrMeFFnjOB7YERF3R8QccAlwStsxG4Cr0tdXN/ZHxJ0R8e309X3AD4BVBZZ12aq16NhUBbBnABMdzlZrTI6PMTY2vwJht5/bXuOYrbQFDtc4zGwJRQaO1cC9mfc7021ZNwOvSV+/Gthf0iHZAyQdD0wAd2U2vzdtwvqQpMlOP1zSmZK2SNqye/fufj7Hkir1est0IzD/F/sgahyzbQsv9ZLjaOw/uL2pyoHDzHow7OT4O4Cfk3QT8HPALqD51JN0OPBp4NciovFn9DnA04HnAAcDf9DpwhFxYURsjIiNq1YVV1mp1qJluhGgOdfTIMZyzFRqzWaqxs/uGjjScq3Yp3NyfNJzVZnZEsYLvPYu4IjM+zXptqa0Geo1AJL2A34xIh5O3x8AfBl4Z0R8M3PO/enLPZI+ThJ8hqZar7dMNwIDznFU6i01jl7GcczXOMot72fnakgwOe7AYWaLK/IJcQNwtKT1kiaA04DN2QMkrZTUKMM5wEXp9gngH0gS55e1nXN4+l3AqcBtBX6Griq1Dr2qBpwcz3af7WUcR2P/Qc3keDoAMF3EKbm1ZmadFRY4IqIKnAVcCdwBXBoRt0s6X9Kr0sNeBGyXdCdwGPDedPsvAy8EzujQ7fazkm4FbgVWAu8p6jP0otqhV1WzO+4AmqqSFftaF5Hq2qsqTZ53ynE4v2Fm3RTZVEVEXAFc0bbt3Mzry4DLOpz3GeAzi1zzpL1czL5U69Ey3QgMuMZR6Zwcj4hFaw6L5jjm6h78Z2ZduTG7T8k4juE2VWVrCVPlEhHza5F3PCct1wFT45TG1DKOw2M4zKwbB44+VWuxoDvuVHMJ18E0VbXnOBrblzoH5tfwcFOVmeXhwNGnSn3xAYCDmnIk+7DvZQxJy3Kzme677bUXM7NOHDj61Ck5Xi6NMT7W27oY/VqQ4yh3r+3MVGqUS6JcGmOqXGom8WcqtWZtycxsMQ4cfap26I4LjckGBzA7boccB3SpcWSat7JNVe09tMzMOvFTok+V+sIaByR5jtlqsTWOej3YU23tCTU/M+/iQWs207w1PVFqmavKTVVm1o0DR586TXII6UC8gpPjjZ5TnZqqlsqvzGTmt5pqT467qcrMunDg6FO1tnCSQ+htzqh+NRdeykwR0ss8WdmEetJUVW+eMznuwGFmS3Pg6FOlvnCSQ+htBHe/Oi312ssYktlKncnsUrPNcRx11zjMrCsHjj51WjoWeptssF8zbeuNZ193S443kuCNkebVWp25Wt05DjPryoGjT50GAEJr0rkonRZe6mXZ2mwSvJHjmG3kSxw4zKwLB44+Jb2qRrCpaqkcRyY53miqatZe3FRlZl04cPQp6VXVocYxiMAxt7DGkXscx0SSxPeysWbWKweOPkQE1XpQ7jAAcGqi+AGAzV5VmYd9aUxMjC/do2u2rVdVtR78eLbSfG9mthQHjj7U6gGwaI1jYDmOtualbmNIsqPNG0HnoUfTwDHh/xJmtjQ/JfpQbQaOxXMcEVHYz+/UVJX92Z1ERMtAv8b3Bx+bA/B6HGbWVaGBQ9ImSdsl7ZB0dof9R0r6mqRbJF0jaU1m3xslfTv9emNm+7Ml3Zpe8881xHVOK7WkKaq8SK+qWj2o1AoMHIvkJZIutp2byeZqdepBy1xVAA+ngcNNVWbWTWErAEoqARcALwV2AjdI2hwR2zKHfYBkXfFPSjoJ+CPgVyUdDLwb2AgEcGN67kPAR4E3A9eRrC64CfjHoj7HUqq1xWscjQfzrodn2G8yuc0HTI/nHpn9n3uqi/aQevDR9GHf1lQ1VS7xo5kKux/Zs+CcR9pyGY3vux6a6XgtM7N2RS4dezywIyLuBpB0CXAKkA0cG4C3pa+vBi5PX78c+EpEPJie+xVgk6RrgAMi4pvp9k8BpzKkwFGpJ3/Vd8px7J8Gixd/4JrmtqMO3Y+vvu3ner7+Dx6Z5fnvu5q52uJJ9nJJTI63/vz9J8f5lzt385z3fnXR8/abGm/5/tf/cnfyfrLQ1YTN7AmgyKfEauDezPudwHPbjrkZeA3wYeDVwP6SDlnk3NXp184O2xeQdCZwJsDatWuX/SGW0qhxdOpVdfLPHE4QzKXH/PPt3+f67zyY6/oPPDLHXK3O6cevZcNTDuh4zLpD9lmwtvj5p/40N3z3oUWvO1ES//WYwwF47vpD+NNf+lkeq9Q4ZN8J1qzYJ1cZzezJZ9h/Xr4D+IikM4B/AXYBe6UrUkRcCFwIsHHjxkISDfNNVQtrHPtNjvO658wHrB/+5x7+37cfoF4PxjoEmo7XT2s0L3n6ofz8hsN6LtfTf+IAnv4TnQNNu4nxMX7x2Wu6H2hmlioyOb4LOCLzfk26rSki7ouI10TEccA7020PL3HurvT1otccpEZTVaeR4+2a053nWKOjskQOxcxsWIoMHDcAR0taL2kCOA3YnD1A0kpJjTKcA1yUvr4SeJmkFZJWAC8DroyI+4EfSzoh7U31BuCLBX6GJTVrHB16VbVrrgWeY+LDaqPXVocajZnZsBT2RIqIKnAWSRC4A7g0Im6XdL6kV6WHvQjYLulO4DDgvem5DwL/hyT43ACc30iUA78F/A2wA7iLISXGYb47bi81gl6mAmnXHCfSY9OWmdkgFJrjiIgrSLrMZredm3l9GXDZIudexHwNJLt9C/DMvVvS5Wk82HM1VeUIHPOByTUOMxsdfiL1odGU1FNTVXPW2t7nr2r22nKOw8xGiANHH/Ikr5s5jlxNVb0HJjOzQfETqQ95HuzLyXFUXOMwsxHkwNGHpaYcabecHEd1iZHpZmbD4idSH5aa5LBdL0u6Lry+e1WZ2ehx4OjDUtOqt5sqJ7c63ziORlOV/5nMbHT4idSHZo0jR1PVspLjznGY2Qhx4OhDnpHjfSXH3avKzEaIn0h9yFMjmBwfQ2LJJV0XXD/HyHQzs0Fx4OhDJUcOQtKSS7p2kieHYmY2KA4cfZgfOd7bgz1v4MjTa8vMbFD8ROrDfI2gt9s4VS7lnnJkTPS8foeZ2SA4cPQhzySHkIzlyDWOo1734D8zGzl+KvUhzySHkL+pqlqLjsvSmpkNkwNHH/LOJTVdLuUaAFirh2scZjZyCn0qSdokabukHZLO7rB/raSrJd0k6RZJJ6fbXy9pa+arLunYdN816TUb+w4t8jMspVqvUxoTyWKE3U1N5E+Oe4JDMxs1hS3kJKkEXAC8FNgJ3CBpc0Rsyxz2LpKVAT8qaQPJok/rIuKzwGfT6xwDXB4RWzPnvT5d0GmoqrXINY/UdHmMH/w4X1NVyU1VZjZiiqxxHA/siIi7I2IOuAQ4pe2YAA5IXx8I3NfhOqen546cSi1yzSM1VV5Gctxdcc1sxBT5VFoN3Jt5vzPdlnUe8CuSdpLUNt7S4TqvAy5u2/bxtJnqD7VIO5GkMyVtkbRl9+7dy/oA3VTr9VyD85aVHHdTlZmNmGH/OXs68ImIWAOcDHxaUrNMkp4LPBYRt2XOeX1EHAO8IP361U4XjogLI2JjRGxctWpVIYWv1CJXjWAqZ3K86u64ZjaCinwq7QKOyLxfk27LehNwKUBEXAtMASsz+0+jrbYREbvS748AnyNpEhuKas7kdTKOo/cBgJWcORQzs0EoMnDcABwtab2kCZIgsLntmHuAlwBIegZJ4Nidvh8DfplMfkPSuKSV6esy8ErgNoakWo/cTVVztXpz/EfX69fqXovDzEZOYb2qIqIq6SzgSqAEXBQRt0s6H9gSEZuBtwMfk/RWkkT5GRER6SVeCNwbEXdnLjsJXJkGjRLwVeBjRX2Gbiq1eq55pJrLx1br7NdDQMgbmMzMBqGwwAEQEVeQJL2z287NvN4GnLjIudcAJ7RtexR49l4v6DJVa/ke7FPp8rEzczX2m+x+6/MGJjOzQfBTqQ/VnN1lmzWOHntW5Q1MZmaD4MDRh0rO7rJ5l4+teMoRMxtBfir1IW932emJ5Nheu+RWa3VPcmhmI8eBow95u8vmXXfcTVVmNoocOPqQt7ts/qYqDwA0s9Hjp1If8naXbdQ4ZntuqvJ6HGY2ehw4+pC3qWp+HEfvOQ7XOMxs1Pip1IdqLWd33OY4jt5GjlfqnuTQzEaPA0cflttU1Xty3NOqm9no8VOpD5VlJsc9ANDMHs8cOPqQdwXAckmUxtTzOI5K3ZMcmtno8VOpD3kHAErKtZhT3sBkZjYIDhx9qC4jeT3VY+CIiDSH4n8iMxstPT2VJL1a0oGZ9wdJOrWwUj1OVHOuAAjJtCO9jOOo1pPZ5V3jMLNR0+tT790R8aPGm4h4GHh3ISV6HKnkXAEQel93vFpLA4eT42Y2YnoNHJ2OK3Qtj8eD5Sy01GvgqNSTsR5ej8PMRk2vT6Utkj4o6SfTrw8CN3Y7SdImSdsl7ZB0dof9ayVdLekmSbdIOjndvk7SjKSt6ddfZc55tqRb02v+uaSh/EkeEdTq+ZuqpsqlnnpVucZhZqOq16feW4A54PMka4DPAr+91AmSSsAFwCuADcDpkja0HfYu4NKIOI5kTfK/zOy7KyKOTb9+I7P9o8CbgaPTr009foa9qpI+2JeTHO9lHEdjXXInx81s1PTU3JQu2bqgxtDF8cCOxprhki4BTgG2ZS8NHJC+PhC4b6kLSjocOCAivpm+/xRwKvCPOcu2LJdcfw9PXbUfx68/mGp9eQ/26XKJ+zOB4/KbdnHIfhO84OhVLcc1kuOe5NDMRk2vvaq+IumgzPsVkq7sctpq4N7M+53ptqzzgF+RtJNkbfK3ZPatT5uwvi7pBZlr7uxyzUYZz5S0RdKW3bt3dylqbz74lTv53HXfA2CumgSOibyBY6I1x/EXV32bT/7bdxccN99U5RqHmY2WXp9KK9OeVABExEPAoXvh558OfCIi1gAnA5+WNAbcD6xNm7DeBnxO0gFLXGeBiLgwIjZGxMZVq1Z1P6EHM5Uas5V68zXMT1zYq6Span6Sw9lKveV9QzM57hyHmY2YXgNHXdLaxhtJ60iamZayCzgi835Nui3rTcClABFxLTBFEqT2RMQP0+03AncBT0vPX9PlmoWZrdSaAaOR4G7MP9Wr6XKpZRzHTOaaWc0ah3tVmdmI6fWp9E7gXyV9WtJngK8D53Q55wbgaEnrJU2QJL83tx1zD/ASAEnPIAkcuyWtSpPrSHoqSRL87oi4H/ixpBPS3lRvAL7Y42foS6VWp1KL+cCRfp/KGzgmxloCxcxcrWMvq0ozOe4ah5mNll6T4/8kaSNwJnATcDkw0+WcqqSzgCuBEnBRRNwu6XxgS0RsBt4OfEzSW0lqMGdEREh6IXC+pApQB34jIh5ML/1bwCeAaZKk+EAS442eUO3f8zZVTZdLVOtBpVZnfExp81eHGkd9eb22zMyK1lPgkPTrwO+QNA1tBU4ArgVOWuq8iLiCJOmd3XZu5vU24MQO5/098PeLXHML8Mxeyr03tTdRNfISeZuqsmtyNBLrnZuq0hqHm6rMbMT0+lT6HeA5wPci4sXAccDDRRVqFO1JA0Vj2ddl5zgm5tcdn21r9sqqeACgmY2oXqcNmY2IWUlImoyIf5f0U4WWbMTM1zjae1XlH8fROL8WSXDo3FTV6FXlGoeZjZZeA8fOdBzH5cBXJD0EfK+oQo2i+SaqPpPj2cBRbwSOOvV6MJYZ7Dffq8o1DjMbLb0mx1+dvjxP0tUko7z/qbBSjaBsb6qImE+O581xpE1VM3M1auPzPZr3VOstifZGryrXOMxs1OSe4TYivl5EQUZdI3DU6pF0y51bXq+qqfH5Gkc95gPHTKXWcq3mehzOcZjZiHnST43eq8UG7TUCQa+ayfFKjXpmwHh7grziXlVmNqIcOHqUfbA3RpBPjI+15CV60cxxzNWpZe5++yDA6jJn3zUzK5oDR4/aR3vPztVy5zegc68qWNizarmz75qZFc2Bo0czHZqqlhM4ptLuu0lTVWuOI6u53od7VZnZiHHg6FG2RpAEjnruxDjM1zhmM91xoVNTlWscZjaaHDh61JLjSCcmzDuGAzJTjszVqI4vXuNo9KoqucZhZiPGgaNHLWtoVGvsqdaYLuevDZRLY5RL6prjWO7StGZmRXPg6FFrcrzOzFxtWU1VkNQ62gPHok1V7o5rZiPGT6Uezc7VUPrHfz/JcUgXc6rUWq65oMbhadXNbEQ5cPRoplJjxT4TzdczleXlOCBdd3yu1nbN1uVjq7U6pTEhOXCY2Whx4OhR8pAvA+mU6MscxwFJjaPRM+vA6TJS5+S4Jzg0s1FUaOCQtEnSdkk7JJ3dYf9aSVdLuknSLZJOTre/VNKNkm5Nv5+UOeea9Jpb069Di/wMDTNzC2scy81xTJZLzFTSPEm51Gy6yqrU6p7g0MxGUmHJ8XTN8AuAlwI7gRskbU5X/Wt4F3BpRHxU0gaS1QLXAQ8AvxAR90l6Jsnys6sz570+XQlwYGYrNVbsO8FEaaz/pqryGLNzNerjwfREEjg6TTniCQ7NbBQV+Sft8cCOiLg7IuaAS4BT2o4J4ID09YHAfQARcVNE3Jduvx2YljRZYFm7mqnUmBovMVke47E9VWYr9T4CR6klwd7oZZVVrdfdo8rMRlKRT6bVwL2Z9ztprTUAnAf8iqSdJLWNt3S4zi8C34qIPZltH0+bqf5Qi2SPJZ0paYukLbt37172h2hoNE1Nl0s8PFMB8q/F0TA9kQaOdBDhVHms45Qj7lFlZqNo2H/Sng58IiLWACcDn5bULJOknwbeD/yPzDmvj4hjgBekX7/a6cIRcWFEbIyIjatWreq7oDNzSQ1jeqLEg4/OASxrACCk4zjSNcenJ5JrznYYx+GmKjMbRUUGjl3AEZn3a9JtWW8CLgWIiGuBKWAlgKQ1wD8Ab4iIuxonRMSu9PsjwOdImsQKN1uZT2Q//Fha41hmcryRDE+av8aaTVdZ1XpQdlOVmY2gIp9MNwBHS1ovaQI4Ddjcdsw9wEsAJD2DJHDsTtc3/zJwdkR8o3GwpHFJjcBSBl4J3FbgZ2hKmqrGmCqXeOixpMbRT45jtjJf4+iY43By3MxGVGGBIyKqwFkkPaLuIOk9dbuk8yW9Kj3s7cCbJd0MXAycERGRnncUcG5bt9tJ4EpJtwBbSWowHyvqMzRUanVq9WjWOB5qNlX1l+N4LNMdd0GvKifHzWxEFTpXVURcQZL0zm47N/N6G3Bih/PeA7xnkcs+e2+WsRfNZWLTHMejy1xvvGGqXKIesKc6nzfpNMmhk+NmNor8J20PZjOBIlvL6GfkePP1RGMAYNuUI/W61+Iws5HkJ1MPGjWOxpiLhn7mqmq+XmQcR6XmKUfMbDQ5cPQgGzimJ+ZvWT+9qrKvGzmPrKqnHDGzEeUnUw8aieuptqaq5dY4pjLjPxrXnKvWW5aSrdbdq8rMRpMDRw8Wa6pabo6j/RqNQJJNkCdNVf7nMbPR4ydTD2b3cuBY0FTVWIc8EziSpirXOMxs9Dhw9GBmLunxNFVubaqaHF/e7WtJjqeDCpOfkwkc9XCvKjMbSX4y9aA1OZ485KfKY4wts9dTe56kcc3Wpqq6e1WZ2Uhy4OhBcwDgxFjzob/cZipYmOPo3FTl7rhmNpocOHqwp0OOo5/A0dpUlQkcLU1VHgBoZqPJT6YeNLvjZpuqljmGAxYmxxvXmmnrVeXkuJmNIgeOHsxUapRLolza+01V2YT7bFuvKnfHNbNR5CdTD7Lri++NwFEaExPjY0hJz6z5wDE/X1Wl7hqHmY2mQmfHfaJoLOIENKccWe50Iw3T5RIlCUnNa7WP4/DIcTMbRQ4cPZiZq2W64bZ+X67pcolS2muqfRxHvR7UAzdVmdlIKvTJJGmTpO2Sdkg6u8P+tZKulnSTpFsknZzZd0563nZJL+/1mkWYydQ49lbgmCqPZa451vw5AJV60mTlpiozG0WFBQ5JJeAC4BXABuB0SRvaDnsXycqAx5EsLfuX6bkb0vc/DWwC/lJSqcdr7nUzlXqHHEd/t24qM0fVRGmMMc0nx6u1ZLJDd8c1s1FUZFPV8cCOiLgbQNIlwCnAtswxARyQvj4QuC99fQpwSUTsAb4jaUd6PXq45l43O7ewxtFPchySHMl4LalRSGK6XOLuBx7l2rt+yGNzVQAPADSzkVRk4FgN3Jt5vxN4btsx5wH/LOktwL7Az2fO/WbbuavT192uCYCkM4EzAdauXZu/9BkzlRqr9p8Ekh5RK/eb5NADpvq65mH7T1GpzfeiOmS/Sb58y/18+Zb7m9sOnC739TPMzIow7OT46cAnIuJPJT0P+LSkZ+6NC0fEhcCFABs3bowuhy8p6Y4732x0xf98Pgf0+VB//2t/JqlvpS4+8wTu+eFjzfflkjj2iIP6+hlmZkUoMnDsAo7IvF+Tbst6E0kOg4i4VtIUsLLLud2uudfNZsZxAH3XNmBhbWL1QdOsPmi67+uamRWtyOzrDcDRktZLmiBJdm9uO+Ye4CUAkp4BTAG70+NOkzQpaT1wNHB9j9fc67LjOMzMnuwKq3FERFXSWcCVQAm4KCJul3Q+sCUiNgNvBz4m6a0kDTdnREQAt0u6lCTpXQV+OyJqAJ2uWdRnaJiZc+AwM2soNMcREVcAV7RtOzfzehtw4iLnvhd4by/XLFJEJOM4+hwpbmb2ROGBAl3M1erUo/8Bf2ZmTxQOHF3MpsvGuqnKzCzhwNFFc9lYN1WZmQEOHF1l1xs3MzMHjq6yq/+ZmZkDR1duqjIza+XA0cWsm6rMzFo4cHTRCBxTfU6jbmb2ROGnYRdOjpuZtXLg6MLJcTOzVg4cXcw6OW5m1sKBows3VZmZtXLg6GImnXLETVVmZgkHji5mKjUmxscoef1vMzPAgaMrL+JkZtbKgaMLL+JkZtaq0MAhaZOk7ZJ2SDq7w/4PSdqaft0p6eF0+4sz27dKmpV0arrvE5K+k9l3bJGfwYs4mZm1KmwFQEkl4ALgpcBO4AZJm9NV/wCIiLdmjn8LcFy6/Wrg2HT7wcAO4J8zl/+9iLisqLJnzVRqToybmWUUWeM4HtgREXdHxBxwCXDKEsefDlzcYftrgX+MiMcKKGNXSY7DLXpmZg1FPhFXA/dm3u9Mty0g6UhgPXBVh92nsTCgvFfSLWlT1+Qi1zxT0hZJW3bv3p2/9KlZN1WZmbUYlT+lTwMui4hadqOkw4FjgCszm88Bng48BzgY+INOF4yICyNiY0RsXLVq1bILNlOpMTXuwGFm1lBk4NgFHJF5vybd1kmnWgXALwP/EBGVxoaIuD8Se4CPkzSJFWZmrsaUaxxmZk1FBo4bgKMlrZc0QRIcNrcfJOnpwArg2g7XWJD3SGshSBJwKnDb3i12q9lK3d1xzcwyCutVFRFVSWeRNDOVgIsi4nZJ5wNbIqIRRE4DLomIyJ4vaR1JjeXrbZf+rKRVgICtwG8U9Rkg7Y7rwGFm1lRY4ACIiCuAK9q2ndv2/rxFzv0uHZLpEXHS3ithdzNzTo6bmWWNSnJ8JEWEx3GYmbVx4FjCnmoyM66bqszM5jlwLKGx+p8HAJqZzfMTcQkzXv3PzGwBB44lNAKHcxxmZvMcOJYw62VjzcwWcOBYwqybqszMFnDgWILXGzczW8iBYwkzbqoyM1vAgWMJTo6bmS3kwLGE2TnnOMzM2jlwLMFNVWZmCzlwLMGBw8xsIQeOJTSmHJkc920yM2vwE3EJs5UaU+UxxsY07KKYmY0MB44leBEnM7OFCg0ckjZJ2i5ph6SzO+z/kKSt6dedkh7O7Ktl9m3ObF8v6br0mp9Pl6UtxKwDh5nZAoUFDkkl4ALgFcAG4HRJG7LHRMRbI+LYiDgW+AvgC5ndM419EfGqzPb3Ax+KiKOAh4A3FfUZZip1ptwV18ysRZE1juOBHRFxd0TMAZcApyxx/OnAxUtdUJKAk4DL0k2fBE7tv6idzcy5xmFm1q7IwLEauDfzficd1hAHkHQksB64KrN5StIWSd+UdGq67RDg4Yio9nDNM9Pzt+zevXtZH+C4tQfxwqetWta5ZmZPVOPDLkDqNOCyiKhlth0ZEbskPRW4StKtwI96vWBEXAhcCLBx48ZYTqF++8VHLec0M7MntCJrHLuAIzLv16TbOjmNtmaqiNiVfr8buAY4DvghcJCkRsBb6ppmZlaAIgPHDcDRaS+oCZLgsLn9IElPB1YA12a2rZA0mb5eCZwIbIuIAK4GXpse+kbgiwV+BjMza1NY4EjzEGcBVwJ3AJdGxO2SzpeU7SV1GnBJGhQangFskXQzSaB4X0RsS/f9AfA2STtIch5/W9RnMDOzhdT6vH5i2rhxY2zZsmXYxTAze1yRdGNEbGzf7pHjZmaWiwOHmZnl4sBhZma5OHCYmVkuT4rkuKTdwPeWefpK4IG9WJy9ZVTLBaNbNpcrn1EtF4xu2Z5o5ToyIhZMn/GkCBz9kLSlU6+CYRvVcsHols3lymdUywWjW7YnS7ncVGVmZrk4cJiZWS4OHN1dOOwCLGJUywWjWzaXK59RLReMbtmeFOVyjsPMzHJxjcPMzHJx4DAzs1wcOJYgaZOk7ZJ2SDp7iOU4QtLVkrZJul3S76TbD5b0FUnfTr+vGFL5SpJukvSl9P16Sdel9+3z6bT6gy7TQZIuk/Tvku6Q9LwRul9vTf8db5N0saSpYdwzSRdJ+oGk2zLbOt4jJf48Ld8tkp414HL9SfpveYukf5B0UGbfOWm5tkt6eVHlWqxsmX1vlxTpUhBDv2fp9rek9+12SX+c2d7fPYsIf3X4AkrAXcBTgQngZmDDkMpyOPCs9PX+wJ3ABuCPgbPT7WcD7x9S+d4GfA74Uvr+UuC09PVfAb85hDJ9Evj19PUEcNAo3C+SpY6/A0xn7tUZw7hnwAuBZwG3ZbZ1vEfAycA/AgJOAK4bcLleBoynr9+fKdeG9HdzkmT56buA0iDLlm4/gmQJie8BK0fknr0Y+Cowmb4/dG/ds4H+0jyevoDnAVdm3p8DnDPscqVl+SLwUmA7cHi67XBg+xDKsgb4GnAS8KX0l+SBzC95y30cUJkOTB/Oats+CvdrNXAvcDDJ0s1fAl4+rHsGrGt72HS8R8BfA6d3Om4Q5Wrb92rgs+nrlt/L9OH9vEHes3TbZcDPAt/NBI6h3jOSP0Z+vsNxfd8zN1UtrvEL3rAz3TZUktaRLKN7HXBYRNyf7vo+cNgQivRnwO8D9fT9IcDDkSzkBcO5b+uB3cDH0ya0v5G0LyNwvyJZEvkDwD3A/cCPgBsZ/j1rWOwejdLvw38n+UseRqBckk4BdkXEzW27hl22pwEvSJtAvy7pOXurXA4cjyOS9gP+HvjdiPhxdl8kfzoMtG+1pFcCP4iIGwf5c3swTlJt/2hEHAc8StLs0jSM+wXJssjAKSTB7SnAvsCmQZejF8O6R0uR9E6gCnx22GUBkLQP8L+Ac4ddlg7GSWq2JwC/B1wqSXvjwg4ci9tF0m7ZsCbdNhSSyiRB47MR8YV0839IOjzdfzjwgwEX60TgVZK+C1xC0lz1YeAgSePpMcO4bzuBnRFxXfr+MpJAMuz7BfDzwHciYndEVIAvkNzHYd+zhsXu0dB/HySdAbwSeH0a1EahXD9J8kfAzenvwRrgW5J+YgTKthP4QiSuJ2kVWLk3yuXAsbgbgKPT3i4TJGujbx5GQdK/Ev4WuCMiPpjZtRl4Y/r6jSS5j4GJiHMiYk1ErCO5P1dFxOtJ1ol/7RDL9X3gXkk/lW56CbCNId+v1D3ACZL2Sf9dG2Ub6j3LWOwebQbekPYUOgH4UaZJq3CSNpE0ib4qIh5rK+9pkiYlrQeOBq4fVLki4taIODQi1qW/BztJOrJ8nyHfM+BykgQ5kp5G0knkAfbGPSsyifR4/yLpFXEnSa+Ddw6xHM8naTK4Bdiafp1Mkk/4GvBtkt4TBw+xjC9ivlfVU9P/iDuAvyPt1THg8hwLbEnv2eXAilG5X8D/Bv4duA34NEnvloHfM+BikjxLheSB96bF7hFJp4cL0t+FW4GNAy7XDpJ2+cb//7/KHP/OtFzbgVcM+p617f8u88nxYd+zCeAz6f+zbwEn7a175ilHzMwsFzdVmZlZLg4cZmaWiwOHmZnl4sBhZma5OHCYmVkuDhxmAyLpd9ORxmaPa+6OazYg6cjijRHxwLDLYtYP1zjMCiBpX0lflnRzuu7Gu0nmprpa0tXpMS+TdK2kb0n6u3QuMiR9V9IfS7pV0vWSjhrmZzFr58BhVoxNwH0R8bMR8UySWYTvA14cES9OF/t5F8m0188iGeX+tsz5P4qIY4CPpOeajQwHDrNi3Aq8VNL7Jb0gIn7Utv8EkgV1viFpK8m8UEdm9l+c+f68ogtrlsd490PMLK+IuDNdKvRk4D2SvtZ2iICvRMTpi11ikddmQ+cah1kBJD0FeCwiPgP8Ccm07o+QLP0L8E3gxEb+Is2JPC1ziddlvl87mFKb9cY1DrNiHAP8iaQ6yYylv0nS5PRPku5L8xxnABdLmkzPeRfJbMwAKyTdAuwBFquVmA2Fu+OajRh327VR56YqMzPLxTUOMzPLxTUOMzPLxYHDzMxyceAwM7NcHDjMzCwXBw4zM8vl/wMNRIVfwmw+cgAAAABJRU5ErkJggg==\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "plt.plot(acc.acc)\n", - "plt.title('acc')\n", - "plt.xlabel('step')\n", - "plt.ylabel('acc')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 预测" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'Acc': 0.95}\n" - ] - } - ], - "source": [ - "predict = np.argmax(ms.ops.Softmax()(model.predict(ms.Tensor(test_x))), axis=1)\n", - "corr = model.eval(test_loader, dataset_sink_mode=False)\n", - "print(corr)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.5" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/tutorials/quantum_approximate_optimization_algorithm.ipynb b/tutorials/quantum_approximate_optimization_algorithm.ipynb deleted file mode 100644 index b56542ebbbb18092ac282ce2e77b1b5121fa8c11..0000000000000000000000000000000000000000 --- a/tutorials/quantum_approximate_optimization_algorithm.ipynb +++ /dev/null @@ -1,383 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 量子近似优化算法\n", - "\n", - "\n", - "`Linux` `CPU` `全流程` `初级` `中级` `高级`\n", - "\n", - "[![](https://gitee.com/mindspore/docs/raw/master/tutorials/training/source_zh_cn/_static/logo_source.png)](https://gitee.com/mindspore/mindquantum/blob/master/tutorials/quantum_approximate_optimization_algorithm.ipynb)\n", - "\n", - "## 概述\n", - "\n", - "量子近似优化算法(Quantum Approximate Optimization Algorithm,QAOA)是利用量子计算机来近似解决组合优化问题的量子算法,最早由Farhi等人于2014年提出。在本教程里,我们将利用QAOA算法来解决最大割问题(Max-Cut),来熟悉MindQuantum中量子线路的搭建和训练。\n", - "\n", - "> 本文档适用于CPU环境。 \n", - "> 你可以在这里找到完整的可运行的样例代码:。\n", - "\n", - "## 环境准备\n", - "\n", - "本教程所需要的额外库:\n", - "\n", - "- networkx\n", - "\n", - "> `networkx`是创建、操作和研究复杂网络的结构、动态和功能库。可通过`pip3 install networkx`来进行安装。\n", - "\n", - "## Max-Cut问题描述\n", - "\n", - "Max-Cut问题是图论中的一个NP-complete问题,它需要将一个图中的顶点分成两部分,并使得两部分被切割的边最多。如下图(a),一个图由五个顶点构成,相互连接的边为```(0, 1), (0, 2), (1, 2), (2, 3), (3, 4), (0, 4)```。为了使得被切割的边最多,我们尝试通过(b)图的分割,将1、2、4分为一组,0、3分成另一组,因此可得到被切割的边有5条。当图中顶点增多时,我们很难找到有效的经典算法来解决Max-Cut问题。下面,我们介绍怎么将Max-Cut问题转化为一个哈密顿量的基态能力求解问题。" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "![max cut](./images/Max_Cut.png)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Max-Cut问题量子化\n", - "\n", - "这里我们将图中的每个顶点赋予一个量子比特,当顶点被分到左边时,我们将该顶点上的量子比特设置为$\\left|0\\right>$态,同理,右边为$\\left|1\\right>$态,当两个顶点被分到不同的集合中时,这两个顶点上的比特将处于不同的量子态。例如对于第0个顶点和第1个顶点,当其连线被切割是,两个顶点上的比特对应的量子态可以为$\\left|\\psi\\right>=\\left|0_11_0\\right>$或$\\left|\\psi\\right>=\\left|1_10_0\\right>$,其中下角标表示顶点的序号。此时,我们选择哈密顿量$H=(Z_1Z_0-1)/2$,这里$Z$为泡利$Z$算符。不难发现:\n", - "$$\\left<\\psi\\right|H\\left|\\psi\\right>=-1$$\n", - "而当顶点被分到同一集合中是,不难验证此时:\n", - "$$\\left<\\psi\\right|H\\left|\\psi\\right>=0$$\n", - "因此,我们只用按照上面的规则,写出图对应的哈密顿量$H$,利用量子计算机求得$H$的基态能量与基态,我们就可以得到该图的Max-Cut切割方案与最大切割边数。我们记所有边的集合为$C$,所有边个数为$c$,则哈密顿量可写为:\n", - "$$H=\\sum_{(i,j)\\in C}(Z_iZ_j-1)/2$$\n", - "\n", - "## 导入相关依赖" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "from mindquantum import Circuit, Hamiltonian, UN, H, ZZ, RX, StateEvolution\n", - "from mindquantum.nn import MindQuantumAnsatzOnlyLayer\n", - "from mindquantum.ops import QubitOperator\n", - "import networkx as nx\n", - "import mindspore.nn as nn\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 搭建所需求解的图\n", - "\n", - "通过`add_path`可在图中添加边。最后画出图的结构。" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "output_type": "display_data", - "data": { - "text/plain": "
", - "image/svg+xml": "\n\n\n \n \n \n \n 2021-08-30T08:18:14.080359\n image/svg+xml\n \n \n Matplotlib v3.4.2, 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", - "image/png": "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\n" - }, - "metadata": {} - } - ], - "source": [ - "g = nx.Graph()\n", - "nx.add_path(g, [0,1])\n", - "nx.add_path(g, [1,2])\n", - "nx.add_path(g, [2,3])\n", - "nx.add_path(g, [3,4])\n", - "nx.add_path(g, [0,4])\n", - "nx.add_path(g, [0,2])\n", - "nx.draw(g,with_labels=True, font_weight='bold')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "如上如,我们得到一个由5个节点和6条边构成的图结构。" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 搭建QAOA量子线路\n", - "\n", - "### 线路搭建\n", - "\n", - "这里我们采用量子绝热近似算法,经过演化将量子态从$X^{\\otimes n}$的本征态演化到图多应哈密的量的基态。\n", - "\n", - "搭建图对应哈密顿量的含时演化线路:" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "def build_hc(g,para):\n", - " hc = Circuit()\n", - " for i in g.edges:\n", - " hc += ZZ(para).on(i)\n", - " return hc" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "搭建$X^{\\otimes n}$含时演化的量子线路:" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "\n", - "def build_hb(g, para):\n", - " hc = Circuit()\n", - " for i in g.nodes:\n", - " hc += RX(para).on(i)\n", - " return hc" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "为了使得最后优化的结果足够准确,我们需要将量子线路重复多次,因此我们通过如下函数搭建多层的训练网络:" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "def build_ansatz(g, p):\n", - " c = Circuit()\n", - " for i in range(p):\n", - " c += build_hc(g,f'g{i}')\n", - " c += build_hb(g,f'b{i}')\n", - " return c" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "构建图对应的哈密顿量:" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "def build_ham(g):\n", - " hc = QubitOperator()\n", - " for i in g.edges:\n", - " hc += QubitOperator(f'Z{i[0]} Z{i[1]}')\n", - " return hc" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 生成完整的量子线路和图所对应的哈密顿量\n", - "\n", - "这里我们选择`p = 4`,表示选用4曾的QAOA量子线路,`ansatz`是求解该问题的量子线路,`init_state_circ`是将量子态制备到均匀叠加态上的量子线路。" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [], - "source": [ - "p = 4\n", - "ham = Hamiltonian(build_ham(g))\n", - "ansatz = build_ansatz(g, p)\n", - "init_state_circ = UN(H, g.nodes)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 搭建待训练量子神经网络\n", - "\n", - "由于该问题不需要编码层量子线路,我们这里使用`MindQuantumAnsatzOnlyLayer`作为待训练的量子神经网络,并采用`Adam`优化器。" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], - "source": [ - "net = MindQuantumAnsatzOnlyLayer(ansatz.para_name, init_state_circ+ansatz, ham)\n", - "opti = nn.Adam(net.trainable_params(), learning_rate=0.05)\n", - "train_net = nn.TrainOneStepCell(net, opti)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 训练并展示结果" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "scrolled": true, - "tags": [] - }, - "outputs": [ - { - "output_type": "stream", - "name": "stdout", - "text": "train step: 0 , cut: [[2.9974403]]\ntrain step: 10 , cut: [[4.118001]]\ntrain step: 20 , cut: [[4.7207108]]\ntrain step: 30 , cut: [[4.8198557]]\ntrain step: 40 , cut: [[4.8452663]]\ntrain step: 50 , cut: [[4.894426]]\ntrain step: 60 , cut: [[4.9361014]]\ntrain step: 70 , cut: [[4.93437]]\ntrain step: 80 , cut: [[4.937895]]\ntrain step: 90 , cut: [[4.93891]]\ntrain step: 100 , cut: [[4.939043]]\ntrain step: 110 , cut: [[4.939199]]\ntrain step: 120 , cut: [[4.9392414]]\ntrain step: 130 , cut: [[4.9392495]]\ntrain step: 140 , cut: [[4.9392548]]\ntrain step: 150 , cut: [[4.939256]]\ntrain step: 160 , cut: [[4.9392567]]\ntrain step: 170 , cut: [[4.939257]]\ntrain step: 180 , cut: [[4.939257]]\ntrain step: 190 , cut: [[4.939257]]\n" - } - ], - "source": [ - "for i in range(200):\n", - " res = train_net()\n", - " if i % 10 == 0:\n", - " print(\"train step:\", i, \", cut:\", (len(g.edges)-res)/2)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "根据上面的训练结果我们发现,该问题哈密顿量的基态能量对应的边切割数趋近与5。\n", - "\n", - "### 量子态展示\n", - "\n", - "前面我们通过训练得到了量子线路中参数的最优值,下面,我们通过`StateEvolution`类的`final_state`来输出量子线路在最优参数时的量子态,其中`ket`参数表示是否将最终量子态表示为右矢形式。" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "tags": [] - }, - "outputs": [ - { - "output_type": "stream", - "name": "stdout", - "text": "(0.01627434976398945+0.03802764043211937j)¦00000⟩\n(-0.021525444462895393+0.010120502673089504j)¦00001⟩\n(0.018090182915329933-0.004060747567564249j)¦00010⟩\n(0.0007923207012936473+0.020563315600156784j)¦00011⟩\n(-0.021525444462895393+0.010120502673089504j)¦00100⟩\n(-0.009984630160033703-0.09121354669332504j)¦00101⟩\n(0.0007923207012936473+0.020563315600156784j)¦00110⟩\n(0.004760198760777712+0.01567949540913105j)¦00111⟩\n(0.012238028459250927-0.001225721905939281j)¦01000⟩\n(-0.3078080117702484+0.3823811709880829j)¦01001⟩\n(-0.04003702476620674-0.010155842639505863j)¦01010⟩\n(-0.3078080117702484+0.3823811709880829j)¦01011⟩\n(-0.028323723003268242-0.004314310383051634j)¦01100⟩\n(-0.04003702476620674-0.010155842639505863j)¦01101⟩\n(-0.028323723003268242-0.004314310383051634j)¦01110⟩\n(0.012238028459250927-0.001225721905939281j)¦01111⟩\n(0.012238028459250927-0.001225721905939281j)¦10000⟩\n(-0.028323723003268242-0.004314310383051634j)¦10001⟩\n(-0.04003702476620674-0.010155842639505863j)¦10010⟩\n(-0.028323723003268242-0.004314310383051634j)¦10011⟩\n(-0.3078080117702484+0.3823811709880829j)¦10100⟩\n(-0.04003702476620674-0.010155842639505863j)¦10101⟩\n(-0.3078080117702484+0.3823811709880829j)¦10110⟩\n(0.012238028459250927-0.001225721905939281j)¦10111⟩\n(0.004760198760777712+0.01567949540913105j)¦11000⟩\n(0.0007923207012936473+0.020563315600156784j)¦11001⟩\n(-0.009984630160033703-0.09121354669332504j)¦11010⟩\n(-0.021525444462895393+0.010120502673089504j)¦11011⟩\n(0.0007923207012936473+0.020563315600156784j)¦11100⟩\n(0.018090182915329933-0.004060747567564249j)¦11101⟩\n(-0.021525444462895393+0.010120502673089504j)¦11110⟩\n(0.01627434976398945+0.03802764043211937j)¦11111⟩\n" - } - ], - "source": [ - "pr = dict(zip(ansatz.para_name, net.weight.asnumpy()))\n", - "print(StateEvolution(init_state_circ+ansatz).final_state(pr, ket=True))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 概率图\n", - "\n", - "我们画出最终量子态在计算基矢下的概率分布" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "output_type": "display_data", - "data": { - "text/plain": "
", - "image/svg+xml": "\n\n\n \n \n \n \n 2021-08-30T08:18:15.952192\n image/svg+xml\n \n \n Matplotlib v3.4.2, 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 \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \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/png": "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\n" - }, - "metadata": { - "needs_background": "light" - } - } - ], - "source": [ - "def show_amp(state):\n", - " amp = np.abs(state)**2\n", - " n_qubits = int(np.log2(len(amp)))\n", - " labels = [bin(i)[2:].zfill(n_qubits) for i in range(len(amp))]\n", - " plt.bar(labels, amp)\n", - " plt.xticks(rotation=45)\n", - " plt.show()\n", - "state = StateEvolution(init_state_circ+ansatz).final_state(pr)\n", - "show_amp(state)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "根据概率分布图我们发现,该Max-Cut问题具有四个简并解,每个解对应的概率大概为25%。\n", - "\n", - "## 总结\n", - "\n", - "这里我们通过量子近似优化算法来解决了Max-Cut问题,并得到了案例中的图对应的最大切割方案。" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 参考文献\n", - "\n", - "[1] Edward Farhi, Jeffrey Goldstone, and Sam Gutmann. [A Quantum Approximate Optimization Algorithm](https://arxiv.org/pdf/1411.4028.pdf)" - ] - } - ], - "metadata": { - "interpreter": { - "hash": "5545a57ef4a1ac7dca167cae0bf17fda051fcd0639773c034bd7ce77ffd97d30" - }, - "kernelspec": { - "display_name": "Python 3.7.5 64-bit", - "language": "python", - "name": "python37564bit6afae4a42a5941c0967cdcfc2650559a" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.5-final" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} \ No newline at end of file diff --git a/tutorials/source/1.parameterized_quantum_circuit.py b/tutorials/source/1.parameterized_quantum_circuit.py new file mode 100644 index 0000000000000000000000000000000000000000..ea94ca3ab1cd890db2dc00cb2b8683075ff93e6f --- /dev/null +++ b/tutorials/source/1.parameterized_quantum_circuit.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +import numpy as np +import mindquantum as mq +from mindquantum.core import X, Y, Z, H, RX, RY, RZ + +print('Gate name:', X) +X.matrix() + +print('Gate name:', Y) +Y.matrix() + +print('Gate name:', Z) +Z.matrix() + +print('Gate name:', H) +H.matrix() + +cnot = X.on(0, 1) +print(cnot) + +rx = RX('theta') +print('Gate name:', rx) +rx.matrix({'theta': 0}) + +ry = RY('theta') +print('Gate name:', ry) +ry.matrix({'theta': np.pi / 2}) + +rz = RZ('theta') +print('Gate name:', rz) +np.round(rz.matrix({'theta': np.pi})) + +from mindquantum.core import Circuit + +encoder = Circuit() +encoder += H.on(0) +encoder += X.on(1, 0) +encoder += RY('theta').on(2) + +print(encoder) +encoder.summary() diff --git a/tutorials/source/2.initial_experience_of_quantum_neural_network.py b/tutorials/source/2.initial_experience_of_quantum_neural_network.py new file mode 100644 index 0000000000000000000000000000000000000000..eff188a5c037c233dd0ec66adc5fbc5808beac78 --- /dev/null +++ b/tutorials/source/2.initial_experience_of_quantum_neural_network.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +import numpy as np +import mindquantum as mq +from mindquantum.core import Circuit +from mindquantum.core import H, RX, RY, RZ + +encoder = Circuit() +encoder += H.on(0) +encoder += RX(f'alpha{0}').on(0) +encoder += RY(f'alpha{1}').on(0) +encoder += RZ(f'alpha{2}').on(0) +encoder = encoder.no_grad() +encoder.summary() +encoder + +alpha0, alpha1, alpha2 = 0.2, 0.3, 0.4 +state = encoder.get_qs(pr={ + 'alpha0': alpha0, + 'alpha1': alpha1, + 'alpha2': alpha2 +}, + ket=True) +print(state) + +ansatz = Circuit() +ansatz += RX(f'theta{0}').on(0) +ansatz += RY(f'theta{1}').on(0) +ansatz + +theta0, theta1 = 0, 0 +state = ansatz.get_qs(pr=dict(zip(ansatz.params_name, [theta0, theta1])), + ket=True) +print(state) + +circuit = encoder + ansatz +circuit + +from mindquantum.core import QubitOperator +from mindquantum.core import Hamiltonian + +ham = Hamiltonian(QubitOperator('Z0', -1)) +print(ham) + +encoder_names = encoder.params_name +ansatz_names = ansatz.params_name + +print('encoder_names = ', encoder.params_name, '\nansatz_names =', + ansatz.params_name) + +# 导入Simulator模块 +from mindquantum.simulator import Simulator + +sim = Simulator('projectq', circuit.n_qubits) + +grad_ops = sim.get_expectation_with_grad(ham, + circuit, + encoder_params_name=encoder_names, + ansatz_params_name=ansatz_names) + +encoder_data = np.array([[alpha0, alpha1, alpha2]]).astype(np.float32) + +ansatz_data = np.array([theta0, theta1]).astype(np.float32) + +measure_result, encoder_grad, ansatz_grad = grad_ops(encoder_data, ansatz_data) + +print('Measurement result: ', measure_result) +print('Gradient of encoder parameters: ', encoder_grad) +print('Gradient of ansatz parameters: ', ansatz_grad) + +from mindquantum.framework import MQLayer +import mindspore as ms + +ms.set_seed(1) +ms.context.set_context(mode=ms.context.PYNATIVE_MODE, device_target="CPU") + +QuantumNet = MQLayer(grad_ops) +QuantumNet + +from mindspore import nn +from mindspore.nn import Adam, TrainOneStepCell + +opti = Adam(QuantumNet.trainable_params(), learning_rate=0.5) +net = TrainOneStepCell(QuantumNet, opti) + +for i in range(200): + res = net(ms.Tensor(encoder_data)) + if i % 10 == 0: + print(i, ': ', res) + +theta0, theta1 = QuantumNet.weight.asnumpy() + +print(QuantumNet.weight.asnumpy()) + +pr = { + 'alpha0': alpha0, + 'alpha1': alpha1, + 'alpha2': alpha2, + 'theta0': theta0, + 'theta1': theta1 +} +state = circuit.get_qs(pr=pr, ket=True) + +print(state) + +state = circuit.get_qs(pr=pr) +fid = np.abs(np.vdot(state, [1, 0]))**2 + +print(fid) diff --git a/tutorials/source/3.classification_of_iris_by_qnn.py b/tutorials/source/3.classification_of_iris_by_qnn.py new file mode 100644 index 0000000000000000000000000000000000000000..b0b60ac524c92120fe4076cc45cef4c29c07de7f --- /dev/null +++ b/tutorials/source/3.classification_of_iris_by_qnn.py @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- +import numpy as np +from sklearn import datasets + +iris_dataset = datasets.load_iris() + +print(iris_dataset.data.shape) +print(iris_dataset.feature_names) +print(iris_dataset.target_names) +print(iris_dataset.target) +print(iris_dataset.target.shape) +X = iris_dataset.data[:100, :].astype(np.float32) +X_feature_names = iris_dataset.feature_names +y = iris_dataset.target[:100].astype(int) +y_target_names = iris_dataset.target_names[:2] + +print(X.shape) +print(X_feature_names) +print(y_target_names) +print(y) +print(y.shape) + +import matplotlib.pyplot as plt + +feature_name = { + 0: 'sepal length', + 1: 'sepal width', + 2: 'petal length', + 3: 'petal width' +} +axes = plt.figure(figsize=(23, 23)).subplots(4, 4) + +colormap = {0: 'r', 1: 'g'} +cvalue = [colormap[i] for i in y] + +for i in range(4): + for j in range(4): + if i != j: + ax = axes[i][j] + ax.scatter(X[:, i], X[:, j], c=cvalue) + ax.set_xlabel(feature_name[i], fontsize=22) + ax.set_ylabel(feature_name[j], fontsize=22) +plt.show() + +alpha = X[:, :3] * X[:, 1:] +X = np.append(X, alpha, axis=1) +print(X.shape) + +from sklearn.model_selection import train_test_split + +X_train, X_test, y_train, y_test = train_test_split(X, + y, + test_size=0.2, + random_state=0, + shuffle=True) + +print(X_train.shape) +print(X_test.shape) + +import mindquantum as mq +from mindquantum.core import Circuit +from mindquantum.core import UN +from mindquantum.core import H, X, RZ + +encoder = Circuit() + +encoder += UN(H, 4) +for i in range(4): + encoder += RZ(f'alpha{i}').on(i) +for j in range(3): #j = 0, 1, 2 + encoder += X.on(j + 1, j) + encoder += RZ(f'alpha{j+4}').on(j + 1) + encoder += X.on(j + 1, j) + +encoder = encoder.no_grad() +encoder.summary() +encoder + +from mindquantum.algorithm import HardwareEfficientAnsatz +from mindquantum.core import RY + +ansatz = HardwareEfficientAnsatz(4, + single_rot_gate_seq=[RY], + entangle_gate=X, + depth=3).circuit +ansatz.summary() +ansatz + +circuit = encoder + ansatz +circuit.summary() +circuit + +from mindquantum.core import QubitOperator +from mindquantum.core import Hamiltonian + +hams = [Hamiltonian(QubitOperator(f'Z{i}')) for i in [2, 3]] +print(hams) + +import mindspore as ms +from mindquantum.framework import MQLayer +from mindquantum.simulator import Simulator + +ms.context.set_context(mode=ms.context.PYNATIVE_MODE, device_target="CPU") +ms.set_seed(1) +sim = Simulator('projectq', circuit.n_qubits) +grad_ops = sim.get_expectation_with_grad(hams, + circuit, + None, + encoder.params_name, + ansatz.params_name, + parallel_worker=5) +QuantumNet = MQLayer(grad_ops) +QuantumNet + +from mindspore.nn import SoftmaxCrossEntropyWithLogits +from mindspore.nn import Adam, Accuracy +from mindspore import Model +from mindspore.dataset import NumpySlicesDataset +from mindspore.train.callback import Callback, LossMonitor + +loss = SoftmaxCrossEntropyWithLogits(sparse=True, reduction='mean') +opti = Adam(QuantumNet.trainable_params(), learning_rate=0.1) + +model = Model(QuantumNet, loss, opti, metrics={'Acc': Accuracy()}) + +train_loader = NumpySlicesDataset({ + 'features': X_train, + 'labels': y_train +}, + shuffle=False).batch(5) +test_loader = NumpySlicesDataset({ + 'features': X_test, + 'labels': y_test +}).batch(5) + + +class StepAcc(Callback): + def __init__(self, model, test_loader): + self.model = model + self.test_loader = test_loader + self.acc = [] + + def step_end(self, run_context): + self.acc.append( + self.model.eval(self.test_loader, dataset_sink_mode=False)['Acc']) + + +monitor = LossMonitor(16) + +acc = StepAcc(model, test_loader) + +model.train(20, + train_loader, + callbacks=[monitor, acc], + dataset_sink_mode=False) + +plt.plot(acc.acc) +plt.title('Statistics of accuracy', fontsize=20) +plt.xlabel('Steps', fontsize=20) +plt.ylabel('Accuracy', fontsize=20) +plt.show() + +from mindspore import ops, Tensor + +predict = np.argmax(ops.Softmax()(model.predict(Tensor(X_test))), axis=1) +correct = model.eval(test_loader, dataset_sink_mode=False) + +print("预测分类结果:", predict) +print("实际分类结果:", y_test) + +print(correct) diff --git a/tutorials/source/4.quantum_phase_estimation.py b/tutorials/source/4.quantum_phase_estimation.py new file mode 100644 index 0000000000000000000000000000000000000000..6834d49976d6a9b57ee609491f691292284a51fa --- /dev/null +++ b/tutorials/source/4.quantum_phase_estimation.py @@ -0,0 +1,36 @@ +from mindquantum import Circuit +from mindquantum import Simulator +from mindquantum import UN, PhaseShift, qft, H, X, BARRIER +import numpy as np + +n = 3 +c = Circuit() +c += UN(H, n) +c += X.on(n) +print(c) + +for i in range(n): + c += PhaseShift({'phi': 2**i}).on(n, n - i - 1) +print(c) + +c += BARRIER +c += qft(range(n)).hermitian() +print(c) + +from mindquantum import Measure +sim = Simulator('projectq', c.n_qubits) +phi = 0.125 +sim.apply_circuit(c, {'phi': 2 * np.pi * phi}) +qs = sim.get_qs() +print(sim.get_qs(ket=True)) +res = sim.sampling(UN(Measure(), c.n_qubits), shots=100) +print(res) + +index = np.argmax(np.abs(qs)) +print(index) + +bit_string = bin(index)[2:].zfill(c.n_qubits)[1:] +print(bit_string) + +theta_exp = int(bit_string[::-1], 2) / 2**n +print(theta_exp) diff --git a/tutorials/source/quantum_approximate_optimization_algorithm.py b/tutorials/source/5.quantum_approximate_optimization_algorithm.py similarity index 70% rename from tutorials/source/quantum_approximate_optimization_algorithm.py rename to tutorials/source/5.quantum_approximate_optimization_algorithm.py index 9117d48fc05ad372c6c6dae5451ff60364585aae..67d984db213eaeb7dfade487bb1f50264890ae60 100644 --- a/tutorials/source/quantum_approximate_optimization_algorithm.py +++ b/tutorials/source/5.quantum_approximate_optimization_algorithm.py @@ -1,7 +1,7 @@ -from mindquantum.gate import Hamiltonian, H, ZZ, RX -from mindquantum.circuit import Circuit, StateEvolution, UN -from mindquantum.nn import MindQuantumAnsatzOnlyLayer -from mindquantum.ops import QubitOperator +# -*- coding: utf-8 -*- +from mindquantum.core import Circuit, Hamiltonian, UN, H, ZZ, RX, QubitOperator +from mindquantum.framework import MQAnsatzOnlyLayer +from mindquantum.simulator import Simulator import networkx as nx import mindspore.nn as nn import numpy as np @@ -15,7 +15,6 @@ nx.add_path(g, [3, 4]) nx.add_path(g, [0, 4]) nx.add_path(g, [0, 2]) nx.draw(g, with_labels=True, font_weight='bold') -plt.show() def build_hc(g, para): @@ -32,13 +31,6 @@ def build_hb(g, para): return hc -def build_ham(g): - hc = QubitOperator() - for i in g.edges: - hc += QubitOperator(f'Z{i[0]} Z{i[1]}') - return hc - - def build_ansatz(g, p): c = Circuit() for i in range(p): @@ -47,13 +39,11 @@ def build_ansatz(g, p): return c -def show_amp(state): - amp = np.abs(state)**2 - n_qubits = int(np.log2(len(amp))) - labels = [bin(i)[2:].zfill(n_qubits) for i in range(len(amp))] - plt.bar(labels, amp) - plt.xticks(rotation=45) - plt.show() +def build_ham(g): + hc = QubitOperator() + for i in g.edges: + hc += QubitOperator(f'Z{i[0]} Z{i[1]}') + return hc p = 4 @@ -61,8 +51,13 @@ ham = Hamiltonian(build_ham(g)) ansatz = build_ansatz(g, p) init_state_circ = UN(H, g.nodes) -net = MindQuantumAnsatzOnlyLayer(ansatz.para_name, init_state_circ + ansatz, - ham) +import mindspore as ms +ms.context.set_context(mode=ms.context.PYNATIVE_MODE, device_target="CPU") + +circ = init_state_circ + ansatz +sim = Simulator('projectq', circ.n_qubits) +grad_ops = sim.get_expectation_with_grad(ham, circ) +net = MQAnsatzOnlyLayer(grad_ops) opti = nn.Adam(net.trainable_params(), learning_rate=0.05) train_net = nn.TrainOneStepCell(net, opti) @@ -70,8 +65,18 @@ for i in range(600): if i % 10 == 0: print("train step:", i, ", cut:", (len(g.edges) - train_net()) / 2) -pr = dict(zip(ansatz.para_name, net.weight.asnumpy())) +pr = dict(zip(ansatz.params_name, net.weight.asnumpy())) +print(circ.get_qs(pr=pr, ket=True)) + + +def show_amp(state): + amp = np.abs(state)**2 + n_qubits = int(np.log2(len(amp))) + labels = [bin(i)[2:].zfill(n_qubits) for i in range(len(amp))] + plt.bar(labels, amp) + plt.xticks(rotation=45) + plt.show() + -print(StateEvolution(init_state_circ + ansatz).final_state(pr, ket=True)) -state = StateEvolution(init_state_circ + ansatz).final_state(pr) +state = circ.get_qs(pr=pr) show_amp(state) diff --git a/tutorials/source/qnn_for_nlp.py b/tutorials/source/6.qnn_for_nlp.py similarity index 88% rename from tutorials/source/qnn_for_nlp.py rename to tutorials/source/6.qnn_for_nlp.py index fdc694e4cee4876cd71c385c0b4950fb9ee8c242..f2193cd5714fb25018489c98fcbcace4b6ad16b6 100644 --- a/tutorials/source/qnn_for_nlp.py +++ b/tutorials/source/6.qnn_for_nlp.py @@ -1,12 +1,13 @@ +# -*- coding: utf-8 -*- import time import numpy as np -from mindquantum.ops import QubitOperator +from mindquantum.core import QubitOperator import mindspore.ops as ops import mindspore.dataset as ds from mindspore import nn from mindspore.train.callback import LossMonitor from mindspore import Model -from mindquantum.nn import MindQuantumLayer +from mindquantum.framework import MQLayer from mindquantum import Hamiltonian, Circuit, RX, RY, X, H, UN @@ -44,34 +45,30 @@ def GenerateEncoderCircuit(n_qubits, prefix=''): GenerateEncoderCircuit(3, prefix='e') -from mindquantum.nn import generate_evolution_operator +from mindquantum.simulator import Simulator from mindspore import context from mindspore import Tensor -n_qubits = 3 # number of qubits of this quantum circuit -label = 2 # label need to encode -label_bin = bin(label)[-1:1:-1].ljust(n_qubits, '0') # binary form of label -label_array = np.array([int(i) * np.pi for i in label_bin - ]).astype(np.float32) # parameter value of encoder -encoder = GenerateEncoderCircuit(n_qubits, prefix='e') # encoder circuit -encoder_para_names = encoder.para_name # parameter names of encoder +n_qubits = 3 +label = 2 +label_bin = bin(label)[-1:1:-1].ljust(n_qubits, '0') +label_array = np.array([int(i) * np.pi for i in label_bin]).astype(np.float32) +encoder = GenerateEncoderCircuit(n_qubits, prefix='e') +encoder_params_names = encoder.params_name print("Label is: ", label) print("Binary label is: ", label_bin) print("Parameters of encoder is: \n", np.round(label_array, 5)) print("Encoder circuit is: \n", encoder) -print("Encoder parameter names are: \n", encoder_para_names) +print("Encoder parameter names are: \n", encoder_params_names) -context.set_context(mode=context.GRAPH_MODE, device_target="CPU") -# quantum state evolution operator -evol = generate_evolution_operator(param_names=encoder_para_names, - circuit=encoder) -state = evol(Tensor(label_array)) +state = encoder.get_qs(pr=dict(zip(encoder_params_names, label_array))) amp = np.round(np.abs(state)**2, 3) print("Amplitude of quantum state is: \n", amp) print("Label in quantum state is: ", np.argmax(amp)) + def GenerateTrainData(sample, word_dict): n_qubits = np.int(np.ceil(np.log2(1 + max(word_dict.values())))) data_x = [] @@ -90,6 +87,7 @@ def GenerateTrainData(sample, word_dict): GenerateTrainData(sample, word_dict) + def GenerateAnsatzCircuit(n_qubits, layers, prefix=''): if len(prefix) != 0 and prefix[-1] != '_': prefix += '_' @@ -105,6 +103,7 @@ def GenerateAnsatzCircuit(n_qubits, layers, prefix=''): GenerateAnsatzCircuit(5, 2, 'a') + def GenerateEmbeddingHamiltonian(dims, n_qubits): hams = [] for i in range(dims): @@ -118,6 +117,7 @@ def GenerateEmbeddingHamiltonian(dims, n_qubits): GenerateEmbeddingHamiltonian(5, 5) + def QEmbedding(num_embedding, embedding_dim, window, layers, n_threads): n_qubits = int(np.ceil(np.log2(num_embedding))) hams = GenerateEmbeddingHamiltonian(embedding_dim, n_qubits) @@ -131,14 +131,11 @@ def QEmbedding(num_embedding, embedding_dim, window, layers, n_threads): encoder.no_grad() circ += encoder circ += ansatz - encoder_param_name.extend(encoder.para_name) - ansatz_param_name.extend(ansatz.para_name) - net = MindQuantumLayer(encoder_param_name, - ansatz_param_name, - circ, - hams, - n_threads=n_threads) - return net + encoder_param_name.extend(encoder.params_name) + ansatz_param_name.extend(ansatz.params_name) + grad_ops = Simulator('projectq', circ.n_qubits).get_expectation_with_grad( + hams, circ, None, encoder_param_name, ansatz_param_name, n_threads) + return MQLayer(grad_ops) class CBOW(nn.Cell): @@ -211,8 +208,7 @@ class LossMonitorWithCollection(LossMonitor): import mindspore as ms from mindspore import context from mindspore import Tensor - -context.set_context(mode=context.GRAPH_MODE, device_target="CPU") +context.set_context(mode=context.PYNATIVE_MODE, device_target="CPU") corpus = """We are about to study the idea of a computational process. Computational processes are abstract beings that inhabit computers. As they evolve, processes manipulate other abstract things called data. @@ -249,6 +245,9 @@ plt.xlabel('Steps') plt.ylabel('Loss') plt.show() +net.embedding.weight.asnumpy() + + class CBOWClassical(nn.Cell): def __init__(self, num_embedding, embedding_dim, window, hidden_dim): super(CBOWClassical, self).__init__() @@ -281,6 +280,8 @@ train_y = np.array(train_y).astype(np.int32) print("train_x shape: ", train_x.shape) print("train_y shape: ", train_y.shape) +context.set_context(mode=context.GRAPH_MODE, device_target="CPU") + train_loader = ds.NumpySlicesDataset({ "around": train_x, "center": train_y diff --git a/tutorials/source/vqe_for_quantum_chemistry.py b/tutorials/source/7.vqe_for_quantum_chemistry.py similarity index 71% rename from tutorials/source/vqe_for_quantum_chemistry.py rename to tutorials/source/7.vqe_for_quantum_chemistry.py index 0a1e38aef1ea9e7485f44f0e2bb1dab247db4455..c64ccc20da000af3769abd48b89b3e4d76cf0c04 100644 --- a/tutorials/source/vqe_for_quantum_chemistry.py +++ b/tutorials/source/7.vqe_for_quantum_chemistry.py @@ -1,16 +1,17 @@ +# -*- coding: utf-8 -*- + import numpy as np from openfermion.chem import MolecularData from openfermionpyscf import run_pyscf import mindquantum as mq -from mindquantum.circuit import generate_uccsd, Circuit -from mindquantum.gate import X, RX, Hamiltonian -from mindquantum.nn import generate_pqc_operator +from mindquantum import Circuit, X, RX, Hamiltonian, Simulator +from mindquantum.algorithm import generate_uccsd import mindspore as ms import mindspore.context as context -from mindspore import Parameter +from mindspore.common.parameter import Parameter from mindspore.common.initializer import initializer -context.set_context(mode=context.GRAPH_MODE, device_target="CPU") +context.set_context(mode=context.PYNATIVE_MODE, device_target="CPU") dist = 1.5 geometry = [ @@ -43,25 +44,13 @@ total_circuit = hartreefock_wfn_circuit + ansatz_circuit total_circuit.summary() print("Number of parameters: %d" % (len(ansatz_parameter_names))) -molecule_pqc = generate_pqc_operator(["null"], ansatz_parameter_names, - RX("null").on(0) + total_circuit, - Hamiltonian(hamiltonian_QubitOp)) - -class PQCNet(ms.nn.Cell): - def __init__(self, pqc): - super(PQCNet, self).__init__() - self.pqc = pqc - self.weight = Parameter(initializer("Zeros", - len(self.pqc.ansatz_params_names)), - name="weight") - self.encoder_data_dummy = ms.Tensor([[0]], self.weight.dtype) +sim = Simulator('projectq', total_circuit.n_qubits) +molecule_pqc = sim.get_expectation_with_grad(Hamiltonian(hamiltonian_QubitOp), + total_circuit) - def construct(self): - energy, _, grads = self.pqc(self.encoder_data_dummy, self.weight) - return energy +from mindquantum.framework import MQAnsatzOnlyLayer - -molecule_pqcnet = PQCNet(molecule_pqc) +molecule_pqcnet = MQAnsatzOnlyLayer(molecule_pqc, 'Zeros') initial_energy = molecule_pqcnet() print("Initial energy: %20.16f" % (initial_energy.asnumpy())) @@ -86,11 +75,11 @@ print("Optimization completed at step %3d" % (iter_idx - 1)) print("Optimized energy: %20.16f" % (energy_i)) print("Optimized amplitudes: \n", molecule_pqcnet.weight.asnumpy()) -from mindquantum.hiqfermion.transforms import Transform -from mindquantum.hiqfermion.ucc import get_qubit_hamiltonian -from mindquantum.hiqfermion.ucc import uccsd_singlet_generator, uccsd_singlet_get_packed_amplitudes -from mindquantum.circuit import TimeEvolution -from mindquantum.nn import MindQuantumAnsatzOnlyLayer +from mindquantum.algorithm.nisq.chem import Transform +from mindquantum.algorithm.nisq.chem import get_qubit_hamiltonian +from mindquantum.algorithm.nisq.chem import uccsd_singlet_generator, uccsd_singlet_get_packed_amplitudes +from mindquantum.core.operators import TimeEvolution +from mindquantum.framework import MQAnsatzOnlyLayer hamiltonian_QubitOp = get_qubit_hamiltonian(molecule_of) @@ -101,7 +90,7 @@ ucc_fermion_ops = uccsd_singlet_generator(molecule_of.n_qubits, ucc_qubit_ops = Transform(ucc_fermion_ops).jordan_wigner() ansatz_circuit = TimeEvolution(ucc_qubit_ops.imag, 1.0).circuit -ansatz_parameter_names = ansatz_circuit.para_name +ansatz_parameter_names = ansatz_circuit.params_name total_circuit = hartreefock_wfn_circuit + ansatz_circuit total_circuit.summary() @@ -113,9 +102,11 @@ init_amplitudes_ccsd = [ init_amplitudes_ccsd[param_i] for param_i in ansatz_parameter_names ] -molecule_pqcnet = MindQuantumAnsatzOnlyLayer( - ansatz_parameter_names, total_circuit, - Hamiltonian(hamiltonian_QubitOp.real)) +grad_ops = Simulator('projectq', + total_circuit.n_qubits).get_expectation_with_grad( + Hamiltonian(hamiltonian_QubitOp.real), total_circuit) + +molecule_pqcnet = MQAnsatzOnlyLayer(grad_ops) molecule_pqcnet.weight = Parameter( ms.Tensor(init_amplitudes_ccsd, molecule_pqcnet.weight.dtype)) diff --git a/tutorials/source/8.grover_search_algorithm_based_on_mindquantum.py b/tutorials/source/8.grover_search_algorithm_based_on_mindquantum.py new file mode 100644 index 0000000000000000000000000000000000000000..419b0e337568a738a62f2677b97c481f20f33c3c --- /dev/null +++ b/tutorials/source/8.grover_search_algorithm_based_on_mindquantum.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- + + +def bitphaseflip_operator(phase_inversion_qubit, n_qubits): + s = [1 for i in range(1 << n_qubits)] + for i in phase_inversion_qubit: + s[i] = -1 + if s[0] == -1: + for i in range(len(s)): + s[i] = -1 * s[i] + circuit = Circuit() + length = len(s) + cz = [] + for i in range(length): + if s[i] == -1: + cz.append([]) + current = i + t = 0 + while current != 0: + if (current & 1) == 1: + cz[-1].append(t) + t += 1 + current = current >> 1 + for j in range(i + 1, length): + if i & j == i: + s[j] = -1 * s[j] + for i in cz: + if i: + if len(i) > 1: + circuit += Z.on(i[-1], i[:-1]) + else: + circuit += Z.on(i[0]) + + return circuit + + +import mindquantum as mq +from mindquantum import Circuit, UN, H, Z +from mindquantum.simulator import Simulator + +n_qubits = 3 +sim = Simulator('projectq', n_qubits) + +circuit = Circuit() +circuit += UN(H, n_qubits) + +sim.apply_circuit(circuit) + +circuit + +print(sim.get_qs(True)) + +sim.reset() + +phase_inversion_qubit = [4] +operator = bitphaseflip_operator(phase_inversion_qubit, n_qubits) + +circuit += operator + +sim.apply_circuit(circuit) + +circuit + +print(sim.get_qs(True)) +print(int('100', 2)) + +n_qubits = 3 +sim1 = Simulator('projectq', n_qubits) + +operator1 = bitphaseflip_operator([i for i in range(1, pow(2, n_qubits))], + n_qubits) + +circuit1 = Circuit() +circuit1 += UN(H, n_qubits) +circuit1 += operator1 + +sim1.apply_circuit(circuit1) + +circuit1 + +print(sim1.get_qs(True)) + + +def G(phase_inversion_qubit, n_qubits): + operator = bitphaseflip_operator(phase_inversion_qubit, n_qubits) + operator += UN(H, n_qubits) + operator += bitphaseflip_operator([i for i in range(1, pow(2, n_qubits))], + n_qubits) + operator += UN(H, n_qubits) + return operator + + +import numpy as np +from numpy import pi, sqrt + +n_qubits = 3 +phase_inversion_qubit = [2] + +N = 2**(n_qubits) +M = len(phase_inversion_qubit) + +r = int(pi / 4 * sqrt(N / M)) + +sim2 = Simulator('projectq', n_qubits) + +circuit2 = Circuit() +circuit2 += UN(H, n_qubits) + +for i in range(r): + circuit2 += G(phase_inversion_qubit, n_qubits) + +sim2.apply_circuit(circuit2) + +circuit2 + +print(sim2.get_qs(True)) +from mindquantum import Measure + +sim2.reset() + +circuit2 += UN(Measure(), circuit2.n_qubits) + +result = sim2.sampling(circuit2, shots=1000) +result + +print(int('010', 2)) + +n_qubits = 5 +phase_inversion_qubit = [5, 11] + +N = 2**(n_qubits) +M = len(phase_inversion_qubit) + +r = int(pi / 4 * sqrt(N / M)) + +sim3 = Simulator('projectq', n_qubits) + +circuit3 = Circuit() +circuit3 += UN(H, n_qubits) + +for i in range(r): + circuit3 += G(phase_inversion_qubit, n_qubits) + +sim3.apply_circuit(circuit3) + +circuit3 + +print(sim3.get_qs(True)) + +sim3.reset() + +circuit3 += UN(Measure(), circuit3.n_qubits) + +result1 = sim3.sampling(circuit3, shots=1000) +print(result1) + +print(int('00101', 2)) +print(int('01011', 2)) diff --git a/tutorials/source/iris_classification.py b/tutorials/source/iris_classification.py deleted file mode 100644 index 4e5dc7432b623e6745978aa36c27babfd155b134..0000000000000000000000000000000000000000 --- a/tutorials/source/iris_classification.py +++ /dev/null @@ -1,151 +0,0 @@ -import os - -os.environ['OMP_NUM_THREADS'] = '2' -from sklearn import datasets -from sklearn import preprocessing -from sklearn.model_selection import train_test_split -import numpy as np - - -def generate_train_and_test(split=0.8, shuffle=True): - iris = datasets.load_iris() - data = iris.data[:100, :].astype(np.float32) - data = preprocessing.minmax_scale(data) * 2 - 1 - label = np.zeros(100).astype(int) - label[50:] = 1 - return train_test_split(data, label, train_size=split, shuffle=True) - - -train_x, test_x, train_y, test_y = generate_train_and_test() -print('train sample and feature shape: ', train_x.shape) - -import matplotlib.pyplot as plt - -feature_name = { - 0: 'speal length', - 1: 'speal width', - 2: 'petal length', - 3: 'petal width' -} -axs = plt.figure(figsize=(18, 18)).subplots(4, 4) -for i in range(4): - for j in range(4): - if i != j: - ax = axs[i][j] - ax.scatter(train_x[:, i], train_x[:, j], c=train_y) - ax.set_xlabel(feature_name[i]) - ax.set_ylabel(feature_name[j]) -plt.show() - -from mindquantum import H, RZ, RX, X, Circuit - - -def encoder(n): - c = Circuit([H.on(i) for i in range(n)]) - for i in range(n): - c += RZ(f'x{i}').on(i) - for i in range(n - 1): - c += X.on(i + 1, i) - c += RZ(f'x{i},{i+1}').on(i + 1) - c += X.on(i + 1, i) - return c - - -enc = encoder(4).no_grad() -enc - -from mindquantum.ansatz import HardwareEfficientAnsatz -from mindquantum import X - -ans = HardwareEfficientAnsatz(4, - single_rot_gate_seq=[RX], - entangle_gate=X, - depth=3).circuit -ans.summary() - -from mindquantum.ops import QubitOperator -from mindquantum import Hamiltonian - -hams = [Hamiltonian(QubitOperator(f'Z{i}')) for i in [0, 1]] -hams - -from mindquantum.nn import MindQuantumLayer - -pqc = MindQuantumLayer(enc.para_name, - ans.para_name, - enc + ans, - hams, - n_threads=5) -pqc - -import mindspore.dataset as ds -import mindspore.nn as nn -import mindspore as ms -from mindspore.train.callback import Callback - - -class StepAcc(Callback): - def __init__(self, model, test_loader): - self.model = model - self.test_loader = test_loader - self.acc = [] - - def step_end(self, run_context): - self.acc.append( - self.model.eval(self.test_loader, dataset_sink_mode=False)['Acc']) - - -class DataPrep(ms.nn.Cell): - def __init__(self): - super(DataPrep, self).__init__() - self.concat = ms.ops.Concat(axis=1) - self.pi = np.pi - - def construct(self, x): - y = (self.pi - x[:, :-1]) * (self.pi - x[:, 1:]) - y = self.concat((x, y)) - return y - - -class QuantumNet(ms.nn.Cell): - def __init__(self, pqc): - super(QuantumNet, self).__init__() - self.dp = DataPrep() - self.pqc = pqc - - def construct(self, x): - x = self.dp(x) - x = self.pqc(x) - return x - - -batch = 5 -train_loader = ds.NumpySlicesDataset({ - 'feats': train_x, - 'labs': train_y -}, - shuffle=False).batch(batch) -test_loader = ds.NumpySlicesDataset({ - 'feats': test_x, - 'labs': test_y -}).batch(batch) -net = QuantumNet(pqc) -loss = ms.nn.SoftmaxCrossEntropyWithLogits(sparse=True, reduction='mean') -opti = ms.nn.Adam(net.trainable_params(), learning_rate=1e-1) -monitor = ms.train.callback.LossMonitor(16) -model = ms.Model(net, loss, opti, metrics={'Acc': ms.nn.Accuracy()}) -acc = StepAcc(model, test_loader) -model.train(10, - train_loader, - callbacks=[monitor, acc], - dataset_sink_mode=False) - -plt.plot(acc.acc) -plt.title('acc') -plt.xlabel('step') -plt.ylabel('acc') -plt.show() - -predict = np.argmax(ms.ops.Softmax()(model.predict(ms.Tensor(test_x))), axis=1) -corr = model.eval(test_loader, dataset_sink_mode=False) -print(corr) diff --git a/tutorials/source/parameterized_quantum_circuit.py b/tutorials/source/parameterized_quantum_circuit.py deleted file mode 100644 index 841f70b6e513f40ae9ec1754b69a809ba32d2a8a..0000000000000000000000000000000000000000 --- a/tutorials/source/parameterized_quantum_circuit.py +++ /dev/null @@ -1,91 +0,0 @@ -import numpy as np -import mindquantum as mq -from mindquantum.gate import H, X, Y, RY, RX - -print('Gate name: ', Y) -print('Gate matrix: \n', Y.matrix()) - -ry = RY('a') -ry.matrix({'a': 0.5}) - -eng = mq.engine.CircuitEngine() -qubits = eng.allocate_qureg(3) -H | qubits[0] -X | (qubits[0], qubits[1]) -RY('p1') | qubits[2] -encoder = eng.circuit -print(encoder) -encoder.summary() - -from mindquantum.engine import circuit_generator - - -@circuit_generator(3) -def encoder(qubits): - H | qubits[0] - X | (qubits[0], qubits[1]) - RY('p1') | qubits[2] - - -print(encoder) -encoder.summary() - -@circuit_generator(3, prefix='encoder') -def encoder(qubits, prefix): - H | qubits[0] - X | (qubits[0], qubits[1]) - RY(prefix + '_1') | qubits[2] - - -print(encoder) -encoder.summary() - -from mindquantum import Circuit - -encoder = Circuit() -encoder += H.on(0) -encoder += X.on(1, 0) -encoder += RY('p1').on(2) -print(encoder) -encoder.summary() - -from mindquantum.ops import QubitOperator - - -@circuit_generator(2) -def encoder(qubits): - RY('a') | qubits[0] - RY('b') | qubits[1] - - -@circuit_generator(2) -def ansatz(qubits): - X | (qubits[0], qubits[1]) - RX('p1') | qubits[0] - RX('p2') | qubits[1] - - -ham = mq.Hamiltonian(QubitOperator('Z1')) -encoder_names = ['a', 'b'] -ansatz_names = ['p1', 'p2'] - -from mindquantum.nn import generate_pqc_operator -from mindspore import Tensor -from mindspore import context - -context.set_context(mode=context.GRAPH_MODE, device_target="CPU") - -pqc = generate_pqc_operator(encoder_names, ansatz_names, encoder + ansatz, ham) -encoder_data = Tensor(np.array([[0.1, 0.2]]).astype(np.float32)) -ansatz_data = Tensor(np.array([0.3, 0.4]).astype(np.float32)) -measure_result, encoder_grad, ansatz_grad = pqc(encoder_data, ansatz_data) -print('Measurement result: ', measure_result.asnumpy()) -print('Gradient of encoder parameters: ', encoder_grad.asnumpy()) -print('Gradient of ansatz parameters: ', ansatz_grad.asnumpy()) - -encoder.no_grad() -pqc = generate_pqc_operator(encoder_names, ansatz_names, encoder + ansatz, ham) -measure_result, encoder_grad, ansatz_grad = pqc(encoder_data, ansatz_data) -print('Measurement result: ', measure_result.asnumpy()) -print('Gradient of encoder parameters: ', encoder_grad.asnumpy()) -print('Gradient of ansatz parameters: ', ansatz_grad.asnumpy())