diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a3146148f2..d8ac259592 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,10 +15,10 @@ jobs: os: ["macOS-latest", "ubuntu-latest", "windows-2019"] steps: - uses: actions/checkout@v2 - - name: Set up Python 3.7 + - name: Set up Python 3.8 uses: actions/setup-python@v2 with: - python-version: 3.7 + python-version: 3.8 - name: Install deps run: pip install "conan<2.0.0" - name: Install openblas @@ -74,10 +74,10 @@ jobs: os: ["ubuntu-latest"] steps: - uses: actions/checkout@v2 - - name: Set up Python 3.7 + - name: Set up Python 3.8 uses: actions/setup-python@v2 with: - python-version: 3.7 + python-version: 3.8 - name: Install deps run: pip install "conan<2.0.0" - name: Install openblas and mpi diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0a41e8d727..fc5b9877d2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/setup-python@v2 name: Install Python with: - python-version: '3.7' + python-version: '3.8' - name: Install cibuildwheel run: | python -m pip install cibuildwheel==2.11.2 @@ -45,7 +45,7 @@ jobs: - uses: actions/setup-python@v2 name: Install Python with: - python-version: '3.7' + python-version: '3.8' - uses: actions-rs/toolchain@v1 with: toolchain: stable @@ -119,28 +119,35 @@ jobs: TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} TWINE_USERNAME: qiskit run: twine upload dist/qiskit* - gpu-build: - name: Build qiskit-aer-gpu wheels + gpu-build-cuda11: + name: Build qiskit-aer-gpu-cu11 wheels runs-on: ubuntu-latest steps: + - name: Maximize build space + uses: easimon/maximize-build-space@master + with: + root-reserve-mb: 40000 + swap-size-mb: 1024 + remove-dotnet: 'true' + remove-android: 'true' + remove-haskell: 'true' + remove-codeql: 'true' + remove-docker-images: 'true' - uses: actions/checkout@v2 - uses: actions/setup-python@v2 name: Install Python with: - python-version: '3.7' - - name: Add msbuild to PATH - uses: microsoft/setup-msbuild@v1.0.2 - if: runner.os == 'Windows' + python-version: '3.8' - name: Install cibuildwheel run: | python -m pip install cibuildwheel==2.11.2 - name: Build wheels env: - CIBW_BEFORE_ALL: "yum install -y yum-utils wget && wget -q https://developer.download.nvidia.com/compute/cuda/10.1/Prod/local_installers/cuda-repo-rhel6-10-1-local-10.1.243-418.87.00-1.0-1.x86_64.rpm && rpm -i cuda-repo-rhel6-10-1-local-10.1.243-418.87.00-1.0-1.x86_64.rpm && yum clean all && yum -y install cuda-10-1 openblas-devel" - CIBW_MANYLINUX_X86_64_IMAGE: quay.io/pypa/manylinux2010_x86_64:latest - CIBW_SKIP: "*-manylinux_i686 cp310* pp* cp36* *musllinux*" - CIBW_TEST_SKIP: "cp*" - CIBW_ENVIRONMENT: QISKIT_AER_PACKAGE_NAME=qiskit-aer-gpu AER_THRUST_BACKEND=CUDA CUDACXX=/usr/local/cuda/bin/nvcc + CIBW_BEFORE_ALL: "yum install -y yum-utils wget && wget -q https://developer.download.nvidia.com/compute/cuda/11.8.0/local_installers/cuda-repo-rhel7-11-8-local-11.8.0_520.61.05-1.x86_64.rpm && rpm -i cuda-repo-rhel7-11-8-local-11.8.0_520.61.05-1.x86_64.rpm && yum clean all && yum -y install cuda && yum -y install openblas-devel && yum-config-manager --add-repo https://developer.download.nvidia.com/compute/cuda/repos/rhel7/x86_64/cuda-rhel7.repo && yum clean all" + CIBW_BEFORE_BUILD : "pip install nvidia-cuda-runtime-cu11 nvidia-cublas-cu11 nvidia-cusolver-cu11 nvidia-cusparse-cu11 cuquantum-cu11" + CIBW_SKIP: "*-manylinux_i686 pp* cp36* *musllinux*" + CIBW_ENVIRONMENT: QISKIT_AER_PACKAGE_NAME=qiskit-aer-gpu-cu11 QISKIT_AER_CUDA_MAJOR=11 CMAKE_VERBOSE_MAKEFILE=true AER_THRUST_BACKEND=CUDA CUDACXX=/usr/local/cuda/bin/nvcc AER_CUDA_ARCH="7.0 7.2 7.5 8.0 8.6 8.7" AER_PYTHON_CUDA_ROOT=/opt/_internal AER_CIBUILD=true + CIBW_REPAIR_WHEEL_COMMAND: 'auditwheel repair --exclude libcudart.so.11.0 --exclude libcustatevec.so.1 --exclude libcutensornet.so.2 --exclude libcutensor.so.1 --exclude libcutensorMg.so.1 --exclude libcusolver.so.11 --exclude libcusolverMg.so.11 --exclude libcusparse.so.11 --exclude libcublas.so.11 --exclude libcublasLt.so.11 -w {dest_dir} {wheel}' run: | python -m cibuildwheel --output-dir wheelhouse - uses: actions/upload-artifact@v2 @@ -153,26 +160,35 @@ jobs: run : | pip install -U twine twine upload wheelhouse/* - gpu-build-310: - name: Build qiskit-aer-gpu wheels + gpu-build-cuda12: + name: Build qiskit-aer-gpu-cu12 wheels runs-on: ubuntu-latest steps: + - name: Maximize build space + uses: easimon/maximize-build-space@master + with: + root-reserve-mb: 40000 + swap-size-mb: 1024 + remove-dotnet: 'true' + remove-android: 'true' + remove-haskell: 'true' + remove-codeql: 'true' + remove-docker-images: 'true' - uses: actions/checkout@v2 - uses: actions/setup-python@v2 name: Install Python with: - python-version: '3.7' - - name: Add msbuild to PATH - uses: microsoft/setup-msbuild@v1.0.2 - if: runner.os == 'Windows' + python-version: '3.8' - name: Install cibuildwheel run: | python -m pip install cibuildwheel==2.11.2 - name: Build wheels env: - CIBW_BEFORE_ALL: "yum install -y yum-utils wget && wget -q https://developer.nvidia.com/compute/cuda/10.1/Prod/local_installers/cuda-repo-rhel7-10-1-local-10.1.105-418.39-1.0-1.x86_64.rpm && rpm -i cuda-repo-rhel7-10-1-local-10.1.105-418.39-1.0-1.x86_64.rpm && yum clean all && yum -y install cuda-10-1 openblas-devel" - CIBW_BUILD: "cp310-manylinux_x86_64" - CIBW_ENVIRONMENT: QISKIT_AER_PACKAGE_NAME=qiskit-aer-gpu AER_THRUST_BACKEND=CUDA CUDACXX=/usr/local/cuda/bin/nvcc + CIBW_BEFORE_ALL: "yum install -y yum-utils wget && wget -q https://developer.download.nvidia.com/compute/cuda/12.1.1/local_installers/cuda-repo-rhel7-12-1-local-12.1.1_530.30.02-1.x86_64.rpm && rpm -i cuda-repo-rhel7-12-1-local-12.1.1_530.30.02-1.x86_64.rpm && yum clean all && yum -y install cuda && yum -y install openblas-devel && yum-config-manager --add-repo https://developer.download.nvidia.com/compute/cuda/repos/rhel7/x86_64/cuda-rhel7.repo && yum clean all" + CIBW_BEFORE_BUILD : "pip install nvidia-cuda-runtime-cu12 nvidia-cublas-cu12 nvidia-cusolver-cu12 nvidia-cusparse-cu12 cuquantum-cu12" + CIBW_SKIP: "*-manylinux_i686 pp* cp36* *musllinux*" + CIBW_ENVIRONMENT: QISKIT_AER_PACKAGE_NAME=qiskit-aer-gpu QISKIT_AER_CUDA_MAJOR=12 CMAKE_VERBOSE_MAKEFILE=true AER_THRUST_BACKEND=CUDA CUDACXX=/usr/local/cuda/bin/nvcc AER_CUDA_ARCH="7.0 7.2 7.5 8.0 8.6 8.7 9.0" AER_PYTHON_CUDA_ROOT=/opt/_internal AER_CIBUILD=true + CIBW_REPAIR_WHEEL_COMMAND: 'auditwheel repair --exclude libcudart.so.12 --exclude libcustatevec.so.1 --exclude libcutensornet.so.2 --exclude libcutensor.so.1 --exclude libcutensorMg.so.1 --exclude libcusolver.so.11 --exclude libcusolverMg.so.11 --exclude libcusolver.so.12 --exclude libcusolverMg.so.12 --exclude libcusparse.so.12 --exclude libcublas.so.12 --exclude libcublasLt.so.12 --exclude libnvJitLink.so.12 -w {dest_dir} {wheel}' run: | python -m cibuildwheel --output-dir wheelhouse - uses: actions/upload-artifact@v2 @@ -197,7 +213,7 @@ jobs: - uses: actions/setup-python@v2 name: Install Python with: - python-version: '3.7' + python-version: '3.8' - uses: actions-rs/toolchain@v1 with: toolchain: stable @@ -233,7 +249,7 @@ jobs: - uses: actions/setup-python@v2 name: Install Python with: - python-version: '3.7' + python-version: '3.8' - uses: actions-rs/toolchain@v1 with: toolchain: stable diff --git a/.github/workflows/docs-publish.yml b/.github/workflows/docs-publish.yml index 4cdb823bfb..6c5ffaae34 100644 --- a/.github/workflows/docs-publish.yml +++ b/.github/workflows/docs-publish.yml @@ -1,5 +1,6 @@ name: Docs Publish on: + workflow_dispatch: push: tags: - "*" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2f43de1ca8..471ecfe0ee 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -49,7 +49,7 @@ jobs: needs: ["lint"] strategy: matrix: - python-version: [3.7, 3.8, 3.9, "3.10", '3.11'] + python-version: [3.8, 3.9, "3.10", '3.11'] platform: [ { os: "ubuntu-latest", python-architecture: "x64" }, ] @@ -92,7 +92,7 @@ jobs: timeout-minutes: 60 strategy: matrix: - python-version: [3.7, 3.8, 3.9, "3.10", "3.11"] + python-version: [3.8, 3.9, "3.10", "3.11"] os: ["ubuntu-latest"] env: AER_THRUST_BACKEND: OMP @@ -190,7 +190,7 @@ jobs: timeout-minutes: 60 strategy: matrix: - python-version: [3.7, 3.8, 3.9, "3.10", '3.11'] + python-version: [3.8, 3.9, "3.10", '3.11'] os: ["macOS-latest"] env: AER_THRUST_BACKEND: OMP @@ -233,7 +233,7 @@ jobs: timeout-minutes: 60 strategy: matrix: - python-version: [3.7, 3.8, 3.9, "3.10", "3.11"] + python-version: [3.8, 3.9, "3.10", "3.11"] os: ["windows-2019"] env: AER_THRUST_BACKEND: OMP diff --git a/CMakeLists.txt b/CMakeLists.txt index 1db8b19893..da36e3fcca 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -259,49 +259,102 @@ if(AER_THRUST_SUPPORTED) set(CMAKE_CUDA_ARCHITECTURES "${AER_CUDA_ARCHITECTURES}") message(STATUS "CMAKE_CUDA_ARCHITECTURES = ${CMAKE_CUDA_ARCHITECTURES}") - set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} ${AER_CUDA_ARCH_FLAGS_EXPAND} -DAER_THRUST_CUDA -I${AER_SIMULATOR_CPP_SRC_DIR} -isystem ${AER_SIMULATOR_CPP_SRC_DIR}/third-party/headers -use_fast_math --expt-extended-lambda") - - set(AER_COMPILER_DEFINITIONS ${AER_COMPILER_DEFINITIONS} THRUST_DEVICE_SYSTEM=THRUST_DEVICE_SYSTEM_CUDA) - set(THRUST_DEPENDANT_LIBS "-L${CUDA_TOOLKIT_ROOT_DIR}/lib64") - if(NOT DEFINED CUQUANTUM_ROOT) - if(DEFINED ENV{CUQUANTUM_ROOT}) - set(CUQUANTUM_ROOT $ENV{CUQUANTUM_ROOT}) - endif() + if(NOT DEFINED AER_PYTHON_CUDA_ROOT AND DEFINED ENV{AER_PYTHON_CUDA_ROOT}) + set(AER_PYTHON_CUDA_ROOT $ENV{AER_PYTHON_CUDA_ROOT}) endif() - if(NOT DEFINED CUTENSOR_ROOT) - if(DEFINED ENV{CUTENSOR_ROOT}) - set(CUTENSOR_ROOT $ENV{CUTENSOR_ROOT}) - endif() + if(NOT DEFINED AER_CIBUILD AND DEFINED ENV{AER_CIBUILD}) + set(AER_CIBUILD $ENV{AER_CIBUILD}) endif() - if(NOT DEFINED AER_ENABLE_CUQUANTUM) - if(DEFINED ENV{AER_ENABLE_CUQUANTUM}) - set(AER_ENABLE_CUQUANTUM $ENV{AER_ENABLE_CUQUANTUM}) + if(AER_PYTHON_CUDA_ROOT) + find_package(Python) + if(AER_CIBUILD) + set(PYTHON_SITE_PATH ${AER_PYTHON_CUDA_ROOT}/cpython-${Python_VERSION_MAJOR}.${Python_VERSION_MINOR}.${Python_VERSION_PATCH}/lib/python${Python_VERSION_MAJOR}.${Python_VERSION_MINOR}/site-packages) + set(STR_ORIGIN "\$$ORIGIN") + else() + set(PYTHON_SITE_PATH ${AER_PYTHON_CUDA_ROOT}/lib/python${Python_VERSION_MAJOR}.${Python_VERSION_MINOR}/site-packages) + set(STR_ORIGIN "$ORIGIN") endif() - endif() - if(AER_ENABLE_CUQUANTUM) + message(STATUS "PYTHON_SITE_PATH = ${PYTHON_SITE_PATH}") + + set(AER_COMPILER_DEFINITIONS ${AER_COMPILER_DEFINITIONS} THRUST_DEVICE_SYSTEM=THRUST_DEVICE_SYSTEM_CUDA) set(AER_COMPILER_DEFINITIONS ${AER_COMPILER_DEFINITIONS} AER_CUSTATEVEC AER_CUTENSORNET) - if(DEFINED CUQUANTUM_ROOT) - set(AER_COMPILER_FLAGS "${AER_COMPILER_FLAGS} -I${CUQUANTUM_ROOT}/include") - set(THRUST_DEPENDANT_LIBS "${THRUST_DEPENDANT_LIBS} -L${CUQUANTUM_ROOT}/lib/${CUDA_VERSION_MAJOR}") + + set(CUDA_USE_STATIC_CUDA_RUNTIME OFF CACHE INTERNAL "") + set(CMAKE_CUDA_RUNTIME_LIBRARY None) + + + set(CUDA_NVCC_FLAGS "${AER_CUDA_ARCH_FLAGS_EXPAND} -DAER_THRUST_CUDA -I${AER_SIMULATOR_CPP_SRC_DIR} -isystem ${AER_SIMULATOR_CPP_SRC_DIR}/third-party/headers -use_fast_math --expt-extended-lambda") + set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -I${PYTHON_SITE_PATH}/cuquantum/include") + set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -I${PYTHON_SITE_PATH}/cutensor/include") + set(THRUST_DEPENDANT_LIBS "${THRUST_DEPENDANT_LIBS} -Wl,--disable-new-dtags") + set(THRUST_DEPENDANT_LIBS "${THRUST_DEPENDANT_LIBS} -Wl,-rpath,'${STR_ORIGIN}/../../nvidia/cublas/lib'") + set(THRUST_DEPENDANT_LIBS "${THRUST_DEPENDANT_LIBS} -Wl,-rpath,'${STR_ORIGIN}/../../nvidia/cusolver/lib'") + set(THRUST_DEPENDANT_LIBS "${THRUST_DEPENDANT_LIBS} -Wl,-rpath,'${STR_ORIGIN}/../../nvidia/cusparse/lib'") + set(THRUST_DEPENDANT_LIBS "${THRUST_DEPENDANT_LIBS} -Wl,-rpath,'${STR_ORIGIN}/../../nvidia/cuda_runtime/lib'") + set(THRUST_DEPENDANT_LIBS "${THRUST_DEPENDANT_LIBS} -Wl,-rpath,'${STR_ORIGIN}/../../cutensor/lib'") + set(THRUST_DEPENDANT_LIBS "${THRUST_DEPENDANT_LIBS} -Wl,-rpath,'${STR_ORIGIN}/../../cuquantum/lib'") + set(THRUST_DEPENDANT_LIBS "${THRUST_DEPENDANT_LIBS} -Wl,--no-as-needed,-l:libcusolver.so.11") + set(THRUST_DEPENDANT_LIBS "${THRUST_DEPENDANT_LIBS} -Wl,--no-as-needed,-l:libcusparse.so.${CUDA_VERSION_MAJOR}") + set(THRUST_DEPENDANT_LIBS "${THRUST_DEPENDANT_LIBS} -L${PYTHON_SITE_PATH}/cuquantum/lib") + set(THRUST_DEPENDANT_LIBS "${THRUST_DEPENDANT_LIBS} -Wl,--no-as-needed,-l:libcustatevec.so.1") + set(THRUST_DEPENDANT_LIBS "${THRUST_DEPENDANT_LIBS} -Wl,--no-as-needed,-l:libcutensornet.so.2") + + + if(CUDA_VERSION_MAJOR STREQUAL "11") + set(THRUST_DEPENDANT_LIBS "${THRUST_DEPENDANT_LIBS} -Wl,--no-as-needed,-l:libcudart.so.${CUDA_VERSION_MAJOR}.0") + else() + set(THRUST_DEPENDANT_LIBS "${THRUST_DEPENDANT_LIBS} -Wl,--no-as-needed,-l:libcudart.so.${CUDA_VERSION_MAJOR},-as-needed") endif() - if(DEFINED CUTENSOR_ROOT) - set(AER_COMPILER_FLAGS "${AER_COMPILER_FLAGS} -I${CUTENSOR_ROOT}/include") - set(THRUST_DEPENDANT_LIBS "${THRUST_DEPENDANT_LIBS} -L${CUTENSOR_ROOT}/lib/${CUDA_VERSION_MAJOR}") + set(THRUST_DEPENDANT_LIBS "${THRUST_DEPENDANT_LIBS} -Wl,--as-needed") + + string(STRIP ${CUDA_NVCC_FLAGS} CUDA_NVCC_FLAGS) + string(STRIP ${THRUST_DEPENDANT_LIBS} THRUST_DEPENDANT_LIBS) + else() + set(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} ${AER_CUDA_ARCH_FLAGS_EXPAND} -DAER_THRUST_CUDA -I${AER_SIMULATOR_CPP_SRC_DIR} -isystem ${AER_SIMULATOR_CPP_SRC_DIR}/third-party/headers -use_fast_math --expt-extended-lambda") + + set(AER_COMPILER_DEFINITIONS ${AER_COMPILER_DEFINITIONS} THRUST_DEVICE_SYSTEM=THRUST_DEVICE_SYSTEM_CUDA) + set(THRUST_DEPENDANT_LIBS "-L${CUDA_TOOLKIT_ROOT_DIR}/lib64") + if(NOT DEFINED CUQUANTUM_ROOT) + if(DEFINED ENV{CUQUANTUM_ROOT}) + set(CUQUANTUM_ROOT $ENV{CUQUANTUM_ROOT}) + endif() endif() - if(CUQUANTUM_STATIC) - set(THRUST_DEPENDANT_LIBS "${THRUST_DEPENDANT_LIBS} -lcustatevec_static -lcutensornet_static -lcutensor_static -lmetis_static -lcusolver_static -lcusparse_static -lcusolver_lapack_static -lcublas_static -lcublasLt_static -lculibos") - else() - set(THRUST_DEPENDANT_LIBS "${THRUST_DEPENDANT_LIBS} -lcustatevec -lcutensornet -lcutensor") + if(NOT DEFINED CUTENSOR_ROOT) + if(DEFINED ENV{CUTENSOR_ROOT}) + set(CUTENSOR_ROOT $ENV{CUTENSOR_ROOT}) + endif() endif() - elseif(CUSTATEVEC_ROOT) - #TODO this is remained for backward compatibility, use CUQUANTUM_ROOT instead - set(AER_COMPILER_DEFINITIONS ${AER_COMPILER_DEFINITIONS} AER_CUSTATEVEC) - set(AER_COMPILER_FLAGS "${AER_COMPILER_FLAGS} -I${CUSTATEVEC_ROOT}/include") - if(CUSTATEVEC_STATIC) - set(THRUST_DEPENDANT_LIBS "${THRUST_DEPENDANT_LIBS} -L${CUSTATEVEC_ROOT}/lib -L${CUSTATEVEC_ROOT}/lib/${CUDA_VERSION_MAJOR} -lcustatevec_static -lcusolver_static -lcusparse_static -lcusolver_lapack_static -lcublas_static -lcublasLt_static -lculibos") - else() - set(THRUST_DEPENDANT_LIBS "${THRUST_DEPENDANT_LIBS} -L${CUSTATEVEC_ROOT}/lib -L${CUSTATEVEC_ROOT}/lib/${CUDA_VERSION_MAJOR} -lcustatevec") + if(NOT DEFINED AER_ENABLE_CUQUANTUM) + if(DEFINED ENV{AER_ENABLE_CUQUANTUM}) + set(AER_ENABLE_CUQUANTUM $ENV{AER_ENABLE_CUQUANTUM}) + endif() + endif() + + if(AER_ENABLE_CUQUANTUM) + set(AER_COMPILER_DEFINITIONS ${AER_COMPILER_DEFINITIONS} AER_CUSTATEVEC AER_CUTENSORNET) + if(DEFINED CUQUANTUM_ROOT) + set(AER_COMPILER_FLAGS "${AER_COMPILER_FLAGS} -I${CUQUANTUM_ROOT}/include") + set(THRUST_DEPENDANT_LIBS "${THRUST_DEPENDANT_LIBS} -L${CUQUANTUM_ROOT}/lib/${CUDA_VERSION_MAJOR}") + endif() + if(DEFINED CUTENSOR_ROOT) + set(AER_COMPILER_FLAGS "${AER_COMPILER_FLAGS} -I${CUTENSOR_ROOT}/include") + set(THRUST_DEPENDANT_LIBS "${THRUST_DEPENDANT_LIBS} -L${CUTENSOR_ROOT}/lib/${CUDA_VERSION_MAJOR}") + endif() + if(CUQUANTUM_STATIC) + set(THRUST_DEPENDANT_LIBS "${THRUST_DEPENDANT_LIBS} -lcustatevec_static -lcutensornet_static -lcutensor_static -lmetis_static -lcusolver_static -lcusparse_static -lcusolver_lapack_static -lcublas_static -lcublasLt_static -lculibos") + else() + set(THRUST_DEPENDANT_LIBS "${THRUST_DEPENDANT_LIBS} -lcustatevec -lcutensornet -lcutensor") + endif() + elseif(CUSTATEVEC_ROOT) + #TODO this is remained for backward compatibility, use CUQUANTUM_ROOT instead + set(AER_COMPILER_DEFINITIONS ${AER_COMPILER_DEFINITIONS} AER_CUSTATEVEC) + set(AER_COMPILER_FLAGS "${AER_COMPILER_FLAGS} -I${CUSTATEVEC_ROOT}/include") + if(CUSTATEVEC_STATIC) + set(THRUST_DEPENDANT_LIBS "${THRUST_DEPENDANT_LIBS} -L${CUSTATEVEC_ROOT}/lib -L${CUSTATEVEC_ROOT}/lib/${CUDA_VERSION_MAJOR} -lcustatevec_static -lcusolver_static -lcusparse_static -lcusolver_lapack_static -lcublas_static -lcublasLt_static -lculibos") + else() + set(THRUST_DEPENDANT_LIBS "${THRUST_DEPENDANT_LIBS} -L${CUSTATEVEC_ROOT}/lib -L${CUSTATEVEC_ROOT}/lib/${CUDA_VERSION_MAJOR} -lcustatevec") + endif() endif() endif() elseif(AER_THRUST_BACKEND STREQUAL "TBB") @@ -359,16 +412,17 @@ set(AER_LIBRARIES AER_DEPENDENCY_PKG::nlohmann_json AER_DEPENDENCY_PKG::spdlog Threads::Threads - ${CMAKE_DL_LIBS} - ${THRUST_DEPENDANT_LIBS} - ${MPI_DEPENDANT_LIBS}) + ${CMAKE_DL_LIBS}) set(AER_COMPILER_DEFINITIONS ${AER_COMPILER_DEFINITIONS} ${CONAN_DEFINES}) if(SKBUILD) # Terra Addon build set(AER_LIBRARIES ${AER_LIBRARIES} AER_DEPENDENCY_PKG::muparserx) add_subdirectory(qiskit_aer/backends/wrappers) - add_subdirectory(src/open_pulse) else() # Standalone build + set(AER_LIBRARIES + ${AER_LIBRARIES} + ${THRUST_DEPENDANT_LIBS} + ${MPI_DEPENDANT_LIBS}) function(build_cuda target src_file is_exec) if(CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" OR CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" OR CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "amd64") @@ -452,8 +506,7 @@ else() # Standalone build install(TARGETS qasm_simulator DESTINATION bin) - # build libaer.so (currently, Linux on x86 is supportted) - if (CMAKE_SYSTEM_NAME STREQUAL "Linux") + if (CMAKE_SYSTEM_NAME STREQUAL "Linux" OR CMAKE_SYSTEM_NAME STREQUAL "Darwin") set(AER_RUNTIME_SOURCE "${PROJECT_SOURCE_DIR}/contrib/runtime/aer_runtime.cpp") if(CUDA_FOUND AND AER_THRUST_BACKEND STREQUAL "CUDA") build_cuda(aer ${AER_RUNTIME_SOURCE} FALSE) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e33c2700d0..5e11aa1bc1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,8 +20,8 @@ please ensure that: which will run these checks and report any issues. If your code fails the local style checks, you can use `tox -eblack` - and `tox -eclang` to automatically fix update the code formatting - in python and C++ respectively. + and `tox -eclang` to automatically fix and update the code formatting + in python and C++, respectively. 2. The documentation has been updated accordingly. In particular, if a function or class has been modified during the PR, please update the @@ -39,7 +39,7 @@ automation. This works through a combination of the git log and the pull request. When a release is tagged and pushed to GitHub, the release automation bot looks at all commit messages from the git log for the release. It takes the PR numbers from the git log (assuming a squash merge) and checks if that PR had -a `Changelog:` label on it. If there is a label it will add the git commit +a `Changelog:` label on it. If there is a label, it will add the git commit message summary line from the git log for the release to the changelog. If there are multiple `Changelog:` tags on a PR, the git commit message summary @@ -69,7 +69,7 @@ understand if they need to update their program which uses Qiskit, and how they would go about doing that. It ideally should explain why they need to make this change too, to provide the necessary context. -To make sure we don't forget a release note or if the details of user-facing +To make sure we don't forget a release note if the details of user-facing changes over a release cycle, we require that all user facing changes include documentation at the same time as the code. To accomplish this, we use the [reno](https://docs.openstack.org/reno/latest/) tool which enables a git-based @@ -193,14 +193,14 @@ particular will be located at `docs/_build/html/release_notes.html` ## Style and lint -Qiskit Aer uses 3 tools for verify code formatting and lint checking. The +Qiskit Aer uses 3 tools for verifying code formatting and lint checking. The first tool is [black](https://github.com/psf/black) which is a Python code formatting tool that will automatically update the code formatting to a consistent style. The second tool is [pylint](https://www.pylint.org/) which is a code linter which does a deeper analysis of the Python code to find both style issues and potential bugs and other common issues in Python. The third tool is [clang-format](https://clang.llvm.org/docs/ClangFormat.html) which is a -C++ code formatting tool that will automatically update codes with a consitent style. +C++ code formatting tool that will automatically update codes with a consistent style. You can check that your local modifications conform to the style rules by running `tox -elint` which will run `black`, `pylint` and `clang-format` @@ -240,7 +240,7 @@ When it is time to release a new minor version of qiskit-aer, we will: 1. Create a new tag with the version number and push it to github 2. Change the `main` version to the next release version. -The release automation processes will be triggered by the new tag and perform +The release automation processes will be triggered by the new tag and will perform the following steps: 1. Create a stable branch for the new minor version from the release tag @@ -463,7 +463,7 @@ As any other Python package, we can install from source code by just running: qiskit-aer$ pip install . This will build and install `Aer` with the default options which is probably suitable for most of the users. -There's another Pythonic approach to build and install software: build the wheels distributable file. +There's another Pythonic approach to building and installing software: build the wheels distributable file. qiskit-aer$ python ./setup.py bdist_wheel @@ -566,7 +566,7 @@ As any other Python package, we can install from source code by just running: (QiskitDevEnv) qiskit-aer > pip install . This will build and install `Aer` with the default options which is probably suitable for most of the users. -There's another Pythonic approach to build and install software: build the wheels distributable file. +There's another Pythonic approach to building and installing software: build the wheels distributable file. (QiskitDevEnv) qiskit-aer > python ./setup.py bdist_wheel @@ -637,7 +637,7 @@ options we have on `Aer` to CMake, we use its native mechanism: Qiskit Aer can exploit GPU's horsepower to accelerate some simulations, specially the larger ones. GPU access is supported via CUDA® (NVIDIA® chipset), so to build with GPU support, you need -to have CUDA® >= 10.1 preinstalled. See install instructions [here](https://developer.nvidia.com/cuda-toolkit-archive) +to have CUDA® >= 11.2 preinstalled. See install instructions [here](https://developer.nvidia.com/cuda-toolkit-archive) Please note that we only support GPU acceleration on Linux platforms at the moment. Once CUDA® is properly installed, you only need to set a flag so the build system knows what to do: @@ -648,43 +648,51 @@ AER_THRUST_BACKEND=CUDA For example, - qiskit-aer$ python ./setup.py bdist_wheel -- -DAER_THRUST_BACKEND=CUDA + qiskit-aer$ python ./setup.py bdist_wheel -- -DAER_THRUST_BACKEND=CUDA -- If you want to specify the CUDA® architecture instead of letting the build system auto detect it, you can use the AER_CUDA_ARCH flag (can also be set as an ENV variable with the same name, although the flag takes precedence). For example: - qiskit-aer$ python ./setup.py bdist_wheel -- -DAER_THRUST_BACKEND=CUDA -DAER_CUDA_ARCH="5.2" + qiskit-aer$ python ./setup.py bdist_wheel -- -DAER_THRUST_BACKEND=CUDA -DAER_CUDA_ARCH="7.0" -- or - qiskit-aer$ export AER_CUDA_ARCH="5.2" - qiskit-aer$ python ./setup.py bdist_wheel -- -DAER_THRUST_BACKEND=CUDA + qiskit-aer$ export AER_CUDA_ARCH="7.0" + qiskit-aer$ python ./setup.py bdist_wheel -- -DAER_THRUST_BACKEND=CUDA -- This will reduce the amount of compilation time when, for example, the architecture auto detection fails and the build system compiles all common architectures. Few notes on GPU builds: 1. Building takes considerable more time than non-GPU build, so be patient :) -2. CUDA® >= 10.1 imposes the restriction of building with g++ version not newer than 8 +2. CUDA® >= 11.2 imposes the restriction of building with g++ version not newer than 8 3. We don't need NVIDIA® drivers for building, but we need them for running simulations 4. Only Linux platforms are supported Qiskit Aer now supports cuQuantum optimized Quantum computing APIs from NVIDIA®. cuStateVec APIs can be exploited to accelerate statevector, density_matrix and unitary methods. cuTensorNet APIs can be exploited to tensor_network merthod. -This implementation requires CUDA toolkit version 11.2 or higher and Volta or Ampare architecture GPUs. +This implementation requires CUDA® toolkit version 11.2 or higher and Volta or Ampare architecture GPUs. + +Before building Qiskit Aer with cuQuantum support, install required components via pip install as following. + + qiskit-aer$ pip install nvidia-cuda-runtime-cu11 nvidia-cublas-cu11 nvidia-cusolver-cu11 nvidia-cusparse-cu11 cuquantum-cu11 + +This example is for CUDA 11. Please replace cu11 to cu12 if your system has CUDA 12. + +Then to build with cuQuantum support, set the value `AER_PYTHON_CUDA_ROOT=` as following example. + + qiskit-aer$ python ./setup.py bdist_wheel -- -DAER_THRUST_BACKEND=CUDA -DAER_PYTHON_CUDA_ROOT=qiskit-aer-venv -- + -To build Qiskit Aer with cuQuantum support, please set the path to cuQuantum root directory to CUQUANTUM_ROOT -and directory to cuTensor to CUTENSOR_ROOT then set AER_ENABLE_CUQUANTUM=true. -as following. +If you want to link cuQuantum library statically, cuQuantum SDK and cuTENSOR should be installed in your system from NVIDIA®. +Then set `CUQUANTUM_ROOT` `CUTENSOR_ROOT` and `CUQUANTUM_STATIC` to setup.py. For example, - qiskit-aer$ python ./setup.py bdist_wheel -- -DAER_THRUST_BACKEND=CUDA -DCUQUANTUM_ROOT=path_to_cuQuantum -DCUTENSOR_ROOT=path_to_cuTENSOR -DAER_ENABLE_CUQUANTUM=true -- + qiskit-aer$ python ./setup.py bdist_wheel -- -DAER_THRUST_BACKEND=CUDA -DCUQUANTUM_ROOT=path_to_cuQuantum -DCUTENSOR_ROOT=path_to_cuTENSOR -DAER_ENABLE_CUQUANTUM=true -DCUQUANTUM_STATIC=true -- -if you want to link cuQuantum library statically, set `CUQUANTUM_STATIC` to setup.py. -Otherwise you also have to set environmental variable LD_LIBRARY_PATH to indicate path to the cuQuantum libraries. To run with cuStateVec, set `device='GPU'` to AerSimulator option and set `cuStateVec_enable=True` to option in execute method. @@ -731,7 +739,7 @@ AER_DISABLE_GDR=True For example, - qiskit-aer$ python ./setup.py bdist_wheel -- -DAER_MPI=True -DAER_DISABLE_GDR=True + qiskit-aer$ python ./setup.py bdist_wheel -- -DAER_MPI=True -DAER_DISABLE_GDR=True -- ### Running with multiple-GPUs and/or multiple nodes diff --git a/README.md b/README.md index 199654cfce..1d8fe978de 100755 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ **Qiskit** is an open-source framework for working with noisy quantum computers at the level of pulses, circuits, and algorithms. -Qiskit is made up of elements that each work together to enable quantum computing. This element is **Aer**, which provides high-performance quantum computing simulators with realistic noise models. +Qiskit is made up of elements that work together to enable quantum computing. This element is **Aer**, which provides high-performance quantum computing simulators with realistic noise models. ## Installation @@ -14,13 +14,13 @@ We encourage installing Qiskit via the pip tool (a python package manager). The pip install qiskit qiskit-aer ``` -Pip will handle all dependencies automatically for us and you will always install the latest (and well-tested) version. +Pip will handle all dependencies automatically for us, and you will always install the latest (and well-tested) version. To install from source, follow the instructions in the [contribution guidelines](CONTRIBUTING.md). ## Installing GPU support -In order to install and run the GPU supported simulators on Linux, you need CUDA® 10.1 or newer previously installed. +In order to install and run the GPU supported simulators on Linux, you need CUDA® 11.2 or newer previously installed. CUDA® itself would require a set of specific GPU drivers. Please follow CUDA® installation procedure in the NVIDIA® [web](https://www.nvidia.com/drivers). If you want to install our GPU supported simulators, you have to install this other package: @@ -29,12 +29,17 @@ If you want to install our GPU supported simulators, you have to install this ot pip install qiskit-aer-gpu ``` +The package above is for CUDA® 12, so if your system has CUDA® 11 installed, install separate package: +```bash +pip install qiskit-aer-gpu-cu11 +``` + This will overwrite your current `qiskit-aer` package installation giving you the same functionality found in the canonical `qiskit-aer` package, plus the ability to run the GPU supported simulators: statevector, density matrix, and unitary. **Note**: This package is only available on x86_64 Linux. For other platforms -that have CUDA support you will have to build from source. You can refer to +that have CUDA support, you will have to build from source. You can refer to the [contributing guide](CONTRIBUTING.md#building-with-gpu-support) for instructions on doing this. @@ -47,8 +52,8 @@ $ python ```python import qiskit -from qiskit import IBMQ from qiskit_aer import AerSimulator +from qiskit.providers.fake_provider import FakeManilaV2 # Generate 3-qubit GHZ state circ = qiskit.QuantumCircuit(3) @@ -61,32 +66,31 @@ circ.measure_all() aersim = AerSimulator() # Perform an ideal simulation -result_ideal = qiskit.execute(circ, aersim).result() +result_ideal = aersim.run(circ).result() counts_ideal = result_ideal.get_counts(0) print('Counts(ideal):', counts_ideal) # Counts(ideal): {'000': 493, '111': 531} # Construct a noisy simulator backend from an IBMQ backend # This simulator backend will be automatically configured -# using the device configuration and noise model -provider = IBMQ.load_account() -backend = provider.get_backend('ibmq_athens') +# using the device configuration and noise model +backend = FakeManilaV2() aersim_backend = AerSimulator.from_backend(backend) # Perform noisy simulation -result_noise = qiskit.execute(circ, aersim_backend).result() +result_noise = aersim_backend.run(circ).result() counts_noise = result_noise.get_counts(0) print('Counts(noise):', counts_noise) -# Counts(noise): {'000': 492, '001': 6, '010': 8, '011': 14, '100': 3, '101': 14, '110': 18, '111': 469} +# Counts(noise): {'101': 16, '110': 48, '100': 7, '001': 31, '010': 7, '000': 464, '011': 15, '111': 436} ``` ## Contribution Guidelines If you'd like to contribute to Qiskit, please take a look at our -[contribution guidelines](CONTRIBUTING.md). This project adheres to Qiskit's [code of conduct](CODE_OF_CONDUCT.md). By participating, you are expect to uphold to this code. +[contribution guidelines](CONTRIBUTING.md). This project adheres to Qiskit's [code of conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. -We use [GitHub issues](https://github.com/Qiskit/qiskit-aer/issues) for tracking requests and bugs. Please use our [slack](https://qiskit.slack.com) for discussion and simple questions. To join our Slack community use the [link](https://qiskit.slack.com/join/shared_invite/zt-fybmq791-hYRopcSH6YetxycNPXgv~A#/). For questions that are more suited for a forum we use the Qiskit tag in the [Stack Exchange](https://quantumcomputing.stackexchange.com/questions/tagged/qiskit). +We use [GitHub issues](https://github.com/Qiskit/qiskit-aer/issues) for tracking requests and bugs. Please use our [slack](https://qiskit.slack.com) for discussion and simple questions. To join our Slack community use the [link](https://qiskit.slack.com/join/shared_invite/zt-fybmq791-hYRopcSH6YetxycNPXgv~A#/). For questions that are more suited for a forum, we use the Qiskit tag in the [Stack Exchange](https://quantumcomputing.stackexchange.com/questions/tagged/qiskit). ## Next Steps diff --git a/constraints.txt b/constraints.txt index a0a43723c5..1f3bd7a329 100644 --- a/constraints.txt +++ b/constraints.txt @@ -1,7 +1,7 @@ pylint==2.4.4 astroid==2.3.3 six>1.10,<=1.14 -numpy>=1.16.3 +numpy>=1.16.3,<1.25 scipy>=1.0 # stevedore, used by Terra currently (as of 3.4.0) issues deprecation warnings # with modern importlib-metadata (4.8.1). importlib-metadata is only needed on diff --git a/contrib/runtime/aer_runtime.cpp b/contrib/runtime/aer_runtime.cpp index 037b60da7c..784a626d51 100644 --- a/contrib/runtime/aer_runtime.cpp +++ b/contrib/runtime/aer_runtime.cpp @@ -13,6 +13,7 @@ */ #include "controllers/state_controller.hpp" #include +#include // initialize and return state extern "C" { @@ -22,10 +23,9 @@ void *aer_state() { return handler; }; -void *aer_state_initialize(void *handler) { +void aer_state_initialize(void *handler) { AER::AerState *state = reinterpret_cast(handler); state->initialize(); - return handler; }; // finalize state @@ -51,8 +51,7 @@ uint_t aer_allocate_qubits(void *handler, uint_t num_qubits) { // measure qubits uint_t aer_apply_measure(void *handler, uint_t *qubits_, size_t num_qubits) { AER::AerState *state = reinterpret_cast(handler); - std::vector qubits; - qubits.insert(qubits.end(), &(qubits_[0]), &(qubits[num_qubits - 1])); + std::vector qubits(qubits_, qubits_ + num_qubits); return state->apply_measure(qubits); }; @@ -76,6 +75,13 @@ complex_t *aer_release_statevector(void *handler) { return sv.move_to_buffer(); }; +// u3 gate +void aer_apply_u3(void *handler, uint_t qubit, double theta, double phi, + double lambda) { + AER::AerState *state = reinterpret_cast(handler); + state->apply_u(qubit, theta, phi, lambda); +} + // phase gate void aer_apply_p(void *handler, uint_t qubit, double lambda) { AER::AerState *state = reinterpret_cast(handler); diff --git a/contrib/runtime/aer_runtime_api.h b/contrib/runtime/aer_runtime_api.h index 2e27a6fcc0..5d3040c0c4 100644 --- a/contrib/runtime/aer_runtime_api.h +++ b/contrib/runtime/aer_runtime_api.h @@ -20,7 +20,7 @@ typedef uint_fast64_t uint_t; void* aer_state(); // initialize aer state -void* aer_state_initialize(); +void aer_state_initialize(void* state); // finalize state void aer_state_finalize(void* state); @@ -45,6 +45,9 @@ double complex aer_amplitude(void* state, uint_t outcome); // returned pointer must be freed in the caller double complex* aer_release_statevector(void* state); +// u3 gate +void aer_apply_u3(void* state, uint_t qubit, double theta, double phi, double lambda); + // phase gate void aer_apply_p(void* state, uint_t qubit, double lambda); diff --git a/docs/apidocs/aer.rst b/docs/apidocs/aer.rst index b739136bf1..6c671a5cd8 100644 --- a/docs/apidocs/aer.rst +++ b/docs/apidocs/aer.rst @@ -12,8 +12,6 @@ Qiskit Aer API Reference aer_library aer_noise aer_primitives - aer_pulse aer_utils aer_quantum_info - circuit - parallel + circuit \ No newline at end of file diff --git a/docs/apidocs/aer_pulse.rst b/docs/apidocs/aer_pulse.rst deleted file mode 100644 index 37424762ff..0000000000 --- a/docs/apidocs/aer_pulse.rst +++ /dev/null @@ -1,6 +0,0 @@ -.. _aer-pulse: - -.. automodule:: qiskit_aer.pulse - :no-members: - :no-inherited-members: - :no-special-members: diff --git a/docs/conf.py b/docs/conf.py index d43ff14a6f..9f4cb9514f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -44,8 +44,6 @@ copyright = f"2017-{datetime.date.today().year}, Qiskit Development Team" # pylint: disable=redefined-builtin author = 'Qiskit Development Team' -import qiskit_sphinx_theme - # The short X.Y version version = '0.13.0' # The full version, including alpha/beta/rc tags @@ -75,6 +73,7 @@ 'matplotlib.sphinxext.plot_directive', 'sphinx.ext.intersphinx', 'nbsphinx', + "sphinx_design", 'qiskit_sphinx_theme' ] diff --git a/docs/getting_started.rst b/docs/getting_started.rst new file mode 100644 index 0000000000..725260c52e --- /dev/null +++ b/docs/getting_started.rst @@ -0,0 +1,211 @@ +:orphan: + +############### +Getting started +############### + +Installation +============ +Qiskit Aer depends on the main Qiskit package which has its own +`Qiskit Getting Started `__ detailing the +installation options for Qiskit and its supported environments/platforms. You should refer to +that first. Then the information here can be followed which focuses on the additional installation +specific to Qiskit Aer. + + +.. tab-set:: + + .. tab-item:: Start locally + + The simplest way to get started is to follow the getting started 'Start locally' for Qiskit + here `Qiskit Getting Started `__ + + In your virtual environment where you installed Qiskit simply add ``aer`` to the + extra list in a similar manner to how the extra ``visualization`` support is installed for + Qiskit, i.e: + + .. code:: sh + + pip install qiskit-aer + + **Installing GPU support** + + In order to install and run the GPU supported simulators on Linux, you need CUDA® 10.1 or newer + previously installed. CUDA® itself would require a set of specific GPU drivers. + Please follow CUDA® installation procedure in the NVIDIA® `web `_. + + If you want to install our GPU supported simulators, you have to install this other package: + + .. code:: sh + + pip install qiskit-aer-gpu + + This will overwrite your current qiskit-aer package installation giving you the same functionality found + in the canonical qiskit-aer package, plus the ability to run the GPU supported + simulators: statevector, density matrix, and unitary. + + *Note: This package is only available on x86_64 Linux. + For other platforms that have CUDA support you will have to build from source.* + + + .. tab-item:: Install from source + + + Installing Qiskit Aer from source allows you to access the most recently + updated version under development instead of using the version in the Python Package + Index (PyPI) repository. This will give you the ability to inspect and extend + the latest version of the Qiskit Aer code more efficiently. + + Since Qiskit Aer depends on Qiskit, and its latest changes may require new or changed + features of Qiskit, you should first follow Qiskit's `"Install from source"` instructions + here `Qiskit Getting Started `__ + + .. raw:: html + +

Installing Qiskit Aer from Source

+ + + Clone the ``Qiskit Aer`` repo via *git*. + + .. code:: sh + + git clone https://github.com/Qiskit/qiskit-aer + + The common dependencies can then be installed via *pip*, using the ``requirements-dev.txt`` file, e.g.: + + .. code:: sh + + cd qiskit-aer + pip install -r requirements-dev.txt + + As any other Python package, we can install from source code by just running: + + .. code:: sh + + qiskit-aer$ pip install . + + This will build and install ``Aer`` with the default options which is + probably suitable for most of the users. There’s another Pythonic + approach to build and install software: build the wheels distributable + file. + + .. code:: sh + + qiskit-aer$ pip install build + qiskit-aer$ python -I -m build --wheel + + + See `here `__ + for detailed installation information. + + .. raw:: html + +

Building with GPU support

+ + Qiskit Aer can exploit GPU’s horsepower to accelerate some simulations, + specially the larger ones. GPU access is supported via CUDA® (NVIDIA® + chipset), so to build with GPU support, you need to have CUDA® >= 10.1 + preinstalled. See install instructions + `here `__ Please note + that we only support GPU acceleration on Linux platforms at the moment. + + Once CUDA® is properly installed, you only need to set a flag so the + build system knows what to do: + + .. code:: sh + + AER_THRUST_BACKEND=CUDA + + For example, + + .. code:: sh + + qiskit-aer$ python ./setup.py bdist_wheel -- -DAER_THRUST_BACKEND=CUDA + + See `here `__ + for detailed GPU support information. + + .. raw:: html + +

Building with MPI support

+ + Qiskit Aer can parallelize its simulation on the cluster systems by + using MPI. This can extend available memory space to simulate quantum + circuits with larger number of qubits and also can accelerate the + simulation by parallel computing. To use MPI support, any MPI library + (i.e. OpenMPI) should be installed and configured on the system. + + Qiskit Aer supports MPI both with and without GPU support. Currently + following simulation methods are supported to be parallelized by MPI. + + - statevector + - density_matrix + - unitary + + To enable MPI support, the following flag is needed for build system + based on CMake. + + .. code:: sh + + AER_MPI=True + + For example, + + .. code:: sh + + qiskit-aer$ python ./setup.py bdist_wheel -- -DAER_MPI=True + + See `here `__ + for detailed MPI support information. + + +Simulating your first quantum program with Qiskit Aer +===================================================== +Now that you have Qiskit Aer installed, you can start simulating a quantum circuit. +Here is a basic example: + +.. code:: python + + import qiskit + from qiskit_aer import AerSimulator + + # Generate 3-qubit GHZ state + circ = qiskit.QuantumCircuit(3) + circ.h(0) + circ.cx(0, 1) + circ.cx(1, 2) + circ.measure_all() + + # Construct an ideal simulator + aersim = AerSimulator() + + # Perform an ideal simulation + result_ideal = qiskit.execute(circ, aersim).result() + counts_ideal = result_ideal.get_counts(0) + print('Counts(ideal):', counts_ideal) + # Counts(ideal): {'000': 493, '111': 531} + +Ready to get going?... +====================== + +.. raw:: html + +
+
+ +.. qiskit-call-to-action-item:: + :description: Find out about Qiskit Aer + :header: Dive into the tutorials + :button_link: ./tutorials/index.html + :button_text: Qiskit Aer tutorials + +.. raw:: html + +
+
+ + +.. Hiding - Indices and tables + :ref:`genindex` + :ref:`modindex` + :ref:`search` \ No newline at end of file diff --git a/docs/howtos/index.rst b/docs/howtos/index.rst new file mode 100644 index 0000000000..3176842da7 --- /dev/null +++ b/docs/howtos/index.rst @@ -0,0 +1,18 @@ +########################### +Qiskit Aer How-To Guides +########################### + +This section of the documentation provides concrete step-by-step instructions for how to +do specific useful actions in Qiskit Aer. + +.. toctree:: + :caption: How to... + :maxdepth: 1 + :glob: + + * + +.. Hiding - Indices and tables + :ref:`genindex` + :ref:`modindex` + :ref:`search` diff --git a/docs/apidocs/parallel.rst b/docs/howtos/parallel.rst similarity index 100% rename from docs/apidocs/parallel.rst rename to docs/howtos/parallel.rst diff --git a/docs/howtos/running_gpu.rst b/docs/howtos/running_gpu.rst new file mode 100644 index 0000000000..31c3c72bb2 --- /dev/null +++ b/docs/howtos/running_gpu.rst @@ -0,0 +1,92 @@ +.. _running_gpu: + +Running with multiple-GPUs and/or multiple nodes +================================================ + +Qiskit Aer parallelizes simulations by distributing quantum states into +distributed memory space. To decrease data transfer between spaces the +distributed states are managed as chunks that is a sub-state for smaller +qubits than the input circuits. + +For example, 30-qubits circuit is distributed into 2^10 chunks with +20-qubits. + +To decrease data exchange between chunks and also to simplify the +implementation, we are applying cache blocking technique. This technique +allows applying quantum gates to each chunk independently without data +exchange, and serial simulation codes can be reused without special +implementation. Before the actual simulation, we apply transpilation to +remap the input circuits to the equivalent circuits that has all the +quantum gates on the lower qubits than the chunk’s number of qubits. And +the (noiseless) swap gates are inserted to exchange data. + +Please refer to this paper (https://arxiv.org/abs/2102.02957) for more +detailed algorithm and implementation of parallel simulation. + +So to simulate by using multiple GPUs or multiple nodes on the cluster, +following configurations should be set to backend options. (If there is +not enough memory to simulate the input circuit, Qiskit Aer +automatically set following options, but it is recommended to explicitly +set them) + +- blocking_enable + +should be set to True for distributed parallelization. (Default = False) + +- blocking_qubits + +this flag sets the qubit number for chunk, should be smaller than the +smallest memory space on the system (i.e. GPU). Set this parameter to +satisfy +``sizeof(complex)*2^(blocking_qubits+4) < size of the smallest memory space`` +in byte. + +Here is an example how we parallelize simulation with multiple GPUs. + +.. code:: python + + sim = AerSimulator(method='statevector', device='GPU') + circ = transpile(QuantumVolume(qubit, 10, seed = 0)) + circ.measure_all() + result = execute(circ, sim, shots=100, blocking_enable=True, blocking_qubits=23).result() + +To run Qiskit Aer with Python script with MPI parallelization, MPI +executer such as mpirun should be used to submit a job on the cluster. +Following example shows how to run Python script using 4 processes by +using mpirun. + +.. code:: sh + + mpirun -np 4 python example.py + +MPI_Init function is called inside Qiskit Aer, so you do not have to +manage MPI processes in Python script. Following metadatas are useful to +find on which process is this script running. + +- num_mpi_processes : shows number of processes using for this + simulation +- mpi_rank : shows zero based rank (process ID) + +Here is an example how to get my rank. + +.. code:: python + + sim = AerSimulator(method='statevector', device='GPU') + result = execute(circuit, sim, blocking_enable=True, blocking_qubits=23).result() + dict = result.to_dict() + meta = dict['metadata'] + myrank = meta['mpi_rank'] + +Multiple shots are also distributed to multiple nodes when setting +``device=GPU`` and ``batched_shots_gpu=True``. The results are +distributed to each processes. + +Note : In the script, make sure that the same random seed should be used +for all processes so that the consistent circuits and parameters are +passed to Qiskit Aer. To do so add following option to the script. + +.. code:: python + + from qiskit.utils import algorithm_globals + algorithm_globals.random_seed = consistent_seed_to_all_processes + diff --git a/docs/index.rst b/docs/index.rst index 35ff654abf..4c45d3da72 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -15,8 +15,10 @@ https://github.com/Qiskit/qiskit-aer .. toctree:: :hidden: + Getting Started API Documentation Tutorials + How-Tos Release Notes .. Hiding - Indices and tables diff --git a/docs/tutorials/1_aer_provider.ipynb b/docs/tutorials/1_aer_provider.ipynb index 53d573c801..9e165a5701 100755 --- a/docs/tutorials/1_aer_provider.ipynb +++ b/docs/tutorials/1_aer_provider.ipynb @@ -63,8 +63,7 @@ " AerSimulator('aer_simulator_superop'),\n", " QasmSimulator('qasm_simulator'),\n", " StatevectorSimulator('statevector_simulator'),\n", - " UnitarySimulator('unitary_simulator'),\n", - " PulseSimulator('pulse_simulator')]" + " UnitarySimulator('unitary_simulator'),\n" ] }, "execution_count": 2, diff --git a/pyproject.toml b/pyproject.toml index 46e2c752c8..8bcfe1bdce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ build-backend = "setuptools.build_meta" [tool.cibuildwheel] manylinux-x86_64-image = "manylinux2014" manylinux-i686-image = "manylinux2014" -skip = "pp* cp36* *musllinux*" +skip = "pp* cp36* cp37* *musllinux*" test-skip = "cp310-win32 cp310-manylinux_i686 cp311-win32 cp311-manylinux_i686" test-command = "python {project}/tools/verify_wheels.py" # We need to use pre-built versions of Numpy and Scipy in the tests; they have a @@ -30,9 +30,9 @@ before-all = "yum install -y openblas-devel" environment = { CMAKE_GENERATOR = "Visual Studio 16 2019"} [[tool.cibuildwheel.overrides]] -select = "cp3{7,8,9,10,11}-manylinux_i686" +select = "cp3{8,9,10,11}-manylinux_i686" before-all = "yum install -y wget && bash {project}/tools/install_openblas_i686.sh && bash {project}/tools/install_rust.sh" [tool.black] line-length = 100 -target-version = ['py37', 'py38', 'py39', 'py310', 'py311'] +target-version = ['py38', 'py39', 'py310', 'py311'] diff --git a/qiskit_aer/__init__.py b/qiskit_aer/__init__.py index 8a76e0305f..0c3aac5b18 100644 --- a/qiskit_aer/__init__.py +++ b/qiskit_aer/__init__.py @@ -32,7 +32,6 @@ :toctree: ../stubs/ AerSimulator - PulseSimulator Legacy Simulator Backends ========================= @@ -72,7 +71,6 @@ from qiskit_aer.aererror import AerError from qiskit_aer.backends import * from qiskit_aer import library -from qiskit_aer import pulse from qiskit_aer import quantum_info from qiskit_aer import noise from qiskit_aer import utils diff --git a/qiskit_aer/aerprovider.py b/qiskit_aer/aerprovider.py index 5ad1956495..3f1b08e98a 100644 --- a/qiskit_aer/aerprovider.py +++ b/qiskit_aer/aerprovider.py @@ -13,7 +13,6 @@ # pylint: disable=invalid-name """Provider for Qiskit Aer backends.""" -import warnings from qiskit.providers import ProviderV1 as Provider from qiskit.providers.providerutils import filter_backends @@ -22,7 +21,6 @@ from .backends.qasm_simulator import QasmSimulator from .backends.statevector_simulator import StatevectorSimulator from .backends.unitary_simulator import UnitarySimulator -from .backends.pulse_simulator import PulseSimulator class AerProvider(Provider): @@ -57,21 +55,12 @@ def _get_backends(): ("qasm_simulator", QasmSimulator, None, None), ("statevector_simulator", StatevectorSimulator, None, None), ("unitary_simulator", UnitarySimulator, None, None), - ("pulse_simulator", PulseSimulator, None, None), ] AerProvider._BACKENDS = backends return AerProvider._BACKENDS def get_backend(self, name=None, **kwargs): - if name == "pulse_simulator": - warnings.warn( - "The Pulse simulator backend in Qiskit Aer is deprecated and will " - "be removed in a future release. Instead the qiskit-dynamics " - "library should be used instead for simulating at the pulse level.", - DeprecationWarning, - stacklevel=2, - ) return super().get_backend(name=name, **kwargs) def backends(self, name=None, filters=None, **kwargs): diff --git a/qiskit_aer/backends/__init__.py b/qiskit_aer/backends/__init__.py index 70d7977044..61440080be 100644 --- a/qiskit_aer/backends/__init__.py +++ b/qiskit_aer/backends/__init__.py @@ -18,4 +18,3 @@ from .qasm_simulator import QasmSimulator from .statevector_simulator import StatevectorSimulator from .unitary_simulator import UnitarySimulator -from .pulse_simulator import PulseSimulator diff --git a/qiskit_aer/backends/aer_compiler.py b/qiskit_aer/backends/aer_compiler.py index b5281288f3..2e7930d198 100644 --- a/qiskit_aer/backends/aer_compiler.py +++ b/qiskit_aer/backends/aer_compiler.py @@ -104,7 +104,9 @@ def _inline_initialize(self, circ, optype): return circ for inst, _, _ in circ.data: - if isinstance(inst, Initialize) and not isinstance(inst.params[0], complex): + if isinstance(inst, Initialize) and ( + (not isinstance(inst.params[0], complex)) or (len(inst.params) == 1) + ): break else: return circ @@ -112,7 +114,9 @@ def _inline_initialize(self, circ, optype): new_circ = circ.copy() new_circ.data = [] for inst, qargs, cargs in circ.data: - if isinstance(inst, Initialize) and not isinstance(inst.params[0], complex): + if isinstance(inst, Initialize) and ( + (not isinstance(inst.params[0], complex)) or (len(inst.params) == 1) + ): # Assume that the decomposed circuit of inst.definition consists of basis gates new_circ.compose(inst.definition.decompose(), qargs, cargs, inplace=True) else: @@ -558,7 +562,13 @@ def assemble_circuit(circuit: QuantumCircuit): qreg_sizes = [] creg_sizes = [] - global_phase = float(circuit.global_phase) + if ( + isinstance(circuit.global_phase, ParameterExpression) + and len(circuit.global_phase.parameters) > 0 + ): + global_phase = 0.0 + else: + global_phase = float(circuit.global_phase) for qreg in circuit.qregs: qreg_sizes.append([qreg.name, qreg.size]) @@ -576,9 +586,6 @@ def assemble_circuit(circuit: QuantumCircuit): global_phase=global_phase, ) - if circuit.metadata is not None: - header.metadata = circuit.metadata - qubit_indices = {qubit: idx for idx, qubit in enumerate(circuit.qubits)} clbit_indices = {clbit: idx for idx, clbit in enumerate(circuit.clbits)} @@ -588,6 +595,8 @@ def assemble_circuit(circuit: QuantumCircuit): aer_circ.num_memory = num_memory aer_circ.global_phase_angle = global_phase + num_of_aer_ops = 0 + index_map = [] for inst in circuit.data: # To convert to a qobj-style conditional, insert a bfunc prior # to the conditional instruction to map the creg ?= val condition @@ -607,11 +616,15 @@ def assemble_circuit(circuit: QuantumCircuit): val |= ((ctrl_val >> list(ctrl_reg).index(clbit)) & 1) << idx conditional_reg = num_memory + max_conditional_idx aer_circ.bfunc(f"0x{mask:X}", f"0x{val:X}", "==", conditional_reg) + num_of_aer_ops += 1 max_conditional_idx += 1 - _assemble_op(aer_circ, inst, qubit_indices, clbit_indices, is_conditional, conditional_reg) + num_of_aer_ops += _assemble_op( + aer_circ, inst, qubit_indices, clbit_indices, is_conditional, conditional_reg + ) + index_map.append(num_of_aer_ops - 1) - return aer_circ + return aer_circ, index_map def _assemble_op(aer_circ, inst, qubit_indices, clbit_indices, is_conditional, conditional_reg): @@ -630,62 +643,14 @@ def _assemble_op(aer_circ, inst, qubit_indices, clbit_indices, is_conditional, c copied = True params[i] = 0.0 + num_of_aer_ops = 1 + # fmt: off if name in { - "ccx", - "ccz", - "cp", - "cswap", - "csx", - "cx", - "cy", - "cz", - "delay", - "ecr", - "h", - "id", - "mcp", - "mcphase", - "mcr", - "mcrx", - "mcry", - "mcrz", - "mcswap", - "mcsx", - "mcu", - "mcu1", - "mcu2", - "mcu3", - "mcx", - "mcx_gray", - "mcy", - "mcz", - "p", - "r", - "rx", - "rxx", - "ry", - "ryy", - "rz", - "rzx", - "rzz", - "s", - "sdg", - "swap", - "sx", - "sxdg", - "t", - "tdg", - "u", - "x", - "y", - "z", - "u1", - "u2", - "u3", - "cu", - "cu1", - "cu2", - "cu3", + "ccx", "ccz", "cp", "cswap", "csx", "cx", "cy", "cz", "delay", "ecr", "h", + "id", "mcp", "mcphase", "mcr", "mcrx", "mcry", "mcrz", "mcswap", "mcsx", + "mcu", "mcu1", "mcu2", "mcu3", "mcx", "mcx_gray", "mcy", "mcz", "p", "r", + "rx", "rxx", "ry", "ryy", "rz", "rzx", "rzz", "s", "sdg", "swap", "sx", "sxdg", + "t", "tdg", "u", "x", "y", "z", "u1", "u2", "u3", "cu", "cu1", "cu2", "cu3", }: aer_circ.gate(name, qubits, params, [], conditional_reg, label if label else name) elif name == "measure": @@ -757,7 +722,7 @@ def _assemble_op(aer_circ, inst, qubit_indices, clbit_indices, is_conditional, c elif name == "superop": aer_circ.superop(qubits, params[0], conditional_reg) elif name == "barrier": - aer_circ.barrier(qubits) + num_of_aer_ops = 0 elif name == "jump": aer_circ.jump(qubits, params, conditional_reg) elif name == "mark": @@ -772,6 +737,8 @@ def _assemble_op(aer_circ, inst, qubit_indices, clbit_indices, is_conditional, c else: raise AerError(f"unknown instruction: {name}") + return num_of_aer_ops + def assemble_circuits(circuits: List[QuantumCircuit]) -> List[AerCircuit]: """converts a list of Qiskit circuits into circuits mapped AER::Circuit @@ -780,7 +747,8 @@ def assemble_circuits(circuits: List[QuantumCircuit]) -> List[AerCircuit]: circuits: circuit(s) to be converted Returns: - circuits to be run on the Aer backends + a list of circuits to be run on the Aer backends and + a list of index mapping from Qiskit instructions to Aer operations of the circuits Examples: @@ -794,6 +762,7 @@ def assemble_circuits(circuits: List[QuantumCircuit]) -> List[AerCircuit]: qc.cx(0, 1) qc.measure_all() # Generate AerCircuit from the input circuit - aer_qc_list = assemble_circuits(circuits=[qc]) + aer_qc_list, idx_maps = assemble_circuits(circuits=[qc]) """ - return [assemble_circuit(circuit) for circuit in circuits] + aer_circuits, idx_maps = zip(*[assemble_circuit(circuit) for circuit in circuits]) + return list(aer_circuits), list(idx_maps) diff --git a/qiskit_aer/backends/aer_simulator.py b/qiskit_aer/backends/aer_simulator.py index db601d6546..e438ad50ca 100644 --- a/qiskit_aer/backends/aer_simulator.py +++ b/qiskit_aer/backends/aer_simulator.py @@ -208,7 +208,6 @@ class AerSimulator(AerBackend): qubits which do not affect the simulation outcome from the simulated circuits (Default: True). - * ``zero_threshold`` (double): Sets the threshold for truncating small values to zero in the result data (Default: 1e-10). @@ -288,6 +287,9 @@ class AerSimulator(AerBackend): threads per GPU. This parameter is used to optimize Pauli noise simulation with multiple-GPUs (Default: 1). + * ``accept_distributed_results`` (bool): This option enables storing + results independently in each process (Default: None). + These backend options only apply when using the ``"statevector"`` simulation method: @@ -308,9 +310,7 @@ class AerSimulator(AerBackend): simulation method: * ``stabilizer_max_snapshot_probabilities`` (int): set the maximum - qubit number for the - `~qiskit_aer.extensions.SnapshotProbabilities` - instruction (Default: 32). + qubit number for the :class:`~qiskit_aer.library.SaveProbabilities` instruction (Default: 32). These backend options only apply when using the ``"extended_stabilizer"`` simulation method: @@ -407,6 +407,13 @@ class AerSimulator(AerBackend): Possible values are "mps_swap_right" and "mps_swap_left". (Default: "mps_swap_left") + * ``chop_threshold`` (float): This option sets a threshold for + truncating snapshots (Default: 1e-8). + + * ``mps_parallel_threshold`` (int): This option sets OMP number threshold (Default: 14). + + * ``mps_omp_threads`` (int): This option sets the number of OMP threads (Default: 1). + These backend options only apply when using the ``tensor_network`` simulation method: @@ -940,5 +947,9 @@ def _set_method_config(self, method=None): # Clear options to default description = None n_qubits = None + + if self._configuration.coupling_map: + n_qubits = max(list(map(max, self._configuration.coupling_map))) + 1 + self._set_configuration_option("description", description) self._set_configuration_option("n_qubits", n_qubits) diff --git a/qiskit_aer/backends/aerbackend.py b/qiskit_aer/backends/aerbackend.py index 06481ae144..7266572ae9 100644 --- a/qiskit_aer/backends/aerbackend.py +++ b/qiskit_aer/backends/aerbackend.py @@ -35,6 +35,9 @@ from .aer_compiler import compile_circuit, assemble_circuits, generate_aer_config from .backend_utils import format_save_type, circuit_optypes +# pylint: disable=import-error, no-name-in-module +from .controller_wrappers import AerConfig + # Logger logger = logging.getLogger(__name__) @@ -79,45 +82,51 @@ def __init__( if backend_options is not None: self.set_options(**backend_options) - def _convert_circuit_binds(self, circuit, binds): + def _convert_circuit_binds(self, circuit, binds, idx_map): parameterizations = [] + + def append_param_values(index, bind_pos, param): + if param in binds: + parameterizations.append([(index, bind_pos), binds[param]]) + elif isinstance(param, ParameterExpression): + # If parameter expression has no unbound parameters + # it's already bound and should be skipped + if not param.parameters: + return + if not binds: + raise AerError("The element of parameter_binds is empty.") + len_vals = len(next(iter(binds.values()))) + bind_list = [ + { + parameter: binds[parameter][i] + for parameter in param.parameters & binds.keys() + } + for i in range(len_vals) + ] + bound_values = [float(param.bind(x)) for x in bind_list] + parameterizations.append([(index, bind_pos), bound_values]) + + append_param_values(AerConfig.GLOBAL_PHASE_POS, -1, circuit.global_phase) + for index, instruction in enumerate(circuit.data): if instruction.operation.is_parameterized(): for bind_pos, param in enumerate(instruction.operation.params): - if param in binds: - parameterizations.append([(index, bind_pos), binds[param]]) - elif isinstance(param, ParameterExpression): - # If parameter expression has no unbound parameters - # it's already bound and should be skipped - if not param.parameters: - continue - if not binds: - raise AerError("The element of parameter_binds is empty.") - len_vals = len(next(iter(binds.values()))) - bind_list = [ - { - parameter: binds[parameter][i] - for parameter in param.parameters & binds.keys() - } - for i in range(len_vals) - ] - bound_values = [float(param.bind(x)) for x in bind_list] - parameterizations.append([(index, bind_pos), bound_values]) + append_param_values(idx_map[index] if idx_map else index, bind_pos, param) return parameterizations - def _convert_binds(self, circuits, parameter_binds): + def _convert_binds(self, circuits, parameter_binds, idx_maps=None): if isinstance(circuits, QuantumCircuit): if len(parameter_binds) > 1: raise AerError("More than 1 parameter table provided for a single circuit") - return [self._convert_circuit_binds(circuits, parameter_binds[0])] + return [self._convert_circuit_binds(circuits, parameter_binds[0], None)] elif len(parameter_binds) != len(circuits): raise AerError( "Number of input circuits does not match number of input " "parameter bind dictionaries" ) parameterizations = [ - self._convert_circuit_binds(circuit, parameter_binds[idx]) + self._convert_circuit_binds(circuit, parameter_binds[idx], idx_maps[idx]) for idx, circuit in enumerate(circuits) ] return parameterizations @@ -186,8 +195,8 @@ def run(self, circuits, validate=False, parameter_binds=None, **run_options): for key, value in circuits.config.__dict__.items(): if key not in run_options and value is not None: run_options[key] = value - if "parameter_binds" in run_options: - parameter_binds = run_options.pop("parameter_binds") + if "parameter_binds" in run_options: + parameter_binds = run_options.pop("parameter_binds") return self._run_qobj(circuits, validate, parameter_binds, **run_options) only_circuits = True @@ -227,20 +236,15 @@ def run(self, circuits, validate=False, parameter_binds=None, **run_options): def _run_circuits(self, circuits, parameter_binds, **run_options): """Run circuits by generating native circuits.""" - circuits, noise_model = self._compile(circuits, **run_options) - if parameter_binds: - run_options["parameterizations"] = self._convert_binds(circuits, parameter_binds) - config = generate_aer_config(circuits, self.options, **run_options) - # Submit job job_id = str(uuid.uuid4()) aer_job = AerJob( self, job_id, self._execute_circuits_job, + parameter_binds=parameter_binds, circuits=circuits, - noise_model=noise_model, - config=config, + run_options=run_options, ) aer_job.submit() @@ -418,24 +422,34 @@ def _execute_qobj_job(self, qobj, job_id="", format_result=True): return self._format_results(output) return output - def _execute_circuits_job(self, circuits, noise_model, config, job_id="", format_result=True): + def _execute_circuits_job( + self, circuits, parameter_binds, run_options, job_id="", format_result=True + ): """Run a job""" # Start timer start = time.time() - # Take metadata from headers of experiments to work around JSON serialization error - metadata_list = [] - for idx, circ in enumerate(circuits): - metadata_list.append(circ.metadata) - # TODO: we test for True-like on purpose here to condition against both None and {}, - # which allows us to support versions of Terra before and after QuantumCircuit.metadata - # accepts None as a valid value. This logic should be revisited after terra>=0.24.0 is - # required. - if circ.metadata: - circ.metadata = {"metadata_index": idx} + # Compile circuits + circuits, noise_model = self._compile(circuits, **run_options) + + aer_circuits, idx_maps = assemble_circuits(circuits) + if parameter_binds: + run_options["parameterizations"] = self._convert_binds( + circuits, parameter_binds, idx_maps + ) + elif not all([len(circuit.parameters) == 0 for circuit in circuits]): + raise AerError("circuits have parameters but parameter_binds is not specified.") + + for circ_id, aer_circuit in enumerate(aer_circuits): + aer_circuit.circ_id = circ_id + + config = generate_aer_config(circuits, self.options, **run_options) # Run simulation - aer_circuits = assemble_circuits(circuits) + metadata_map = { + aer_circuit.circ_id: circuit.metadata + for aer_circuit, circuit in zip(aer_circuits, circuits) + } output = self._execute_circuits(aer_circuits, noise_model, config) # Validate output @@ -453,17 +467,9 @@ def _execute_circuits_job(self, circuits, noise_model, config, job_id="", format # Push metadata to experiment headers for result in output["results"]: - if ( - "header" in result - and "metadata" in result["header"] - and result["header"]["metadata"] - and "metadata_index" in result["header"]["metadata"] - ): - metadata_index = result["header"]["metadata"]["metadata_index"] - result["header"]["metadata"] = metadata_list[metadata_index] - - for circ, metadata in zip(circuits, metadata_list): - circ.metadata = metadata + if "header" not in result: + continue + result["header"]["metadata"] = metadata_map[result.pop("circ_id")] # Add execution time output["time_taken"] = time.time() - start diff --git a/qiskit_aer/backends/pulse_simulator.py b/qiskit_aer/backends/pulse_simulator.py deleted file mode 100644 index 4fea7f0974..0000000000 --- a/qiskit_aer/backends/pulse_simulator.py +++ /dev/null @@ -1,371 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2019, 2020. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -# pylint: disable=arguments-differ, missing-return-type-doc -""" -Qiskit Aer pulse simulator backend. -""" - -import copy -import logging -from warnings import warn -from numpy import inf - -from qiskit.circuit import QuantumCircuit -from qiskit.compiler import schedule -from qiskit.providers.options import Options -from qiskit.providers.models import BackendConfiguration, PulseDefaults -from qiskit.providers.backend import BackendV2 - -from ..version import __version__ -from ..aererror import AerError -from ..pulse.controllers.pulse_controller import pulse_controller -from ..pulse.system_models.pulse_system_model import PulseSystemModel -from .aerbackend import AerBackend - -logger = logging.getLogger(__name__) - -DEFAULT_CONFIGURATION = { - "backend_name": "pulse_simulator", - "backend_version": __version__, - "n_qubits": 20, - "coupling_map": None, - "url": "https://github.com/Qiskit/qiskit-aer", - "simulator": True, - "meas_levels": [1, 2], - "local": True, - "conditional": True, - "open_pulse": True, - "memory": False, - "max_shots": int(1e6), - "description": "A Pulse-based Hamiltonian simulator for Pulse Qobj files", - "gates": [], - "basis_gates": [], - "parametric_pulses": [], -} - - -class PulseSimulator(AerBackend): - r"""Deprecated: Pulse schedule simulator backend. - - .. warning:: - - This simulator is deprecated having been superseded by the - `Qiskit Dynamics `__ library. - If you need to perform pulse level simulation you should use the - Qiskit Dynamics library instead. - - The ``PulseSimulator`` simulates continuous time Hamiltonian dynamics of a quantum system, - with controls specified by pulse :class:`~qiskit.Schedule` objects, and the model of the - physical system specified by :class:`~qiskit_aer.pulse.PulseSystemModel` objects. - Results are returned in the same format as when jobs are submitted to actual devices. - - **Examples** - - The minimal information a ``PulseSimulator`` needs to simulate is a - :class:`~qiskit_aer.pulse.PulseSystemModel`, which can be supplied either by - setting the backend option before calling ``run``, e.g.: - - .. code-block:: python - - backend_sim = qiskit_aer.PulseSimulator() - - # Set the pulse system model for the simulator - backend_sim.set_options(system_model=system_model) - - # Assemble schedules using PulseSimulator as the backend - pulse_qobj = assemble(schedules, backend=backend_sim) - - # Run simulation - results = backend_sim.run(pulse_qobj) - - or by supplying the system model at runtime, e.g.: - - .. code-block:: python - - backend_sim = qiskit_aer.PulseSimulator() - - # Assemble schedules using PulseSimulator as the backend - pulse_qobj = assemble(schedules, backend=backend_sim) - - # Run simulation on a PulseSystemModel object - results = backend_sim.run(pulse_qobj, system_model=system_model) - - Alternatively, an instance of the ``PulseSimulator`` may be further configured to contain more - information present in a real backend. The simplest way to do this is to instantiate the - ``PulseSimulator`` from a real backend: - - .. code-block:: python - - armonk_sim = qiskit_aer.PulseSimulator.from_backend(FakeArmonk()) - pulse_qobj = assemble(schedules, backend=armonk_sim) - armonk_sim.run(pulse_qobj) - - In the above example, the ``PulseSimulator`` copies all configuration and default data from - ``FakeArmonk()``, and as such has the same affect as ``FakeArmonk()`` when passed as an - argument to ``assemble``. Furthermore it constructs a - :class:`~qiskit_aer.pulse.PulseSystemModel` from the model details in the supplied - backend, which is then used in simulation. - - **Supported PulseQobj parameters** - - * ``qubit_lo_freq``: Local oscillator frequencies for each :class:`DriveChannel`. - Defaults to either the value given in the - :class:`~qiskit_aer.pulse.PulseSystemModel`, or is calculated directly - from the Hamiltonian. - * ``meas_level``: Type of desired measurement output, in ``[1, 2]``. - ``1`` gives complex numbers (IQ values), and ``2`` gives discriminated states ``|0>`` and - ``|1>``. Defaults to ``2``. - * ``meas_return``: Measurement type, ``'single'`` or ``'avg'``. Defaults to ``'avg'``. - * ``shots``: Number of shots per experiment. Defaults to ``1024``. - * ``executor``: Set a custom executor for asynchronous running of simulation - * ``max_job_size`` (int or None): If the number of run schedules - exceeds this value simulation will be run as a set of of sub-jobs - on the executor. If ``None`` simulation of all schedules are submitted - to the executor as a single job (Default: None). - * ``max_shot_size`` (int or None): If the number of shots of a noisy - circuit exceeds this value simulation will be split into multi - circuits for execution and the results accumulated. If ``None`` - circuits will not be split based on shots. When splitting circuits - use the ``max_job_size`` option to control how these split circuits - should be submitted to the executor (Default: None). - - jobs (Default: None). - - **Simulation details** - - The simulator uses the ``zvode`` differential equation solver method through ``scipy``. - Simulation is performed in the rotating frame of the diagonal of the drift Hamiltonian - contained in the :class:`~qiskit_aer.pulse.PulseSystemModel`. Measurements - are performed in the `dressed basis` of the drift Hamiltonian. - - **Other options** - - Additional valid keyword arguments for ``run()``: - - * ``'solver_options'``: A ``dict`` for solver options. Accepted keys - are ``'atol'``, ``'rtol'``, ``'nsteps'``, ``'max_step'``, ``'num_cpus'``, ``'norm_tol'``, - and ``'norm_steps'``. - """ - - def __init__( - self, configuration=None, properties=None, defaults=None, provider=None, **backend_options - ): - warn( - "The Pulse simulator backend in Qiskit Aer is deprecated and will " - "be removed in a future release. Instead the qiskit-dynamics " - "library should be used instead for simulating at the pulse level.", - DeprecationWarning, - stacklevel=2, - ) - - if configuration is None: - configuration = BackendConfiguration.from_dict(DEFAULT_CONFIGURATION) - else: - configuration = copy.copy(configuration) - configuration.meas_levels = self._meas_levels(configuration.meas_levels) - configuration.open_pulse = True - configuration.parametric_pulses = [] - - if defaults is None: - defaults = PulseDefaults( - qubit_freq_est=[inf], meas_freq_est=[inf], buffer=0, cmd_def=[], pulse_library=[] - ) - - super().__init__( - configuration, - properties=properties, - defaults=defaults, - provider=provider, - backend_options=backend_options, - ) - - # Set up default system model - subsystem_list = backend_options.get("subsystem_list", None) - if backend_options.get("system_model") is None: - if hasattr(configuration, "hamiltonian"): - system_model = PulseSystemModel.from_config(configuration, subsystem_list) - self._set_system_model(system_model) - - @classmethod - def _default_options(cls): - return Options( - shots=1024, - meas_level=None, - meas_return=None, - meas_map=None, - qubit_lo_freq=None, - solver_options=None, - subsystem_list=None, - system_model=None, - seed=None, - qubit_freq_est=inf, - q_level_meas=1, - noise_model=None, - initial_state=None, - executor=None, - memory_slots=1, - max_job_size=None, - max_shot_size=None, - ) - - # pylint: disable=arguments-differ, missing-param-doc - def run(self, schedules, validate=True, **run_options): - """Run a qobj on the backend. - - Args: - schedules (Schedule or list): The pulse :class:`~qiskit.pulse.Schedule` - (or list of ``Schedule`` objects) to be executed. - validate (bool): validate the Qobj before running (default: True). - run_options (kwargs): additional run time backend options. - - Returns: - AerJob: The simulation job. - - Additional Information: - * kwarg options specified in ``run_options`` will override options - of the same kwarg specified in the simulator options, the - ``backend_options`` and the ``Qobj.config``. - """ - if isinstance(schedules, list): - new_schedules = [] - for i in schedules: - if isinstance(i, QuantumCircuit): - new_schedules.append(schedule(i, self)) - else: - new_schedules.append(i) - schedules = new_schedules - elif isinstance(schedules, QuantumCircuit): - schedules = schedule(schedules, self) - return super().run(schedules, validate=validate, **run_options) - - @property - def _system_model(self): - return getattr(self._options, "system_model") - - @classmethod - def from_backend(cls, backend, **options): - """Initialize simulator from backend.""" - if isinstance(backend, BackendV2): - raise AerError("PulseSimulator.from_backend does not currently support V2 Backends.") - configuration = copy.copy(backend.configuration()) - defaults = copy.copy(backend.defaults()) - properties = copy.copy(backend.properties()) - - backend_name = "pulse_simulator({})".format(configuration.backend_name) - description = "A Pulse-based simulator configured from the backend: " - description += configuration.backend_name - - sim = cls( - configuration=configuration, - properties=properties, - defaults=defaults, - backend_name=backend_name, - description=description, - **options, - ) - return sim - - def _execute_qobj(self, qobj): - """Execute a qobj on the backend. - - Args: - qobj (PulseQobj): simulator input. - - Returns: - dict: return a dictionary of results. - """ - qobj.config.qubit_freq_est = self.defaults().qubit_freq_est - return pulse_controller(qobj) - - def _execute_circuits(self, aer_circuits, noise_model, config): - """Execute circuits on the backend.""" - raise TypeError("pulse simulator does not support circuit execution") - - def set_option(self, key, value): - """Set pulse simulation options and update backend.""" - if key == "meas_levels": - self._set_configuration_option(key, self._meas_levels(value)) - return - - # Handle cases that require updating two places - if key in ["dt", "u_channel_lo"]: - self._set_configuration_option(key, value) - if self._system_model is not None: - setattr(self._system_model, key, value) - return - - if key == "hamiltonian": - # if option is hamiltonian, set in configuration and reconstruct pulse system model - subsystem_list = getattr(self._options.get, "subsystem_list", None) - system_model = PulseSystemModel.from_config(self.configuration(), subsystem_list) - super().set_option("system_model", system_model) - self._set_configuration_option(key, value) - return - - # if system model is specified directly - if key == "system_model": - if hasattr(self.configuration(), "hamiltonian"): - warn( - "Specifying both a configuration with a Hamiltonian and a " - "system model may result in inconsistencies." - ) - # Set config dt and u_channel_lo to system model values - self._set_system_model(value) - return - - if key == "qubit_lo_freq": - value = [freq * 1e-9 for freq in value] - - # Set all other options from AerBackend - super().set_option(key, value) - - def _set_system_model(self, system_model): - """Set system model option""" - self._set_configuration_option("dt", getattr(system_model, "dt", [])) - self._set_configuration_option("u_channel_lo", getattr(system_model, "u_channel_lo", [])) - super().set_option("system_model", system_model) - - def _validate(self, qobj): - """Validation of qobj. - - Ensures that exactly one Acquire instruction is present in each - schedule. Checks SystemModel is in qobj config - """ - if getattr(qobj.config, "system_model", None) is None: - raise AerError("PulseSimulator requires a system model to run.") - - for exp in qobj.experiments: - num_acquires = 0 - for instruction in exp.instructions: - if instruction.name == "acquire": - num_acquires += 1 - - if num_acquires > 1: - raise AerError( - "PulseSimulator does not support multiple Acquire " - "instructions in a single schedule." - ) - - if num_acquires == 0: - raise AerError( - "PulseSimulator requires at least one Acquire " "instruction per schedule." - ) - - @staticmethod - def _meas_levels(meas_levels): - """Function for setting meas_levels in a pulse simulator configuration.""" - if 0 in meas_levels: - warn("Measurement level 0 not supported in pulse simulator.") - tmp = copy.copy(meas_levels) - tmp.remove(0) - return tmp - return meas_levels diff --git a/qiskit_aer/backends/qasm_simulator.py b/qiskit_aer/backends/qasm_simulator.py index a7978da705..ade47cd01e 100644 --- a/qiskit_aer/backends/qasm_simulator.py +++ b/qiskit_aer/backends/qasm_simulator.py @@ -200,9 +200,7 @@ class QasmSimulator(AerBackend): simulation method: * ``stabilizer_max_snapshot_probabilities`` (int): set the maximum - qubit number for the - `~qiskit_aer.extensions.SnapshotProbabilities` - instruction (Default: 32). + qubit number for the :class:`~qiskit_aer.library.SaveProbabilities` instruction (Default: 32). These backend options only apply when using the ``"extended_stabilizer"`` simulation method: diff --git a/qiskit_aer/backends/wrappers/CMakeLists.txt b/qiskit_aer/backends/wrappers/CMakeLists.txt index 88803336f3..0430fc42d2 100644 --- a/qiskit_aer/backends/wrappers/CMakeLists.txt +++ b/qiskit_aer/backends/wrappers/CMakeLists.txt @@ -7,6 +7,11 @@ find_package(Pybind11 REQUIRED) # shared library. string(REPLACE " -static " "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") +set(AER_LIBRARIES + ${AER_LIBRARIES} + ${THRUST_DEPENDANT_LIBS} + ${MPI_DEPENDANT_LIBS}) + if(CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "x86_64" OR CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "AMD64" OR CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "amd64") if (NOT CMAKE_OSX_ARCHITECTURES STREQUAL "arm64") # We build SIMD filed separately, because they will be reached only if the diff --git a/qiskit_aer/backends/wrappers/aer_circuit_binding.hpp b/qiskit_aer/backends/wrappers/aer_circuit_binding.hpp index 456793aaf7..12f3eeba5a 100644 --- a/qiskit_aer/backends/wrappers/aer_circuit_binding.hpp +++ b/qiskit_aer/backends/wrappers/aer_circuit_binding.hpp @@ -40,7 +40,7 @@ using namespace AER; template void bind_aer_circuit(MODULE m) { - py::class_ aer_circuit(m, "AerCircuit"); + py::class_> aer_circuit(m, "AerCircuit"); aer_circuit.def(py::init()); aer_circuit.def("__repr__", [](const Circuit &circ) { std::stringstream ss; @@ -62,6 +62,7 @@ void bind_aer_circuit(MODULE m) { return ss.str(); }); + aer_circuit.def_readwrite("circ_id", &Circuit::circ_id); aer_circuit.def_readwrite("shots", &Circuit::shots); aer_circuit.def_readwrite("num_qubits", &Circuit::num_qubits); aer_circuit.def_readwrite("num_memory", &Circuit::num_memory); diff --git a/qiskit_aer/backends/wrappers/aer_controller_binding.hpp b/qiskit_aer/backends/wrappers/aer_controller_binding.hpp index d862c8690f..bf5296b18a 100644 --- a/qiskit_aer/backends/wrappers/aer_controller_binding.hpp +++ b/qiskit_aer/backends/wrappers/aer_controller_binding.hpp @@ -50,7 +50,7 @@ class ControllerExecutor { #endif } - py::object execute(std::vector &circuits, + py::object execute(std::vector> &circuits, Noise::NoiseModel &noise_model, AER::Config &config) const { return AerToPy::to_python( @@ -91,7 +91,7 @@ void bind_aer_controller(MODULE m) { }); aer_ctrl.def("execute", [aer_ctrl](ControllerExecutor &self, - std::vector &circuits, + std::vector> &circuits, py::object noise_model, AER::Config &config) { Noise::NoiseModel noise_model_native; if (noise_model) @@ -235,6 +235,9 @@ void bind_aer_controller(MODULE m) { // system configurations aer_config.def_readwrite("library_dir", &Config::library_dir); + aer_config.def_property_readonly_static( + "GLOBAL_PHASE_POS", + [](const py::object &) { return Config::GLOBAL_PHASE_POS; }); aer_config.def_readwrite("parameterizations", &Config::param_table); aer_config.def_property( "n_qubits", [](const Config &config) { return config.n_qubits.val; }, diff --git a/qiskit_aer/jobs/aerjob.py b/qiskit_aer/jobs/aerjob.py index 545fbe773e..7f309a51a5 100644 --- a/qiskit_aer/jobs/aerjob.py +++ b/qiskit_aer/jobs/aerjob.py @@ -34,8 +34,8 @@ def __init__( fn, qobj=None, circuits=None, - noise_model=None, - config=None, + parameter_binds=None, + run_options=None, executor=None, ): """Initializes the asynchronous job. @@ -49,9 +49,9 @@ def __init__( qobj(QasmQobj): qobj to execute circuits(list of QuantumCircuit): circuits to execute. If `qobj` is set, this argument is ignored. - noise_model(NoiseModel): noise_model to execute. + parameter_binds(list): parameters for circuits. If `qobj` is set, this argument is ignored. - config(dict): configuration to execute. + run_options(dict): run_options to execute. If `qobj` is set, this argument is ignored. executor(ThreadPoolExecutor or dask.distributed.client): The executor to be used to submit the job. @@ -64,13 +64,13 @@ def __init__( if qobj: self._qobj = qobj self._circuits = None - self._noise_model = None - self._config = None + self._parameter_binds = None + self._run_options = None elif circuits: self._qobj = None self._circuits = circuits - self._noise_model = noise_model - self._config = config + self._parameter_binds = parameter_binds + self._run_options = run_options else: raise JobError("AerJob needs a qobj or circuits") self._executor = executor or DEFAULT_EXECUTOR @@ -90,7 +90,7 @@ def submit(self): self._future = self._executor.submit(self._fn, self._qobj, self._job_id) else: self._future = self._executor.submit( - self._fn, self._circuits, self._noise_model, self._config, self._job_id + self._fn, self._circuits, self._parameter_binds, self._run_options, self._job_id ) @requires_submit diff --git a/qiskit_aer/library/default_qubits.py b/qiskit_aer/library/default_qubits.py index df251d158d..bc446e73f9 100644 --- a/qiskit_aer/library/default_qubits.py +++ b/qiskit_aer/library/default_qubits.py @@ -13,7 +13,7 @@ Helper function """ -from qiskit.circuit import QuantumCircuit, QuantumRegister +from qiskit.circuit import QuantumRegister from qiskit.extensions.exceptions import ExtensionError @@ -38,18 +38,8 @@ def default_qubits(circuit, qubits=None): if isinstance(qubits, QuantumRegister): qubits = qubits[:] if not qubits: - tuples = [] - if isinstance(circuit, QuantumCircuit): - for register in circuit.qregs: - tuples.append(register) - if not tuples: + qubits = list(circuit.qubits) + if len(qubits) == 0: raise ExtensionError("no qubits for snapshot") - qubits = [] - for tuple_element in tuples: - if isinstance(tuple_element, QuantumRegister): - for j in range(tuple_element.size): - qubits.append(tuple_element[j]) - else: - qubits.append(tuple_element) return qubits diff --git a/qiskit_aer/noise/__init__.py b/qiskit_aer/noise/__init__.py index dfe18526ad..b03ac45f4e 100644 --- a/qiskit_aer/noise/__init__.py +++ b/qiskit_aer/noise/__init__.py @@ -26,30 +26,72 @@ The :class:`NoiseModel` class is used to represent noise model for the :class:`~qiskit_aer.QasmSimulator`. It can be used to construct -custom noise models for simulator, to to automatically generate a basic -device noise model for an IBMQ backend. +custom noise models for simulator, to automatically generate a basic +device noise model for an IBMQ or fake backend. Device Noise Models ------------------- A simplified approximate :class:`NoiseModel` can be generated automatically -from the properties of real device backends from the IBMQ provider using the -:meth:`NoiseModel.from_backend` method. See the method documentation for -details. +from the properties of real device backends from the IBMQ provider or +fake backends of the `fake_provider` using the :meth:`NoiseModel.from_backend` +method. See the method documentation for details. + **Example: Basic device noise model** .. code-block:: python - from qiskit import QuantumCircuit, execute - from qiskit import IBMQ, Aer + from qiskit import IBMQ + from qiskit.providers.aer.noise import NoiseModel + from qiskit import QuantumCircuit, transpile + from qiskit_aer import AerSimulator from qiskit.visualization import plot_histogram from qiskit_aer.noise import NoiseModel - # Build noise model from backend properties + # Make a circuit + circ = QuantumCircuit(3, 3) + circ.h(0) + circ.cx(0, 1) + circ.cx(1, 2) + circ.measure([0, 1, 2], [0, 1, 2]) + + # Get the noise model of ibmq_lima provider = IBMQ.load_account() - backend = provider.get_backend('ibmq_vigo') + provider = IBMQ.get_provider(hub='ibm-q', group='open', project='main') + backend_lima = provider.get_backend('ibmq_lima') + noise_model = NoiseModel.from_backend(backend_lima) + + # Get coupling map from backend + coupling_map = backend_lima.configuration().coupling_map + + # Get basis gates from noise model + basis_gates = noise_model.basis_gates + + # Perform a noise simulation + backend = AerSimulator(noise_model=noise_model, + coupling_map=coupling_map, + basis_gates=basis_gates) + transpiled_circuit = transpile(circ, backend) + result = backend.run(transpiled_circuit).result() + + counts = result.get_counts(0) + plot_histogram(counts) + + +**Example: Basic device noise model using a `fake_provider` backend** + +.. code-block:: python + + from qiskit import QuantumCircuit, transpile + from qiskit_aer import AerSimulator + from qiskit.visualization import plot_histogram + from qiskit_aer.noise import NoiseModel + from qiskit.providers.fake_provider import FakeVigo + + # Build noise model from backend properties + backend = FakeVigo() noise_model = NoiseModel.from_backend(backend) # Get coupling map from backend @@ -66,10 +108,12 @@ circ.measure([0, 1, 2], [0, 1, 2]) # Perform a noise simulation - result = execute(circ, Aer.get_backend('qasm_simulator'), - coupling_map=coupling_map, - basis_gates=basis_gates, - noise_model=noise_model).result() + backend = AerSimulator(noise_model=noise_model, + coupling_map=coupling_map, + basis_gates=basis_gates) + transpiled_circuit = transpile(circ, backend) + result = backend.run(transpiled_circuit).result() + counts = result.get_counts(0) plot_histogram(counts) @@ -88,8 +132,9 @@ .. code-block:: python - from qiskit import QuantumCircuit, execute, Aer + from qiskit import QuantumCircuit, transpile, Aer from qiskit.visualization import plot_histogram + from qiskit_aer import AerSimulator import qiskit_aer.noise as noise # Error probabilities @@ -116,12 +161,16 @@ circ.measure([0, 1, 2], [0, 1, 2]) # Perform a noise simulation - result = execute(circ, Aer.get_backend('qasm_simulator'), - basis_gates=basis_gates, - noise_model=noise_model).result() + backend = AerSimulator(noise_model=noise_model, + coupling_map=coupling_map, + basis_gates=basis_gates) + transpiled_circuit = transpile(circ, backend) + result = backend.run(transpiled_circuit).result() + counts = result.get_counts(0) plot_histogram(counts) + Classes ======= diff --git a/qiskit_aer/noise/device/models.py b/qiskit_aer/noise/device/models.py index 480c571a2e..528a9ffa1c 100644 --- a/qiskit_aer/noise/device/models.py +++ b/qiskit_aer/noise/device/models.py @@ -106,21 +106,26 @@ def basic_device_gate_errors( Args: properties (BackendProperties): device backend properties. gate_error (bool): Include depolarizing gate errors (Default: True). - thermal_relaxation (Bool): Include thermal relaxation errors - (Default: True). + thermal_relaxation (Bool): Include thermal relaxation errors (Default: True). + If no ``t1`` and ``t2`` values are provided (i.e. None) in ``target`` for a qubit, + an identity ``QuantumError` (i.e. effectively no thermal relaxation error) + will be added to the qubit even if this flag is set to True. + If no ``frequency`` is not defined (i.e. None) in ``target`` for a qubit, + no excitation is considered in the thermal relaxation error on the qubit + even with non-zero ``temperature``. gate_lengths (list): Override device gate times with custom values. If None use gate times from backend properties. (Default: None). - gate_length_units (str): Time units for gate length values in gate_lengths. + gate_length_units (str): Time units for gate length values in ``gate_lengths``. Can be 'ns', 'ms', 'us', or 's' (Default: 'ns'). temperature (double): qubit temperature in milli-Kelvin (mK) (Default: 0). warnings (bool): DEPRECATED, Display warnings (Default: None). target (Target): device backend target (Default: None). When this is supplied, several options are disabled: - `properties`, `gate_lengths` and `gate_length_units` are not used + ``properties``, ``gate_lengths`` and ``gate_length_units`` are not used during the construction of gate errors. - Default values are always used for `warnings`. + Default values are always used for ``warnings``. Returns: list: A list of tuples ``(label, qubits, QuantumError)``, for gates @@ -339,6 +344,10 @@ def _device_thermal_relaxation_error( for qubit in qubits: t1, t2, freq = relax_params[qubit] t2 = _truncate_t2_value(t1, t2) + if t1 is None: + t1 = inf + if t2 is None: + t2 = inf population = _excited_population(freq, temperature) if first: error = thermal_relaxation_error(t1, t2, gate_time, population) @@ -351,14 +360,15 @@ def _device_thermal_relaxation_error( def _truncate_t2_value(t1, t2): """Return t2 value truncated to 2 * t1 (for t2 > 2 * t1)""" - new_t2 = t2 - if t2 > 2 * t1: - new_t2 = 2 * t1 - return new_t2 + if t1 is None or t2 is None: + return t2 + return min(t2, 2 * t1) def _excited_population(freq, temperature): """Return excited state population from freq [GHz] and temperature [mK].""" + if freq is None or temperature is None: + return 0 population = 0 if freq != inf and temperature != 0: # Compute the excited state population from qubit frequency and temperature diff --git a/qiskit_aer/noise/noise_model.py b/qiskit_aer/noise/noise_model.py index 29d852dbb6..a32fcc2020 100644 --- a/qiskit_aer/noise/noise_model.py +++ b/qiskit_aer/noise/noise_model.py @@ -435,8 +435,8 @@ def from_backend( t1s = [prop.t1 for prop in all_qubit_properties] t2s = [_truncate_t2_value(prop.t1, prop.t2) for prop in all_qubit_properties] delay_pass = RelaxationNoisePass( - t1s=t1s, - t2s=t2s, + t1s=[np.inf if x is None else x for x in t1s], # replace None with np.inf + t2s=[np.inf if x is None else x for x in t2s], # replace None with np.inf dt=dt, op_types=Delay, excited_state_populations=excited_state_populations, @@ -474,10 +474,14 @@ def from_backend_properties( Args: backend_properties (BackendProperties): The property of backend. gate_error (Bool): Include depolarizing gate errors (Default: True). - readout_error (Bool): Include readout errors in model - (Default: True). - thermal_relaxation (Bool): Include thermal relaxation errors - (Default: True). + readout_error (Bool): Include readout errors in model (Default: True). + thermal_relaxation (Bool): Include thermal relaxation errors (Default: True). + If no ``t1`` and ``t2`` values are provided (i.e. None) in ``target`` for a qubit, + an identity ``QuantumError` (i.e. effectively no thermal relaxation error) + will be added to the qubit even if this flag is set to True. + If no ``frequency`` is not defined (i.e. None) in ``target`` for a qubit, + no excitation is considered in the thermal relaxation error on the qubit + even with non-zero ``temperature``. temperature (double): qubit temperature in milli-Kelvin (mK) for thermal relaxation errors (Default: 0). gate_lengths (Optional[list]): Custom gate times for thermal relaxation errors. diff --git a/qiskit_aer/primitives/estimator.py b/qiskit_aer/primitives/estimator.py index 77c2d551dc..6e34dda648 100644 --- a/qiskit_aer/primitives/estimator.py +++ b/qiskit_aer/primitives/estimator.py @@ -19,9 +19,10 @@ from collections import defaultdict from collections.abc import Sequence from copy import copy +from warnings import warn import numpy as np -from qiskit.circuit import QuantumCircuit +from qiskit.circuit import ParameterExpression, QuantumCircuit from qiskit.compiler import transpile from qiskit.opflow import PauliSumOp from qiskit.primitives import BaseEstimator, EstimatorResult @@ -50,12 +51,21 @@ class Estimator(BaseEstimator): .. note:: Precedence of seeding for ``seed_simulator`` is as follows: - 1. ``seed_simulator`` in runtime (i.e. in :meth:`__call__`) - 2. ``seed`` in runtime (i.e. in :meth:`__call__`) + 1. ``seed_simulator`` in runtime (i.e. in :meth:`run`) + 2. ``seed`` in runtime (i.e. in :meth:`run`) 3. ``seed_simulator`` of ``backend_options``. 4. default. ``seed`` is also used for sampling from a normal distribution when approximation is True. + + When combined with the approximation option, we get the expectation values as follows: + + * shots is None and approximation=False: Return an expectation value with sampling-noise w/ + warning. + * shots is int and approximation=False: Return an expectation value with sampling-noise. + * shots is None and approximation=True: Return an exact expectation value. + * shots is int and approximation=True: Return expectation value with sampling-noise using a + normal distribution approximation. """ def __init__( @@ -145,12 +155,24 @@ def _run( self._observable_ids[_observable_key(observable)] = len(self._observables) self._observables.append(observable) job = PrimitiveJob( - self._call, circuit_indices, observable_indices, parameter_values, **run_options + self._call, + circuit_indices, + observable_indices, + parameter_values, + **run_options, ) job.submit() return job def _compute(self, circuits, observables, parameter_values, run_options): + if "shots" in run_options and run_options["shots"] is None: + warn( + "If `shots` is None and `approximation` is False, " + "the number of shots is automatically set to backend options' " + f"shots={self._backend.options.shots}.", + RuntimeWarning, + ) + # Key for cache key = (tuple(circuits), tuple(observables), self.approximation) @@ -299,14 +321,12 @@ def _calculate_result_index(circ_ind, obs_ind, term_ind, param_val, obs_maps, ex result_index = 0 for _circ_ind, basis_map in exp_map.items(): - for _basis_ind, (_, param_vals) in basis_map.items(): + for _basis_ind, (_, (_, param_vals)) in enumerate(basis_map.items()): if circ_ind == _circ_ind and basis_ind == _basis_ind: result_index += param_vals.index(param_val) return result_index result_index += len(param_vals) - raise AerError( - "Bug. Please report from isssue: https://github.com/Qiskit/qiskit-aer/issues" - ) + raise AerError("Bug. Please report from issue: https://github.com/Qiskit/qiskit-aer/issues") def _create_post_processing( self, circuits, observables, parameter_values, obs_maps, exp_map @@ -344,55 +364,80 @@ def _compute_with_approximation( ): # Key for cache key = (tuple(circuits), tuple(observables), self.approximation) - parameter_binds = [] shots = run_options.pop("shots", None) + # Create expectation value experiments. if key in self._cache: # Use a cache - experiments, experiment_data = self._cache[key] + parameter_binds = defaultdict(dict) for i, j, value in zip(circuits, observables, parameter_values): self._validate_parameter_length(value, i) - parameter_binds.append({k: [v] for k, v in zip(self._parameters[i], value)}) + for k, v in zip(self._parameters[i], value): + if k in parameter_binds[(i, j)]: + parameter_binds[(i, j)][k].append(v) + else: + parameter_binds[(i, j)][k] = [v] + experiment_manager = self._cache[key] + experiment_manager.parameter_binds = list(parameter_binds.values()) else: self._transpile_circuits(circuits) - experiments = [] - experiment_data = [] + experiment_manager = _ExperimentManager() for i, j, value in zip(circuits, observables, parameter_values): - self._validate_parameter_length(value, i) - circuit = ( - self._circuits[i].copy() - if self._skip_transpilation - else self._transpiled_circuits[i].copy() - ) - observable = self._observables[j] - experiment_data.append(observable) - if shots is None: - circuit.save_expectation_value(observable, self._layouts[i]) + if (i, j) in experiment_manager.keys: + self._validate_parameter_length(value, i) + experiment_manager.append( + key=(i, j), + parameter_bind=dict(zip(self._parameters[i], value)), + ) else: - for term_ind, pauli in enumerate(observable.paulis): - circuit.save_expectation_value(pauli, self._layouts[i], label=str(term_ind)) - experiments.append(circuit) - parameter_binds.append({k: [v] for k, v in zip(self._parameters[i], value)}) - self._cache[key] = (experiments, experiment_data) - parameter_binds = parameter_binds if any(parameter_binds) else None + self._validate_parameter_length(value, i) + circuit = ( + self._circuits[i].copy() + if self._skip_transpilation + else self._transpiled_circuits[i].copy() + ) + + observable = self._observables[j] + if shots is None: + circuit.save_expectation_value(observable, self._layouts[i]) + else: + for term_ind, pauli in enumerate(observable.paulis): + circuit.save_expectation_value( + pauli, self._layouts[i], label=str(term_ind) + ) + experiment_manager.append( + key=(i, j), + parameter_bind=dict(zip(self._parameters[i], value)), + experiment_circuit=circuit, + ) + + self._cache[key] = experiment_manager result = self._backend.run( - experiments, parameter_binds=parameter_binds, **run_options + experiment_manager.experiment_circuits, + parameter_binds=experiment_manager.parameter_binds, + **run_options, ).result() # Post processing (calculate expectation values) if shots is None: - expectation_values = [result.data(i)["expectation_value"] for i in range(len(circuits))] + expectation_values = [ + result.data(i)["expectation_value"] for i in experiment_manager.experiment_indices + ] metadata = [ - {"simulator_metadata": result.results[i].metadata} for i in range(len(experiments)) + {"simulator_metadata": result.results[i].metadata} + for i in experiment_manager.experiment_indices ] else: expectation_values = [] rng = np.random.default_rng(seed) metadata = [] - for i in range(len(experiments)): + experiment_indices = experiment_manager.experiment_indices + for i in range(len(experiment_manager)): combined_expval = 0.0 combined_var = 0.0 - coeffs = np.real_if_close(experiment_data[i].coeffs) - for term_ind, expval in result.data(i).items(): + result_index = experiment_indices[i] + observable_key = experiment_manager.get_observable_key(i) + coeffs = np.real_if_close(self._observables[observable_key].coeffs) + for term_ind, expval in result.data(result_index).items(): var = 1 - expval**2 coeff = coeffs[int(term_ind)] combined_expval += expval * coeff @@ -404,7 +449,7 @@ def _compute_with_approximation( { "variance": np.real_if_close(combined_var).item(), "shots": shots, - "simulator_metadata": result.results[i].metadata, + "simulator_metadata": result.results[result_index].metadata, } ) @@ -457,7 +502,10 @@ def _expval_with_variance(counts) -> tuple[float, float]: class _PostProcessing: def __init__( - self, result_indices: list[int], paulis: list[PauliList], coeffs: list[list[float]] + self, + result_indices: list[int], + paulis: list[PauliList], + coeffs: list[list[float]], ): self._result_indices = result_indices self._paulis = paulis @@ -552,3 +600,49 @@ def _paulis2basis(paulis: PauliList) -> Pauli: np.logical_or.reduce(paulis.x), # pylint:disable=no-member ) ) + + +class _ExperimentManager: + def __init__(self): + self.keys: list[tuple[int, int]] = [] + self.experiment_circuits: list[QuantumCircuit] = [] + self.parameter_binds: list[dict[ParameterExpression, list[float]]] = [] + self._input_indices: list[list[int]] = [] + self._num_experiment: int = 0 + + def __len__(self): + return self._num_experiment + + @property + def experiment_indices(self): + """indices of experiments""" + return sum(self._input_indices, []) + + def append( + self, + key: tuple[int, int], + parameter_bind: dict[ParameterExpression, float], + experiment_circuit: QuantumCircuit | None = None, + ): + """append experiments""" + if experiment_circuit is not None: + self.experiment_circuits.append(experiment_circuit) + + if key in self.keys: + key_index = self.keys.index(key) + for k, vs in self.parameter_binds[key_index].items(): + vs.append(parameter_bind[k]) + self._input_indices[key_index].append(self._num_experiment) + else: + self.keys.append(key) + self.parameter_binds.append({k: [v] for k, v in parameter_bind.items()}) + self._input_indices.append([self._num_experiment]) + + self._num_experiment += 1 + + def get_observable_key(self, index): + """return key of observables""" + for i, inputs in enumerate(self._input_indices): + if index in inputs: + return self.keys[i][1] + raise AerError("Unexpected behavior.") diff --git a/qiskit_aer/pulse/__init__.py b/qiskit_aer/pulse/__init__.py deleted file mode 100644 index f15cd681d7..0000000000 --- a/qiskit_aer/pulse/__init__.py +++ /dev/null @@ -1,56 +0,0 @@ -# -*- coding: utf-8 -*- - -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2019, 2020. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -""" -======================================================= -Pulse System Models (:mod:`qiskit_aer.pulse`) -======================================================= - -.. currentmodule:: qiskit_aer.pulse - -This module contains classes and functions to build a pulse system model -for simulating a Qiskit pulse schedule. - - -Classes -======= - -.. autosummary:: - :toctree: ../stubs/ - - PulseSystemModel - - -Functions -========= - -These functions can be used to generate a pulse system model for certain types -of systems. - -.. autosummary:: - :toctree: ../stubs/ - - duffing_system_model -""" - -import sysconfig -import numpy as np - -from .system_models.duffing_model_generators import duffing_system_model -from .system_models.pulse_system_model import PulseSystemModel - -# Remove -Wstrict-prototypes from cflags -CFG_VARS = sysconfig.get_config_vars() -if "CFLAGS" in CFG_VARS: - CFG_VARS["CFLAGS"] = CFG_VARS["CFLAGS"].replace("-Wstrict-prototypes", "") diff --git a/qiskit_aer/pulse/controllers/__init__.py b/qiskit_aer/pulse/controllers/__init__.py deleted file mode 100644 index 246cafc0c3..0000000000 --- a/qiskit_aer/pulse/controllers/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- - -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2019, 2020. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Controllers are general simulation routines that orchestrate different types of -computations that are part of the same "simulation". - -May need a notion of "instruction", so that the controllers know what to call. -""" diff --git a/qiskit_aer/pulse/controllers/digest_pulse_qobj.py b/qiskit_aer/pulse/controllers/digest_pulse_qobj.py deleted file mode 100644 index ea278a2b84..0000000000 --- a/qiskit_aer/pulse/controllers/digest_pulse_qobj.py +++ /dev/null @@ -1,511 +0,0 @@ -# -*- coding: utf-8 -*- - -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2019, 2020. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -# pylint: disable=invalid-name, import-error - -"""Interpretation and storage of PulseQobj information for pulse simulation -""" - -from collections import OrderedDict -import numpy as np - -from qiskit.pulse import DriveChannel -from ...aererror import AerError - -# pylint: disable=no-name-in-module -from .pulse_utils import oplist_to_array - - -class DigestedPulseQobj: - """Container class for information extracted from PulseQobj.""" - - def __init__(self): - # #################################### - # Some "Simulation description" - # #################################### - - # parameters related to memory/measurements - self.shots = None - self.meas_level = None - self.meas_return = None - self.memory_slots = None - self.memory = None - self.n_registers = None - - # #################################### - # Signal portion - # #################################### - - # these contain a particular undocumented data structure - self.pulse_array = None - self.pulse_indices = None - self.pulse_to_int = None - - self.qubit_lo_freq = None - - # ############################################# - # Mix of both signal and simulation description - # ############################################# - - # These should be turned into an internal "simulation events" - # structure - - # "experiments" contains a combination of signal information and - # other experiment descriptions, which should be separated - self.experiments = None - - -def digest_pulse_qobj(qobj, channels, dt, qubit_list): - """Given a PulseQobj (and other parameters), returns a DigestedPulseQobj - containing relevant extracted information - - Parameters: - qobj (qobj): the PulseQobj - channels (OrderedDict): channel dictionary - dt (float): pulse sample width - qubit_list (list): list of qubits to include - - Returns: - DigestedPulseQobj: digested pulse qobj - - Raises: - ValueError: for missing parameters - AerError: for unsupported features or invalid qobj - TypeError: for arguments of invalid type - """ - digested_qobj = DigestedPulseQobj() - - qobj_dict = qobj.to_dict() - qobj_config = qobj_dict["config"] - - # extract schedule_los - if qobj_config.get("schedule_los") is not None: - for exp, schedule_lo in zip(qobj_dict["experiments"], qobj_config["schedule_los"]): - if exp.get("config") is None: - exp["config"] = {} - - schedule_lo_list = [] - for idx in qubit_list: - freq = schedule_lo.get(DriveChannel(idx), None) - if freq is None: - raise ValueError( - """A qubit in the simulation is missing an entry in - schedule_los.""" - ) - - schedule_lo_list.append(freq * 1e-9) - - exp["config"]["qubit_lo_freq"] = schedule_lo_list - - # raises errors for unsupported features - _unsupported_errors(qobj_dict) - - if "memory_slots" not in qobj_config: - raise ValueError("Number of memory_slots must be specific in Qobj config") - - # set memory and measurement details - digested_qobj.shots = int(qobj_config.get("shots", 1024)) - digested_qobj.meas_level = int(qobj_config.get("meas_level", 2)) - digested_qobj.meas_return = qobj_config.get("meas_return", "avg") - digested_qobj.memory_slots = qobj_config.get("memory_slots", 0) - digested_qobj.memory = qobj_config.get("memory", False) - digested_qobj.n_registers = qobj_config.get("n_registers", 0) - - # set qubit_lo_freq as given in qobj - if "qubit_lo_freq" in qobj_config and qobj_config["qubit_lo_freq"] != [np.inf]: - # qobj frequencies are divided by 1e9, so multiply back - digested_qobj.qubit_lo_freq = [freq * 1e9 for freq in qobj_config["qubit_lo_freq"]] - - # build pulse arrays from qobj - pulses, pulses_idx, pulse_dict = build_pulse_arrays( - qobj_dict["experiments"], qobj_config["pulse_library"] - ) - - digested_qobj.pulse_array = pulses - digested_qobj.pulse_indices = pulses_idx - digested_qobj.pulse_to_int = pulse_dict - - experiments = [] - - for exp in qobj_dict["experiments"]: - exp_struct = experiment_to_structs(exp, channels, pulses_idx, pulse_dict, dt, qubit_list) - experiments.append(exp_struct) - - digested_qobj.experiments = experiments - - return digested_qobj - - -def _unsupported_errors(qobj_dict): - """Raises errors for untested/unsupported features. - - Parameters: - qobj_dict (dict): qobj in dictionary form - Returns: - Raises: - AerError: for unsupported features - """ - - # Warnings that don't stop execution - warning_str = "{} are an untested feature, and therefore may not behave as expected." - if _contains_pv_instruction(qobj_dict["experiments"]): - raise AerError(warning_str.format("PersistentValue instructions")) - - error_str = """{} are not directly supported by PulseSimulator. Convert to - explicit WaveForms to simulate.""" - if _contains_parametric_pulse(qobj_dict["experiments"]): - raise AerError(error_str.format("Parametric Pulses")) - - error_str = """Schedules contain {}, are not supported by PulseSimulator.""" - if _contains_frequency_instruction(qobj_dict["experiments"]): - raise AerError(error_str.format("shift frequency and/or set frequency instructions")) - - required_str = "{} are required for simulation, and none were specified." - if not _contains_acquire_instruction(qobj_dict["experiments"]): - raise AerError(required_str.format("Acquire instructions")) - - -def _contains_acquire_instruction(experiments): - """Return True if the list of experiments contains an Acquire instruction - Parameters: - experiments (list): list of schedules - Returns: - True or False: whether or not the schedules contain an Acquire command - Raises: - """ - - for exp in experiments: - for inst in exp["instructions"]: - if inst["name"] == "acquire": - return True - return False - - -def _contains_pv_instruction(experiments): - """Return True if the list of experiments contains a PersistentValue instruction. - - Parameters: - experiments (list): list of schedules - Returns: - True or False: whether or not the schedules contain a PersistentValue command - Raises: - """ - for exp in experiments: - for inst in exp["instructions"]: - if inst["name"] == "pv": - return True - return False - - -def _contains_frequency_instruction(experiments): - """Return True if the list of experiments contains either a set fruquency or shift - frequency instruction. - - Parameters: - experiments (list): list of schedules - Returns: - True or False: whether or not the schedules contain one of the mentioned instructions. - Raises: - """ - for exp in experiments: - for inst in exp["instructions"]: - if inst["name"] == "setf" or inst["name"] == "shiftf": - return True - return False - - -def _contains_parametric_pulse(experiments): - """Return True if the list of experiments contains a parametric pulse. - - Parameters: - experiments (list): list of schedules - Returns: - True or False: whether or not the schedules contain a PersistentValue command - Raises: - """ - for exp in experiments: - for inst in exp["instructions"]: - if inst["name"] == "parametric_pulse": - return True - return False - - -def build_pulse_arrays(experiments, pulse_library): - """Build pulses and pulse_idx arrays, and a pulse_dict - used in simulations and mapping of experimental pulse - sequencies to pulse_idx sequencies and timings. - - Parameters: - experiments (list): list of experiments - pulse_library (list): list of pulses - - Returns: - tuple: Returns all pulses in one array, - an array of start indices for pulses, and dict that - maps pulses to the index at which the pulses start. - """ - pulse_dict = {} - total_pulse_length = 0 - - num_pulse = 0 - for pulse in pulse_library: - pulse_dict[pulse["name"]] = num_pulse - total_pulse_length += len(pulse["samples"]) - num_pulse += 1 - - idx = num_pulse + 1 - # now go through experiments looking for PV gates - pv_pulses = [] - for exp in experiments: - for pulse in exp["instructions"]: - if pulse["name"] == "pv": - if pulse["val"] not in [pval[1] for pval in pv_pulses] and pulse["val"] != 0: - pv_pulses.append((pulse["val"], idx)) - idx += 1 - total_pulse_length += 1 - - pulse_dict["pv"] = pv_pulses - - pulses = np.empty(total_pulse_length, dtype=complex) - pulses_idx = np.zeros(idx + 1, dtype=np.uint32) - - stop = 0 - ind = 1 - for _, pulse in enumerate(pulse_library): - stop = pulses_idx[ind - 1] + len(pulse["samples"]) - pulses_idx[ind] = stop - oplist_to_array(format_pulse_samples(pulse["samples"]), pulses, pulses_idx[ind - 1]) - ind += 1 - - for pv in pv_pulses: - stop = pulses_idx[ind - 1] + 1 - pulses_idx[ind] = stop - oplist_to_array(format_pulse_samples([pv[0]]), pulses, pulses_idx[ind - 1]) - ind += 1 - - return pulses, pulses_idx, pulse_dict - - -def format_pulse_samples(pulse_samples): - """Converts input into a list of complex numbers, where each complex numbers is - given as a list of length 2. If it is already of this format, it simply returns it. - - This function assumes the input is either an ndarray, a list of numpy complex number types, - or a list already in the desired format. - - Args: - pulse_samples (list): An ndarray of complex numbers or a list - - Returns: - list: list of the required format - """ - - new_samples = list(pulse_samples) - - if not np.iscomplexobj(new_samples[0]): - return new_samples - - return [[samp.real, samp.imag] for samp in new_samples] - - -def experiment_to_structs(experiment, ham_chans, pulse_inds, pulse_to_int, dt, qubit_list=None): - """Converts an experiment to a better formatted structure - - Args: - experiment (dict): An experiment. - ham_chans (dict): The channels in the Hamiltonian. - pulse_inds (array): Array of pulse indices. - pulse_to_int (array): Qobj pulses labeled by ints. - dt (float): Pulse time resolution. - qubit_list (list): List of qubits. - - Returns: - dict: The output formatted structure. - - Raises: - ValueError: Channel not in Hamiltonian. - TypeError: Incorrect snapshot type. - """ - # TO DO: Error check that operations are restricted to qubit list - max_time = 0 - structs = {} - structs["header"] = experiment["header"] - structs["channels"] = OrderedDict() - for chan_name in ham_chans: - structs["channels"][chan_name] = [[], []] - structs["acquire"] = [] - structs["cond"] = [] - structs["snapshot"] = [] - structs["tlist"] = [] - structs["can_sample"] = True - - # set an experiment qubit_lo_freq if present in experiment - structs["qubit_lo_freq"] = None - if "config" in experiment: - if ( - "qubit_lo_freq" in experiment["config"] - and experiment["config"]["qubit_lo_freq"] is not None - ): - freq_list = experiment["config"]["qubit_lo_freq"] - freq_list = [freq * 1e9 for freq in freq_list] - structs["qubit_lo_freq"] = freq_list - # This is a list that tells us whether - # the last PV pulse on a channel needs to - # be assigned a final time based on the next pulse on that channel - pv_needs_tf = [0] * len(ham_chans) - - # The instructions are time-ordered so just loop through them. - for inst in experiment["instructions"]: - # Do D and U channels - if "ch" in inst.keys() and inst["ch"][0] in ["d", "u"]: - chan_name = inst["ch"].upper() - if chan_name not in ham_chans.keys(): - raise ValueError("Channel {} is not in Hamiltonian model".format(inst["ch"])) - - # If last pulse on channel was a PV then need to set - # its final time to be start time of current pulse - if pv_needs_tf[ham_chans[chan_name]]: - structs["channels"][chan_name][0][-3] = inst["t0"] * dt - pv_needs_tf[ham_chans[chan_name]] = 0 - - # Get condtional info - if "conditional" in inst.keys(): - cond = inst["conditional"] - else: - cond = -1 - # PV's - if inst["name"] == "pv": - # Get PV index - for pv in pulse_to_int["pv"]: - if pv[0] == inst["val"]: - index = pv[1] - break - structs["channels"][chan_name][0].extend([inst["t0"] * dt, None, index, cond]) - pv_needs_tf[ham_chans[chan_name]] = 1 - - # ShiftPhase instructions - elif inst["name"] == "fc": - # get current phase value - current_phase = 0 - if len(structs["channels"][chan_name][1]) > 0: - current_phase = structs["channels"][chan_name][1][-2] - - structs["channels"][chan_name][1].extend( - [inst["t0"] * dt, current_phase + inst["phase"], cond] - ) - - # SetPhase instruction - elif inst["name"] == "setp": - structs["channels"][chan_name][1].extend([inst["t0"] * dt, inst["phase"], cond]) - # Delay instruction - elif inst["name"] == "delay": - pass # nothing to be done in this case - # A standard pulse - else: - start = inst["t0"] * dt - pulse_int = pulse_to_int[inst["name"]] - pulse_width = (pulse_inds[pulse_int + 1] - pulse_inds[pulse_int]) * dt - stop = start + pulse_width - structs["channels"][chan_name][0].extend([start, stop, pulse_int, cond]) - - max_time = max(max_time, stop) - - # Take care of acquires and snapshots (bfuncs added ) - else: - # measurements - if inst["name"] == "acquire": - # Better way?? - qlist2 = [] - mlist2 = [] - if qubit_list is None: - qlist2 = inst["qubits"] - mlist2 = inst["memory_slot"] - else: - for qind, qb in enumerate(inst["qubits"]): - if qb in qubit_list: - qlist2.append(qb) - mlist2.append(inst["memory_slot"][qind]) - - acq_vals = [ - inst["t0"] * dt, - np.asarray(qlist2, dtype=np.uint32), - np.asarray(mlist2, dtype=np.uint32), - ] - if "register_slot" in inst.keys(): - acq_vals.append(np.asarray(inst["register_slot"], dtype=np.uint32)) - else: - acq_vals.append(None) - structs["acquire"].append(acq_vals) - - # update max_time - max_time = max(max_time, (inst["t0"] + inst["duration"]) * dt) - - # Add time to tlist - if inst["t0"] * dt not in structs["tlist"]: - structs["tlist"].append(inst["t0"] * dt) - - # conditionals - elif inst["name"] == "bfunc": - bfun_vals = [ - inst["t0"] * dt, - inst["mask"], - inst["relation"], - inst["val"], - inst["register"], - ] - if "memory" in inst.keys(): - bfun_vals.append(inst["memory"]) - else: - bfun_vals.append(None) - - structs["cond"].append(acq_vals) - - # update max_time - max_time = max(max_time, inst["t0"] * dt) - - # Add time to tlist - if inst["t0"] * dt not in structs["tlist"]: - structs["tlist"].append(inst["t0"] * dt) - - # snapshots - elif inst["name"] == "snapshot": - if inst["type"] != "state": - raise TypeError("Snapshots must be of type 'state'") - structs["snapshot"].append([inst["t0"] * dt, inst["label"]]) - - # Add time to tlist - if inst["t0"] * dt not in structs["tlist"]: - structs["tlist"].append(inst["t0"] * dt) - - # update max_time - max_time = max(max_time, inst["t0"] * dt) - - # If any PVs still need time then they are at the end - # and should just go til final time - ham_keys = list(ham_chans.keys()) - for idx, pp in enumerate(pv_needs_tf): - if pp: - structs["channels"][ham_keys[idx]][0][-3] = max_time - pv_needs_tf[idx] = 0 - - # Convert lists to numpy arrays - for key in structs["channels"].keys(): - structs["channels"][key][0] = np.asarray(structs["channels"][key][0], dtype=float) - structs["channels"][key][1] = np.asarray(structs["channels"][key][1], dtype=float) - - structs["tlist"] = np.asarray([0] + structs["tlist"], dtype=float) - - if structs["tlist"][-1] > structs["acquire"][-1][0]: - structs["can_sample"] = False - - return structs diff --git a/qiskit_aer/pulse/controllers/mc_controller.py b/qiskit_aer/pulse/controllers/mc_controller.py deleted file mode 100644 index 00e14d7f84..0000000000 --- a/qiskit_aer/pulse/controllers/mc_controller.py +++ /dev/null @@ -1,225 +0,0 @@ -# -*- coding: utf-8 -*- - -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2019, 2020. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -# This file is part of QuTiP: Quantum Toolbox in Python. -# -# Copyright (c) 2011 and later, Paul D. Nation and Robert J. Johansson. -# All rights reserved. -# pylint: disable=no-name-in-module, import-error, invalid-name - -""" -Controller for Monte Carlo state-vector solver method. -""" - -from math import log -import time -import numpy as np -from scipy.linalg.blas import get_blas_funcs -from qiskit.tools.parallel import parallel_map, CPU_COUNT -from .pulse_sim_options import PulseSimOptions -from .pulse_de_solver import setup_de_solver -from .pulse_utils import occ_probabilities, write_shots_memory, spmv, cy_expect_psi - -dznrm2 = get_blas_funcs("znrm2", dtype=np.float64) - - -def run_monte_carlo_experiments(pulse_sim_desc, pulse_de_model, solver_options=None): - """Runs monte carlo experiments for a given op_system - - Parameters: - pulse_sim_desc (PulseSimDescription): description of pulse simulation - pulse_de_model (PulseInternalDEModel): description of de model - solver_options (PulseSimOptions): options - - Returns: - tuple: two lists with experiment results - - Raises: - Exception: if initial state is of incorrect format - """ - - solver_options = PulseSimOptions() if solver_options is None else solver_options - - if not pulse_sim_desc.initial_state.data.ndim != 1: - raise Exception("Initial state must be a state vector.") - - y0 = pulse_sim_desc.initial_state.data.ravel() - - # set num_cpus to the value given in settings if none in Options - if not solver_options.num_cpus: - solver_options.num_cpus = CPU_COUNT - - # setup seeds array - seed = pulse_sim_desc.seed or np.random.randint(np.iinfo(np.int32).max - 1) - prng = np.random.RandomState(seed) - for exp in pulse_sim_desc.experiments: - exp["seed"] = prng.randint(np.iinfo(np.int32).max - 1) - - map_kwargs = {"num_processes": solver_options.num_cpus} - - exp_results = [] - exp_times = [] - - # needs to be configured ahead of time - pulse_de_model._config_internal_data() - - for exp in pulse_sim_desc.experiments: - start = time.time() - rng = np.random.RandomState(exp["seed"]) - seeds = rng.randint(np.iinfo(np.int32).max - 1, size=pulse_sim_desc.shots) - exp_res = parallel_map( - monte_carlo_evolution, - seeds, - task_args=( - exp, - y0, - pulse_sim_desc, - pulse_de_model, - solver_options, - ), - **map_kwargs, - ) - - # exp_results is a list for each shot - # so transform back to an array of shots - exp_res2 = [] - for exp_shot in exp_res: - exp_res2.append(exp_shot[0].tolist()) - - end = time.time() - exp_times.append(end - start) - exp_results.append(np.array(exp_res2)) - - return exp_results, exp_times - - -def monte_carlo_evolution(seed, exp, y0, pulse_sim_desc, pulse_de_model, solver_options=None): - """Performs a single monte carlo run for the given op_system, experiment, and seed - - Parameters: - seed (int): seed for random number generation - exp (dict): dictionary containing experiment description - y0 (array): initial state - pulse_sim_desc (PulseSimDescription): container for simulation description - pulse_de_model (PulseInternalDEModel): container for de model - solver_options (PulseSimOptions): options - - Returns: - array: results of experiment - - Raises: - Exception: if ODE solving has errors - """ - - solver_options = PulseSimOptions() if solver_options is None else solver_options - - rng = np.random.RandomState(seed) - tlist = exp["tlist"] - # Init memory - memory = np.zeros((1, pulse_sim_desc.memory_slots), dtype=np.uint8) - - # Get number of acquire - num_acq = len(exp["acquire"]) - acq_idx = 0 - - collapse_times = [] - collapse_operators = [] - - # first rand is collapse norm, second is which operator - rand_vals = rng.rand(2) - - # make array for collapse operator inds - cinds = np.arange(pulse_de_model.c_num) - n_dp = np.zeros(pulse_de_model.c_num, dtype=float) - - ODE = setup_de_solver(exp, y0, pulse_de_model, solver_options.de_options) - - # RUN ODE UNTIL EACH TIME IN TLIST - for stop_time in tlist: - # ODE WHILE LOOP FOR INTEGRATE UP TO TIME TLIST[k] - while ODE.t < stop_time: - t_prev = ODE.t - y_prev = ODE.y - norm2_prev = dznrm2(ODE.y) ** 2 - # integrate up to stop_time, one step at a time. - ODE.integrate(stop_time, step=1) - if not ODE.successful(): - raise Exception("Integration step failed!") - norm2_psi = dznrm2(ODE.y) ** 2 - - if norm2_psi <= rand_vals[0]: - # collapse has occured: - # find collapse time to within specified tolerance - # ------------------------------------------------ - ii = 0 - t_final = ODE.t - while ii < solver_options.norm_steps: - ii += 1 - t_guess = t_prev + log(norm2_prev / rand_vals[0]) / log( - norm2_prev / norm2_psi - ) * (t_final - t_prev) - ODE.y = y_prev - ODE.t = t_prev - ODE.integrate(t_guess, step=0) - if not ODE.successful(): - raise Exception("Integration failed after adjusting step size!") - norm2_guess = dznrm2(ODE.y) ** 2 - if abs(rand_vals[0] - norm2_guess) < solver_options.norm_tol * rand_vals[0]: - break - - if norm2_guess < rand_vals[0]: - # t_guess is still > t_jump - t_final = t_guess - norm2_psi = norm2_guess - else: - # t_guess < t_jump - t_prev = t_guess - y_prev = ODE.y - norm2_prev = norm2_guess - if ii > solver_options.norm_steps: - raise Exception( - "Norm tolerance not reached. " - + "Increase accuracy of ODE solver or " - + "Options.norm_steps." - ) - - collapse_times.append(ODE.t) - # all constant collapse operators. - for i in range(n_dp.shape[0]): - n_dp[i] = cy_expect_psi(pulse_de_model.n_ops_data[i], ODE.y, True) - # determine which operator does collapse and store it - _p = np.cumsum(n_dp / np.sum(n_dp)) - j = cinds[_p >= rand_vals[1]][0] - collapse_operators.append(j) - - state = spmv(pulse_de_model.c_ops_data[j], ODE.y) - state /= dznrm2(state) - ODE.y = state - rand_vals = rng.rand(2) - - # after while loop (Do measurement or conditional) - # ------------------------------------------------ - out_psi = ODE.y / dznrm2(ODE.y) - - for aind in range(acq_idx, num_acq): - if exp["acquire"][aind][0] == stop_time: - current_acq = exp["acquire"][aind] - qubits = current_acq[1] - memory_slots = current_acq[2] - probs = occ_probabilities(qubits, out_psi, pulse_sim_desc.measurement_ops) - rand_vals = rng.rand(memory_slots.shape[0]) - write_shots_memory(memory, memory_slots, probs, rand_vals) - acq_idx += 1 - - return memory diff --git a/qiskit_aer/pulse/controllers/pulse_controller.py b/qiskit_aer/pulse/controllers/pulse_controller.py deleted file mode 100644 index 74b4e8ff40..0000000000 --- a/qiskit_aer/pulse/controllers/pulse_controller.py +++ /dev/null @@ -1,492 +0,0 @@ -# -*- coding: utf-8 -*- - -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2019, 2020. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -# pylint: disable=invalid-name, no-name-in-module, import-error - -""" -Entry/exit point for pulse simulation specified through PulseSimulator backend -""" - -from warnings import warn -from copy import copy -from typing import Callable -import numpy as np -from qiskit.quantum_info.operators.operator import Operator -from ..system_models.string_model_parser.string_model_parser import NoiseParser -from ..system_models.string_model_parser import operator_generators as op_gen -from .digest_pulse_qobj import digest_pulse_qobj -from .pulse_sim_options import PulseSimOptions -from .unitary_controller import run_unitary_experiments -from .mc_controller import run_monte_carlo_experiments -from .pulse_utils import get_ode_rhs_functor - - -def pulse_controller(qobj): - """Interprets PulseQobj input, runs simulations, and returns results - - Parameters: - qobj (PulseQobj): pulse qobj containing a list of pulse schedules - - Returns: - list: simulation results - - Raises: - ValueError: if input is of incorrect format - Exception: for invalid ODE options - """ - - pulse_sim_desc = PulseSimDescription() - pulse_de_model = PulseInternalDEModel() - - config = qobj.config - - # ############################### - # ### Extract model parameters - # ############################### - - system_model = config.system_model - - # Get qubit list and number - qubit_list = system_model.subsystem_list - if qubit_list is None: - raise ValueError("Model must have a qubit list to simulate.") - n_qubits = len(qubit_list) - - # get Hamiltonian - if system_model.hamiltonian is None: - raise ValueError("Model must have a Hamiltonian to simulate.") - ham_model = system_model.hamiltonian - - # Extract DE model information - pulse_de_model.system = ham_model._system - pulse_de_model.variables = ham_model._variables - pulse_de_model.channels = ham_model._channels - pulse_de_model.h_diag = ham_model._h_diag - pulse_de_model.evals = ham_model._evals - pulse_de_model.estates = ham_model._estates - dim_qub = ham_model._subsystem_dims - dim_osc = {} - # convert estates into a Qutip qobj - estates = [op_gen.state(state) for state in ham_model._estates.T[:]] - - # initial state set here - if getattr(config, "initial_state", None) is not None: - pulse_sim_desc.initial_state = op_gen.state(config.initial_state) - else: - pulse_sim_desc.initial_state = estates[0] - - # Get dt - if system_model.dt is None: - raise ValueError("System model must have a dt value to simulate.") - - pulse_de_model.dt = system_model.dt - - # Parse noise - noise_model = getattr(config, "noise_model", None) - - # post warnings for unsupported features - _unsupported_warnings(noise_model) - - if noise_model: - noise = NoiseParser(noise_dict=noise_model, dim_osc=dim_osc, dim_qub=dim_qub) - noise.parse() - - pulse_de_model.noise = noise.compiled - if any(pulse_de_model.noise): - pulse_sim_desc.can_sample = False - - # ############################### - # ### Parse qobj_config settings - # ############################### - digested_qobj = digest_pulse_qobj(qobj, pulse_de_model.channels, system_model.dt, qubit_list) - - # extract simulation-description level qobj content - pulse_sim_desc.shots = digested_qobj.shots - pulse_sim_desc.meas_level = digested_qobj.meas_level - pulse_sim_desc.meas_return = digested_qobj.meas_return - pulse_sim_desc.memory_slots = digested_qobj.memory_slots - pulse_sim_desc.memory = digested_qobj.memory - - # extract model-relevant information - pulse_de_model.n_registers = digested_qobj.n_registers - pulse_de_model.pulse_array = digested_qobj.pulse_array - pulse_de_model.pulse_indices = digested_qobj.pulse_indices - pulse_de_model.pulse_to_int = digested_qobj.pulse_to_int - - pulse_sim_desc.experiments = digested_qobj.experiments - - # Handle qubit_lo_freq - qubit_lo_freq = digested_qobj.qubit_lo_freq - - # if it wasn't specified in the PulseQobj, draw from system_model - if qubit_lo_freq is None: - default_freq = getattr(config, "qubit_freq_est", [np.inf]) - if default_freq != [np.inf]: - qubit_lo_freq = default_freq - - # if still None, or is the placeholder value draw from the Hamiltonian - if qubit_lo_freq is None: - qubit_lo_freq = system_model.hamiltonian.get_qubit_lo_from_drift() - if getattr(qobj.config, "schedule_los", None) is None: - warn( - "Warning: qubit_lo_freq was not specified in PulseQobj and there is no default, " - "so it is being automatically determined from the drift Hamiltonian." - ) - - pulse_de_model.freqs = system_model.calculate_channel_frequencies(qubit_lo_freq=qubit_lo_freq) - pulse_de_model.calculate_channel_frequencies = system_model.calculate_channel_frequencies - - # ############################### - # ### Parse backend_options - # # solver-specific information should be extracted in the solver - # ############################### - - pulse_sim_desc.seed = int(config.seed) if hasattr(config, "seed") else None - pulse_sim_desc.q_level_meas = int(getattr(config, "q_level_meas", 1)) - - # solver options - allowed_solver_options = [ - "atol", - "rtol", - "nsteps", - "max_step", - "num_cpus", - "norm_tol", - "norm_steps", - "method", - ] - solver_options = getattr(config, "solver_options", {}) - for key in solver_options: - if key not in allowed_solver_options: - raise Exception("Invalid solver_option: {}".format(key)) - solver_options = PulseSimOptions(**solver_options) - - # Set the ODE solver max step to be the half the - # width of the smallest pulse - min_width = np.iinfo(np.int32).max - for key, val in pulse_de_model.pulse_to_int.items(): - if key != "pv": - stop = pulse_de_model.pulse_indices[val + 1] - start = pulse_de_model.pulse_indices[val] - min_width = min(min_width, stop - start) - solver_options.de_options.max_step = min_width / 2 * pulse_de_model.dt - - # ######################################## - # Determination of measurement operators. - # ######################################## - pulse_sim_desc.measurement_ops = [None] * n_qubits - - for exp in pulse_sim_desc.experiments: - # Add in measurement operators - # Not sure if this will work for multiple measurements - # Note: the extraction of multiple measurements works, but the simulation routines - # themselves implicitly assume there is only one measurement at the end - if any(exp["acquire"]): - for acq in exp["acquire"]: - for jj in acq[1]: - if jj > qubit_list[-1]: - continue - if not pulse_sim_desc.measurement_ops[qubit_list.index(jj)]: - q_level_meas = pulse_sim_desc.q_level_meas - pulse_sim_desc.measurement_ops[ - qubit_list.index(jj) - ] = op_gen.qubit_occ_oper_dressed( - jj, estates, h_osc=dim_osc, h_qub=dim_qub, level=q_level_meas - ) - - if not exp["can_sample"]: - pulse_sim_desc.can_sample = False - - # trim measurement operators to relevant qubits once constructed - meas_ops_reduced = [] - for op in pulse_sim_desc.measurement_ops: - if op is not None: - meas_ops_reduced.append(op) - pulse_sim_desc.measurement_ops = meas_ops_reduced - - run_experiments = ( - run_unitary_experiments if pulse_sim_desc.can_sample else run_monte_carlo_experiments - ) - exp_results, exp_times = run_experiments(pulse_sim_desc, pulse_de_model, solver_options) - - output = { - "results": format_exp_results(exp_results, exp_times, pulse_sim_desc), - "success": True, - "qobj_id": qobj.qobj_id, - } - return output - - -def format_exp_results(exp_results, exp_times, pulse_sim_desc): - """format simulation results - - Parameters: - exp_results (list): simulation results - exp_times (list): simulation times - pulse_sim_desc (PulseSimDescription): object containing all simulation information - - Returns: - list: formatted simulation results - """ - - # format the data into the proper output - all_results = [] - for idx_exp, exp in enumerate(pulse_sim_desc.experiments): - m_lev = pulse_sim_desc.meas_level - m_ret = pulse_sim_desc.meas_return - - # populate the results dictionary - results = { - "seed_simulator": exp["seed"], - "shots": pulse_sim_desc.shots, - "status": "DONE", - "success": True, - "time_taken": exp_times[idx_exp], - "header": exp["header"], - "meas_level": m_lev, - "meas_return": m_ret, - "data": {}, - } - - if pulse_sim_desc.can_sample: - memory = exp_results[idx_exp][0] - results["data"]["statevector"] = [] - for coef in exp_results[idx_exp][1]: - results["data"]["statevector"].append([np.real(coef), np.imag(coef)]) - results["header"]["ode_t"] = exp_results[idx_exp][2] - else: - memory = exp_results[idx_exp] - - # meas_level 2 return the shots - if m_lev == 2: - # convert the memory **array** into a n - # integer - # e.g. [1,0] -> 2 - int_mem = memory.dot(np.power(2.0, np.arange(memory.shape[1]))).astype(int) - - # if the memory flag is set return each shot - if pulse_sim_desc.memory: - hex_mem = [hex(val) for val in int_mem] - results["data"]["memory"] = hex_mem - - # Get hex counts dict - unique = np.unique(int_mem, return_counts=True) - hex_dict = {} - for kk in range(unique[0].shape[0]): - key = hex(unique[0][kk]) - hex_dict[key] = unique[1][kk] - results["data"]["counts"] = hex_dict - - # meas_level 1 returns the - elif m_lev == 1: - if m_ret == "avg": - memory = [np.mean(memory, 0)] - - # convert into the right [real, complex] pair form for json - results["data"]["memory"] = [] - for mem_shot in memory: - results["data"]["memory"].append([]) - for mem_slot in mem_shot: - results["data"]["memory"][-1].append([np.real(mem_slot), np.imag(mem_slot)]) - - if m_ret == "avg": - results["data"]["memory"] = results["data"]["memory"][0] - - all_results.append(results) - return all_results - - -def _unsupported_warnings(noise_model): - """Warns the user about untested/unsupported features. - - Parameters: - noise_model (dict): backend_options for simulation - Returns: - Raises: - AerError: for unsupported features - """ - - # Warnings that don't stop execution - warning_str = "{} are an untested feature, and therefore may not behave as expected." - if noise_model is not None: - warn(warning_str.format("Noise models")) - - -class PulseInternalDEModel: - """Container of information required for de RHS construction""" - - def __init__(self): - # The system Hamiltonian in numerical format - self.system = None - # The noise (if any) in numerical format - self.noise = None - # System variables - self.variables = None - # Channels in the Hamiltonian string - # these tell the order in which the channels - # are evaluated in the RHS solver. - self.channels = None - # Array containing all pulse samples - self.pulse_array = None - # Array of indices indicating where a pulse starts in the self.pulse_array - self.pulse_indices = None - # A dict that translates pulse names to integers for use in self.pulse_indices - self.pulse_to_int = None - # dt for pulse schedules - self.dt = None - # holds default frequencies for the channels - self.freqs = {} - # frequency calculation function for overriding defaults - self.calculate_channel_frequencies = None - # diagonal elements of the hamiltonian - self.h_diag = None - # eigenvalues of the time-independent hamiltonian - self.evals = None - # eigenstates of the time-independent hamiltonian - self.estates = None - - self.n_registers = None - - # attributes used in RHS function - self.vars = None - self.vars_names = None - self.num_h_terms = None - self.c_num = None - self.c_ops_data = None - self.n_ops_data = None - self.h_diag_elems = None - - self.h_ops_data = None - - self._rhs_dict = None - - def _config_internal_data(self): - """Preps internal data into format required by RHS function.""" - - self.vars = list(self.variables.values()) - # Need this info for evaluating the hamiltonian vars in the c++ solver - self.vars_names = list(self.variables.keys()) - - num_h_terms = len(self.system) - H = [hpart[0] for hpart in self.system] - self.num_h_terms = num_h_terms - - self.c_ops_data = [] - self.n_ops_data = [] - - self.h_diag_elems = self.h_diag - - # if there are any collapse operators - self.c_num = 0 - - if self.noise: - self.c_num = len(self.noise) - self.num_h_terms += 1 - H_noise = Operator(np.zeros(self.noise[0].data.shape)) - for kk in range(self.c_num): - c_op = self.noise[kk] - n_op = c_op.adjoint() & c_op - # collapse ops - self.c_ops_data.append(c_op.data) - # norm ops - self.n_ops_data.append(n_op.data) - # Norm ops added to time-independent part of - # Hamiltonian to decrease norm - H_noise = Operator(H_noise.data - 0.5j * n_op.data) - - H = H + [H_noise] - - # construct data sets - self.h_ops_data = [-1.0j * hpart.data for hpart in H] - - self._rhs_dict = { - "freqs": list(self.freqs.values()), - "pulse_array": self.pulse_array, - "pulse_indices": self.pulse_indices, - "vars": self.vars, - "vars_names": self.vars_names, - "num_h_terms": self.num_h_terms, - "h_ops_data": self.h_ops_data, - "h_diag_elems": self.h_diag_elems, - } - - def init_rhs(self, exp): - """Set up and return rhs function corresponding to this model for a given - experiment exp - """ - - # if _rhs_dict has not been set up, config the internal data - if self._rhs_dict is None: - self._config_internal_data() - - channels = dict(self.channels) - - # Init register - register = np.ones(self.n_registers, dtype=np.uint8) - - rhs_dict = setup_rhs_dict_freqs(self._rhs_dict, exp, self.calculate_channel_frequencies) - ode_rhs_obj = get_ode_rhs_functor(rhs_dict, exp, self.system, channels, register) - - def rhs(t, y): - return ode_rhs_obj(t, y) - - return rhs - - -def setup_rhs_dict_freqs( - default_rhs_dict: dict, exp: dict, calculate_channel_frequencies: Callable -): - """Standalone function for overriding channel frequencies in a given experiment. - - Args: - default_rhs_dict: Dictionary containing default RHS data. - exp: Dictionary containing experiment data. - calculate_channel_frequencies: Function for computing all channel frequencies from - a list of DriveChannel frequencies. - - Returns: - dict: Dictionary with frequencies potentially overriden by those in exp. - """ - - if "qubit_lo_freq" in exp and exp["qubit_lo_freq"] is not None: - # copy to not overwrite defaults - default_rhs_dict = copy(default_rhs_dict) - freqs_dict = calculate_channel_frequencies(exp["qubit_lo_freq"]) - default_rhs_dict["freqs"] = list(freqs_dict.values()) - - return default_rhs_dict - - -class PulseSimDescription: - """Object for holding any/all information required for simulation. - Needs to be refactored into different pieces. - """ - - def __init__(self): - self.initial_state = None - # Channels in the Hamiltonian string - # these tell the order in which the channels - # are evaluated in the RHS solver. - self.experiments = [] - # Can experiments be simulated once then sampled - self.can_sample = True - - self.shots = None - self.meas_level = None - self.meas_return = None - self.memory_slots = None - self.memory = None - - self.seed = None - self.q_level_meas = None - self.measurement_ops = None diff --git a/qiskit_aer/pulse/controllers/pulse_de_solver.py b/qiskit_aer/pulse/controllers/pulse_de_solver.py deleted file mode 100644 index ab6081f06b..0000000000 --- a/qiskit_aer/pulse/controllers/pulse_de_solver.py +++ /dev/null @@ -1,44 +0,0 @@ -# -*- coding: utf-8 -*- - -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2019, 2020. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -# This file is part of QuTiP: Quantum Toolbox in Python. -# -# Copyright (c) 2011 and later, Paul D. Nation and Robert J. Johansson. -# All rights reserved. -# pylint: disable=invalid-name - -"""Set up DE solver for problems in qutip format.""" - -from ..de.DE_Methods import method_from_string - - -def setup_de_solver(exp, y0, pulse_de_model, de_options): - """Constructs a scipy ODE solver for a given exp and op_system - - Parameters: - exp (dict): dict containing experiment description - y0 (array): initial state - pulse_de_model (PulseInternalDEModel): container for de model - de_options (DE_Options): options for DE method - - Returns: - solver: ODE_Method instance initialized with state and rhs function - """ - - method = method_from_string(de_options.method) - - rhs = pulse_de_model.init_rhs(exp) - - solver = method(0.0, y0, rhs, de_options) - return solver diff --git a/qiskit_aer/pulse/controllers/pulse_sim_options.py b/qiskit_aer/pulse/controllers/pulse_sim_options.py deleted file mode 100644 index 5ceefb7b9c..0000000000 --- a/qiskit_aer/pulse/controllers/pulse_sim_options.py +++ /dev/null @@ -1,125 +0,0 @@ -# -*- coding: utf-8 -*- - -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2019, 2020. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Pulse solver options""" - -from ..de.DE_Options import DE_Options - - -class PulseSimOptions: - """ - Class of options for pulse solver routines. Options can be specified either as - arguments to the constructor:: - - opts = Options(order=10, ...) - - or by changing the class attributes after creation:: - - opts = Options() - opts.order = 10 - - Returns options class to be used as options in evolution solvers. - - Attributes: - method (str, 'zvode-adams'): Integration method. - atol (float, 1e-8): Absolute tolerance for variable step solvers. - rtol (float, 1e-6): Relative tolerance for variable step solvers. - order (int, 12): Order of integrator. - nsteps (int, 10**6): Max. number of internal steps per time interval for variable step - solvers. - first_step (float, None): Size of initial step for variable step solvers. - min_step (float, None): Minimum step size for variable step solvers. - max_step (float, None): Maximum step size for variable step solvers. - max_dt (float, 1e-3): Max step size for fixed step solver. - num_cpus (int): Number of cpus used by mcsolver (default = # of cpus). - norm_tol (float, 1e-3): Tolerance used when finding wavefunction norm. - norm_steps (int, 5): Max. number of steps used to find wavefunction norm - to within norm_tol - shots (int, 1024): Number of shots to run. - seeds (ndarray, None): Array containing random number seeds for - repeatible shots. - reuse_seeds (bool, False): Reuse seeds, if already generated. - store_final_state (bool, False): Whether or not to store the final state - of the evolution. - """ - - def __init__( - self, - method="zvode-adams", - atol=1e-8, - rtol=1e-6, - order=12, - nsteps=10**6, - first_step=None, - max_step=None, - min_step=None, - max_dt=10**-3, - num_cpus=0, - norm_tol=1e-3, - norm_steps=5, - progress_bar=True, - shots=1024, - store_final_state=False, - seeds=None, - reuse_seeds=False, - ): - # set DE specific options - self.de_options = DE_Options( - method=method, - atol=atol, - rtol=rtol, - order=order, - nsteps=nsteps, - first_step=first_step, - max_step=max_step, - min_step=min_step, - max_dt=max_dt, - ) - - self.shots = shots - self.seeds = seeds - self.reuse_seeds = reuse_seeds - self.progress_bar = progress_bar - self.num_cpus = num_cpus - self.norm_tol = norm_tol - self.norm_steps = norm_steps - self.store_final_state = store_final_state - - def copy(self): - """Create a copy.""" - return PulseSimOptions( - method=self.de_options.method, - atol=self.de_options.atol, - rtol=self.de_options.rtol, - order=self.de_options.order, - nsteps=self.de_options.nsteps, - first_step=self.de_options.first_step, - max_step=self.de_options.max_step, - min_step=self.de_options.min_step, - max_dt=self.de_options.max_dt, - num_cpus=self.num_cpus, - norm_tol=self.norm_tol, - norm_steps=self.norm_steps, - progress_bar=self.progress_bar, - shots=self.shots, - store_final_state=self.store_final_state, - seeds=self.seeds, - reuse_seeds=self.reuse_seeds, - ) - - def __str__(self): - return str(vars(self)) - - def __repr__(self): - return self.__str__() diff --git a/qiskit_aer/pulse/controllers/unitary_controller.py b/qiskit_aer/pulse/controllers/unitary_controller.py deleted file mode 100644 index 29a8afb8e5..0000000000 --- a/qiskit_aer/pulse/controllers/unitary_controller.py +++ /dev/null @@ -1,159 +0,0 @@ -# -*- coding: utf-8 -*- - -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2019, 2020. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -# pylint: disable=no-name-in-module, import-error, invalid-name - -""" -Controller for solving unitary evolution of a state-vector. -""" - -import time -import numpy as np -from scipy.linalg.blas import get_blas_funcs -from qiskit.tools.parallel import parallel_map, CPU_COUNT -from .pulse_sim_options import PulseSimOptions -from .pulse_de_solver import setup_de_solver - -from .pulse_utils import occ_probabilities, write_shots_memory - -dznrm2 = get_blas_funcs("znrm2", dtype=np.float64) - - -def _full_simulation(exp, y0, pulse_sim_desc, pulse_de_model, solver_options=None): - """ - Set up full simulation, i.e. combining different (ideally modular) computational - resources into one function. - """ - - solver_options = PulseSimOptions() if solver_options is None else solver_options - - psi, ode_t = unitary_evolution(exp, y0, pulse_de_model, solver_options) - - # ############### - # do measurement - # ############### - rng = np.random.RandomState(exp["seed"]) - - shots = pulse_sim_desc.shots - # Init memory - memory = np.zeros((shots, pulse_sim_desc.memory_slots), dtype=np.uint8) - - qubits = [] - memory_slots = [] - tlist = exp["tlist"] - for acq in exp["acquire"]: - if acq[0] == tlist[-1]: - qubits += list(acq[1]) - memory_slots += list(acq[2]) - qubits = np.array(qubits, dtype="uint32") - memory_slots = np.array(memory_slots, dtype="uint32") - - probs = occ_probabilities(qubits, psi, pulse_sim_desc.measurement_ops) - rand_vals = rng.rand(memory_slots.shape[0] * shots) - write_shots_memory(memory, memory_slots, probs, rand_vals) - - return [memory, psi, ode_t] - - -def run_unitary_experiments(pulse_sim_desc, pulse_de_model, solver_options=None): - """Runs unitary experiments for a given op_system - - Parameters: - pulse_sim_desc (PulseSimDescription): description of pulse simulation - pulse_de_model (PulseInternalDEModel): description of de model - solver_options (PulseSimOptions): options - - Returns: - tuple: two lists with experiment results - - Raises: - Exception: if initial state is of incorrect format - """ - - solver_options = PulseSimOptions() if solver_options is None else solver_options - - if not pulse_sim_desc.initial_state.data.ndim != 1: - raise Exception("Initial state must be a state vector.") - - y0 = pulse_sim_desc.initial_state.data.ravel() - - # set num_cpus to the value given in settings if none in Options - if not solver_options.num_cpus: - solver_options.num_cpus = CPU_COUNT - - # setup seeds array - seed = pulse_sim_desc.seed or np.random.randint(np.iinfo(np.int32).max - 1) - prng = np.random.RandomState(seed) - for exp in pulse_sim_desc.experiments: - exp["seed"] = prng.randint(np.iinfo(np.int32).max - 1) - - map_kwargs = {"num_processes": solver_options.num_cpus} - - # run simulation on each experiment in parallel - start = time.time() - exp_results = parallel_map( - _full_simulation, - pulse_sim_desc.experiments, - task_args=( - y0, - pulse_sim_desc, - pulse_de_model, - solver_options, - ), - **map_kwargs, - ) - end = time.time() - exp_times = ( - np.ones(len(pulse_sim_desc.experiments)) * (end - start) / len(pulse_sim_desc.experiments) - ) - - return exp_results, exp_times - - -def unitary_evolution(exp, y0, pulse_de_model, solver_options=None): - """ - Calculates evolution when there is no noise, or any measurements that are not at the end - of the experiment. - - Parameters: - exp (dict): dictionary containing experiment description - y0 (array): initial state - pulse_de_model (PulseInternalDEModel): container for de model - solver_options (PulseSimOptions): options - - Returns: - array: results of experiment - - Raises: - Exception: if ODE solving has errors - """ - - solver_options = PulseSimOptions() if solver_options is None else solver_options - - ODE = setup_de_solver(exp, y0, pulse_de_model, solver_options.de_options) - - tlist = exp["tlist"] - - for t in tlist[1:]: - ODE.integrate(t) - if ODE.successful(): - psi = ODE.y / dznrm2(ODE.y) - else: - err_msg = "ODE method exited with status: %s" % ODE.return_code() - raise Exception(err_msg) - - # apply final rotation to come out of rotating frame - psi_rot = np.exp(-1j * pulse_de_model.h_diag_elems * ODE.t) - psi *= psi_rot - - return psi, ODE.t diff --git a/qiskit_aer/pulse/de/DE_Methods.py b/qiskit_aer/pulse/de/DE_Methods.py deleted file mode 100644 index 812cd53618..0000000000 --- a/qiskit_aer/pulse/de/DE_Methods.py +++ /dev/null @@ -1,450 +0,0 @@ -# -*- coding: utf-8 -*- - -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -# This file is part of QuTiP: Quantum Toolbox in Python. -# -# Copyright (c) 2011 and later, Paul D. Nation and Robert J. Johansson. -# All rights reserved. -# pylint: disable=invalid-name, attribute-defined-outside-init - -"""DE methods.""" - -from abc import ABC, abstractmethod -import warnings -import numpy as np -from scipy.integrate import ode, solve_ivp -from scipy.integrate._ode import zvode -from .DE_Options import DE_Options -from .type_utils import StateTypeConverter - - -class ODE_Method(ABC): - """Abstract wrapper class for an ODE solving method, providing an expected interface - for integrating a new method/solver. - - Class Attributes: - method_spec (dict): Container of general information about the method. - Currently supports keys: - - 'inner_state_spec': description of the datatype a solver requires, - with accepted descriptions given in type_utils - - Instance attributes: - _t, t (float): private and public time variable. - _y, y (array): private and public state variable. - rhs (dict): rhs-related functions as values Currently supports key 'rhs'. - """ - - method_spec = {"inner_state_spec": {"type": "array"}} - - def __init__(self, t0=None, y0=None, rhs=None, options=None): - # set_options is first as options may influence the behaviour of other functions - self.set_options(options) - - self._t = t0 - self.set_y(y0, reset=False) - self.set_rhs(rhs) - - # attributes for error reporting - self._successful = True - self._return_code = None - - def integrate_over_interval(self, y0, interval, rhs=None, **kwargs): - """Integrate over an interval, with additional options to reset the rhs functions. - - Args: - y0 (array): state at the start of the interval - interval (tuple or list): initial and start time, e.g. (t0, tf) - rhs (callable or dict): Either the rhs function itself, or a dict of rhs-related - functions. If not given, will use the already-stored rhs. - kwargs (dict): additional keyword arguments for the integrate function of a concrete - method - - Returns: - state: state at the end of the interval - """ - t0 = interval[0] - tf = interval[1] - - self._t = t0 - self.set_y(y0, reset=False) - if rhs is not None: - self.set_rhs(rhs, reset=False) - - self._reset_method() - - self.integrate(tf, **kwargs) - - return self.y - - @property - def t(self): - """Time property.""" - return self._t - - @t.setter - def t(self, new_t): - """Time setter.""" - self._t = new_t - self._reset_method() - - @property - def y(self): - """State property.""" - return self._state_type_converter.inner_to_outer(self._y) - - @y.setter - def y(self, new_y): - """State setter.""" - self.set_y(new_y) - - def set_y(self, new_y, reset=True): - """Method for logic of setting internal state of solver with more control""" - - # instantiate internal StateTypeConverter based on the provided new_y and the - # general type required internally by the solver - type_spec = self.method_spec.get("inner_state_spec") - self._state_type_converter = StateTypeConverter.from_outer_instance_inner_type_spec( - new_y, type_spec - ) - - # set internal state - self._y = self._state_type_converter.outer_to_inner(new_y) - - self._reset_method(reset) - - def set_rhs(self, rhs=None, reset=True): - """Set rhs functions. - - Args: - rhs (dict or callable): Either a dict with callable values, - e.g. {'rhs': f}, or a callable f, which - produces equivalent behaviour as the input {'rhs': f} - reset (bool): Whether or not to reset solver - - Raises: - Exception: if rhs dict is mis-specified - """ - - if rhs is None: - rhs = {"rhs": None} - - if callable(rhs): - rhs = {"rhs": rhs} - - if "rhs" not in rhs: - raise Exception("ODE_Method requires at minimum a specification of an rhs function.") - - # transform rhs function into a function that accepts/returns inner state type - self.rhs = self._state_type_converter.transform_rhs_funcs(rhs) - - self._reset_method(reset) - - def successful(self): - """Return if whether method is successful.""" - return self._successful - - def return_code(self): - """Get return code.""" - return self._return_code - - @abstractmethod - def integrate(self, tf, **kwargs): - """Integrate up to a time tf. - - Args: - tf (float): time to integrate up to - kwargs (dict): key word arguments specific to a given method - """ - pass - - def _reset_method(self, reset=True): - """Reset any parameters of internal numerical solving method, e.g. delete persistent memory - for multi-step methods. - - Args: - reset (bool): Whether or not to reset method - """ - pass - - def set_options(self, options): - """Setup options for the method.""" - pass - - -class ScipyODE(ODE_Method): - """Method wrapper for scipy.integrate.solve_ivp. - - To use: - - Specify a method acceptable by the keyword argument 'method' scipy.integrate.solve_ivp - in DE_Options attribute 'method'. Methods that currently work are: - - 'RK45', 'RK23', and 'BDF' - - Default if not specified is 'RK45' - - Additional notes: - - solve_ivp requires states to be 1d - - Enabling other methods requires adding dtype handling to type_utils for solvers that - do not handle complex types - """ - - method_spec = {"inner_state_spec": {"type": "array", "ndim": 1}} - - def integrate(self, tf, **kwargs): - """Integrate up to a time tf.""" - t0 = self.t - y0 = self._y - rhs = self.rhs.get("rhs") - - # solve problem and silence warnings for options that don't apply to a given method - kept_warnings = [] - with warnings.catch_warnings(record=True) as ws: - results = solve_ivp( - rhs, - (t0, tf), - y0, - method=self.options.method, - atol=self.options.atol, - rtol=self.options.rtol, - max_step=self.options.max_step, - min_step=self.options.min_step, - first_step=self.options.first_step, - **kwargs, - ) - - # update the internal state - self._y = results.y[:, -1] - self._t = results.t[-1] - - # discard warnings for arguments with no effect - for w in ws: - if "The following arguments have no effect" not in str(w.message): - kept_warnings.append(w) - - # display warnings we don't want to silence - for w in kept_warnings: - warnings.warn(w.message, type(w)) - - def set_options(self, options): - # establish method - if options is None: - options = DE_Options() - options.method = "RK45" - else: - options = options.copy() - if "scipy-" in options.method: - options.method = options.method[6:] - - self.options = options - - # handle defaults for None-type arguments - if self.options.max_step is None: - self.options.max_step = np.inf - - -class QiskitZVODE(ODE_Method): - """Wrapper for zvode solver available through Scipy. - - Notes: - - Internally this - """ - - method_spec = {"inner_state_spec": {"type": "array", "ndim": 1}} - - def __init__(self, t0=None, y0=None, rhs=None, options=None): - # all de specification arguments are necessary to instantiate scipy ode object - if (t0 is None) or (y0 is None) or (rhs is None): - raise Exception("QiskitZVODE solver requires t0, y0, and rhs at instantiation.") - - # initialize internal attribute for storing scipy ode object - self._ODE = None - - super().__init__(t0, y0, rhs, options) - - @property - def t(self): - return self._t - - @t.setter - def t(self, new_t): - self._t = new_t - self._ODE.t = new_t - self._reset_method() - - def set_y(self, new_y, reset=True): - """Method for logic of setting internal state of solver with more control""" - type_spec = self.method_spec.get("inner_state_spec") - self._state_type_converter = StateTypeConverter.from_outer_instance_inner_type_spec( - new_y, type_spec - ) - - self._y = self._state_type_converter.outer_to_inner(new_y) - - if self._ODE is not None: - self._ODE._y = self._y - - self._reset_method(reset) - - def set_rhs(self, rhs=None, reset=True): - """This set_rhs function fully instantiates the scipy ode object behind the scenes.""" - - if rhs is None: - rhs = {"rhs": None} - - if callable(rhs): - rhs = {"rhs": rhs} - - if "rhs" not in rhs: - raise Exception("ODE_Method requires at minimum a specification of an rhs function.") - - self.rhs = self._state_type_converter.transform_rhs_funcs(rhs) - - self._ODE = ode(self.rhs["rhs"]) - - self._ODE._integrator = qiskit_zvode( - method=self.options.method, - order=self.options.order, - atol=self.options.atol, - rtol=self.options.rtol, - nsteps=self.options.nsteps, - first_step=self.options.first_step, - min_step=self.options.min_step, - max_step=self.options.max_step, - ) - - # Forces complex ODE solving - if not self._ODE._y: - self._ODE.t = 0.0 - self._ODE._y = np.array([0.0], complex) - self._ODE._integrator.reset(len(self._ODE._y), self._ODE.jac is not None) - - self._ODE.set_initial_value(self._y, self._t) - - self._reset_method(reset) - - def integrate(self, tf, **kwargs): - """Integrate up to a time tf. - - Args: - tf (float): time to integrate up to - kwargs (dict): Supported kwargs: - - 'step': if False, integrates up to tf, if True, only implements a - single step of the solver - """ - - step = kwargs.get("step", False) - - self._ODE.integrate(tf, step=step) - - # update state stored locally - self._y = self._ODE.y - self._t = self._ODE.t - - # update success parameters - self._successful = self._ODE.successful() - self._return_code = self._ODE.get_return_code() - - def _reset_method(self, reset=True): - """Discard internal memory.""" - if reset: - self._ODE._integrator.call_args[3] = 1 - - def set_options(self, options): - # establish method - if options is None: - options = DE_Options(method="adams") - else: - options = options.copy() - if "zvode-" in options.method: - options.method = options.method[6:] - - # handle None-type defaults - if options.first_step is None: - options.first_step = 0 - - if options.max_step is None: - options.max_step = 0 - - if options.min_step is None: - options.min_step = 0 - - self.options = options - - -class qiskit_zvode(zvode): - """Customized ZVODE with modified stepper so that - it always stops at a given time in tlist; - by default, it over shoots the time. - """ - - def step(self, *args): - itask = self.call_args[2] - self.rwork[0] = args[4] - self.call_args[2] = 5 - # pylint: disable=no-value-for-parameter - r = self.run(*args) - self.call_args[2] = itask - return r - - -class RK4(ODE_Method): - """Single-step RK4 solver. Serves as a simple/minimal example of a concrete ODE_Method - subclass. - """ - - def integrate(self, tf, **kwargs): - """Integrate up to a time tf.""" - - delta_t = tf - self.t - steps = int((delta_t // self._max_dt) + 1) - h = delta_t / steps - for _ in range(steps): - self._integration_step(h) - - def _integration_step(self, h): - """Integration step for RK4""" - y0 = self._y - t0 = self._t - rhs = self.rhs.get("rhs") - - k1 = rhs(t0, y0) - t_mid = t0 + (h / 2) - k2 = rhs(t_mid, y0 + (h * k1 / 2)) - k3 = rhs(t_mid, y0 + (h * k2 / 2)) - t_end = t0 + h - k4 = rhs(t_end, y0 + h * k3) - self._y = y0 + (1.0 / 6) * h * (k1 + (2 * k2) + (2 * k3) + k4) - self._t = t_end - - def set_options(self, options): - self._max_dt = options.max_dt - - -def method_from_string(method_str): - """Returns an ODE_Method specified by a string. - - Args: - method_str (str): string specifying method - - Returns: - method: instance of an ODE_Method object - """ - - if "scipy-" in method_str: - return ScipyODE - - if "zvode-" in method_str: - return QiskitZVODE - - method_dict = {"RK4": RK4, "scipy": ScipyODE, "zvode": QiskitZVODE} - - return method_dict.get(method_str) diff --git a/qiskit_aer/pulse/de/DE_Options.py b/qiskit_aer/pulse/de/DE_Options.py deleted file mode 100644 index 5fe1081326..0000000000 --- a/qiskit_aer/pulse/de/DE_Options.py +++ /dev/null @@ -1,88 +0,0 @@ -# -*- coding: utf-8 -*- - -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2019, 2020. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -# pylint: disable=invalid-name - -"""Options for DE_Methods""" - - -class DE_Options: - """ - Container class for options and defaults specific for DE_Methods. Options can be specified - either as arguments to the constructor:: - - opts = DE_Options(order=10, ...) - - or by changing the class attributes after creation:: - - opts = Options() - opts.order = 10 - - Notes: - - Not all options are used by all methods; see each method description for a list of - relevant options. - - Attributes: - method (str, 'zvode-adams'): Integration method. - atol (float, 1e-8): Absolute tolerance for variable step solvers. - rtol (float, 1e-6): Relative tolerance for variable step solvers. - order (int, 12): Order of integrator. - nsteps (int, 10**6): Max. number of internal steps per time interval for variable step - solvers. - first_step (float, None): Size of initial step for variable step solvers. - min_step (float, None): Minimum step size for variable step solvers. - max_step (float, None): Maximum step size for variable step solvers. - max_dt (float, 1e-3): Max step size for fixed step solver. - """ - - def __init__( - self, - method="zvode-adams", - atol=1e-8, - rtol=1e-6, - order=12, - nsteps=10**6, - first_step=None, - max_step=None, - min_step=None, - max_dt=10**-3, - ): - self.method = method - self.atol = atol - self.rtol = rtol - self.order = order - self.nsteps = nsteps - self.first_step = first_step - self.max_step = max_step - self.min_step = min_step - self.max_dt = max_dt - - def copy(self): - """Create a copy of the object.""" - return DE_Options( - method=self.method, - atol=self.atol, - rtol=self.rtol, - order=self.order, - nsteps=self.nsteps, - first_step=self.first_step, - max_step=self.max_step, - min_step=self.min_step, - max_dt=self.max_dt, - ) - - def __str__(self): - return str(vars(self)) - - def __repr__(self): - return self.__str__() diff --git a/qiskit_aer/pulse/de/__init__.py b/qiskit_aer/pulse/de/__init__.py deleted file mode 100644 index badc6b943b..0000000000 --- a/qiskit_aer/pulse/de/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- - -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2019, 2020. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""DE solvers and method interfaces -""" diff --git a/qiskit_aer/pulse/de/type_utils.py b/qiskit_aer/pulse/de/type_utils.py deleted file mode 100644 index e445c81df1..0000000000 --- a/qiskit_aer/pulse/de/type_utils.py +++ /dev/null @@ -1,178 +0,0 @@ -# -*- coding: utf-8 -*- - -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -# pylint: disable=invalid-name - -"""Utilities for type handling/conversion for DE classes. - -A type specification is a dictionary describing a specific expected type, e.g. an array of a given -shape. Currently only handled types are numpy arrays, specified via: - - {'type': 'array', 'shape': tuple} -""" - -import numpy as np - - -class StateTypeConverter: - """Contains descriptions of two type specifications for DE solvers/methods, with functions for - converting states and rhs functions between representations. - - While this class stores exact type specifications, it can be instantiated with a - concrete type and a more general type. This facilitates the situation - in which a solver requires a 1d array, which is specified by the type: - - {'type': 'array', 'ndim': 1} - """ - - def __init__(self, inner_type_spec, outer_type_spec=None): - """Instantiate with the inner and return types for the state. - - Args: - inner_type_spec (dict): inner type - outer_type_spec (dict): outer type - """ - - self.inner_type_spec = inner_type_spec - - self.outer_type_spec = self.inner_type_spec if outer_type_spec is None else outer_type_spec - - @classmethod - def from_instances(cls, inner_y, outer_y=None): - """Instantiate from concrete instances. Type of instances must be supported by - type_spec_from_instance. If outer_y is None the outer type is set to the inner type - - Args: - inner_y (array): concrete representative of inner type - outer_y (array): concrete representative of outer type - - Returns: - StateTypeConverter: type converter as specified by args - """ - inner_type_spec = type_spec_from_instance(inner_y) - - outer_type_spec = None - if outer_y is not None: - outer_type_spec = type_spec_from_instance(outer_y) - - return cls(inner_type_spec, outer_type_spec) - - @classmethod - def from_outer_instance_inner_type_spec(cls, outer_y, inner_type_spec=None): - """Instantiate from concrete instance of the outer type, and an inner type-spec. - The inner type spec can be either be fully specified, or be more general (i.e. to - facilitate the situation in which a solver needs a 1d array). - - Accepted general data types: - - {'type': 'array'} - - {'type': 'array', 'ndim': 1} - - Args: - outer_y (array): concrete outer data type - inner_type_spec (dict): inner, potentially general, type spec - - Returns: - StateTypeConverter: type converter as specified by args - - Raises: - Exception: if inner_type_spec is not properly specified or is not a handled type - """ - - # if no inner_type_spec given just instantiate both inner and outer to the outer_y - if inner_type_spec is None: - return cls.from_instances(outer_y) - - inner_type = inner_type_spec.get("type") - if inner_type is None: - raise Exception("inner_type_spec needs a 'type' key.") - - if inner_type == "array": - outer_y_as_array = np.array(outer_y) - - # if a specific shape is given attempt to instantiate from a reshaped outer_y - shape = inner_type_spec.get("shape") - if shape is not None: - return cls.from_instances(outer_y_as_array.reshape(shape), outer_y) - - # handle the case that ndim == 1 is given - ndim = inner_type_spec.get("ndim") - if ndim == 1: - return cls.from_instances(outer_y_as_array.flatten(), outer_y) - - # if neither shape nor ndim is given, assume it can be an array of any shape - return cls.from_instances(outer_y_as_array, outer_y) - - raise Exception("inner_type_spec not a handled type.") - - def inner_to_outer(self, y): - """Convert a state of inner type to one of outer type.""" - return convert_state(y, self.outer_type_spec) - - def outer_to_inner(self, y): - """Convert a state of outer type to one of inner type.""" - return convert_state(y, self.inner_type_spec) - - def transform_rhs_funcs(self, rhs_funcs): - """Convert RHS funcs passed in a dictionary from functions taking/returning outer type, - to functions taking/returning inner type. - - Currently supports: - - rhs_funcs['rhs'] - standard differential equation rhs function f(t, y) - - Args: - rhs_funcs (dict): contains various rhs functions - - Returns: - dict: transformed rhs funcs - """ - - new_rhs_funcs = {} - - # transform standard rhs function - rhs = rhs_funcs.get("rhs") - - if rhs is not None: - - def new_rhs(t, y): - outer_y = self.inner_to_outer(y) - rhs_val = rhs(t, outer_y) - return self.outer_to_inner(rhs_val) - - new_rhs_funcs["rhs"] = new_rhs - - return new_rhs_funcs - - -def convert_state(y, type_spec): - """Convert the de state y into the type specified by type_spec. Accepted values of type_spec - are given at the beginning of the file.""" - - new_y = None - - if type_spec["type"] == "array": - # default array data type to complex - new_y = np.array(y, dtype=type_spec.get("dtype", "complex")) - - shape = type_spec.get("shape") - if shape is not None: - new_y = new_y.reshape(shape) - - return new_y - - -def type_spec_from_instance(y): - """Determine type spec from an instance.""" - type_spec = {} - if isinstance(y, np.ndarray): - type_spec["type"] = "array" - type_spec["shape"] = y.shape - - return type_spec diff --git a/qiskit_aer/pulse/system_models/__init__.py b/qiskit_aer/pulse/system_models/__init__.py deleted file mode 100644 index 963d7fbcff..0000000000 --- a/qiskit_aer/pulse/system_models/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- - -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2019, 2020. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Models for objects built around tensor product systems. -""" diff --git a/qiskit_aer/pulse/system_models/duffing_model_generators.py b/qiskit_aer/pulse/system_models/duffing_model_generators.py deleted file mode 100644 index fd1cc09039..0000000000 --- a/qiskit_aer/pulse/system_models/duffing_model_generators.py +++ /dev/null @@ -1,478 +0,0 @@ -# -*- coding: utf-8 -*- - -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2019, 2020. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -# pylint: disable=invalid-name - -"Helper functions for creating HamiltonianModel and PulseSystemModel objects" - -from warnings import warn -from collections.abc import Iterable -from qiskit.providers.models.backendconfiguration import UchannelLO -from .hamiltonian_model import HamiltonianModel -from .pulse_system_model import PulseSystemModel - - -def duffing_system_model( - dim_oscillators, oscillator_freqs, anharm_freqs, drive_strengths, coupling_dict, dt -): - r"""Returns a :class:`PulseSystemModel` representing a physical model for a - collection of Duffing oscillators. - - In the model, each individual oscillator is specified by the parameters: - - * Frequency: :math:`\nu`, specified in the list ``oscillator_freqs`` - * Anharmonicity: :math:`\alpha`, specified in the list ``anharm_freqs``, and - * Drive strength: :math:`r`, specified in the list ``drive_strengths``. - - For each oscillator, the above parameters enter into the Hamiltonian via the terms: - - .. math:: - \pi(2 \nu - \alpha)a^\dagger a + - \pi \alpha (a^\dagger a)^2 + 2 \pi r D(t) (a + a^\dagger), - - where :math:`a^\dagger` and :math:`a` are, respectively, the creation and annihilation - operators for the oscillator, and :math:`D(t)` is the drive signal for the oscillator. - - Each coupling term between a pair of oscillators is specified by: - - * Oscillator pair: :math:`(i,k)`, and - * Coupling strength: :math:`j`, - - which are passed in the argument ``coupling_dict``, which is a ``dict`` with keys - being the ``tuple`` ``(i,k)``, and values the strength ``j``. Specifying a coupling - results in the Hamiltonian term: - - .. math:: - 2 \pi j (a_i^\dagger a_k + a_i a_k^\dagger). - - Finally, the returned :class:`PulseSystemModel` is setup for performing cross-resonance - drives between coupled qubits. The index for the :class:`ControlChannel` corresponding - to a particular cross-resonance drive channel is retreived by calling - :meth:`PulseSystemModel.control_channel_index` with the tuple ``(drive_idx, target_idx)``, - where ``drive_idx`` is the index of the oscillator being driven, and ``target_idx`` is - the target oscillator (see example below). - - Note: In this model, all frequencies are in frequency units (as opposed to radial). - - **Example** - - Constructing a three Duffing Oscillator :class:``PulseSystemModel``. - - .. code-block:: python - - # cutoff dimensions - dim_oscillators = 3 - - # single oscillator drift parameters - oscillator_freqs = [5.0e9, 5.1e9, 5.2e9] - anharm_freqs = [-0.33e9, -0.33e9, -0.33e9] - - # drive strengths - drive_strengths = [0.02e9, 0.02e9, 0.02e9] - - # specify coupling as a dictionary; here the qubit pair (0,1) is coupled with - # strength 0.002e9, and the qubit pair (1,2) is coupled with strength 0.001e9 - coupling_dict = {(0,1): 0.002e9, (1,2): 0.001e9} - - # time - dt = 1e-9 - - # create the model - three_qubit_model = duffing_system_model(dim_oscillators=dim_oscillators, - oscillator_freqs=oscillator_freqs, - anharm_freqs=anharm_freqs, - drive_strengths=drive_strengths, - coupling_dict=coupling_dict, - dt=dt) - - In the above model, qubit pairs (0,1) and (1,2) are coupled. To perform a - cross-resonance drive on qubit 1 with target 0, use the :class:`ControlChannel` - with index: - - .. code-block:: python - - three_qubit_model.control_channel_index((1,0)) - - Args: - dim_oscillators (int): Dimension of truncation for each oscillator. - oscillator_freqs (list): Oscillator frequencies in frequency units. - anharm_freqs (list): Anharmonicity values in frequency units. - drive_strengths (list): Drive strength values in frequency units. - coupling_dict (dict): Coupling graph with keys being edges, and values - the coupling strengths in frequency units. - dt (float): Sample width for pulse instructions. - - Returns: - PulseSystemModel: The generated Duffing system model - """ - - # set symbols for string generation - freq_symbol = "v" - anharm_symbol = "alpha" - drive_symbol = "r" - coupling_symbol = "j" - coupling_edges = coupling_dict.keys() - - # construct coupling graph, and raise warning if coupling_edges contains duplicate edges - coupling_graph = CouplingGraph(coupling_edges) - if len(coupling_graph.graph) < len(coupling_edges): - warn( - "Warning: The coupling_dict contains diplicate edges, and the second appearance of \ - the same edge will be ignored." - ) - - # construct the HamiltonianModel - num_oscillators = len(oscillator_freqs) - oscillators = list(range(num_oscillators)) - oscillator_dims = [dim_oscillators] * num_oscillators - freq_symbols = _str_list_generator(freq_symbol + "{0}", oscillators) - anharm_symbols = _str_list_generator(anharm_symbol + "{0}", oscillators) - drive_symbols = _str_list_generator(drive_symbol + "{0}", oscillators) - sorted_coupling_edges = coupling_graph.sorted_graph - # populate coupling strengths in sorted order (vertex indices are now also sorted within edges, - # so this needs to be accounted for when retrieving weights from coupling_dict) - coupling_strengths = [] - for edge in sorted_coupling_edges: - edge_strength = coupling_dict.get(edge) - if edge_strength is None: - edge_strength = coupling_dict.get((edge[1], edge[0])) - coupling_strengths.append(edge_strength) - - coupling_symbols = _str_list_generator(coupling_symbol + "{0}{1}", *zip(*sorted_coupling_edges)) - cr_idx_dict = coupling_graph.two_way_graph_dict - - hamiltonian_dict = _duffing_hamiltonian_dict( - oscillators=oscillators, - oscillator_dims=oscillator_dims, - oscillator_freqs=oscillator_freqs, - freq_symbols=freq_symbols, - anharm_freqs=anharm_freqs, - anharm_symbols=anharm_symbols, - drive_strengths=drive_strengths, - drive_symbols=drive_symbols, - ordered_coupling_edges=sorted_coupling_edges, - coupling_strengths=coupling_strengths, - coupling_symbols=coupling_symbols, - cr_idx_dict=cr_idx_dict, - ) - - hamiltonian_model = HamiltonianModel.from_dict(hamiltonian_dict) - - # construct the u_channel_lo list - u_channel_lo = _cr_lo_list(cr_idx_dict) - - # construct and return the PulseSystemModel - return PulseSystemModel( - hamiltonian=hamiltonian_model, - u_channel_lo=u_channel_lo, - control_channel_labels=coupling_graph.sorted_two_way_graph, - subsystem_list=oscillators, - dt=dt, - ) - - -# Helper functions for creating pieces necessary to construct oscillator system models - - -def _duffing_hamiltonian_dict( - oscillators, - oscillator_dims, - oscillator_freqs, - freq_symbols, - anharm_freqs, - anharm_symbols, - drive_strengths, - drive_symbols, - ordered_coupling_edges, - coupling_strengths, - coupling_symbols, - cr_idx_dict, -): - """Creates a hamiltonian string dict for a duffing oscillator model - - Note, this function makes the following assumptions: - - oscillators, oscillator_dims, oscillator_freqs, freq_symbols, anharm_freqs, - anharm_symbols, drive_strengths, and drive_symbols are all lists of the same length - (i.e. the total oscillator number) - - ordered_coupling_edges, coupling_strengths, and coupling_symbols are lists of the same - length - - Args: - oscillators (list): ints for oscillator labels - oscillator_dims (list): ints for oscillator dimensions - oscillator_freqs (list): oscillator frequencies - freq_symbols (list): symbols to be used for oscillator frequencies - anharm_freqs (list): anharmonicity values - anharm_symbols (list): symbols to be used for anharmonicity terms - drive_strengths (list): drive strength coefficients - drive_symbols (list): symbols for drive coefficients - ordered_coupling_edges (list): tuples of two ints specifying oscillator couplings. Order - corresponds to order of coupling_strengths and - coupling_symbols - coupling_strengths (list): strength of each coupling term (corresponds to ordering of - ordered_coupling_edges) - coupling_symbols (list): symbols for coupling coefficients - cr_idx_dict (dict): A dict with keys given by tuples containing two ints, and value an int, - representing cross resonance drive channels. E.g. an entry {(0,1) : 1} - specifies a CR drive on oscillator 0 with oscillator 1 as target, with - u_channel index 1. - - Returns: - dict: hamiltonian string format - """ - - # single oscillator terms - hamiltonian_str = _single_duffing_drift_terms(freq_symbols, anharm_symbols, oscillators) - hamiltonian_str += _drive_terms(drive_symbols, oscillators) - - # exchange terms - if len(ordered_coupling_edges) > 0: - hamiltonian_str += _exchange_coupling_terms(coupling_symbols, ordered_coupling_edges) - - # cr terms - if len(cr_idx_dict) > 0: - driven_system_indices = [key[0] for key in cr_idx_dict.keys()] - cr_drive_symbols = [drive_symbols[idx] for idx in driven_system_indices] - cr_channel_idx = cr_idx_dict.values() - hamiltonian_str += _cr_terms(cr_drive_symbols, driven_system_indices, cr_channel_idx) - - # construct vars dictionary - var_dict = {} - for idx in oscillators: - var_dict[freq_symbols[idx]] = oscillator_freqs[idx] - var_dict[anharm_symbols[idx]] = anharm_freqs[idx] - var_dict[drive_symbols[idx]] = drive_strengths[idx] - - if len(coupling_symbols) > 0: - for symbol, strength in zip(coupling_symbols, coupling_strengths): - var_dict[symbol] = strength - - dim_dict = {str(oscillator): dim for oscillator, dim in zip(oscillators, oscillator_dims)} - - return {"h_str": hamiltonian_str, "vars": var_dict, "qub": dim_dict} - - -def _cr_lo_list(cr_idx_dict): - """Generates u_channel_lo list for a PulseSystemModel from a cr_idx_dict. - - Args: - cr_idx_dict (dict): A dictionary with keys given by tuples of ints with int values. A key, - e.g. (0,1), signifies CR drive on system 0 with target 1, and the - value is the u channel index corresponding to that drive. - Note: this function assumes that - cr_idx_dict.values() == range(len(cr_idx_dict)). - - Returns: - list: u_channel_lo format required by the simulator - """ - - # populate list of u channel lo for cr gates - lo_list = [0] * len(cr_idx_dict) - for system_pair, u_idx in cr_idx_dict.items(): - lo_list[u_idx] = [UchannelLO(system_pair[1], 1.0 + 0.0j)] - - return lo_list - - -# Functions for creating Hamiltonian strings for various types of terms - - -def _single_duffing_drift_terms(freq_symbols, anharm_symbols, system_list): - """Harmonic and anharmonic drift terms - - Args: - freq_symbols (list): coefficients for harmonic part - anharm_symbols (list): coefficients for anharmonic part - system_list (list): list of system indices - Returns: - list: drift term strings - """ - - harm_terms = _str_list_generator( - "np.pi*(2*{0}-{1})*O{2}", freq_symbols, anharm_symbols, system_list - ) - anharm_terms = _str_list_generator("np.pi*{0}*O{1}*O{1}", anharm_symbols, system_list) - - return harm_terms + anharm_terms - - -def _drive_terms(drive_symbols, system_list): - """Drive terms for single oscillator - - Args: - drive_symbols (list): coefficients of drive terms - system_list (list): list of system indices - Returns: - list: drive term strings - """ - - return _str_list_generator("2*np.pi*{0}*X{1}||D{1}", drive_symbols, system_list) - - -def _exchange_coupling_terms(coupling_symbols, ordered_edges): - """Exchange coupling terms between systems - - Args: - coupling_symbols (list): coefficients of exchange couplings - ordered_edges (list): list tuples of system indices for the couplings - Returns: - list: exchange coupling strings - """ - - idx1_list, idx2_list = zip(*list(ordered_edges)) - - return _str_list_generator( - "2*np.pi*{0}*(Sp{1}*Sm{2}+Sm{1}*Sp{2})", coupling_symbols, idx1_list, idx2_list - ) - - -def _cr_terms(drive_symbols, driven_system_indices, u_channel_indices): - """Cross resonance drive terms - - Args: - drive_symbols (list): coefficients for drive terms - driven_system_indices (list): list of indices for systems that drive is applied to - u_channel_indices (list): indicies for the u_channels corresponding to each term - Returns: - list: cr term strings - """ - - return _str_list_generator( - "2*np.pi*{0}*X{1}||U{2}", drive_symbols, driven_system_indices, u_channel_indices - ) - - -def _str_list_generator(str_template, *args): - """Given a string template, returns a list where each entry is the template formatted by the - zip of args. It is assumed that either args is a tuple of lists each of the same length, or - is a tuple with each entry beign either an str or int. - E.g. - 1. _str_list_generator('First: {0}, Second: {1}', 'a0', 'b0') returns ['First: a0, Second: b0'] - 2. _str_list_generator('First: {0}, Second: {1}', ['a0', 'a1'], ['b0', 'b1']) returns - ['First: a0, Second: b0', 'First: a1, Second: b1'] - - Args: - str_template (str): string template - args (tuple): assumed to be either tuple of iterables of the same length, or a tuple with - entries that are either type str or int - - Returns: - list: list of str_template formated by args lists - """ - - args = [_arg_to_iterable(arg) for arg in args] - return [str_template.format(*zipped_arg) for zipped_arg in zip(*args)] - - -def _arg_to_iterable(arg): - """Check if arg is an iterable, if not put it into a list. The purpose is to allow arguments - of functions to be either lists or singletons, e.g. instead of having to pass ['a'], 'a' can be - passed directly. - - Args: - arg (Iterable): argument to be checked and turned into an interable if necessary - - Returns: - Iterable: either arg, or arg transformed into a list - """ - # catch expected types (issue is str is iterable) - if isinstance(arg, (int, str)): - return [arg] - - if isinstance(arg, Iterable): - return arg - - return [arg] - - -# Helper classes - - -class CouplingGraph: - """ - Helper class containing functionality for representing coupling graphs, with the main goal to - construct different representations for different purposes: - - self.graph: graph as a set of edges stored as frozen sets, e.g. - {frozenset({0,1}), frozenset({1,2}), frozenset({2,3})} - - self.sorted_graph: graph as a list of tuples in lexicographic order, e.g. - [(0,1), (1,2), (2,3)] - Note: these are actively ordered by the object, as the point is to have a canonical - ordering of edges. The integers in the tuples are also ordered. - - self.sorted_two_way_graph: list of tuples where each edge is repeated with the vertices - reversed. The ordering is the same as in sorted_graph, with the duplicate appearing - immediately after the original, e.g. - [(0,1), (1,0), (1,2), (2,1), (2,3), (3,2)] - - self.two_way_graph_dict: same as above, but in dict form, e.g. - {(0,1) : 0, (1,0) : 1, (1,2) : 2, (2,1) : 3, (2,3) : 4, (3,2) : 5} - """ - - def __init__(self, edges): - """returns CouplingGraph object - - Args: - edges (Iterable): An iterable of iterables, where the inner interables are assumed to - contain two elements, e.g. [(0,1), (2,3)], or ((0,1), (2,3)) - - Returns: - CouplingGraph: coupling graph specified by edges - """ - - # create the set representation of the graph - self.graph = {frozenset({idx1, idx2}) for idx1, idx2 in edges} - - # created the sorted list representation - graph_list = [] - for edge in self.graph: - edge_list = list(edge) - edge_list.sort() - graph_list.append(tuple(edge_list)) - - graph_list.sort() - self.sorted_graph = graph_list - - # create the sorted_two_way_graph - two_way_graph_list = [] - for edge in self.sorted_graph: - two_way_graph_list.append(edge) - two_way_graph_list.append((edge[1], edge[0])) - - self.sorted_two_way_graph = two_way_graph_list - - # create the dictionary version - self.two_way_graph_dict = { - self.sorted_two_way_graph[k]: k for k in range(len(self.sorted_two_way_graph)) - } - - def sorted_edge_index(self, edge): - """Given an edge, returns the index in self.sorted_graph. Order in edge does not matter. - - Args: - edge (Iterable): an iterable containing two integers - - Returns: - int: index of edge - """ - edge_list = list(edge) - edge_list.sort() - return self.sorted_graph.index(tuple(edge_list)) - - def two_way_edge_index(self, directed_edge): - """Given a directed edge, returns the index in self.sorted_two_way_graph - - Args: - directed_edge (Iterable): an iterable containing two integers - - Returns: - int: index of directed_edge - """ - return self.two_way_graph_dict[tuple(directed_edge)] diff --git a/qiskit_aer/pulse/system_models/hamiltonian_model.py b/qiskit_aer/pulse/system_models/hamiltonian_model.py deleted file mode 100644 index 90978b3419..0000000000 --- a/qiskit_aer/pulse/system_models/hamiltonian_model.py +++ /dev/null @@ -1,338 +0,0 @@ -# -*- coding: utf-8 -*- - -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2019, 2020. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -# pylint: disable=eval-used, exec-used, invalid-name, missing-return-type-doc - -"HamiltonianModel class for system specification for the PulseSimulator" - -from collections import OrderedDict -import numpy as np -import numpy.linalg as la -from ...aererror import AerError -from .string_model_parser.string_model_parser import HamiltonianParser - - -class HamiltonianModel: - """Hamiltonian model for pulse simulator.""" - - def __init__(self, system=None, variables=None, subsystem_dims=None): - """Initialize a Hamiltonian model. - - Args: - system (list): List of Qobj objects representing operator form of the Hamiltonian. - variables (OrderedDict): Ordered dict for parameter values in Hamiltonian. - subsystem_dims (dict): dict of subsystem dimensions. - - Raises: - AerError: if arguments are invalid. - """ - - # Initialize internal variables - # The system Hamiltonian in numerical format - self._system = system - # System variables - self._variables = variables - # Channels in the Hamiltonian string - # Qubit subspace dimensinos - self._subsystem_dims = subsystem_dims or {} - - # The rest are computed from the previous - - # These tell the order in which the channels are evaluated in - # the RHS solver. - self._channels = None - # Diagonal elements of the hamiltonian - self._h_diag = None - # Eigenvalues of the time-independent hamiltonian - self._evals = None - # Eigenstates of the time-indepedent hamiltonian - self._estates = None - - # populate self._channels - self._calculate_hamiltonian_channels() - - if len(self._channels) == 0: - raise AerError("HamiltonianModel must contain channels to simulate.") - - # populate self._h_diag, self._evals, self._estates - self._compute_drift_data() - - @classmethod - def from_dict(cls, hamiltonian, subsystem_list=None): - """Initialize from a Hamiltonian string specification. - - Args: - hamiltonian (dict): dictionary representing Hamiltonian in string specification. - subsystem_list (list or None): List of subsystems to extract from the hamiltonian. - - Returns: - HamiltonianModel: instantiated from hamiltonian dictionary - - Raises: - ValueError: if arguments are invalid. - """ - - _hamiltonian_pre_parse_exceptions(hamiltonian) - - # get variables - variables = OrderedDict() - if "vars" in hamiltonian: - variables = OrderedDict(hamiltonian["vars"]) - - # Get qubit subspace dimensions - if "qub" in hamiltonian: - if subsystem_list is None: - subsystem_list = [int(qubit) for qubit in hamiltonian["qub"]] - else: - # if user supplied, make a copy and sort it - subsystem_list = subsystem_list.copy() - subsystem_list.sort() - - # force keys in hamiltonian['qub'] to be ints - qub_dict = {int(key): val for key, val in hamiltonian["qub"].items()} - - subsystem_dims = {int(qubit): qub_dict[int(qubit)] for qubit in subsystem_list} - else: - subsystem_dims = {} - - # Get oscillator subspace dimensions - if "osc" in hamiltonian: - oscillator_dims = {int(key): val for key, val in hamiltonian["osc"].items()} - else: - oscillator_dims = {} - - # Parse the Hamiltonian - system = HamiltonianParser( - h_str=hamiltonian["h_str"], dim_osc=oscillator_dims, dim_qub=subsystem_dims - ) - system.parse(subsystem_list) - system = system.compiled - - return cls(system, variables, subsystem_dims) - - def get_qubit_lo_from_drift(self): - """Computes a list of qubit frequencies corresponding to the exact energy - gap between the ground and first excited states of each qubit. - - If the keys in self._subsystem_dims skips over a qubit, it will default to outputting - a 0 frequency for that qubit. - - Returns: - qubit_lo_freq (list): the list of frequencies - """ - # need to specify frequencies up to max qubit index - qubit_lo_freq = [0] * (max(self._subsystem_dims.keys()) + 1) - - # compute difference between first excited state of each qubit and - # the ground energy - min_eval = np.min(self._evals) - for q_idx in self._subsystem_dims.keys(): - single_excite = _first_excited_state(q_idx, self._subsystem_dims) - dressed_eval = _eval_for_max_espace_overlap(single_excite, self._evals, self._estates) - qubit_lo_freq[q_idx] = (dressed_eval - min_eval) / (2 * np.pi) - - return qubit_lo_freq - - def _calculate_hamiltonian_channels(self): - """Get all the qubit channels D_i and U_i in the string - representation of a system Hamiltonian. - - Raises: - Exception: Missing index on channel. - """ - channels = [] - for _, ham_str in self._system: - chan_idx = [i for i, letter in enumerate(ham_str) if letter in ["D", "U"]] - for ch in chan_idx: - if (ch + 1) == len(ham_str) or not ham_str[ch + 1].isdigit(): - raise Exception("Channel name must include" + "an integer labeling the qubit.") - for kk in chan_idx: - done = False - offset = 0 - while not done: - offset += 1 - if not ham_str[kk + offset].isdigit(): - done = True - # In case we hit the end of the string - elif (kk + offset + 1) == len(ham_str): - done = True - offset += 1 - temp_chan = ham_str[kk : kk + offset] - if temp_chan not in channels: - channels.append(temp_chan) - channels.sort(key=lambda x: (int(x[1:]), x[0])) - - channel_dict = OrderedDict() - for idx, val in enumerate(channels): - channel_dict[val] = idx - - self._channels = channel_dict - - def _compute_drift_data(self): - """Calculate the the drift Hamiltonian. - - This computes the dressed frequencies and eigenstates of the - diagonal part of the Hamiltonian. - - Raises: - Exception: Missing index on channel. - """ - # Get the diagonal elements of the hamiltonian with all the - # drive terms set to zero - for chan in self._channels: - exec("%s=0" % chan) - - # might be a better solution to replace the 'var' in the hamiltonian - # string with 'op_system.vars[var]' - for var in self._variables: - exec("%s=%f" % (var, self._variables[var])) - - full_dim = np.prod(list(self._subsystem_dims.values())) - - ham_full = np.zeros((full_dim, full_dim), dtype=complex) - for ham_part in self._system: - ham_full += ham_part[0].data * eval(ham_part[1]) - # Remap eigenvalues and eigenstates - evals, estates = la.eigh(ham_full) - - evals_mapped = np.zeros(evals.shape, dtype=evals.dtype) - estates_mapped = np.zeros(estates.shape, dtype=estates.dtype) - - # order the eigenvalues and eigenstates according to overlap with computational basis - pos_list = [] - min_overlap = 1 - for i, estate in enumerate(estates.T): - # make a copy and set entries with indices in pos_list to 0 - estate_copy = estate.copy() - estate_copy[pos_list] = 0 - - pos = np.argmax(np.abs(estate_copy)) - pos_list.append(pos) - min_overlap = min(np.abs(estate_copy)[pos] ** 2, min_overlap) - - evals_mapped[pos] = evals[i] - estates_mapped[:, pos] = estate - - self._evals = evals_mapped - self._estates = estates_mapped - self._h_diag = np.ascontiguousarray(np.diag(ham_full).real) - - -def _hamiltonian_pre_parse_exceptions(hamiltonian): - """Raises exceptions for hamiltonian specification. - - Parameters: - hamiltonian (dict): dictionary specification of hamiltonian - Returns: - Raises: - AerError: if some part of the hamiltonian dictionary is unsupported - """ - - ham_str = hamiltonian.get("h_str", []) - if ham_str in ([], [""]): - raise AerError("Hamiltonian dict requires a non-empty 'h_str' entry.") - - if hamiltonian.get("qub", {}) == {}: - raise AerError("Hamiltonian dict requires non-empty 'qub' entry with subsystem dimensions.") - - if hamiltonian.get("osc", {}) != {}: - raise AerError("Oscillator-type systems are not supported.") - - -def _first_excited_state(qubit_idx, subsystem_dims): - """ - Returns the vector corresponding to all qubits in the 0 state, except for - qubit_idx in the 1 state. - - Parameters: - qubit_idx (int): the qubit to be in the 1 state - - subsystem_dims (dict): a dictionary with keys being subsystem index, and - value being the dimension of the subsystem - - Returns: - vector: the state with qubit_idx in state 1, and the rest in state 0 - """ - vector = np.array([1.0]) - # iterate through qubits, tensoring on the state - qubit_indices = [int(qubit) for qubit in subsystem_dims] - qubit_indices.sort() - for idx in qubit_indices: - new_vec = np.zeros(subsystem_dims[idx]) - if idx == qubit_idx: - new_vec[1] = 1 - else: - new_vec[0] = 1 - vector = np.kron(new_vec, vector) - - return vector - - -def _eval_for_max_espace_overlap(u, evals, evecs, decimals=14): - """Return the eigenvalue for eigenvector closest to input. - - Given an eigenvalue decomposition evals, evecs, as output from - get_diag_hamiltonian, returns the eigenvalue from evals corresponding - to the eigenspace that the vector vec has the maximum overlap with. - - Args: - u (numpy.array): the vector of interest - evals (numpy.array): list of eigenvalues - evecs (numpy.array): eigenvectors corresponding to evals - decimals (int): rounding option, to try to handle numerical - error if two evals should be the same but are - slightly different - - Returns: - complex: eigenvalue corresponding to eigenspace for which vec has - maximal overlap. - """ - # get unique evals (with rounding for numerical error) - rounded_evals = evals.copy().round(decimals=decimals) - unique_evals = np.unique(rounded_evals) - - # compute overlaps to unique evals - overlaps = np.zeros(len(unique_evals)) - for idx, val in enumerate(unique_evals): - overlaps[idx] = _proj_norm(evecs[:, val == rounded_evals], u) - - # return eval with largest overlap - return unique_evals[np.argmax(overlaps)] - - -def _proj_norm(mat, vec): - """ - Compute the projection form of a vector an matrix. - - Given a matrix ``mat`` and vector ``vec``, computes the norm of the - projection of ``vec`` onto the column space of ``mat`` using least - squares. - - Note: ``mat`` can also be specified as a 1d numpy.array, in which - case it will convert it into a matrix with one column - - Parameters: - mat (numpy.array): 2d array, a matrix. - vec (numpy.array): 1d array, a vector. - - Returns: - complex: the norm of the projection - """ - - # if specified as a single vector, turn it into a column vector - if mat.ndim == 1: - mat = np.array([mat]).T - - lsq_vec = la.lstsq(mat, vec, rcond=None)[0] - - return la.norm(mat @ lsq_vec) diff --git a/qiskit_aer/pulse/system_models/pulse_system_model.py b/qiskit_aer/pulse/system_models/pulse_system_model.py deleted file mode 100644 index c4741d991f..0000000000 --- a/qiskit_aer/pulse/system_models/pulse_system_model.py +++ /dev/null @@ -1,214 +0,0 @@ -# -*- coding: utf-8 -*- - -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2019, 2020. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -# pylint: disable=eval-used, exec-used, invalid-name - -"System Model class for system specification for the PulseSimulator" - -from warnings import warn -from collections import OrderedDict -from qiskit.providers import Backend -from ...aererror import AerError -from .hamiltonian_model import HamiltonianModel - - -class PulseSystemModel: - r"""Physical model object for pulse simulator. - - This class contains model information required by the - :class:`~qiskit_aer.PulseSimulator`. It contains: - - * ``"hamiltonian"``: a :class:`HamiltonianModel` object representing the - Hamiltonian of the system. - * ``"u_channel_lo"``: A description of :class:`ControlChannel` local oscillator - frequencies in terms of qubit local oscillator frequencies. - * ``"control_channel_labels"``: Optional list of identifying information for - each :class:`ControlChannel` that the model supports. - * ``"subsystem_list"``: List of subsystems in the model. - * ``"dt"``: Sample width size for OpenPulse instructions. - - A model can be instantiated from the helper function :func:`duffing_system_model`, - or using the :meth:`PulseSystemModel.from_backend` constructor. - - **Example** - - Constructing from a backend: - - .. code-block: python - - provider = IBMQ.load_account() - armonk_backend = provider.get_backend('ibmq_armonk') - - system_model = PulseSystemModel.from_backend(armonk_backend) - """ - - def __init__( - self, - hamiltonian=None, - u_channel_lo=None, - control_channel_labels=None, - subsystem_list=None, - dt=None, - ): - """Initialize a PulseSystemModel. - - Args: - hamiltonian (HamiltonianModel): The Hamiltonian of the system. - u_channel_lo (list): list of ControlChannel frequency specifications. - control_channel_labels (list): list of labels for control channels, which can be of - any type. - subsystem_list (list): list of valid qubit indicies for the model. - dt (float): pixel size for pulse Instructions. - Raises: - AerError: if hamiltonian is not None or a HamiltonianModel - """ - - # necessary values - if hamiltonian is not None and not isinstance(hamiltonian, HamiltonianModel): - raise AerError("hamiltonian must be a HamiltonianModel object") - self.hamiltonian = hamiltonian - self.u_channel_lo = u_channel_lo - self.control_channel_labels = control_channel_labels or [] - self.subsystem_list = subsystem_list - self.dt = dt - - @classmethod - def from_backend(cls, backend, subsystem_list=None): - """Returns a PulseSystemModel constructed from an OpenPulse enabled backend object. - - Args: - backend (Backend): backend object to draw information from. - subsystem_list (list): a list of ints for which qubits to include in the model. - - Returns: - PulseSystemModel: the PulseSystemModel constructed from the backend. - - Raises: - AerError: If channel or u_channel_lo are invalid. - """ - - if not isinstance(backend, Backend): - raise AerError("{} is not a Qiskit backend".format(backend)) - - # get relevant information from backend - config = backend.configuration() - - if not config.open_pulse: - raise AerError("{} is not an open pulse backend".format(backend)) - - return cls.from_config(config, subsystem_list) - - @classmethod - def from_config(cls, configuration, subsystem_list=None): - """Construct a model from configuration and defaults.""" - - # draw from configuration - # if no subsystem_list, use all for device - subsystem_list = subsystem_list or list(range(configuration.n_qubits)) - ham_string = configuration.hamiltonian - hamiltonian = HamiltonianModel.from_dict(ham_string, subsystem_list) - u_channel_lo = getattr(configuration, "u_channel_lo", None) - dt = getattr(configuration, "dt", None) - - control_channel_labels = [None] * len(u_channel_lo) - # populate control_channel_dict - # for now it assumes the u channel drives a single qubit, and we return the index - # of the driven qubit, along with the frequency description - if u_channel_lo is not None: - for u_idx, u_lo in enumerate(u_channel_lo): - # find drive index - drive_idx = None - while drive_idx is None: - u_str_label = "U{0}".format(str(u_idx)) - for h_term_str in ham_string["h_str"]: - # check if this string corresponds to this u channel - if u_str_label in h_term_str: - # get index of X operator drive term - x_idx = h_term_str.find("X") - # if 'X' is found, and is not at the end of the string, drive_idx - # is the subsequent character - if x_idx != -1 and x_idx + 1 < len(h_term_str): - drive_idx = int(h_term_str[x_idx + 1]) - - if drive_idx is not None: - # construct string for u channel - u_string = "" - for u_term_dict in u_lo: - scale = getattr(u_term_dict, "scale", [1.0, 0]) - q_idx = getattr(u_term_dict, "q") - if len(u_string) > 0: - u_string += " + " - if isinstance(scale, complex): - u_string += str(scale) + "q" + str(q_idx) - else: - u_string += str(scale[0] + scale[1] * 1j) + "q" + str(q_idx) - control_channel_labels[u_idx] = {"driven_q": drive_idx, "freq": u_string} - - return cls( - hamiltonian=hamiltonian, - u_channel_lo=u_channel_lo, - control_channel_labels=control_channel_labels, - subsystem_list=subsystem_list, - dt=dt, - ) - - def control_channel_index(self, label): - """Return the index of the control channel with identifying label. - - Args: - label (Any): label that identifies a control channel - - Returns: - int or None: index of the ControlChannel - """ - if label not in self.control_channel_labels: - warn("There is no listed ControlChannel matching the provided label.") - return None - else: - return self.control_channel_labels.index(label) - - def calculate_channel_frequencies(self, qubit_lo_freq=None): - """Calculate frequencies for each channel given qubit_lo_freq. - - Args: - qubit_lo_freq (list or None): list of qubit linear - oscillator drive frequencies. - - Returns: - OrderedDict: a dictionary of channel frequencies. - - Raises: - ValueError: If channel or u_channel_lo are invalid. - """ - if not qubit_lo_freq: - raise ValueError("qubit_lo_freq is a required function parameter.") - - if self.u_channel_lo is None: - raise ValueError("{} has no u_channel_lo.".format(self.__class__.__name__)) - - # Setup freqs for the channels - freqs = OrderedDict() - for key in self.hamiltonian._channels: - chidx = int(key[1:]) - if key[0] == "D": - freqs[key] = qubit_lo_freq[chidx] - elif key[0] == "U": - freqs[key] = 0 - for u_lo_idx in self.u_channel_lo[chidx]: - if u_lo_idx.q < len(qubit_lo_freq): - qfreq = qubit_lo_freq[u_lo_idx.q] - qscale = u_lo_idx.scale.real - freqs[key] += qfreq * qscale - else: - raise ValueError("Channel is not D or U") - return freqs diff --git a/qiskit_aer/pulse/system_models/string_model_parser/__init__.py b/qiskit_aer/pulse/system_models/string_model_parser/__init__.py deleted file mode 100644 index f4d9ece0bc..0000000000 --- a/qiskit_aer/pulse/system_models/string_model_parser/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- - -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2019, 2020. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Folder for string model parsers. -""" diff --git a/qiskit_aer/pulse/system_models/string_model_parser/apply_str_func_to_qobj.py b/qiskit_aer/pulse/system_models/string_model_parser/apply_str_func_to_qobj.py deleted file mode 100644 index 4e4646fc26..0000000000 --- a/qiskit_aer/pulse/system_models/string_model_parser/apply_str_func_to_qobj.py +++ /dev/null @@ -1,32 +0,0 @@ -# -*- coding: utf-8 -*- - -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2019, 2020. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -""" -Functions for applying scalar functions in __fundict to the operators -represented in qutip Qobj. -""" - - -def dag(qobj): - """Qiskit wrapper of adjoint""" - return qobj.dag() - - -def apply_func(name, qobj): - """Apply function of given name, or do nothing if func not found""" - return __funcdict.get(name, lambda x: x)(qobj) - - -# pylint: disable=invalid-name -__funcdict = {"dag": dag} diff --git a/qiskit_aer/pulse/system_models/string_model_parser/gen_operator.py b/qiskit_aer/pulse/system_models/string_model_parser/gen_operator.py deleted file mode 100644 index 9c957ec486..0000000000 --- a/qiskit_aer/pulse/system_models/string_model_parser/gen_operator.py +++ /dev/null @@ -1,141 +0,0 @@ -# -*- coding: utf-8 -*- - -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2019, 2020. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -""" -Functions to construct terra operators -""" - -import numpy as np - -from qiskit.quantum_info.operators.operator import Operator - - -def _create_destroy(dim, creation=True): - a_x, b_x = (1, 0) if creation else (0, 1) - data_op = np.zeros((dim, dim)) - for i in range(dim - 1): - data_op[i + a_x, i + b_x] = np.sqrt(i + 1) - return Operator(data_op) - - -def create(dim): - """Creation operator. - Args: - dim (int): Dimension of Hilbert space. - Returns: - Operator: Operator representation of creation operator. - """ - return _create_destroy(dim, creation=True) - - -def destroy(dim): - """Destruction operator. - Args: - dim (int): Dimension of Hilbert space. - Returns: - Operator: Operator representation of destruction operator. - """ - return _create_destroy(dim, creation=False) - - -def num(dim): - """Operator representation for number operator. - Args: - dim (int): The dimension of the Hilbert space. - Returns: - Operator: Operator representation for number operator. - """ - return Operator(np.diag(np.arange(dim))) - - -def sigmax(): - """Operator representation for Pauli spin 1/2 sigma-x operator. - Returns: - Operator: Operator representation for sigma x. - """ - return Operator.from_label("X") - - -def sigmay(): - """Operator representation for Pauli spin 1/2 sigma-y operator. - Returns: - Operator: Operator representation for sigma y. - """ - return Operator.from_label("Y") - - -def sigmaz(): - """Operator representation for Pauli spin 1/2 sigma-z operator. - Returns: - Operator: Operator representation for sigma z. - """ - return Operator.from_label("Z") - - -def identity(dim): - """Identity operator. - Args: - dim (int): Dimension of Hilbert space. - Returns: - Operator: Operator representation of identity operator. - """ - return Operator(np.identity(dim, dtype=np.complex128)) - - -def basis(dim, n=0): - """Vector representation of a Fock state. - Args: - dim (int): Number of Fock states in Hilbert space. - n (int): Integer corresponding to desired number state, default value is 0. - Returns: - Operator: Opertor representing the requested number state ``|n>``. - """ - data_op = np.zeros((dim, 1)) - data_op[n] = 1 - return Operator(data_op) - - -def tensor(op_list): - """Tensor product of input operators. - Args: - op_list (array_like): List or array of Operators objects for tensor product. - Returns: - Operator: Operator representation of tensor product. - """ - ret = op_list[0] - for op in op_list[1:]: - ret = ret.tensor(op) - return ret - - -def state(state_vector): - """Operator representation of state vector. - Args: - state_vector (array_like): array representing the state-vector. - Returns: - Operator: State vector operator representation. - """ - return Operator(state_vector.reshape(1, state_vector.shape[0])) - - -def fock_dm(dim, n=0): - """Density matrix representation of a Fock state - Args: - dim (int): Number of Fock states in Hilbert space. - n (int): Desired number state, defaults to 0 if omitted. - Returns: - Operator: Density matrix Operator representation of Fock state. - """ - psi = basis(dim, n) - return psi.adjoint() & psi diff --git a/qiskit_aer/pulse/system_models/string_model_parser/operator_from_string.py b/qiskit_aer/pulse/system_models/string_model_parser/operator_from_string.py deleted file mode 100644 index 16a8509cf3..0000000000 --- a/qiskit_aer/pulse/system_models/string_model_parser/operator_from_string.py +++ /dev/null @@ -1,77 +0,0 @@ -# -*- coding: utf-8 -*- - -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2019, 2020. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -# pylint: disable=invalid-name - -"""Module for creating quantum operators.""" - -from . import operator_generators as op_gen - - -def gen_oper(opname, index, h_osc, h_qub, states=None): - """Generate quantum operators. - - Args: - opname (str): Name of the operator to be returned. - index (int): Index of operator. - h_osc (dict): Dimension of oscillator subspace - h_qub (dict): Dimension of qubit subspace - states (tuple): State indices of projection operator. - - Returns: - Qobj: quantum operator for target qubit. - """ - - opr_tmp = None - - # get number of levels in Hilbert space - if opname in ["X", "Y", "Z", "Sp", "Sm", "I", "O", "P"]: - is_qubit = True - dim = h_qub.get(index, 2) - - if opname in ["X", "Y", "Z"] and dim > 2: - if opname == "X": - opr_tmp = op_gen.get_oper("A", dim) + op_gen.get_oper("C", dim) - elif opname == "Y": - opr_tmp = -1j * op_gen.get_oper("A", dim) + 1j * op_gen.get_oper("C", dim) - else: - opr_tmp = op_gen.get_oper("I", dim) - 2 * op_gen.get_oper("N", dim) - - else: - is_qubit = False - dim = h_osc.get(index, 5) - - if opname == "P": - opr_tmp = op_gen.get_oper(opname, dim, states) - else: - if opr_tmp is None: - opr_tmp = op_gen.get_oper(opname, dim) - - # reverse sort by index - rev_h_osc = sorted(h_osc.items(), key=lambda x: x[0])[::-1] - rev_h_qub = sorted(h_qub.items(), key=lambda x: x[0])[::-1] - - # osc_n * … * osc_0 * qubit_n * … * qubit_0 - opers = [] - for ii, dd in rev_h_osc: - if ii == index and not is_qubit: - opers.append(opr_tmp) - else: - opers.append(op_gen.qeye(dd)) - for ii, dd in rev_h_qub: - if ii == index and is_qubit: - opers.append(opr_tmp) - else: - opers.append(op_gen.qeye(dd)) - - return op_gen.tensor(opers) diff --git a/qiskit_aer/pulse/system_models/string_model_parser/operator_generators.py b/qiskit_aer/pulse/system_models/string_model_parser/operator_generators.py deleted file mode 100644 index 755100d146..0000000000 --- a/qiskit_aer/pulse/system_models/string_model_parser/operator_generators.py +++ /dev/null @@ -1,163 +0,0 @@ -# -*- coding: utf-8 -*- - -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2019, 2020. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -# pylint: disable=invalid-name, no-name-in-module, import-error - -"""Operators to use in simulator""" - -import numpy as np -from qiskit.quantum_info import Operator -from . import gen_operator - - -def sigmax(dim=2): - """Qiskit wrapper of sigma-X operator.""" - if dim == 2: - return gen_operator.sigmax() - else: - raise Exception("Invalid level specification of the qubit subspace") - - -def sigmay(dim=2): - """Qiskit wrapper of sigma-Y operator.""" - if dim == 2: - return gen_operator.sigmay() - else: - raise Exception("Invalid level specification of the qubit subspace") - - -def sigmaz(dim=2): - """Qiskit wrapper of sigma-Z operator.""" - if dim == 2: - return gen_operator.sigmaz() - else: - raise Exception("Invalid level specification of the qubit subspace") - - -def sigmap(dim=2): - """Qiskit wrapper of sigma-plus operator.""" - return gen_operator.create(dim) - - -def sigmam(dim=2): - """Qiskit wrapper of sigma-minus operator.""" - return gen_operator.destroy(dim) - - -def create(dim): - """Qiskit wrapper of creation operator.""" - return gen_operator.create(dim) - - -def destroy(dim): - """Qiskit wrapper of annihilation operator.""" - return gen_operator.destroy(dim) - - -def num(dim): - """Qiskit wrapper of number operator.""" - return gen_operator.num(dim) - - -def qeye(dim): - """Qiskit wrapper of identity operator.""" - return gen_operator.identity(dim) - - -def project(dim, states): - """Qiskit wrapper of projection operator.""" - ket, bra = states - if ket in range(dim) and bra in range(dim): - return gen_operator.basis(dim, ket) * gen_operator.basis(dim, bra).adjoint() - else: - raise Exception("States are specified on the outside of Hilbert space %s" % states) - - -def tensor(list_qobj): - """Qiskit wrapper of tensor product""" - return gen_operator.tensor(list_qobj) - - -def basis(level, pos): - """Qiskit wrapper of basis""" - return gen_operator.basis(level, pos) - - -def state(state_vec): - """Qiskit wrapper of qobj""" - return gen_operator.state(state_vec) - - -def fock_dm(level, eigv): - """Qiskit wrapper of fock_dm""" - return gen_operator.fock_dm(level, eigv) - - -def qubit_occ_oper_dressed(target_qubit, estates, h_osc, h_qub, level=0): - """Builds the occupation number operator for a target qubit - in a qubit oscillator system, where the oscillator are the first - subsystems, and the qubit last. This does it for a dressed systems - assuming estates has the same ordering - - Args: - target_qubit (int): Qubit for which operator is built. - estates (list): eigenstates in the dressed frame - h_osc (dict): Dict of number of levels in each oscillator. - h_qub (dict): Dict of number of levels in each qubit system. - level (int): Level of qubit system to be measured. - - Returns: - Qobj: Occupation number operator for target qubit. - """ - # reverse sort by index - rev_h_osc = sorted(h_osc.items(), key=lambda x: x[0])[::-1] - rev_h_qub = sorted(h_qub.items(), key=lambda x: x[0])[::-1] - - # osc_n * … * osc_0 * qubit_n * … * qubit_0 - states = [] - proj_op = 0 * fock_dm(len(estates), 0) - for ii, dd in rev_h_osc: - states.append(basis(dd, 0)) - for ii, dd in rev_h_qub: - if ii == target_qubit: - states.append(Operator(basis(dd, level).data.flatten())) - else: - states.append(Operator(np.ones(dd))) - - out_state = tensor(states) - - for ii, estate in enumerate(estates): - if out_state.data.flatten()[ii] == 1: - proj_op += estate & estate.adjoint() - - return proj_op - - -def get_oper(name, *args): - """Return quantum operator of given name""" - return __operdict.get(name, qeye)(*args) - - -__operdict = { - "X": sigmax, - "Y": sigmay, - "Z": sigmaz, - "Sp": create, - "Sm": destroy, - "I": qeye, - "O": num, - "P": project, - "A": destroy, - "C": create, - "N": num, -} diff --git a/qiskit_aer/pulse/system_models/string_model_parser/string_model_parser.py b/qiskit_aer/pulse/system_models/string_model_parser/string_model_parser.py deleted file mode 100644 index 7b3337fca1..0000000000 --- a/qiskit_aer/pulse/system_models/string_model_parser/string_model_parser.py +++ /dev/null @@ -1,422 +0,0 @@ -# -*- coding: utf-8 -*- - -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2019, 2020. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -# pylint: disable=invalid-name - -"""Parser for the string specification of Hamiltonians and noise models""" - -import re -import copy -from collections import namedtuple, OrderedDict -import numpy as np -from qiskit.quantum_info import Operator -from .apply_str_func_to_qobj import apply_func -from .operator_from_string import gen_oper - - -Token = namedtuple("Token", ("type", "name")) - -ham_elements = OrderedDict( - QubOpr=re.compile(r"(?PO|Sp|Sm|X|Y|Z|I)(?P[0-9]+)"), - PrjOpr=re.compile(r"P(?P[0-9]+),(?P[0-9]+),(?P[0-9]+)"), - CavOpr=re.compile(r"(?PA|C|N)(?P[0-9]+)"), - Func=re.compile(r"(?P[a-z]+)\("), - Ext=re.compile(r"\.(?Pdag)"), - Var=re.compile(r"[a-z]+[0-9]*"), - Num=re.compile(r"[0-9.]+"), - MathOrd0=re.compile(r"[*/]"), - MathOrd1=re.compile(r"[+-]"), - BrkL=re.compile(r"\("), - BrkR=re.compile(r"\)"), -) - - -class HamiltonianParser: - """Generate QuTip hamiltonian object from string""" - - def __init__(self, h_str, dim_osc, dim_qub): - """Create new quantum operator generator - - Parameters: - h_str (list): list of Hamiltonian string - dim_osc (dict): dimension of oscillator subspace - dim_qub (dict): dimension of qubit subspace - """ - self.h_str = h_str - self.dim_osc = dim_osc - self.dim_qub = dim_qub - self.__td_hams = [] - self.__tc_hams = [] - self.__str2qopr = {} - - @property - def compiled(self): - """Return Hamiltonian in OpenPulse handler format""" - return self.__tc_hams + self.__td_hams - - def parse(self, qubit_list=None): - """Parse and generate quantum class object""" - self.__td_hams = [] - self.__tc_hams = [] - - # expand sum - self._expand_sum() - - # convert to reverse Polish notation - for ham in self.h_str: - if len(re.findall(r"\|\|", ham)) > 1: - raise Exception("Multiple time-dependent terms in %s" % ham) - p_td = re.search(r"(?P[\S]+)\|\|(?P[\S]+)", ham) - - # find time-dependent term - if p_td: - coef, token = self._tokenizer(p_td.group("opr"), qubit_list) - if token is None: - continue - # combine coefficient to time-dependent term - if coef: - td = "*".join([coef, p_td.group("ch")]) - else: - td = p_td.group("ch") - token = self._shunting_yard(token) - _td = self._token2qobj(token), td - - self.__td_hams.append(_td) - else: - coef, token = self._tokenizer(ham, qubit_list) - if token is None: - continue - token = self._shunting_yard(token) - - if (coef == "") or (coef is None): - coef = "1." - - _tc = self._token2qobj(token), coef - - self.__tc_hams.append(_tc) - - def _expand_sum(self): - """Takes a string-based Hamiltonian list and expands the _SUM action items out.""" - sum_str = re.compile(r"_SUM\[(?P[a-z]),(?P[a-z\d{}+-]+),(?P[a-z\d{}+-]+),") - brk_str = re.compile(r"]") - - ham_list = copy.copy(self.h_str) - ham_out = [] - - while any(ham_list): - ham = ham_list.pop(0) - p_sums = list(sum_str.finditer(ham)) - p_brks = list(brk_str.finditer(ham)) - if len(p_sums) != len(p_brks): - raise Exception("Missing correct number of brackets in %s" % ham) - - # find correct sum-bracket correspondence - if any(p_sums) == 0: - ham_out.append(ham) - else: - itr = p_sums[0].group("itr") - _l = int(p_sums[0].group("l")) - _u = int(p_sums[0].group("u")) - for ii in range(len(p_sums) - 1): - if p_sums[ii + 1].end() > p_brks[ii].start(): - break - else: - ii = len(p_sums) - 1 - - # substitute iterator value - _temp = [] - for kk in range(_l, _u + 1): - trg_s = ham[p_sums[0].end() : p_brks[ii].start()] - # generate replacement pattern - pattern = {} - for p in re.finditer(r"\{(?P[a-z0-9*/+-]+)\}", trg_s): - if p.group() not in pattern: - sub = parse_binop(p.group("op_str"), operands={itr: str(kk)}) - if sub.isdecimal(): - pattern[p.group()] = sub - else: - pattern[p.group()] = "{%s}" % sub - for key, val in pattern.items(): - trg_s = trg_s.replace(key, val) - _temp.append( - "".join([ham[: p_sums[0].start()], trg_s, ham[p_brks[ii].end() :]]) - ) - ham_list.extend(_temp) - - self.h_str = ham_out - - return ham_out - - def _tokenizer(self, op_str, qubit_list=None): - """Convert string to token and coefficient - Check if the index is in qubit_list - """ - - # generate token - _op_str = copy.copy(op_str) - token_list = [] - prev = "none" - while any(_op_str): - for key, parser in ham_elements.items(): - p = parser.match(_op_str) - if p: - # find quantum operators - if key in ["QubOpr", "CavOpr"]: - _key = key - _name = p.group() - if p.group() not in self.__str2qopr.keys(): - idx = int(p.group("idx")) - if qubit_list is not None and idx not in qubit_list: - return 0, None - name = p.group("opr") - opr = gen_oper(name, idx, self.dim_osc, self.dim_qub) - self.__str2qopr[p.group()] = opr - elif key == "PrjOpr": - _key = key - _name = p.group() - if p.group() not in self.__str2qopr.keys(): - idx = int(p.group("idx")) - name = "P" - lvs = int(p.group("ket")), int(p.group("bra")) - opr = gen_oper(name, idx, self.dim_osc, self.dim_qub, lvs) - self.__str2qopr[p.group()] = opr - elif key in ["Func", "Ext"]: - _name = p.group("name") - _key = key - elif key == "MathOrd1": - _name = p.group() - if prev not in ["QubOpr", "PrjOpr", "CavOpr", "Var", "Num"]: - _key = "MathUnitary" - else: - _key = key - else: - _name = p.group() - _key = key - token_list.append(Token(_key, _name)) - _op_str = _op_str[p.end() :] - prev = _key - break - else: - raise Exception("Invalid input string %s is found" % op_str) - - # split coefficient - coef = "" - if any([k.type == "Var" for k in token_list]): - for ii, _ in enumerate(token_list): - if token_list[ii].name == "*": - if all([k.type != "Var" for k in token_list[ii + 1 :]]): - coef = "".join([k.name for k in token_list[:ii]]) - token_list = token_list[ii + 1 :] - break - else: - raise Exception("Invalid order of operators and coefficients in %s" % op_str) - - return coef, token_list - - def _shunting_yard(self, token_list): - """Reformat token to reverse Polish notation""" - stack = [] - queue = [] - while any(token_list): - token = token_list.pop(0) - if token.type in ["QubOpr", "PrjOpr", "CavOpr", "Num"]: - queue.append(token) - elif token.type in ["Func", "Ext"]: - stack.append(token) - elif token.type in ["MathUnitary", "MathOrd0", "MathOrd1"]: - while stack and math_priority(token, stack[-1]): - queue.append(stack.pop(-1)) - stack.append(token) - elif token.type in ["BrkL"]: - stack.append(token) - elif token.type in ["BrkR"]: - while stack[-1].type not in ["BrkL", "Func"]: - queue.append(stack.pop(-1)) - if not any(stack): - raise Exception("Missing correct number of brackets") - pop = stack.pop(-1) - if pop.type == "Func": - queue.append(pop) - else: - raise Exception("Invalid token %s is found" % token.name) - - while any(stack): - queue.append(stack.pop(-1)) - - return queue - - def _token2qobj(self, tokens): - """Generate quantum class object from tokens""" - stack = [] - for token in tokens: - if token.type in ["QubOpr", "PrjOpr", "CavOpr"]: - stack.append(self.__str2qopr[token.name]) - elif token.type == "Num": - stack.append(float(token.name)) - elif token.type in ["MathUnitary"]: - if token.name == "-": - stack.append(-stack.pop(-1)) - elif token.type in ["MathOrd0", "MathOrd1"]: - op2 = stack.pop(-1) - op1 = stack.pop(-1) - if token.name == "+": - stack.append(op1 + op2) - elif token.name == "-": - stack.append(op1 - op2) - elif token.name == "*": - if isinstance(op1, Operator) and isinstance(op2, Operator): - stack.append(op1 & op2) - else: - stack.append(op1 * op2) - elif token.name == "/": - stack.append(op1 / op2) - elif token.type in ["Func", "Ext"]: - stack.append(apply_func(token.name, stack.pop(-1))) - else: - raise Exception("Invalid token %s is found" % token.name) - - if len(stack) > 1: - raise Exception("Invalid mathematical operation in " % tokens) - - return stack[0] - - -class NoiseParser: - """Generate QuTip noise object from dictionary - Qubit noise is given in the format of nested dictionary: - "qubit": { - "0": { - "Sm": 0.006 - } - } - and oscillator noise is given in the format of nested dictionary: - "oscillator": { - "n_th": { - "0": 0.001 - }, - "coupling": { - "0": 0.05 - } - } - these configurations are combined in the same dictionary - """ - - def __init__(self, noise_dict, dim_osc, dim_qub): - """Create new quantum operator generator - - Parameters: - noise_dict (dict): dictionary of noise configuration - dim_osc (dict): dimension of oscillator subspace - dim_qub (dict): dimension of qubit subspace - """ - self.noise_osc = noise_dict.get("oscillator", {"n_th": {}, "coupling": {}}) - self.noise_qub = noise_dict.get("qubit", {}) - self.dim_osc = dim_osc - self.dim_qub = dim_qub - self.__c_list = [] - - @property - def compiled(self): - """Return noise configuration in OpenPulse handler format""" - return self.__c_list - - def parse(self): - """Parse and generate quantum class object""" - # Qubit noise - for index, config in self.noise_qub.items(): - for opname, coef in config.items(): - # TODO: support noise in multi-dimensional system - # TODO: support noise with math operation - if opname in ["X", "Y", "Z", "Sp", "Sm"]: - opr = gen_oper(opname, int(index), self.dim_osc, self.dim_qub) - else: - raise Exception("Unsupported noise operator %s is given" % opname) - self.__c_list.append(np.sqrt(coef) * opr) - # Oscillator noise - ndic = self.noise_osc["n_th"] - cdic = self.noise_osc["coupling"] - for (n_ii, n_coef), (c_ii, c_coef) in zip(ndic.items(), cdic.items()): - if n_ii == c_ii: - if c_coef > 0: - opr = gen_oper("A", int(n_ii), self.dim_osc, self.dim_qub) - if n_coef: - self.__c_list.append(np.sqrt(c_coef * (1 + n_coef)) * opr) - self.__c_list.append(np.sqrt(c_coef * n_coef) * opr.dag()) - else: - self.__c_list.append(np.sqrt(c_coef) * opr) - else: - raise Exception("Invalid oscillator index in noise dictionary.") - - -def math_priority(o1, o2): - """Check priority of given math operation""" - rank = {"MathUnitary": 2, "MathOrd0": 1, "MathOrd1": 0} - diff_ops = rank.get(o1.type, -1) - rank.get(o2.type, -1) - - if diff_ops > 0: - return False - else: - return True - - -# pylint: disable=dangerous-default-value -def parse_binop(op_str, operands={}, cast_str=True): - """Calculate binary operation in string format""" - oprs = OrderedDict( - sum=r"(?P[a-zA-Z0-9]+)\+(?P[a-zA-Z0-9]+)", - sub=r"(?P[a-zA-Z0-9]+)\-(?P[a-zA-Z0-9]+)", - mul=r"(?P[a-zA-Z0-9]+)\*(?P[a-zA-Z0-9]+)", - div=r"(?P[a-zA-Z0-9]+)\/(?P[a-zA-Z0-9]+)", - non=r"(?P[a-zA-Z0-9]+)", - ) - - for key, regr in oprs.items(): - p = re.match(regr, op_str) - if p: - val0 = operands.get(p.group("v0"), p.group("v0")) - if key == "non": - # substitution - retv = val0 - else: - val1 = operands.get(p.group("v1"), p.group("v1")) - # binary operation - if key == "sum": - if val0.isdecimal() and val1.isdecimal(): - retv = int(val0) + int(val1) - else: - retv = "+".join([str(val0), str(val1)]) - elif key == "sub": - if val0.isdecimal() and val1.isdecimal(): - retv = int(val0) - int(val1) - else: - retv = "-".join([str(val0), str(val1)]) - elif key == "mul": - if val0.isdecimal() and val1.isdecimal(): - retv = int(val0) * int(val1) - else: - retv = "*".join([str(val0), str(val1)]) - elif key == "div": - if val0.isdecimal() and val1.isdecimal(): - retv = int(val0) / int(val1) - else: - retv = "/".join([str(val0), str(val1)]) - else: - retv = 0 - break - else: - raise Exception("Invalid string %s" % op_str) - - if cast_str: - return str(retv) - else: - return retv diff --git a/qiskit_aer/quantum_info/states/aer_densitymatrix.py b/qiskit_aer/quantum_info/states/aer_densitymatrix.py index 6bc4ce45c4..f9c5090562 100644 --- a/qiskit_aer/quantum_info/states/aer_densitymatrix.py +++ b/qiskit_aer/quantum_info/states/aer_densitymatrix.py @@ -40,7 +40,7 @@ def __init__(self, data, dims=None, **configs): AerDensityMatrix or QuantumCircuit or qiskit.circuit.Instruction): Data from which the densitymatrix can be constructed. This can be either a complex vector, another densitymatrix or statevector or a ``QuantumCircuit`` or - ``Instruction`` (``Operator`` is not supportted in the current implementation). + ``Instruction`` (``Operator`` is not supported in the current implementation). If the data is a circuit or instruction, the densitymatrix is constructed by assuming that all qubits are initialized to the zero state. dims (int or tuple or list): Optional. The subsystem dimension of diff --git a/qiskit_aer/quantum_info/states/aer_statevector.py b/qiskit_aer/quantum_info/states/aer_statevector.py index f5b91d322d..b243dd2d4f 100644 --- a/qiskit_aer/quantum_info/states/aer_statevector.py +++ b/qiskit_aer/quantum_info/states/aer_statevector.py @@ -40,7 +40,7 @@ def __init__(self, data, dims=None, **configs): qiskit.circuit.Instruction): Data from which the statevector can be constructed. This can be either a complex vector, another statevector or a ``QuantumCircuit`` or ``Instruction`` - (``Operator`` is not supportted in the current implementation). If the data is + (``Operator`` is not supported in the current implementation). If the data is a circuit or instruction, the statevector is constructed by assuming that all qubits are initialized to the zero state. dims (int or tuple or list): Optional. The subsystem dimension of diff --git a/releasenotes/notes/avoid_kernel_crash_in_mac_from_blas_error-bd5b836a23f2e3ee.yaml b/releasenotes/notes/avoid_kernel_crash_in_mac_from_blas_error-bd5b836a23f2e3ee.yaml new file mode 100644 index 0000000000..11a772b091 --- /dev/null +++ b/releasenotes/notes/avoid_kernel_crash_in_mac_from_blas_error-bd5b836a23f2e3ee.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + When BLAS calls are failed, because omp threads do not handle exceptions, + Aer crashes without any error messages. This fix is for omp threads to catch + exceptions correctly and then rethrow them outside of omp loops. diff --git a/releasenotes/notes/check_param_length-eb69cd92825bbca4.yaml b/releasenotes/notes/check_param_length-eb69cd92825bbca4.yaml new file mode 100644 index 0000000000..41c475c67c --- /dev/null +++ b/releasenotes/notes/check_param_length-eb69cd92825bbca4.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Previously, parameters for gates are not validate in C++. If parameters are shorter than + expected (due to custom gate), segmentaion faults are thrown. This commit adds checks + whether parameter lenght is expceted. This commit will fix issues reported in #1612. + https://github.com/Qiskit/qiskit-aer/issues/1612 diff --git a/releasenotes/notes/check_parameter_binds_exist-9d52c665d5f94dde.yaml b/releasenotes/notes/check_parameter_binds_exist-9d52c665d5f94dde.yaml new file mode 100644 index 0000000000..2b61f5281f --- /dev/null +++ b/releasenotes/notes/check_parameter_binds_exist-9d52c665d5f94dde.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Since 0.12.0, parameter values in circuits are temporarily replaced with constant values + and parameter values are assigned in C++ library. Therefore, if `parameter_binds` is specified, + simulator returns results with the constnat values as paramter values. With this commit, + Aer raises an error if `parameter_binds` is not specified though circuits have parameters. diff --git a/releasenotes/notes/do_not_modify_metadata-60bb4b88707bd021.yaml b/releasenotes/notes/do_not_modify_metadata-60bb4b88707bd021.yaml new file mode 100644 index 0000000000..c0886bef97 --- /dev/null +++ b/releasenotes/notes/do_not_modify_metadata-60bb4b88707bd021.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + Previously :class:`~.AerSimulator` modifies circuit metadata to maintain + consistency between input and output of simulation with side effect of + unexpected view of metadata from applicatiln in simiulation. This fix + avoids using circuit metadata to maintain consistency internaly and then + always provides consistent view of metadata to application. diff --git a/releasenotes/notes/estimator-performance-da83a59b9fd69086.yaml b/releasenotes/notes/estimator-performance-da83a59b9fd69086.yaml new file mode 100644 index 0000000000..3e94a3f0ba --- /dev/null +++ b/releasenotes/notes/estimator-performance-da83a59b9fd69086.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - | + Improved performance when the same circuits and multiple parameters are passed to + :class:`~.Estimator` with ``approximation=True``. diff --git a/releasenotes/notes/fix-none-handling-in-noise-model-34fcc9a3e3cbdf6f.yaml b/releasenotes/notes/fix-none-handling-in-noise-model-34fcc9a3e3cbdf6f.yaml new file mode 100644 index 0000000000..e8069c3cee --- /dev/null +++ b/releasenotes/notes/fix-none-handling-in-noise-model-34fcc9a3e3cbdf6f.yaml @@ -0,0 +1,10 @@ +--- +fixes: + - | + Fixed a bug in :meth:`~.NoiseModel.from_backend` that raised an error when + the backend has no T1 and T2 values (i.e. None) for a qubit in its qubit properties. + This commit updates :meth:`NoiseModel.from_backend` and :func:`basic_device_gate_errors` + so that they add an identity ``QuantumError`` (i.e. effectively no thermal relaxation error) + to a qubit with no T1 and T2 values for all gates acting on qubits including the qubit. + Fixed `#1779 `__ + and `#1815 `__. diff --git a/releasenotes/notes/fix-number-qubits-a417ca6afa64264f.yaml b/releasenotes/notes/fix-number-qubits-a417ca6afa64264f.yaml new file mode 100644 index 0000000000..760a5b788d --- /dev/null +++ b/releasenotes/notes/fix-number-qubits-a417ca6afa64264f.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Fix an issue even if the number of qubits is set by a coupling map + or device's configuration, when the simulation method is configured, + the number of qubits is overwritten in accordance with the method. + Fixed `#1769 `__ \ No newline at end of file diff --git a/releasenotes/notes/fix_aer_state_initialize_api-0c2c237a606648ef.yaml b/releasenotes/notes/fix_aer_state_initialize_api-0c2c237a606648ef.yaml new file mode 100644 index 0000000000..3b2436a491 --- /dev/null +++ b/releasenotes/notes/fix_aer_state_initialize_api-0c2c237a606648ef.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + A function ``aer_state_initialize()`` in C API wrongly takes no argument though + it initializes a state created by ``aer_state()``. Example codes pass ``handler`` + and rouboustness of C compiler allows its compilation. This commit corrects for + ``aer_state_initialize()`` to take an argument ``handler`` to be initialized. \ No newline at end of file diff --git a/releasenotes/notes/fix_gpu_binary-1b5b162dff76060d.yaml b/releasenotes/notes/fix_gpu_binary-1b5b162dff76060d.yaml new file mode 100644 index 0000000000..2634dc1b53 --- /dev/null +++ b/releasenotes/notes/fix_gpu_binary-1b5b162dff76060d.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fixing binary distribution package for GPU support, adding some missing + dynamic link pathes for CUDA and cuQuantum libraries. + By this fix, Qiskit Aer can be executed without setting LD_LIBRARY_PATH. diff --git a/releasenotes/notes/fix_omp_nested-a554de2e7fd2a2d6.yaml b/releasenotes/notes/fix_omp_nested-a554de2e7fd2a2d6.yaml new file mode 100644 index 0000000000..8672a4c4b5 --- /dev/null +++ b/releasenotes/notes/fix_omp_nested-a554de2e7fd2a2d6.yaml @@ -0,0 +1,11 @@ +--- +fixes: + - | + OpenMP parallel nested was not correctly set when number of input circuits + is less than number of threads. + Also parallel state update was not correctly set if number of input circuits + is more than 1. + This release fixes these settings to get more speed up with OpenMP. + For single circuit with multiple-shots run, when nested parallel is used + `omp_nested=True` is set in the metadata of result for a circuit. + diff --git a/releasenotes/notes/fix_parameter_indexing-f29f19568270d002.yaml b/releasenotes/notes/fix_parameter_indexing-f29f19568270d002.yaml new file mode 100644 index 0000000000..d09bb7500a --- /dev/null +++ b/releasenotes/notes/fix_parameter_indexing-f29f19568270d002.yaml @@ -0,0 +1,14 @@ +--- +fixes: + - | + If a circuit has conditional and parameters, the circuit was not be + correctly simulated because parameter bindings of Aer used wrong positions + to apply parameters. This is from a lack of consideration of bfunc operations + injected by conditional. With this commit, parameters are set to correct + positions with consideration of injected bfun operations. + - | + Parameters for global phases were not correctly set in #1814. + https://github.com/Qiskit/qiskit-aer/pull/1814 + Parameter values for global phases were copied to a template circuit and not to + actual circuits to be simulated. This commit correctly copies parameter values + to circuits to be simulated. diff --git a/releasenotes/notes/fix_qobj_run-8ea657a93ce9acd2.yaml b/releasenotes/notes/fix_qobj_run-8ea657a93ce9acd2.yaml new file mode 100644 index 0000000000..615dfd366e --- /dev/null +++ b/releasenotes/notes/fix_qobj_run-8ea657a93ce9acd2.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Aer still supports Qobj as an argument of :meth:`~.AerSimulator.run` though + it was deprecated. However, since 0.12.0, it always fails if no ``run_options`` + is specified. This fix enables simulation of Qobj without ``run_options``. diff --git a/releasenotes/notes/fix_required_memory_mb-7aeafa0fe553b85a.yaml b/releasenotes/notes/fix_required_memory_mb-7aeafa0fe553b85a.yaml new file mode 100644 index 0000000000..47d8f4d99c --- /dev/null +++ b/releasenotes/notes/fix_required_memory_mb-7aeafa0fe553b85a.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + requried_memory_mb function for statevector returns wrong value when number + of qubits is very large because of overflow of 64 bits integer. + Now it returns SIZE_MAX value when number of qubits is too large so that + Qiskit Aer can know memory is not sufficient for + statvector/unitary/density matrix methods. diff --git a/releasenotes/notes/primitives-grouping-index-bug-56f69afbdc3e86a0.yaml b/releasenotes/notes/primitives-grouping-index-bug-56f69afbdc3e86a0.yaml new file mode 100644 index 0000000000..109f2aac84 --- /dev/null +++ b/releasenotes/notes/primitives-grouping-index-bug-56f69afbdc3e86a0.yaml @@ -0,0 +1,5 @@ +--- +issues: + - | + Fix a bug that returns wrong expectation values in :class:`~Estimator` when + ``abelian_grouping=True``. diff --git a/releasenotes/notes/remove_aer_circuit_from_metadata-e4fe09029c1a3a3c.yaml b/releasenotes/notes/remove_aer_circuit_from_metadata-e4fe09029c1a3a3c.yaml new file mode 100644 index 0000000000..9d5a5bb0ee --- /dev/null +++ b/releasenotes/notes/remove_aer_circuit_from_metadata-e4fe09029c1a3a3c.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Results of ``backend.run()`` were not serializable because they include :class:`.AerCircuit`\ s. + This commit makes the results serializable by removing :class:`.AerCircuit`\ s from metadata. diff --git a/releasenotes/notes/remove_pulse_simulator-f8de2f6d380f446a.yaml b/releasenotes/notes/remove_pulse_simulator-f8de2f6d380f446a.yaml new file mode 100644 index 0000000000..09bec11a6c --- /dev/null +++ b/releasenotes/notes/remove_pulse_simulator-f8de2f6d380f446a.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - | + Qiskit Aer 0.13 has dropped support for ``PulseSimulator`` following deprecation warnings started in + Qiskit Aer 0.12. Use Qiskit Dynamics to run pulse-level simulation. + https://qiskit.org/ecosystem/dynamics/ diff --git a/releasenotes/notes/renew_gpu_binaries-2cf3eba0853b8407.yaml b/releasenotes/notes/renew_gpu_binaries-2cf3eba0853b8407.yaml new file mode 100644 index 0000000000..0f7ea2e44d --- /dev/null +++ b/releasenotes/notes/renew_gpu_binaries-2cf3eba0853b8407.yaml @@ -0,0 +1,19 @@ +--- +upgrade: + - | + Qiskit Aer now requires CUDA version for GPU simulator to 11.2 or + higher. Previously, CUDA 10.1 was the minimum supported version. + This change was necessary because of changes in the upstream CUDA + ecosystem, including cuQuantum support. To support users running + with different versions of CUDA there is now a separate package available + for running with CUDA 11: ``qiskit-aer-gpu-cu11`` and using the + ``qiskit-aer-gpu`` package now requires CUDA 12. If you're an existing + user of the ``qiskit-aer-gpu`` package and want to use CUDA 11 + you will need to run:: + + pip uninstall qiskit-aer-gpu && pip install -U qiskit-aer-gpu-cu11 + + to go from the previously CUDA 10.x compatible ``qiskit-aer-gpu`` + package's releases to upgrade to the new CUDA 11 compatible + package. If you're running CUDA 12 locally already you can upgrade + the ``qiskit-aer-gpu`` package as normal. diff --git a/releasenotes/notes/save_statevector_for_qasm3_circ-642ade99af3ff0d2.yaml b/releasenotes/notes/save_statevector_for_qasm3_circ-642ade99af3ff0d2.yaml new file mode 100644 index 0000000000..2db81d60ff --- /dev/null +++ b/releasenotes/notes/save_statevector_for_qasm3_circ-642ade99af3ff0d2.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + :meth:``QuantumCircuit.save_statevector()`` does not work if the circuit + is generated from OpenQASM3 text because its quantum registers have duplicated + qubit instances. With this commit, :meth:``QuantumCircuit.save_statevector()`` + uses :data:``QuantumCircuit.qubits`` to get qubits to be saved. diff --git a/releasenotes/notes/support_int_initialize-8491979c4a003908.yaml b/releasenotes/notes/support_int_initialize-8491979c4a003908.yaml new file mode 100644 index 0000000000..ad45dfcbc4 --- /dev/null +++ b/releasenotes/notes/support_int_initialize-8491979c4a003908.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + :meth:``QuantumCircuit.initialize()`` with `int` value was not processed + correctly as reported in `#1821 `. + This commit enables such initialization by decomposing initialize instructions. diff --git a/releasenotes/notes/support_param_for_global_phase-704a97129e7bdbaa.yaml b/releasenotes/notes/support_param_for_global_phase-704a97129e7bdbaa.yaml new file mode 100644 index 0000000000..f4fdaa4241 --- /dev/null +++ b/releasenotes/notes/support_param_for_global_phase-704a97129e7bdbaa.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + :class:`~qiskit.circuit.QuantumCircuit` supports parameterization for its `global_phase`. + However, Aer has not allowed such parameterization and failed when transpiler generates + parameterized global phases. This commit supports parameterization of `global_phase` and + resolve issues related to https://github.com/Qiskit/qiskit-aer/issues/1795, + https://github.com/Qiskit/qiskit-aer/issues/1781, and https://github.com/Qiskit/qiskit-aer/issues/1798. diff --git a/releasenotes/notes/support_u3_runtime_api-42f013f111c319ff.yaml b/releasenotes/notes/support_u3_runtime_api-42f013f111c319ff.yaml new file mode 100644 index 0000000000..72dec64c0b --- /dev/null +++ b/releasenotes/notes/support_u3_runtime_api-42f013f111c319ff.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + `aer_apply_u3` is added to `aer_runtime_api.h` diff --git a/requirements-dev.txt b/requirements-dev.txt index 1ea832f6d7..d1a45eda8b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -13,6 +13,7 @@ reno>=3.4.0 ddt>=1.2.0,!=1.4.0 matplotlib>=3.3 seaborn>=0.9.0 -qiskit_sphinx_theme>=1.10 +qiskit_sphinx_theme~=1.12.0 +sphinx-design>=0.2.0 nbsphinx - +qiskit_qasm3_import diff --git a/setup.py b/setup.py index d35d076c23..5e9285c208 100644 --- a/setup.py +++ b/setup.py @@ -9,8 +9,8 @@ import setuptools from skbuild import setup - PACKAGE_NAME = os.getenv("QISKIT_AER_PACKAGE_NAME", "qiskit-aer") +CUDA_MAJOR = os.getenv("QISKIT_AER_CUDA_MAJOR", "12") extras_requirements = {"dask": ["dask", "distributed"]} @@ -20,6 +20,50 @@ "scipy>=1.0", ] +classifiers = [ + "Environment :: Console", + "License :: OSI Approved :: Apache Software License", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "Operating System :: Microsoft :: Windows", + "Operating System :: MacOS", + "Operating System :: POSIX :: Linux", + "Programming Language :: C++", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Topic :: Scientific/Engineering", +] + +if "gpu" in PACKAGE_NAME: + if "11" in CUDA_MAJOR: + requirements_cuda = [ + "nvidia-cuda-runtime-cu11>=11.8.89", + "nvidia-cublas-cu11>=11.11.3.6", + "nvidia-cusolver-cu11>=11.4.1.48", + "nvidia-cusparse-cu11>=11.7.5.86", + "cuquantum-cu11>=23.3.0", + ] + classifiers_cuda = [ + "Environment :: GPU :: NVIDIA CUDA :: 11", + ] + else: + requirements_cuda = [ + "nvidia-cuda-runtime-cu12>=12.1.105", + "nvidia-cublas-cu12>=12.1.3.1", + "nvidia-cusolver-cu12>=11.4.5.107", + "nvidia-cusparse-cu12>=12.1.0.106", + "cuquantum-cu12>=23.3.0", + ] + classifiers_cuda = [ + "Environment :: GPU :: NVIDIA CUDA :: 12", + ] + requirements.extend(requirements_cuda) + classifiers.extend(classifiers_cuda) + VERSION_PATH = os.path.join(os.path.dirname(__file__), "qiskit_aer", "VERSION.txt") with open(VERSION_PATH, "r") as version_file: VERSION = version_file.read().strip() @@ -46,23 +90,7 @@ author="AER Development Team", author_email="hello@qiskit.org", license="Apache 2.0", - classifiers=[ - "Environment :: Console", - "License :: OSI Approved :: Apache Software License", - "Intended Audience :: Developers", - "Intended Audience :: Science/Research", - "Operating System :: Microsoft :: Windows", - "Operating System :: MacOS", - "Operating System :: POSIX :: Linux", - "Programming Language :: C++", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Topic :: Scientific/Engineering", - ], + classifiers=classifiers, python_requires=">=3.7", install_requires=requirements, include_package_data=False, diff --git a/src/controllers/aer_controller.hpp b/src/controllers/aer_controller.hpp index 7a6988268c..2a45353dcd 100644 --- a/src/controllers/aer_controller.hpp +++ b/src/controllers/aer_controller.hpp @@ -85,8 +85,8 @@ class Controller { template Result execute(const inputdata_t &qobj); - Result execute(std::vector &circuits, Noise::NoiseModel &noise_model, - const Config &config); + Result execute(std::vector> &circuits, + Noise::NoiseModel &noise_model, const Config &config); //----------------------------------------------------------------------- // Config settings @@ -262,7 +262,7 @@ class Controller { // The noise model will be modified to enable superop or kraus sampling // methods if required by the chosen methods. std::vector - simulation_methods(std::vector &circuits, + simulation_methods(std::vector> &circuits, Noise::NoiseModel &noise_model) const; // Return the simulation method to use based on the input circuit @@ -297,9 +297,9 @@ class Controller { void clear_parallelization(); // Set parallelization for experiments - void set_parallelization_experiments(const std::vector &circuits, - const Noise::NoiseModel &noise, - const std::vector &methods); + void set_parallelization_experiments( + const std::vector> &circuits, + const Noise::NoiseModel &noise, const std::vector &methods); // Set circuit parallelization void set_parallelization_circuit(const Circuit &circ, @@ -573,15 +573,15 @@ void Controller::clear_parallelization() { } void Controller::set_parallelization_experiments( - const std::vector &circuits, const Noise::NoiseModel &noise, - const std::vector &methods) { + const std::vector> &circuits, + const Noise::NoiseModel &noise, const std::vector &methods) { std::vector required_memory_mb_list(circuits.size()); max_qubits_ = 0; for (size_t j = 0; j < circuits.size(); j++) { - if (circuits[j].num_qubits > max_qubits_) - max_qubits_ = circuits[j].num_qubits; + if (circuits[j]->num_qubits > max_qubits_) + max_qubits_ = circuits[j]->num_qubits; required_memory_mb_list[j] = - required_memory_mb(circuits[j], noise, methods[j]); + required_memory_mb(*circuits[j], noise, methods[j]); } std::sort(required_memory_mb_list.begin(), required_memory_mb_list.end(), std::greater<>()); @@ -897,7 +897,7 @@ Result Controller::execute(const inputdata_t &input_qobj) { // Experiment execution //------------------------------------------------------------------------- -Result Controller::execute(std::vector &circuits, +Result Controller::execute(std::vector> &circuits, Noise::NoiseModel &noise_model, const Config &config) { // Start QOBJ timer @@ -919,8 +919,8 @@ Result Controller::execute(std::vector &circuits, // check if multi-chunk distribution is required bool multi_chunk_required_ = false; for (size_t j = 0; j < circuits.size(); j++) { - if (circuits[j].num_qubits > 0) { - if (multiple_chunk_required(circuits[j], noise_model, methods[j])) + if (circuits[j]->num_qubits > 0) { + if (multiple_chunk_required(*circuits[j], noise_model, methods[j])) multi_chunk_required_ = true; } } @@ -963,9 +963,9 @@ Result Controller::execute(std::vector &circuits, // nested should be set to zero if num_threads clause will be used #if _OPENMP >= 200805 - omp_set_max_active_levels(0); + omp_set_max_active_levels(2); #else - omp_set_nested(0); + omp_set_nested(1); #endif result.metadata.add(parallel_nested_, "omp_nested"); @@ -981,11 +981,11 @@ Result Controller::execute(std::vector &circuits, reg_t seeds(circuits.size()); reg_t avg_seeds(circuits.size()); for (int_t i = 0; i < circuits.size(); i++) - seeds[i] = circuits[i].seed; + seeds[i] = circuits[i]->seed; MPI_Allreduce(seeds.data(), avg_seeds.data(), circuits.size(), MPI_UINT64_T, MPI_SUM, MPI_COMM_WORLD); for (int_t i = 0; i < circuits.size(); i++) - circuits[i].seed = avg_seeds[i] / num_processes_; + circuits[i]->seed = avg_seeds[i] / num_processes_; } #endif @@ -995,14 +995,15 @@ Result Controller::execute(std::vector &circuits, // in #pragma omp) if (parallel_experiments_ == 1) { for (int j = 0; j < NUM_RESULTS; ++j) { - set_parallelization_circuit(circuits[j], noise_model, methods[j]); - run_circuit(circuits[j], noise_model, methods[j], config, + set_parallelization_circuit(*circuits[j], noise_model, methods[j]); + run_circuit(*circuits[j], noise_model, methods[j], config, result.results[j]); } } else { #pragma omp parallel for num_threads(parallel_experiments_) for (int j = 0; j < NUM_RESULTS; ++j) { - run_circuit(circuits[j], noise_model, methods[j], config, + set_parallelization_circuit(*circuits[j], noise_model, methods[j]); + run_circuit(*circuits[j], noise_model, methods[j], config, result.results[j]); } } @@ -1408,6 +1409,10 @@ void Controller::run_circuit_helper(const Circuit &circ, result.seed = circ.seed; result.metadata.add(parallel_shots_, "parallel_shots"); result.metadata.add(parallel_state_update_, "parallel_state_update"); + if (parallel_shots_ > 1 && parallel_state_update_ > 1) + result.metadata.add(true, "omp_nested"); + else + result.metadata.add(false, "omp_nested"); // Add timer data auto timer_stop = myclock_t::now(); // stop timer @@ -1844,7 +1849,7 @@ void Controller::measure_sampler(InputIterator first_meas, //------------------------------------------------------------------------- std::vector -Controller::simulation_methods(std::vector &circuits, +Controller::simulation_methods(std::vector> &circuits, Noise::NoiseModel &noise_model) const { // Does noise model contain kraus noise bool kraus_noise = @@ -1856,7 +1861,8 @@ Controller::simulation_methods(std::vector &circuits, std::vector sim_methods; bool superop_enabled = false; bool kraus_enabled = false; - for (const auto &circ : circuits) { + for (const auto &_circ : circuits) { + const auto circ = *_circ; auto method = automatic_simulation_method(circ, noise_model); sim_methods.push_back(method); if (!superop_enabled && @@ -1886,7 +1892,7 @@ Controller::simulation_methods(std::vector &circuits, } else if (method_ == Method::tensor_network) { bool has_save_statevec = false; for (const auto &circ : circuits) { - has_save_statevec |= has_statevector_ops(circ); + has_save_statevec |= has_statevector_ops(*circ); if (has_save_statevec) break; } diff --git a/src/controllers/controller_execute.hpp b/src/controllers/controller_execute.hpp index 46ffcdc81a..4c2015461f 100644 --- a/src/controllers/controller_execute.hpp +++ b/src/controllers/controller_execute.hpp @@ -45,7 +45,7 @@ Result controller_execute(const inputdata_t &qobj) { } template -Result controller_execute(std::vector &input_circs, +Result controller_execute(std::vector> &input_circs, AER::Noise::NoiseModel &noise_model, AER::Config &config) { controller_t controller; @@ -65,7 +65,7 @@ Result controller_execute(std::vector &input_circs, // i is the instruction index in the experiment // j is the param index in the instruction // pars = [par0, par1, ...] is a list of different parameterizations - using pos_t = std::pair; + using pos_t = std::pair; using exp_params_t = std::vector>>; std::vector param_table = config.param_table; @@ -75,7 +75,8 @@ Result controller_execute(std::vector &input_circs, R"(Invalid parameterized circuits: "parameterizations" length does not match number of circuits.)"); } - std::vector circs; + std::vector> circs; + std::vector> template_circs; try { // Load circuits @@ -83,39 +84,48 @@ Result controller_execute(std::vector &input_circs, auto &circ = input_circs[i]; if (param_table.empty() || param_table[i].empty()) { // Non parameterized circuit - circ.set_params(truncate); - circ.set_metadata(config, truncate); + circ->set_params(truncate); + circ->set_metadata(config, truncate); circs.push_back(circ); + template_circs.push_back(circ); } else { // Get base circuit without truncation - circ.set_params(false); - circ.set_metadata(config, truncate); + circ->set_params(false); + circ->set_metadata(config, truncate); // Load different parameterizations of the initial circuit const auto circ_params = param_table[i]; const size_t num_params = circ_params[0].second.size(); - const size_t num_instr = circ.ops.size(); + const size_t num_instr = circ->ops.size(); for (size_t j = 0; j < num_params; j++) { // Make a copy of the initial circuit - Circuit param_circ = circ; + auto param_circ = std::make_shared(*circ); for (const auto ¶ms : circ_params) { const auto instr_pos = params.first.first; const auto param_pos = params.first.second; // Validation - if (instr_pos >= num_instr) { - throw std::invalid_argument( - R"(Invalid parameterized qobj: instruction position out of range)"); + if (instr_pos == AER::Config::GLOBAL_PHASE_POS) { + // negative position is for global phase + param_circ->global_phase_angle = params.second[j]; + } else { + if (instr_pos >= num_instr) { + std::cout << "Invalid parameterization: instruction position " + "out of range: " + << instr_pos << std::endl; + throw std::invalid_argument( + R"(Invalid parameterization: instruction position out of range)"); + } + auto &op = param_circ->ops[instr_pos]; + if (param_pos >= op.params.size()) { + throw std::invalid_argument( + R"(Invalid parameterization: instruction param position out of range)"); + } + if (j >= params.second.size()) { + throw std::invalid_argument( + R"(Invalid parameterization: parameterization value out of range)"); + } + // Update the param + op.params[param_pos] = params.second[j]; } - auto &op = param_circ.ops[instr_pos]; - if (param_pos >= op.params.size()) { - throw std::invalid_argument( - R"(Invalid parameterized qobj: instruction param position out of range)"); - } - if (j >= params.second.size()) { - throw std::invalid_argument( - R"(Invalid parameterized qobj: parameterization value out of range)"); - } - // Update the param - op.params[param_pos] = params.second[j]; } // Run truncation. // TODO: Truncation should be performed and parameters should be @@ -123,10 +133,11 @@ Result controller_execute(std::vector &input_circs, // of instructions, which can be changed in truncation. Therefore, // current implementation performs truncation for each parameter set. if (truncate) { - param_circ.set_params(true); - param_circ.set_metadata(config, true); + param_circ->set_params(true); + param_circ->set_metadata(config, true); } - circs.push_back(std::move(param_circ)); + circs.push_back(param_circ); + template_circs.push_back(circ); } } } @@ -144,10 +155,10 @@ Result controller_execute(std::vector &input_circs, if (config.seed_simulator.has_value()) seed = config.seed_simulator.value(); else - seed = circs[0].seed; + seed = circs[0]->seed; for (auto &circ : circs) { - circ.seed = seed + seed_shift; + circ->seed = seed + seed_shift; seed_shift += 2113; } @@ -155,7 +166,12 @@ Result controller_execute(std::vector &input_circs, // Issue: https://github.com/Qiskit/qiskit-aer/issues/1 Hacks::maybe_load_openmp(config.library_dir); controller.set_config(config); - return controller.execute(circs, noise_model, config); + auto ret = controller.execute(circs, noise_model, config); + + for (size_t i = 0; i < ret.results.size(); ++i) + ret.results[i].circ_id = template_circs[i]->circ_id; + + return ret; } } // end namespace AER diff --git a/src/framework/circuit.hpp b/src/framework/circuit.hpp index b8b33c75e0..bc7645d694 100644 --- a/src/framework/circuit.hpp +++ b/src/framework/circuit.hpp @@ -36,6 +36,9 @@ class Circuit { using Op = Operations::Op; using OpType = Operations::OpType; + // circuit id + int circ_id = 0; + // Circuit operations std::vector ops; @@ -130,6 +133,7 @@ class Circuit { const int_t cond_regidx = -1, const std::string label = "") { ops.push_back(Operations::make_gate(name, qubits, params, string_params, cond_regidx, label)); + check_gate_params(ops.back()); } void diagonal(const reg_t &qubits, const cvector_t &vec, diff --git a/src/framework/config.hpp b/src/framework/config.hpp index 2890747a22..56a8015a0b 100644 --- a/src/framework/config.hpp +++ b/src/framework/config.hpp @@ -128,7 +128,9 @@ struct Config { // system configurations std::string library_dir = ""; - using pos_t = std::pair; + const static int_t GLOBAL_PHASE_POS = + -1; // special param position for global phase + using pos_t = std::pair; using exp_params_t = std::vector>>; std::vector param_table; optional n_qubits; diff --git a/src/framework/operations.hpp b/src/framework/operations.hpp index d83af2d00b..da1f575054 100644 --- a/src/framework/operations.hpp +++ b/src/framework/operations.hpp @@ -349,23 +349,29 @@ inline void check_empty_name(const Op &op) { // Raise an exception if qubits list is empty inline void check_empty_qubits(const Op &op) { if (op.qubits.empty()) - throw std::invalid_argument(R"(Invalid qobj ")" + op.name + - R"(" instruction ("qubits" is empty).)"); + throw std::invalid_argument(R"(Invalid operation ")" + op.name + + R"(" ("qubits" is empty).)"); } // Raise an exception if params is empty inline void check_empty_params(const Op &op) { if (op.params.empty()) - throw std::invalid_argument(R"(Invalid qobj ")" + op.name + - R"(" instruction ("params" is empty).)"); + throw std::invalid_argument(R"(Invalid operation ")" + op.name + + R"(" ("params" is empty).)"); +} + +// Raise an exception if qubits is more than expected +inline void check_length_qubits(const Op &op, const size_t size) { + if (op.qubits.size() < size) + throw std::invalid_argument(R"(Invalid operation ")" + op.name + + R"(" ("qubits" is incorrect length).)"); } // Raise an exception if params is empty inline void check_length_params(const Op &op, const size_t size) { - if (op.params.size() != size) - throw std::invalid_argument( - R"(Invalid qobj ")" + op.name + - R"(" instruction ("params" is incorrect length).)"); + if (op.params.size() < size) + throw std::invalid_argument(R"(Invalid operation ")" + op.name + + R"(" ("params" is incorrect length).)"); } // Raise an exception if qubits list contains duplications @@ -373,8 +379,42 @@ inline void check_duplicate_qubits(const Op &op) { auto cpy = op.qubits; std::unique(cpy.begin(), cpy.end()); if (cpy != op.qubits) - throw std::invalid_argument(R"(Invalid qobj ")" + op.name + - R"(" instruction ("qubits" are not unique).)"); + throw std::invalid_argument(R"(Invalid operation ")" + op.name + + R"(" ("qubits" are not unique).)"); +} + +inline void check_gate_params(const Op &op) { + const stringmap_t> param_tables( + {{"u1", {1, 1}}, {"u2", {1, 2}}, {"u3", {1, 3}}, + {"u", {1, 3}}, {"U", {1, 3}}, {"CX", {2, 0}}, + {"cx", {2, 0}}, {"cz", {2, 0}}, {"cy", {2, 0}}, + {"cp", {2, 1}}, {"cu1", {2, 1}}, {"cu2", {2, 2}}, + {"cu3", {2, 3}}, {"swap", {2, 0}}, {"id", {0, 0}}, + {"p", {1, 1}}, {"x", {1, 0}}, {"y", {1, 0}}, + {"z", {1, 0}}, {"h", {1, 0}}, {"s", {1, 0}}, + {"sdg", {1, 0}}, {"t", {1, 0}}, {"tdg", {1, 0}}, + {"r", {1, 2}}, {"rx", {1, 1}}, {"ry", {1, 1}}, + {"rz", {1, 1}}, {"rxx", {2, 1}}, {"ryy", {2, 1}}, + {"rzz", {2, 1}}, {"rzx", {2, 1}}, {"ccx", {3, 0}}, + {"cswap", {3, 0}}, {"mcx", {1, 0}}, {"mcy", {1, 0}}, + {"mcz", {1, 0}}, {"mcu1", {1, 1}}, {"mcu2", {1, 2}}, + {"mcu3", {1, 3}}, {"mcswap", {2, 0}}, {"mcphase", {1, 1}}, + {"mcr", {1, 1}}, {"mcrx", {1, 1}}, {"mcry", {1, 1}}, + {"mcrz", {1, 1}}, {"sx", {1, 0}}, {"sxdg", {1, 0}}, + {"csx", {2, 0}}, {"mcsx", {1, 0}}, {"csxdg", {2, 0}}, + {"mcsxdg", {1, 0}}, {"delay", {1, 0}}, {"pauli", {1, 0}}, + {"mcx_gray", {1, 0}}, {"cu", {2, 4}}, {"mcu", {1, 4}}, + {"mcp", {1, 1}}, {"ecr", {2, 0}}}); + + auto it = param_tables.find(op.name); + if (it == param_tables.end()) { + std::stringstream msg; + msg << "Invalid gate name :\"" << op.name << "\"." << std::endl; + throw std::invalid_argument(msg.str()); + } else { + check_length_qubits(op, std::get<0>(it->second)); + check_length_params(op, std::get<1>(it->second)); + } } //------------------------------------------------------------------------------ @@ -1155,12 +1195,8 @@ Op input_to_op_gate(const inputdata_t &input) { check_empty_name(op); check_empty_qubits(op); check_duplicate_qubits(op); - if (op.name == "u1") - check_length_params(op, 1); - else if (op.name == "u2") - check_length_params(op, 2); - else if (op.name == "u3") - check_length_params(op, 3); + check_gate_params(op); + return op; } @@ -1586,8 +1622,8 @@ Op input_to_op_save_expval(const inputdata_t &input, bool variance) { } // Check edge case of all coefficients being empty - // In this case the operator had all coefficients zero, or sufficiently close - // to zero that they were all truncated. + // In this case the operator had all coefficients zero, or sufficiently + // close to zero that they were all truncated. if (op.expval_params.empty()) { std::string pauli(op.qubits.size(), 'I'); op.expval_params.emplace_back(pauli, 0., 0.); diff --git a/src/framework/qobj.hpp b/src/framework/qobj.hpp index 0e502b978c..01084fd20e 100644 --- a/src/framework/qobj.hpp +++ b/src/framework/qobj.hpp @@ -46,9 +46,9 @@ class Qobj { //---------------------------------------------------------------- // Data //---------------------------------------------------------------- - std::string id; // qobj identifier passed to result - std::string type = "QASM"; // currently we only support QASM - std::vector circuits; // List of circuits + std::string id; // qobj identifier passed to result + std::string type = "QASM"; // currently we only support QASM + std::vector> circuits; // List of circuits json_t header; // (optional) passed through to result json_t config; // (optional) qobj level config data Noise::NoiseModel noise_model; // (optional) noise model @@ -117,7 +117,7 @@ Qobj::Qobj(const inputdata_t &input) { // i is the instruction index in the experiment // j is the param index in the instruction // pars = [par0, par1, ...] is a list of different parameterizations - using pos_t = std::pair; + using pos_t = std::pair; using exp_params_t = std::vector>>; std::vector param_table; Parser::get_value(param_table, "parameterizations", config); @@ -132,38 +132,45 @@ Qobj::Qobj(const inputdata_t &input) { for (size_t i = 0; i < num_circs; i++) { if (param_table.empty() || param_table[i].empty()) { // Get base circuit from qobj - Circuit circuit(static_cast(circs[i]), config, truncation); + auto circuit = std::make_shared( + static_cast(circs[i]), config, truncation); // Non parameterized circuit - circuits.push_back(std::move(circuit)); + circuits.push_back(circuit); } else { // Get base circuit from qobj without truncation - Circuit circuit(static_cast(circs[i]), config, false); + auto circuit = std::make_shared( + static_cast(circs[i]), config, false); // Load different parameterizations of the initial circuit const auto circ_params = param_table[i]; const size_t num_params = circ_params[0].second.size(); - const size_t num_instr = circuit.ops.size(); + const size_t num_instr = circuit->ops.size(); for (size_t j = 0; j < num_params; j++) { // Make a copy of the initial circuit - Circuit param_circuit = circuit; + auto param_circuit = std::make_shared(*circuit); for (const auto ¶ms : circ_params) { const auto instr_pos = params.first.first; const auto param_pos = params.first.second; // Validation - if (instr_pos >= num_instr) { - throw std::invalid_argument( - R"(Invalid parameterized qobj: instruction position out of range)"); + if (instr_pos == AER::Config::GLOBAL_PHASE_POS) { + // negative position is for global phase + param_circuit->global_phase_angle = params.second[j]; + } else { + if (instr_pos >= num_instr) { + throw std::invalid_argument( + R"(Invalid parameterized qobj: instruction position out of range)"); + } + auto &op = param_circuit->ops[instr_pos]; + if (param_pos >= op.params.size()) { + throw std::invalid_argument( + R"(Invalid parameterized qobj: instruction param position out of range)"); + } + if (j >= params.second.size()) { + throw std::invalid_argument( + R"(Invalid parameterized qobj: parameterization value out of range)"); + } + // Update the param + op.params[param_pos] = params.second[j]; } - auto &op = param_circuit.ops[instr_pos]; - if (param_pos >= op.params.size()) { - throw std::invalid_argument( - R"(Invalid parameterized qobj: instruction param position out of range)"); - } - if (j >= params.second.size()) { - throw std::invalid_argument( - R"(Invalid parameterized qobj: parameterization value out of range)"); - } - // Update the param - op.params[param_pos] = params.second[j]; } // Run truncation. // TODO: Truncation should be performed and parameters should be @@ -171,8 +178,8 @@ Qobj::Qobj(const inputdata_t &input) { // instructions, which can be changed in truncation. Therefore, current // implementation performs truncation for each parameter set. if (truncation) - param_circuit.set_params(true); - circuits.push_back(std::move(param_circuit)); + param_circuit->set_params(true); + circuits.push_back(param_circuit); } } } @@ -180,10 +187,10 @@ Qobj::Qobj(const inputdata_t &input) { // We shift the seed for each successive experiment // So that results aren't correlated between experiments if (!has_simulator_seed) { - seed = circuits[0].seed; + seed = circuits[0]->seed; } for (auto &circuit : circuits) { - circuit.seed = seed + seed_shift; + circuit->seed = seed + seed_shift; seed_shift += 2113; // Shift the seed } } diff --git a/src/framework/results/experiment_result.hpp b/src/framework/results/experiment_result.hpp index 00604f1ad6..b956e5f06b 100644 --- a/src/framework/results/experiment_result.hpp +++ b/src/framework/results/experiment_result.hpp @@ -15,6 +15,7 @@ #ifndef _aer_framework_results_experiment_result_hpp_ #define _aer_framework_results_experiment_result_hpp_ +#include "framework/circuit.hpp" #include "framework/config.hpp" #include "framework/creg.hpp" #include "framework/opset.hpp" @@ -40,6 +41,7 @@ struct ExperimentResult { uint_t shots; uint_t seed; double time_taken; + int circ_id; // Success and status Status status = Status::empty; diff --git a/src/framework/results/pybind_result.hpp b/src/framework/results/pybind_result.hpp index 3a30477f7c..fbfcc24155 100644 --- a/src/framework/results/pybind_result.hpp +++ b/src/framework/results/pybind_result.hpp @@ -44,6 +44,7 @@ py::object AerToPy::to_python(AER::ExperimentResult &&result) { py::dict pyexperiment; pyexperiment["shots"] = result.shots; + pyexperiment["circ_id"] = result.circ_id; pyexperiment["seed_simulator"] = result.seed; pyexperiment["data"] = AerToPy::to_python(std::move(result.data)); diff --git a/src/noise/noise_model.hpp b/src/noise/noise_model.hpp index 09a7a8fe21..d1207fa4b2 100644 --- a/src/noise/noise_model.hpp +++ b/src/noise/noise_model.hpp @@ -380,22 +380,40 @@ NoiseModel::NoiseOps NoiseModel::sample_noise_op(const Operations::Op &op, void NoiseModel::enable_superop_method(int num_threads) { if (enabled_methods_.find(Method::superop) == enabled_methods_.end()) { + std::vector exs; + exs.resize(std::max(num_threads, 1)); #pragma omp parallel for if (num_threads > 1 && quantum_errors_.size() > 10) \ num_threads(num_threads) for (int i = 0; i < quantum_errors_.size(); i++) { - quantum_errors_[i].compute_superoperator(); + try { + quantum_errors_[i].compute_superoperator(); + } catch (...) { + exs[omp_get_num_threads()] = std::current_exception(); + } } + for (const auto &ex : exs) + if (ex) + std::rethrow_exception(ex); enabled_methods_.insert(Method::superop); } } void NoiseModel::enable_kraus_method(int num_threads) { if (enabled_methods_.find(Method::kraus) == enabled_methods_.end()) { + std::vector exs; + exs.resize(std::max(num_threads, 1)); #pragma omp parallel for if (num_threads > 1 && quantum_errors_.size() > 10) \ num_threads(num_threads) for (int i = 0; i < quantum_errors_.size(); i++) { - quantum_errors_[i].compute_kraus(); + try { + quantum_errors_[i].compute_kraus(); + } catch (...) { + exs[omp_get_num_threads()] = std::current_exception(); + } } + for (const auto &ex : exs) + if (ex) + std::rethrow_exception(ex); enabled_methods_.insert(Method::kraus); } } diff --git a/src/open_pulse/CMakeLists.txt b/src/open_pulse/CMakeLists.txt deleted file mode 100644 index ecc6463f6f..0000000000 --- a/src/open_pulse/CMakeLists.txt +++ /dev/null @@ -1,19 +0,0 @@ -find_package(Pybind11 REQUIRED) -set(Python_EXECUTABLE ${PYTHON_EXECUTABLE}) -# We need to remove the -static flag, because Python Extension system only supports -# dynamic linked libraries, but we want to build a shared libraries with the least -# dependencies we can, so some of these dependencies are linked statically into our -# shared library. -string(REPLACE " -static " "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") - -function(numpy_pybind11_add_module target_name) - basic_pybind11_add_module(${target_name} ${ARGN}) - target_include_directories(${target_name} PRIVATE ${AER_SIMULATOR_CPP_SRC_DIR} - PRIVATE ${AER_SIMULATOR_CPP_EXTERNAL_LIBS}) - target_link_libraries(${target_name} ${AER_LIBRARIES}) -endfunction() - -numpy_pybind11_add_module(pulse_utils pulse_utils_bindings.cpp pulse_utils.cpp numeric_integrator.cpp zspmv.cpp) -numpy_pybind11_add_module(test_python_to_cpp test_python_to_cpp.cpp) - -install(TARGETS pulse_utils test_python_to_cpp LIBRARY DESTINATION "qiskit_aer/pulse/controllers/") diff --git a/src/open_pulse/eval_hamiltonian.hpp b/src/open_pulse/eval_hamiltonian.hpp deleted file mode 100644 index dfa2197e25..0000000000 --- a/src/open_pulse/eval_hamiltonian.hpp +++ /dev/null @@ -1,117 +0,0 @@ -/** - * This code is part of Qiskit. - * - * (C) Copyright IBM 2018, 2019. - * - * This code is licensed under the Apache License, Version 2.0. You may - * obtain a copy of this license in the LICENSE.txt file in the root directory - * of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. - * - * Any modifications or derivative works of this code must retain this - * copyright notice, and modified files need to carry a notice indicating - * that they have been altered from the originals. - */ - -#ifndef _EVAL_HAMILTONIAN_HPP -#define _EVAL_HAMILTONIAN_HPP - -#include "framework/types.hpp" -#include "iterators.hpp" -#include "misc/warnings.hpp" -#include -#include -#include -#include -DISABLE_WARNING_PUSH -#include -DISABLE_WARNING_POP - -struct ParserValues { - ParserValues(std::unique_ptr parser, const std::string &expr) - : parser(std::move(parser)), expr(expr) {} - std::unique_ptr parser; - std::string expr; - std::unordered_map> var_values; -}; - -namespace std { -template <> -struct hash { - std::size_t operator()(const ParserValues &p) const { - return std::hash()(p.expr); - } -}; -} // namespace std - -// TODO: Document -AER::complex_t evaluate_hamiltonian_expression( - const std::string &expr_string, const std::vector &vars, - const std::vector &vars_names, - const std::unordered_map &chan_values) { - - static std::unordered_map> - parser_expr; - auto parser_iter = parser_expr.find(expr_string); - if (parser_iter == parser_expr.end()) { - auto parserx = std::make_unique(); - // Value pi(M_PI); - // parser->DefineVar("npi", Variable(&pi)); - const auto replace = [](const std::string &from, const std::string &to, - std::string where) -> std::string { - size_t start_pos = 0; - while ((start_pos = where.find(from, start_pos)) != std::string::npos) { - where.replace(start_pos, from.length(), to); - start_pos += to.length(); - } - return where; - }; - parserx->SetExpr(replace("np.pi", "pi", expr_string)); - auto parser = - std::make_unique(std::move(parserx), expr_string); - // std::cout << "Creating parser: " << std::hex << parser.get() << " for - // expr: " << expr_string << "\n"; - parser_expr.emplace(expr_string, std::move(parser)); - } - auto *parser = parser_expr[expr_string].get(); - - // std::cout << "Getting parser " << std::hex << parser << "\n"; - - auto maybe_update_value = [parser](const std::string &var_name, - const AER::complex_t &var_value) { - if (parser->var_values.find(var_name) == parser->var_values.end()) { - parser->var_values.emplace(var_name, - std::make_unique(var_value)); - parser->parser->DefineVar( - var_name, mup::Variable(parser->var_values[var_name].get())); - } else { // There's already a variable defined for this expresion - // std::cout << var_name << " is now: " << - // std::to_string(var_value.real()) << "," << - // std::to_string(var_value.imag()) << "\n"; - auto *ref = parser->var_values[var_name].get(); - // Update the value from the container - *ref = var_value; - } - }; - - for (const auto &idx_var : enumerate(vars)) { - size_t index = idx_var.first; - auto var_value = static_cast(idx_var.second); - maybe_update_value(vars_names[index], var_value); - } - - for (const auto &idx_channel : chan_values) { - auto channel = idx_channel.first; // The string of the channel - auto var_value = idx_channel.second; // The complex_t of the map - maybe_update_value(channel, var_value); - } - - try { - mup::Value result = parser->parser->Eval(); - return result.GetComplex(); - } catch (std::exception ex) { - std::cout << ex.what(); - } - return 0.; -} - -#endif //_EVAL_HAMILTONIAN_HPP diff --git a/src/open_pulse/iterators.hpp b/src/open_pulse/iterators.hpp deleted file mode 100644 index 9fb2cbe8ae..0000000000 --- a/src/open_pulse/iterators.hpp +++ /dev/null @@ -1,89 +0,0 @@ -/** - * This code is part of Qiskit. - * - * (C) Copyright IBM 2018, 2019. - * - * This code is licensed under the Apache License, Version 2.0. You may - * obtain a copy of this license in the LICENSE.txt file in the root directory - * of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. - * - * Any modifications or derivative works of this code must retain this - * copyright notice, and modified files need to carry a notice indicating - * that they have been altered from the originals. - */ - -#ifndef _ITERATORS_HPP -#define _ITERATORS_HPP - -template -struct iterator_extractor { - using type = typename T::iterator; -}; - -template -struct iterator_extractor { - using type = typename T::const_iterator; -}; - -/** - * Python-like `enumerate()` for C++14 ranged-for - * - * I wish I'd had this included in the STL :) - * - * Usage: - * ```c++ - * for(auto& elem: index(vec)){ - * std::cout << "Index: " << elem.first << " Element: " << elem.second; - * } - * ``` - **/ -template -class Indexer { -public: - class _Iterator { - using inner_iterator = typename iterator_extractor::type; - using inner_reference = - typename std::iterator_traits::reference; - - public: - using reference = std::pair; - - _Iterator(inner_iterator it) : _pos(0), _it(it) {} - - reference operator*() const { return reference(_pos, *_it); } - - _Iterator &operator++() { - ++_pos; - ++_it; - return *this; - } - - _Iterator operator++(int) { - _Iterator tmp(*this); - ++*this; - return tmp; - } - - bool operator==(_Iterator const &it) const { return _it == it._it; } - bool operator!=(_Iterator const &it) const { return !(*this == it); } - - private: - size_t _pos; - inner_iterator _it; - }; - - Indexer(T &t) : _container(t) {} - - _Iterator begin() const { return _Iterator(_container.begin()); } - _Iterator end() const { return _Iterator(_container.end()); } - -private: - T &_container; -}; // class Indexer - -template -Indexer enumerate(T &t) { - return Indexer(t); -} - -#endif \ No newline at end of file diff --git a/src/open_pulse/log.hpp b/src/open_pulse/log.hpp deleted file mode 100644 index bdc8c695de..0000000000 --- a/src/open_pulse/log.hpp +++ /dev/null @@ -1,106 +0,0 @@ -/** - * This code is part of Qiskit. - * - * (C) Copyright IBM 2018, 2019. - * - * This code is licensed under the Apache License, Version 2.0. You may - * obtain a copy of this license in the LICENSE.txt file in the root directory - * of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. - * - * Any modifications or derivative works of this code must retain this - * copyright notice, and modified files need to carry a notice indicating - * that they have been altered from the originals. - */ - -#ifndef _LOG_HPP -#define _LOG_HPP - -#include "nparray.hpp" -#include "python_to_cpp.hpp" -#include "types.hpp" -#include -#include -#include -#include -#include - -template -void jlog(const std::string &msg, const T &value) { - spdlog::debug("{}: {}", msg, value); -} - -template <> -void jlog(const std::string &msg, const complex_t &values) { - spdlog::debug("{}: [{},{}i]", msg, values.real(), values.imag()); -} - -template -void jlog(const std::string &msg, const NpArray &values) { - spdlog::debug("{}", msg); - spdlog::debug(".shape: "); - for (const auto &shape : values.shape) - spdlog::debug("{} ", shape); - - spdlog::debug("\n.data: "); - for (const auto &val : values.data) { - jlog("", val); - } -} - -template -void jlog(const std::string &msg, const std::vector &values) { - spdlog::debug("{}", msg); - for (const auto &val : values) { - jlog("", val); - } -} - -template <> -void jlog(const std::string &msg, - const std::unordered_map>> &values) { - spdlog::debug("{}", msg); - for (const auto &val : values) { - for (const auto &inner : val.second) { - for (const auto &inner2 : inner) { - spdlog::debug("{}:{} ", val.first, inner2); - } - } - } -} - -template <> -void jlog(const std::string &msg, - const std::unordered_map &values) { - spdlog::debug("{}", msg); - for (const auto &val : values) { - spdlog::debug("{}:{} ", val.first, val.second); - } -} - -template <> -void jlog(const std::string &msg, - const std::unordered_map>> - &values) { - spdlog::debug("{}", msg); - for (const auto &val : values) { - for (const auto &inner : val.second) { - jlog(val.first, inner); - } - } -} - -template <> -void jlog( - const std::string &msg, - const ordered_map>> &values) { - spdlog::debug("{}", msg); - using order_map_t = ordered_map>>; - for (const auto &val : const_cast(values)) { - for (const auto &inner : val.second) { - jlog(val.first, inner); - } - } -} - -#endif //_LOG_HPP \ No newline at end of file diff --git a/src/open_pulse/numeric_integrator.cpp b/src/open_pulse/numeric_integrator.cpp deleted file mode 100644 index 0d3957492e..0000000000 --- a/src/open_pulse/numeric_integrator.cpp +++ /dev/null @@ -1,275 +0,0 @@ -#include -#include -#include -#include -#include -#define _USE_MATH_DEFINES -#include - -#include "misc/warnings.hpp" -DISABLE_WARNING_PUSH -#include -DISABLE_WARNING_POP - -#ifdef DEBUG -#include "misc/warnings.hpp" -DISABLE_WARNING_PUSH -#include -#include -#include -DISABLE_WARNING_POP -#endif -#include "numeric_integrator.hpp" -#include "python_to_cpp.hpp" - -#ifdef DEBUG -class Unregister { -public: - ~Unregister() { spdlog::drop_all(); } -}; -#endif - -/** - * Python // operator-like division - */ -int32_t floor_div(int32_t a, int32_t b) { - int32_t q = a / b; - int32_t r = a - q * b; - q -= ((r != 0) & ((r ^ b) < 0)); - return q; -} - -complex_t chan_value(double t, unsigned int chan_num, const double freq_ch, - const NpArray &chan_pulse_times, - const NpArray &pulse_array, - const NpArray &pulse_indexes, - const NpArray &fc_array, - const NpArray ®) { - - static const auto get_arr_idx = [](double t, double start, double stop, - size_t len_array) -> int { - return static_cast( - std::floor((t - start) / (stop - start) * len_array)); - }; - - complex_t out = {0., 0.}; - auto num_times = floor_div(static_cast(chan_pulse_times.shape[0]), 4); - - for (auto i = 0; i < num_times; ++i) { - auto start_time = chan_pulse_times[4 * i]; - auto stop_time = chan_pulse_times[4 * i + 1]; - if (start_time <= t && t < stop_time) { - auto cond = static_cast(chan_pulse_times[4 * i + 3]); - if (cond < 0 || reg[cond]) { - auto temp_idx = static_cast(chan_pulse_times[4 * i + 2]); - auto start_idx = pulse_indexes[temp_idx]; - auto stop_idx = pulse_indexes[temp_idx + 1]; - auto offset_idx = - get_arr_idx(t, start_time, stop_time, stop_idx - start_idx); - out = pulse_array[start_idx + offset_idx]; - } - } - } - - // TODO floating point comparsion with complex ?! - // Seems like this is equivalent to: out != complex_t(0., 0.) - if (out != 0.) { - num_times = floor_div(fc_array.shape[0], 3); - - // get the index of the phase change - // this loop will result in finding the index of the phase to use +1 - auto phase_idx = 0; - while (phase_idx < num_times) { - if (t < fc_array[3 * phase_idx]) - break; - phase_idx++; - } - - double phase = 0.; - if (phase_idx > 0) { - phase = fc_array[3 * (phase_idx - 1) + 1]; - } - - if (phase != 0.) { - out *= std::exp(complex_t(0., 1.) * phase); - } - out *= std::exp(complex_t(0., 1.) * 2. * M_PI * freq_ch * t); - } - return out.real(); -} - -struct RhsData { - RhsData(py::object the_global_data, py::object the_exp, py::object the_system, - py::object the_channels, py::object the_reg) { - - PyObject *py_global_data = the_global_data.ptr(); - PyObject *py_exp = the_exp.ptr(); - PyObject *py_system = the_system.ptr(); - PyObject *py_register = the_reg.ptr(); - - if (py_global_data == nullptr || py_exp == nullptr || - py_system == nullptr || py_register == nullptr) { - std::string msg = "These arguments cannot be null: "; - msg += (py_global_data == nullptr ? "py_global_data " : ""); - msg += (py_exp == nullptr ? "py_exp " : ""); - msg += (py_system == nullptr ? "py_system " : ""); - msg += (py_register == nullptr ? "py_register " : ""); - throw std::invalid_argument(msg); - } - - pulses = get_ordered_map_from_dict_item>>( - py_exp, "channels"); - freqs = get_vec_from_dict_item(py_global_data, "freqs"); - pulse_array = get_value_from_dict_item>(py_global_data, - "pulse_array"); - pulse_indices = - get_value_from_dict_item>(py_global_data, "pulse_indices"); - reg = get_value>(py_register); - - systems = get_value>(py_system); - vars = get_vec_from_dict_item(py_global_data, "vars"); - vars_names = - get_vec_from_dict_item(py_global_data, "vars_names"); - num_h_terms = get_value_from_dict_item(py_global_data, "num_h_terms"); - auto tmp_datas = get_vec_from_dict_item>(py_global_data, - "h_ops_data"); - for (const auto &data : tmp_datas) { - auto datas_back = datas.emplace(datas.end()); - auto idxs_back = idxs.emplace(idxs.end()); - auto ptrs_back = ptrs.emplace(ptrs.end()); - ptrs_back->push_back(0); - auto first_j = 0; - auto last_j = 0; - for (auto i = 0; i < data.shape[0]; i++) { - for (auto j = 0; j < data.shape[1]; j++) { - if (std::abs(data(i, j)) > 1e-15) { - datas_back->push_back(data(i, j)); - idxs_back->push_back(j); - last_j++; - } - } - ptrs_back->push_back(last_j); - } - } - energy = get_value_from_dict_item>(py_global_data, - "h_diag_elems"); - } - - ordered_map>> pulses; - std::vector freqs; - NpArray pulse_array; - NpArray pulse_indices; - NpArray reg; - - std::vector systems; - std::vector vars; - std::vector vars_names; - long num_h_terms; - std::vector> datas; - std::vector> idxs; - std::vector> ptrs; - NpArray energy; - - std::vector osc_terms_no_t; -}; - -py::array_t inner_ode_rhs(double t, py::array_t the_vec, - const RhsData &rhs_data) { - if (the_vec.ptr() == nullptr) { - throw std::invalid_argument("py_vec cannot be null"); - } - - auto vec = static_cast(the_vec.request().ptr); - auto num_rows = the_vec.size(); - py::array_t out_arr(num_rows); - auto out = static_cast(out_arr.request().ptr); - memset(&out[0], 0, num_rows * sizeof(complex_t)); - - std::unordered_map chan_values; - chan_values.reserve(rhs_data.pulses.size()); - for (const auto &elem : enumerate(rhs_data.pulses)) { - /** - * eleme is map of string as key type, and vector of vectors of doubles. - * elem["D0"] = [[0.,1.,2.][0.,1.,2.]] - **/ - auto i = elem.first; - auto channel = elem.second.first; - auto pulse = elem.second.second; - - auto val = - chan_value(t, i, rhs_data.freqs[i], pulse[0], rhs_data.pulse_array, - rhs_data.pulse_indices, pulse[1], rhs_data.reg); - chan_values.emplace(channel, val); - } - - // 4. Eval the time-dependent terms and do SPMV. - for (int h_idx = 0; h_idx < rhs_data.num_h_terms; h_idx++) { - // TODO: Refactor - std::string term; - if (h_idx == rhs_data.systems.size() && - rhs_data.num_h_terms > rhs_data.systems.size()) { - term = "1.0"; - } else if (h_idx < rhs_data.systems.size()) { - term = rhs_data.systems[h_idx].term; - } else { - continue; - } - - auto td = evaluate_hamiltonian_expression(term, rhs_data.vars, - rhs_data.vars_names, chan_values); - if (std::abs(td) > 1e-15) { - for (auto i = 0; i < num_rows; i++) { - complex_t dot = {0., 0.}; - auto row_start = rhs_data.ptrs[h_idx][i]; - auto row_end = rhs_data.ptrs[h_idx][i + 1]; - for (auto j = row_start; j < row_end; ++j) { - auto tmp_idx = rhs_data.idxs[h_idx][j]; - auto osc_term = - std::exp(complex_t(0., 1.) * - (rhs_data.energy[i] - rhs_data.energy[tmp_idx]) * t); - complex_t coef = (i < tmp_idx ? std::conj(td) : td); - dot += coef * osc_term * rhs_data.datas[h_idx][j] * vec[tmp_idx]; - } - out[i] += dot; - } - } - } /* End of systems */ - for (auto i = 0; i < num_rows; ++i) { - out[i] += complex_t(0., 1.) * rhs_data.energy[i] * vec[i]; - } - - return out_arr; -} - -RhsFunctor::RhsFunctor(py::object the_global_data, py::object the_exp, - py::object the_system, py::object the_channels, - py::object the_reg) - : rhs_data_(std::make_shared(the_global_data, the_exp, the_system, - the_channels, the_reg)) {} - -py::array_t RhsFunctor::operator()(double t, - py::array_t the_vec) { - return inner_ode_rhs(t, the_vec, *rhs_data_); -} - -py::array_t td_ode_rhs(double t, py::array_t the_vec, - py::object the_global_data, - py::object the_exp, py::object the_system, - py::object the_channels, py::object the_reg) { -#ifdef DEBUG - CALLGRIND_START_INSTRUMENTATION; -#endif - - // I left this commented on porpose so we can use logging eventually - // This is just a RAII for the logger - // const Unregister unregister; - // auto file_logger = spdlog::basic_logger_mt("basic_logger", - // "logs/td_ode_rhs.txt"); spdlog::set_default_logger(file_logger); - // spdlog::set_level(spdlog::level::debug); // Set global log level to debug - // spdlog::flush_on(spdlog::level::debug); - - auto rhs_data = - RhsData(the_global_data, the_exp, the_system, the_channels, the_reg); - return inner_ode_rhs(t, the_vec, rhs_data); -} diff --git a/src/open_pulse/numeric_integrator.hpp b/src/open_pulse/numeric_integrator.hpp deleted file mode 100644 index bdbeef2d5a..0000000000 --- a/src/open_pulse/numeric_integrator.hpp +++ /dev/null @@ -1,51 +0,0 @@ -/** - * This code is part of Qiskit. - * - * (C) Copyright IBM 2018, 2019. - * - * This code is licensed under the Apache License, Version 2.0. You may - * obtain a copy of this license in the LICENSE.txt file in the root directory - * of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. - * - * Any modifications or derivative works of this code must retain this - * copyright notice, and modified files need to carry a notice indicating - * that they have been altered from the originals. - */ - -#ifndef _NUMERIC_INTEGRATOR_HPP -#define _NUMERIC_INTEGRATOR_HPP - -#include "misc/warnings.hpp" -#include -#include -#include -#include -DISABLE_WARNING_PUSH -#include -#include -DISABLE_WARNING_POP - -#include "types.hpp" - -namespace py = pybind11; - -struct RhsData; - -py::array_t td_ode_rhs(double t, py::array_t vec, - py::object global_data, py::object exp, - py::object system, py::object channels, - py::object reg); - -class RhsFunctor { -public: - RhsFunctor(py::object the_global_data, py::object the_exp, - py::object the_system, py::object the_channels, - py::object the_reg); - - py::array_t operator()(double t, py::array_t the_vec); - -private: - std::shared_ptr rhs_data_; -}; - -#endif // _NUMERIC_INTEGRATOR_HPP \ No newline at end of file diff --git a/src/open_pulse/ordered_map.hpp b/src/open_pulse/ordered_map.hpp deleted file mode 100644 index 571744effc..0000000000 --- a/src/open_pulse/ordered_map.hpp +++ /dev/null @@ -1,131 +0,0 @@ -/** - * This code is part of Qiskit. - * - * (C) Copyright IBM 2018, 2019. - * - * This code is licensed under the Apache License, Version 2.0. You may - * obtain a copy of this license in the LICENSE.txt file in the root directory - * of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. - * - * Any modifications or derivative works of this code must retain this - * copyright notice, and modified files need to carry a notice indicating - * that they have been altered from the originals. - */ - -#ifndef _ORDERED_MAP_HPP -#define _ORDERED_MAP_HPP - -#include -#include -#include -#include - -template , - class KeyEqual = std::equal_to, - class Allocator = std::allocator>> -class ordered_map { -public: - using unordered_map_t = std::unordered_map; - using vector_t = std::vector; - - ordered_map() {} - ordered_map(const ordered_map &other) - : internal_map(other.internal_map), order(other.order) { - it.reset(new ordered_map_iterator_t(internal_map, - order)); - } - - ordered_map &operator=(const ordered_map &other) { - internal_map = other.internal_map; - order = other.order; - it.reset(new ordered_map_iterator_t(internal_map, - order)); - return *this; - } - - auto reserve(size_t size) { - order.reserve(size); - return internal_map.reserve(size); - } - - template - decltype(auto) emplace(Args &&...args) { - const auto first = std::get<0>(std::forward_as_tuple(args...)); - order.emplace_back(first); - return internal_map.emplace(std::forward(args)...); - } - - size_t size() const { return internal_map.size(); } - - const T &operator[](const std::string &index) const { - return internal_map[index]; - } - - // This is needed so we can use the container in an iterator context like - // ranged fors. - template - class ordered_map_iterator_t { - using unordered_map_iter_t = typename _map_t::iterator; - using vec_iter_t = typename _vec_t::iterator; - - _map_t ↦ - _vec_t &vec; - - unordered_map_iter_t map_iter; - vec_iter_t vec_iter; - - public: - using reference = typename unordered_map_iter_t::reference; - using difference_type = typename unordered_map_iter_t::difference_type; - using value_type = typename unordered_map_iter_t::value_type; - using pointer = typename unordered_map_iter_t::reference; - using iterator_category = typename unordered_map_iter_t::iterator_category; - - ordered_map_iterator_t(_map_t &map, _vec_t &vec) : map(map), vec(vec) {} - - ordered_map_iterator_t begin() { - vec_iter = vec.begin(); - map_iter = map.find(*vec_iter); - return *this; - } - - ordered_map_iterator_t end() { - vec_iter = vec.end(); - map_iter = map.find(*(vec_iter - 1)); - return *this; - } - - ordered_map_iterator_t operator++() { - auto tmp = ++vec_iter; - tmp = (tmp == vec.end() ? --tmp : tmp); - map_iter = map.find(*tmp); - return *this; - } - - bool operator==(const ordered_map_iterator_t &rhs) const { - return vec_iter == rhs.vec_iter; - } - - bool operator!=(const ordered_map_iterator_t &rhs) const { - return vec_iter != rhs.vec_iter; - } - - const reference operator*() const { return *map_iter; } - }; - - using iterator = ordered_map_iterator_t; - using const_iterator = ordered_map_iterator_t; - - std::unique_ptr it = - std::make_unique(internal_map, order); - - const_iterator begin() const { return it->begin(); } - - const_iterator end() const { return it->end(); } - -private: - unordered_map_t internal_map; - vector_t order; -}; - -#endif //_ORDERED_MAP_HPP \ No newline at end of file diff --git a/src/open_pulse/pulse_utils.cpp b/src/open_pulse/pulse_utils.cpp deleted file mode 100644 index 9e7049d0f7..0000000000 --- a/src/open_pulse/pulse_utils.cpp +++ /dev/null @@ -1,163 +0,0 @@ -#include "pulse_utils.hpp" -#include "zspmv.hpp" - -complex_t internal_expect_psi_csr(const py::array_t &data, - const py::array_t &ind, - const py::array_t &ptr, - const py::array_t &vec) { - auto data_raw = data.unchecked<1>(); - auto vec_raw = vec.unchecked<1>(); - auto ind_raw = ind.unchecked<1>(); - auto ptr_raw = ptr.unchecked<1>(); - - auto nrows = vec.shape(0); - complex_t temp, expt = 0; - - for (decltype(nrows) row = 0; row < nrows; row++) { - temp = 0; - auto vec_conj = std::conj(vec_raw[row]); - for (auto j = ptr_raw[row]; j < ptr_raw[row + 1]; j++) { - temp += data_raw[j] * vec_raw[ind_raw[j]]; - } - expt += vec_conj * temp; - } - return expt; -} - -complex_t internal_expect_psi(const py::array_t &data, - const py::array_t &vec) { - auto data_raw = data.unchecked<2>(); - auto vec_raw = vec.unchecked<1>(); - - auto nrows = data.shape(0); - auto ncols = data.shape(1); - complex_t temp, expt = 0; - - for (decltype(nrows) i = 0; i < nrows; i++) { - temp = 0; - auto vec_conj = std::conj(vec_raw[i]); - for (auto j = 0; j < ncols; j++) { - temp += data_raw(i, j) * vec_raw[j]; - } - expt += vec_conj * temp; - } - return expt; -} - -py::object expect_psi_csr(py::array_t data, py::array_t ind, - py::array_t ptr, py::array_t vec, - bool isherm) { - complex_t expt = internal_expect_psi_csr(data, ind, ptr, vec); - if (isherm) { - return py::cast(std::real(expt)); - } - return py::cast(expt); -} - -py::object expect_psi(py::array_t data, py::array_t vec, - bool isherm) { - complex_t expt = internal_expect_psi(data, vec); - if (isherm) { - return py::cast(std::real(expt)); - } - return py::cast(expt); -} - -py::array_t occ_probabilities(py::array_t qubits, - py::array_t state, - py::list meas_ops) { - auto meas_size = meas_ops.size(); - py::array_t probs(meas_size); - auto probs_raw = probs.mutable_unchecked<1>(); - for (decltype(meas_size) i = 0; i < meas_size; i++) { - auto data = - meas_ops[i].attr("data").attr("data").cast>(); - probs_raw[i] = std::real(internal_expect_psi(data, state)); - } - - return probs; -} - -void write_shots_memory(py::array_t mem, - py::array_t mem_slots, - py::array_t probs, - py::array_t rand_vals) { - auto nrows = mem.shape(0); - auto nprobs = probs.shape(0); - - unsigned char temp; - - auto mem_raw = mem.mutable_unchecked<2>(); - auto mem_slots_raw = mem_slots.unchecked<1>(); - auto probs_raw = probs.unchecked<1>(); - auto rand_vals_raw = rand_vals.unchecked<1>(); - - for (decltype(nrows) i = 0; i < nrows; i++) { - for (decltype(nprobs) j = 0; j < nprobs; j++) { - temp = static_cast(probs_raw[j] > - rand_vals_raw[nprobs * i + j]); - if (temp) { - mem_raw(i, mem_slots_raw[j]) = temp; - } - } - } -} - -void oplist_to_array(py::list A, py::array_t B, int start_idx) { - auto lenA = A.size(); - if ((start_idx + lenA) > B.shape(0)) { - throw std::runtime_error( - std::string("Input list does not fit into array if start_idx is ") + - std::to_string(start_idx) + "."); - } - - auto B_raw = B.mutable_unchecked<1>(); - for (decltype(lenA) kk = 0; kk < lenA; kk++) { - auto item = A[kk].cast(); - B_raw[start_idx + kk] = - complex_t(item[0].cast(), item[1].cast()); - } -} - -template -T *get_raw_data(py::array_t array) { - return static_cast(array.request().ptr); -} - -py::array_t spmv_csr(py::array_t data, - py::array_t ind, py::array_t ptr, - py::array_t vec) { - auto data_raw = get_raw_data(data); - auto ind_raw = get_raw_data(ind); - auto ptr_raw = get_raw_data(ptr); - auto vec_raw = get_raw_data(vec); - - auto num_rows = vec.shape(0); - - py::array_t out(num_rows); - auto out_raw = get_raw_data(out); - memset(&out_raw[0], 0, num_rows * sizeof(complex_t)); - zspmvpy(data_raw, ind_raw, ptr_raw, vec_raw, 1.0, out_raw, num_rows); - - return out; -} - -py::array_t spmv(py::array_t data, - py::array_t vec) { - auto data_raw = get_raw_data(data); - auto vec_raw = get_raw_data(vec); - - auto num_columns = data.shape(0); - auto num_rows = data.shape(1); - - py::array_t out(num_rows); - auto out_raw = get_raw_data(out); - memset(&out_raw[0], 0, num_rows * sizeof(complex_t)); - for (auto row = 0; row < num_rows; row++) { - for (auto jj = 0; jj < num_columns; jj++) { - out_raw[row] += data_raw[row * num_columns + jj] * vec_raw[jj]; - } - } - - return out; -} \ No newline at end of file diff --git a/src/open_pulse/pulse_utils.hpp b/src/open_pulse/pulse_utils.hpp deleted file mode 100644 index adee3439c8..0000000000 --- a/src/open_pulse/pulse_utils.hpp +++ /dev/null @@ -1,94 +0,0 @@ -/** - * This code is part of Qiskit. - * - * (C) Copyright IBM 2018, 2019, 2020. - * - * This code is licensed under the Apache License, Version 2.0. You may - * obtain a copy of this license in the LICENSE.txt file in the root directory - * of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. - * - * Any modifications or derivative works of this code must retain this - * copyright notice, and modified files need to carry a notice indicating - * that they have been altered from the originals. - */ - -#ifndef PULSE_UTILS_H -#define PULSE_UTILS_H - -#include "types.hpp" -#include -#include - -namespace py = pybind11; - -py::object expect_psi_csr(py::array_t data, py::array_t ind, - py::array_t ptr, py::array_t vec, - bool isherm); - -py::object expect_psi(py::array_t data, py::array_t vec, - bool isherm); - -//============================================================================ -// Computes the occupation probabilities of the specifed qubits for -// the given state. -// Args: -// qubits (int array): Ints labelling which qubits are to be measured. -//============================================================================ -py::array_t occ_probabilities(py::array_t qubits, - py::array_t state, - py::list meas_ops); - -//============================================================================ -// Converts probabilities back into shots -// Args: -// mem -// mem_slots -// probs: expectation value -// rand_vals: random values used to convert back into shots -//============================================================================ -void write_shots_memory(py::array_t mem, - py::array_t mem_slots, - py::array_t probs, - py::array_t rand_vals); - -//============================================================================ -// Takes a list of complex numbers represented by a list -// of pairs of floats, and inserts them into a complex NumPy -// array at a given starting index. -// -// Parameters: -// A (list): A nested-list of [re, im] pairs. -// B(ndarray): Array for storing complex numbers from list A. -// start_idx (int): The starting index at which to insert elements. -//============================================================================ -void oplist_to_array(py::list A, py::array_t B, int start_idx); - -//============================================================================ -// Sparse matrix, dense vector multiplication. -// Here the vector is assumed to have one-dimension. -// Matrix must be in CSR format and have complex entries. -// -// Parameters -// ---------- -// data : array -// Data for sparse matrix. -// idx : array -// Indices for sparse matrix data. -// ptr : array -// Pointers for sparse matrix data. -// vec : array -// Dense vector for multiplication. Must be one-dimensional. -// -// Returns -// ------- -// out : array -// Returns dense array. -//============================================================================ -py::array_t spmv_csr(py::array_t data, - py::array_t ind, py::array_t ptr, - py::array_t vec); - -py::array_t spmv(py::array_t data, - py::array_t vec); - -#endif // PULSE_UTILS_H diff --git a/src/open_pulse/pulse_utils_bindings.cpp b/src/open_pulse/pulse_utils_bindings.cpp deleted file mode 100644 index 19a8b1b16a..0000000000 --- a/src/open_pulse/pulse_utils_bindings.cpp +++ /dev/null @@ -1,65 +0,0 @@ -/** - * This code is part of Qiskit. - * - * (C) Copyright IBM 2018, 2019, 2020. - * - * This code is licensed under the Apache License, Version 2.0. You may - * obtain a copy of this license in the LICENSE.txt file in the root directory - * of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. - * - * Any modifications or derivative works of this code must retain this - * copyright notice, and modified files need to carry a notice indicating - * that they have been altered from the originals. - */ - -#include "numeric_integrator.hpp" -#include "pulse_utils.hpp" - -#include "misc/warnings.hpp" -DISABLE_WARNING_PUSH -#include -DISABLE_WARNING_POP - -RhsFunctor get_ode_rhs_functor(py::object the_global_data, py::object the_exp, - py::object the_system, py::object the_channels, - py::object the_reg) { - return RhsFunctor(the_global_data, the_exp, the_system, the_channels, - the_reg); -} - -class OccProbabilitiesFunctor { -public: - OccProbabilitiesFunctor() = default; - py::array_t operator()(py::array_t qubits, - py::array_t state, - py::list meas_ops) { - return occ_probabilities(qubits, state, meas_ops); - } -}; - -PYBIND11_MODULE(pulse_utils, m) { - m.doc() = - "Utility functions for pulse simulator"; // optional module docstring - - m.def("td_ode_rhs_static", &td_ode_rhs, "Compute rhs for ODE"); - m.def("cy_expect_psi_csr", &expect_psi_csr, "Expected value for a operator"); - m.def("cy_expect_psi", &expect_psi, "Expected value for a operator"); - m.def("occ_probabilities", &occ_probabilities, - "Computes the occupation probabilities of the specifed qubits for the " - "given state"); - m.def("write_shots_memory", &write_shots_memory, - "Converts probabilities back into shots"); - m.def("oplist_to_array", &oplist_to_array, - "Insert list of complex numbers into numpy complex array"); - m.def("spmv_csr", &spmv_csr, "Sparse matrix, dense vector multiplication."); - m.def("spmv", &spmv, "Matrix vector multiplication."); - - py::class_ ode_rhs_func(m, "OdeRhsFunctor"); - ode_rhs_func.def("__call__", &RhsFunctor::operator()); - ode_rhs_func.def("__reduce__", [ode_rhs_func](const RhsFunctor &self) { - return py::make_tuple(ode_rhs_func, py::tuple()); - }); - - m.def("get_ode_rhs_functor", &get_ode_rhs_functor, - "Get ode_rhs functor to allow caching of parameters"); -} diff --git a/src/open_pulse/python_to_cpp.hpp b/src/open_pulse/python_to_cpp.hpp deleted file mode 100644 index 6d760ad028..0000000000 --- a/src/open_pulse/python_to_cpp.hpp +++ /dev/null @@ -1,490 +0,0 @@ -/** - * This code is part of Qiskit. - * - * (C) Copyright IBM 2018, 2019. - * - * This code is licensed under the Apache License, Version 2.0. You may - * obtain a copy of this license in the LICENSE.txt file in the root directory - * of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. - * - * Any modifications or derivative works of this code must retain this - * copyright notice, and modified files need to carry a notice indicating - * that they have been altered from the originals. - */ - -#ifndef _PYTHON_TO_CPP_HPP -#define _PYTHON_TO_CPP_HPP - -#include -#include -#include -#include -#include -#include -#include -#ifdef DEBUG -#include -#include -#endif -#include "misc/warnings.hpp" -DISABLE_WARNING_PUSH -#include -#include -DISABLE_WARNING_POP -#include "eval_hamiltonian.hpp" -#include "iterators.hpp" -#include "ordered_map.hpp" -#include "types.hpp" - -bool check_is_integer(PyObject *value) { - if (value == nullptr) - throw std::invalid_argument("PyObject is null!"); - - // Seems like this function checks every integer type - if (!PyLong_Check(value)) - return false; - - return true; -} - -bool check_is_string(PyObject *value) { - if (value == nullptr) - throw std::invalid_argument("PyObject is null!"); - - if (!PyUnicode_Check(value)) - return false; - - return true; -} - -bool check_is_floating_point(PyObject *value) { - if (value == nullptr) - throw std::invalid_argument("PyObject is null!"); - - if (!PyFloat_Check(value)) - return false; - - return true; -} - -bool check_is_complex(PyObject *value) { - if (value == nullptr) - throw std::invalid_argument("PyObject is null!"); - - if (!PyComplex_Check(value)) - return false; - - return true; -} - -bool check_is_list(PyObject *value) { - if (value == nullptr) - throw std::invalid_argument("Pyhton list is null!"); - - // Check that it's a list - if (!PyList_Check(value)) - return false; - - return true; -} - -bool check_is_tuple(PyObject *value) { - if (value == nullptr) - throw std::invalid_argument("Pyhton tuple is null!"); - - // Check that it's a tuple - if (!PyTuple_Check(value)) - return false; - - return true; -} - -bool check_is_dict(PyObject *value) { - if (value == nullptr) - throw std::invalid_argument("Pyhton dict is null!"); - - // Check that it's a dict - if (!PyDict_Check(value)) - return false; - - return true; -} - -bool check_is_np_array(py::array value) { - if (value.ptr() == nullptr) - throw std::invalid_argument("Numpy ndarray is null!"); - // Check that it's a numpy ndarray - if (value.ndim() == 0) - return false; - - return true; -} - -// Simon Brand technique to achive partial specialization on function templates -// https://www.fluentcpp.com/2017/08/15/function-templates-partial-specialization-cpp/ -// This "type" struct will carry T, but wil be ignored by the compiler later. -// It's like a help you give to the compiler so it can resolve the -// specialization -template -struct type {}; - -template -T get_value(type _, PyObject *value) { - throw std::invalid_argument("Cannot get value for this type!"); -} - -// TODO: We might want to expose only these two functions -template -T get_value(PyObject *value) { - return get_value(type{}, value); -} - -template -const T get_value(py::array value) { - return get_value(type{}, value); -} -// - -template <> -uint8_t get_value(type _, PyObject *value) { - return get_value(value); -} - -template <> -long get_value(type _, PyObject *value) { - if (!check_is_integer(value)) - throw std::invalid_argument("PyObject is not a long!"); - - long c_value = PyLong_AsLong(value); - auto ex = PyErr_Occurred(); - if (ex) - throw ex; - - return c_value; -} - -template <> -double get_value(type _, PyObject *value) { - if (!check_is_floating_point(value)) { - // it's not a floating point, but maybe an integer? - if (check_is_integer(value)) - return static_cast(get_value(value)); - - throw std::invalid_argument("PyObject is not a double!"); - } - - double c_value = PyFloat_AsDouble(value); - auto ex = PyErr_Occurred(); - if (ex) - throw ex; - - return c_value; -} - -template <> -std::complex get_value(type> _, PyObject *value) { - if (!check_is_complex(value)) - throw std::invalid_argument("PyObject is not a complex number!"); - - Py_complex c_value = PyComplex_AsCComplex(value); - auto ex = PyErr_Occurred(); - if (ex) - throw ex; - - return std::complex(c_value.real, c_value.imag); -} - -template <> -std::string get_value(type _, PyObject *value) { - if (!check_is_string(value)) - throw std::invalid_argument("PyObject is not a string!"); - - auto bytes_str = PyUnicode_AsUTF8String(value); - auto c_str = PyBytes_AsString(bytes_str); - - if (c_str == nullptr) - throw std::invalid_argument("Conversion to utf-8 has failed!"); - - return std::string(c_str); -} - -template -std::vector get_value(type> _, PyObject *value) { - if (!check_is_list(value)) - throw std::invalid_argument("PyObject is not a List!"); - - auto size = PyList_Size(value); - std::vector vector; - vector.reserve(size); - for (auto i = 0; i < size; ++i) { - auto py_item = PyList_GetItem(value, i); - if (py_item == nullptr) - continue; - auto item = get_value(py_item); - vector.emplace_back(item); - } - return vector; -} - -template -std::pair get_value(type> _, PyObject *value) { - if (!check_is_tuple(value)) - throw std::invalid_argument("PyObject is not a Tuple!"); - - if (PyTuple_Size(value) > 2) - throw std::invalid_argument( - "Tuples with more than 2 elements are not supported yet!!"); - - auto first_py_item = PyTuple_GetItem(value, 0); - if (first_py_item == nullptr) - throw std::invalid_argument("The tuple must have a first element"); - - auto second_py_item = PyTuple_GetItem(value, 1); - if (second_py_item == nullptr) - throw std::invalid_argument("The tuple must have a second element"); - - auto first_item = get_value(first_py_item); - auto second_item = get_value(second_py_item); - - return std::make_pair(first_item, second_item); -} - -template -std::unordered_map -get_value(type> _, PyObject *value) { - if (!check_is_dict(value)) - throw std::invalid_argument("PyObject is not a dictonary!!"); - - auto size = PyDict_Size(value); - std::unordered_map map; - map.reserve(size); - - PyObject *key, *val; - Py_ssize_t pos = 0; - while (PyDict_Next(value, &pos, &key, &val)) { - auto inner_key = get_value(key); - auto inner_value = get_value(val); - map.emplace(inner_key, inner_value); - } - return map; -} - -template -const ordered_map -get_value(type> _, PyObject *value) { - if (!check_is_dict(value)) - throw std::invalid_argument("PyObject is not a dictonary!!"); - - auto size = PyDict_Size(value); - ordered_map map; - map.reserve(size); - - PyObject *key, *val; - Py_ssize_t pos = 0; - while (PyDict_Next(value, &pos, &key, &val)) { - auto inner_key = get_value(key); - auto inner_value = get_value(val); - map.emplace(inner_key, inner_value); - } - return map; -} - -template <> -TermExpression get_value(type _, PyObject *value) { - if (!check_is_tuple(value)) - throw std::invalid_argument("PyObject is not a Tuple!"); - - if (PyTuple_Size(value) > 2) - throw std::invalid_argument( - "Tuples with more than 2 elements are not supported yet!!"); - - auto term = PyTuple_GetItem(value, 1); // 0 is first - if (term == nullptr) - throw std::invalid_argument("The tuple must have a second element"); - - auto term_expr = get_value(term); - return TermExpression(term_expr); -} - -template -class NpArray { -public: - NpArray() {} - NpArray(py::array array) { - if (array.ndim() > 2) { - throw std::runtime_error("NpArray can only wrap 1D or 2D arrays."); - } - _populate_data(array); - _populate_shape(array); - size = array.ndim() == 2 ? array.shape(0) * array.shape(1) : array.shape(0); - } - - const VecType *data = nullptr; - size_t size = 0; - - /** - * The shape of the array: like - * ```pyhton - * arr = np.array([0,1,2],[3,4,5]) - * arr.shape - **/ - std::vector shape; - - const VecType &operator[](size_t index) const { return data[index]; } - - const VecType &operator()(size_t i, size_t j) const { - return data[i * shape[1] + j]; - } - - bool operator==(const NpArray &other) const { - if (other.size != size || other.shape.size() != shape.size()) - return false; - - for (auto i = 0; i < other.size; ++i) { - if (data[i] != other[i]) - return false; - } - - for (auto i = 0; i < other.shape.size(); ++i) { - if (shape[i] != other.shape[i]) - return false; - } - - return true; - } - -private: - void _populate_shape(py::array array) { - if (!check_is_np_array(array)) - throw std::invalid_argument("py::array is not a numpy array!"); - - auto num_dims = array.ndim(); - shape.reserve(num_dims); - for (auto i = 0; i < num_dims; ++i) { - shape.emplace_back(array.shape(i)); - } - if (shape.size() == 1) { - shape.emplace_back(0); - } - } - - void _populate_data(py::array array) { - data = reinterpret_cast(array.request().ptr); - } -}; - -template -const NpArray get_value(type> _, py::array value) { - if (!check_is_np_array(value)) - throw std::invalid_argument("py::array is not a numpy array!"); - - return NpArray(value); -} - -template -const NpArray get_value(type> _, PyObject *value) { - py::array array = py::cast(value); - return get_value>(array); -} - -PyObject *_get_py_value_from_py_dict(PyObject *dict, const std::string &key) { - if (!check_is_dict(dict)) - throw std::invalid_argument("Python dictionary is null!"); - return PyDict_GetItemString(dict, key.c_str()); -} - -/** - * Returns a C++ vector from a Python list that is inside a Pyhton dictionary - *under a key. - * - * We assume that the item indexed by the key, it's a list: - * ```python - * my_dict = { "key": [1,2,3,4] } - * ``` - * ```c++ - * auto v = get_vec_from_dict_item(pyobj_dict, "key") - * for(auto& elem: v) - * std::cout << elem; - * ``` - * Output: - * ``` - * 1234 - * ``` - * - * @param dict PyObject* A pointer to a PyObject type representing a dictionary - * @return A vector of type VecType from the Pyhton's dictionary key. - **/ -template -const std::vector get_vec_from_dict_item(PyObject *dict, - const std::string &item_key) { - PyObject *py_value = _get_py_value_from_py_dict(dict, item_key); - return get_value>(py_value); -} - -/** - * Returns a C++ unordered_map from a Python dictionary that is inside another - *Pyhton dictionary under a key. - * - * We assume that the item indexed by the key, it's a dictionary: - * ```python - * my_dict = { "key": {"inner": 1, "renni": 255} } - * ``` - * ```c++ - * auto m = get_map_from_dict_item(pyobj_dict, "key"); - * for(auto& item: m) - * std::cout << " key:" << item.first << " val:" << item.second; - * ``` - * Output: - * ``` - * key:inner val:1 key:renni val:255 - * ``` - * - * @param dict PyObject* A pointer to a PyObject type representing a dictionary - * @return An unordered map of type from the Pyhton's - *dictionary key. - **/ -template -const std::unordered_map -get_map_from_dict_item(PyObject *dict, const std::string &item_key) { - PyObject *py_value = _get_py_value_from_py_dict(dict, item_key); - return get_value>(py_value); -} - -template -const ordered_map -get_ordered_map_from_dict_item(PyObject *dict, const std::string &item_key) { - PyObject *py_value = _get_py_value_from_py_dict(dict, item_key); - return get_value>(py_value); -} - -/** - * Returns a C++ value of type ValueTyep from a Python numeric that is inside a - *Pyhton dictionary under a key. - * - * We assume that the item indexed by the key, it's a numeric: - * ```python - * my_dict = { "key": 255} } - * ``` - * ```c++ - * auto l = get_value_from_dict_item(pyobj_dict, "key"); - * std::cout << "val: " << l; - * ``` - * Output: - * ``` - * 255 - * ``` - * - * @param dict PyObject* A pointer to a PyObject type representing a dictionary - * @return A long from the Pyhton's dictionary key. - **/ -template -ValueType get_value_from_dict_item(PyObject *dict, - const std::string &item_key) { - PyObject *py_value = _get_py_value_from_py_dict(dict, item_key); - if (py_value == Py_None) - return {}; - - return get_value(py_value); -} - -#endif //_PYTHON_TO_CPP_HPP diff --git a/src/open_pulse/test_python_to_cpp.cpp b/src/open_pulse/test_python_to_cpp.cpp deleted file mode 100644 index a67434f607..0000000000 --- a/src/open_pulse/test_python_to_cpp.cpp +++ /dev/null @@ -1,80 +0,0 @@ -#include "test_python_to_cpp.hpp" - -#include "python_to_cpp.hpp" -#include -#include -#include -#include -#include -#include - -bool cpp_test_py_list_to_cpp_vec(PyObject *val) { - // val = [1., 2., 3.] - auto vec = get_value>(val); - auto expected = std::vector{1., 2., 3.}; - return vec == expected; -} - -bool cpp_test_py_list_of_lists_to_cpp_vector_of_vectors(PyObject *val) { - // val = [[1., 2., 3.]] - auto vec = get_value>>(val); - auto expected = std::vector>{{1., 2., 3.}}; - return vec == expected; -} - -bool cpp_test_py_dict_string_numeric_to_cpp_map_string_numeric(PyObject *val) { - // val = {"key": 1} - auto map = get_value>(val); - auto expected = std::unordered_map{{"key", 1}}; - return map == expected; -} - -bool cpp_test_py_dict_string_list_of_list_of_doubles_to_cpp_map_string_vec_of_vecs_of_doubles( - PyObject *val) { - // val = {"key": [[1., 2., 3.]]} - auto map = get_value< - std::unordered_map>>>(val); - auto expected = - std::unordered_map>>{ - {"key", {{1., 2., 3.}}}}; - return map == expected; -} - -bool cpp_test_np_array_of_doubles(py::array val) { - // val = np.array([0., 1., 2., 3.]) - auto vec = get_value>(val); - if (vec[0] != 0. || vec[1] != 1. || vec[2] != 2. || vec[3] != 3.) - return false; - - return true; -} - -bool cpp_test_np_2D_array_of_doubles(py::array val) { - // val = np.array([[0., 1., 2., 3.],[10.,20.,30.,40]]) - auto vec = get_value>(val); - if (vec(0, 0) != 0. || vec(0, 1) != 1. || vec(0, 2) != 2. || - vec(0, 3) != 3. || vec(1, 0) != 10. || vec(1, 1) != 20. || - vec(1, 2) != 30. || vec(1, 3) != 40.) - return false; - - return true; -} - -bool cpp_test_evaluate_hamiltonians(PyObject *val) { - // TODO: Add tests! - return false; -} - -bool cpp_test_py_ordered_map(PyObject *val) { - // Ordered map should guarantee insertion order. - // val = {"D0": 1, "U0": 2, "D1": 3, "U1": 4} - std::vector order = {"D0", "U0", "D1", "U1"}; - auto ordered = get_value>(val); - size_t i = 0; - for (const auto &elem : ordered) { - auto key = elem.first; - if (key != order[i++]) - return false; - } - return true; -} diff --git a/src/open_pulse/test_python_to_cpp.hpp b/src/open_pulse/test_python_to_cpp.hpp deleted file mode 100644 index 67777ec385..0000000000 --- a/src/open_pulse/test_python_to_cpp.hpp +++ /dev/null @@ -1,88 +0,0 @@ -/** - * This code is part of Qiskit. - * - * (C) Copyright IBM 2018, 2019. - * - * This code is licensed under the Apache License, Version 2.0. You may - * obtain a copy of this license in the LICENSE.txt file in the root directory - * of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. - * - * Any modifications or derivative works of this code must retain this - * copyright notice, and modified files need to carry a notice indicating - * that they have been altered from the originals. - */ - -#ifndef _TEST_HELPERS_HPP -#define _TEST_HELPERS_HPP - -#include "misc/warnings.hpp" -DISABLE_WARNING_PUSH -#include -#include -DISABLE_WARNING_POP - -namespace py = pybind11; - -// TODO: Test QuantumObj -// TODO: Test Hamiltonian - -bool cpp_test_py_list_to_cpp_vec(PyObject *val); -bool cpp_test_py_list_of_lists_to_cpp_vector_of_vectors(PyObject *val); -bool cpp_test_py_dict_string_numeric_to_cpp_map_string_numeric(PyObject *val); -bool cpp_test_py_dict_string_list_of_list_of_doubles_to_cpp_map_string_vec_of_vecs_of_doubles( - PyObject *val); -bool cpp_test_np_array_of_doubles(py::array val); -bool cpp_test_np_2D_array_of_doubles(py::array val); -bool cpp_test_evaluate_hamiltonians(PyObject *val); -bool cpp_test_py_ordered_map(PyObject *val); - -PYBIND11_MODULE(test_python_to_cpp, m) { - m.doc() = "pybind11 test_python_to_cpp"; // optional module docstring - - m.def( - "test_py_list_to_cpp_vec", - [](py::list list) { return cpp_test_py_list_to_cpp_vec(list.ptr()); }, - ""); - m.def( - "test_py_list_of_lists_to_cpp_vector_of_vectors", - [](py::list list) { - return cpp_test_py_list_of_lists_to_cpp_vector_of_vectors(list.ptr()); - }, - ""); - m.def( - "test_py_dict_string_numeric_to_cpp_map_string_numeric", - [](py::dict dict) { - return cpp_test_py_dict_string_numeric_to_cpp_map_string_numeric( - dict.ptr()); - }, - ""); - m.def( - "test_py_dict_string_list_of_list_of_doubles_to_cpp_map_string_vec_of_" - "vecs_of_doubles", - [](py::dict dict) { - return cpp_test_py_dict_string_list_of_list_of_doubles_to_cpp_map_string_vec_of_vecs_of_doubles( - dict.ptr()); - }, - ""); - m.def( - "test_np_array_of_doubles", - [](py::array_t array_doubles) { - return cpp_test_np_array_of_doubles(array_doubles); - }, - ""); - m.def( - "test_np_2D_array_of_doubles", - [](py::array_t array_doubles) { - return cpp_test_np_2D_array_of_doubles(array_doubles); - }, - ""); - m.def( - "test_evaluate_hamiltonians", - [](py::list list) { return cpp_test_evaluate_hamiltonians(list.ptr()); }, - ""); - m.def( - "test_py_ordered_map", - [](py::dict dict) { return cpp_test_py_ordered_map(dict.ptr()); }, ""); -} - -#endif // _TEST_HELPERS_HPP \ No newline at end of file diff --git a/src/open_pulse/types.hpp b/src/open_pulse/types.hpp deleted file mode 100644 index 26a9470d53..0000000000 --- a/src/open_pulse/types.hpp +++ /dev/null @@ -1,27 +0,0 @@ -/** - * This code is part of Qiskit. - * - * (C) Copyright IBM 2018, 2019. - * - * This code is licensed under the Apache License, Version 2.0. You may - * obtain a copy of this license in the LICENSE.txt file in the root directory - * of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. - * - * Any modifications or derivative works of this code must retain this - * copyright notice, and modified files need to carry a notice indicating - * that they have been altered from the originals. - */ - -#ifndef _TEST_TYPES_HPP -#define _TEST_TYPES_HPP - -#include - -using complex_t = std::complex; - -struct TermExpression { - TermExpression(const std::string &term) : term(term) {} - std::string term; -}; - -#endif // _TEST_TYPES_HPP \ No newline at end of file diff --git a/src/open_pulse/zspmv.cpp b/src/open_pulse/zspmv.cpp deleted file mode 100644 index 44493c3f92..0000000000 --- a/src/open_pulse/zspmv.cpp +++ /dev/null @@ -1,160 +0,0 @@ -// This file is part of QuTiP: Quantum Toolbox in Python. -// -// Copyright (c) 2011 and later, QuSTaR. -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// 1. Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright -// notice, this list of conditions and the following disclaimer in the -// documentation and/or other materials provided with the distribution. -// -// 3. Neither the name of the QuTiP: Quantum Toolbox in Python nor the names -// of its contributors may be used to endorse or promote products derived -// from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A -// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -// ############################################################################# -#include - -#if defined(__GNUC__) && defined(__SSE3__) // Using GCC or CLANG and SSE3 -#include -void zspmvpy(const std::complex *__restrict__ data, - const int *__restrict__ ind, const int *__restrict__ ptr, - const std::complex *__restrict__ vec, - const std::complex a, - std::complex *__restrict__ out, const unsigned int nrows) { - size_t row, jj; - unsigned int row_start, row_end; - __m128d num1, num2, num3, num4; - for (row = 0; row < nrows; row++) { - num4 = _mm_setzero_pd(); - row_start = ptr[row]; - row_end = ptr[row + 1]; - for (jj = row_start; jj < row_end; jj++) { - num1 = _mm_loaddup_pd(&reinterpret_cast(data[jj])[0]); - num2 = _mm_set_pd(std::imag(vec[ind[jj]]), std::real(vec[ind[jj]])); - num3 = _mm_mul_pd(num2, num1); - num1 = _mm_loaddup_pd(&reinterpret_cast(data[jj])[1]); - num2 = _mm_shuffle_pd(num2, num2, 1); - num2 = _mm_mul_pd(num2, num1); - num3 = _mm_addsub_pd(num3, num2); - num4 = _mm_add_pd(num3, num4); - } - num1 = _mm_loaddup_pd(&reinterpret_cast(a)[0]); - num3 = _mm_mul_pd(num4, num1); - num1 = _mm_loaddup_pd(&reinterpret_cast(a)[1]); - num4 = _mm_shuffle_pd(num4, num4, 1); - num4 = _mm_mul_pd(num4, num1); - num3 = _mm_addsub_pd(num3, num4); - num2 = _mm_loadu_pd((double *)&out[row]); - num3 = _mm_add_pd(num2, num3); - _mm_storeu_pd((double *)&out[row], num3); - } -} -#elif defined(__GNUC__) // Using GCC or CLANG but no SSE3 -void zspmvpy(const std::complex *__restrict__ data, - const int *__restrict__ ind, const int *__restrict__ ptr, - const std::complex *__restrict__ vec, - const std::complex a, - std::complex *__restrict__ out, const unsigned int nrows) { - size_t row, jj; - unsigned int row_start, row_end; - std::complex dot; - for (row = 0; row < nrows; row++) { - dot = 0; - row_start = ptr[row]; - row_end = ptr[row + 1]; - for (jj = row_start; jj < row_end; jj++) { - dot += data[jj] * vec[ind[jj]]; - } - out[row] += a * dot; - } -} -#elif defined(_MSC_VER) && defined(__AVX__) // Visual Studio with AVX -#include -void zspmvpy(const std::complex *__restrict data, - const int *__restrict ind, const int *__restrict ptr, - const std::complex *__restrict vec, - const std::complex a, std::complex *__restrict out, - const unsigned int nrows) { - size_t row, jj; - unsigned int row_start, row_end; - __m128d num1, num2, num3, num4; - for (row = 0; row < nrows; row++) { - num4 = _mm_setzero_pd(); - row_start = ptr[row]; - row_end = ptr[row + 1]; - for (jj = row_start; jj < row_end; jj++) { - num1 = _mm_loaddup_pd(&reinterpret_cast(data[jj])[0]); - num2 = _mm_set_pd(std::imag(vec[ind[jj]]), std::real(vec[ind[jj]])); - num3 = _mm_mul_pd(num2, num1); - num1 = _mm_loaddup_pd(&reinterpret_cast(data[jj])[1]); - num2 = _mm_shuffle_pd(num2, num2, 1); - num2 = _mm_mul_pd(num2, num1); - num3 = _mm_addsub_pd(num3, num2); - num4 = _mm_add_pd(num3, num4); - } - num1 = _mm_loaddup_pd(&reinterpret_cast(a)[0]); - num3 = _mm_mul_pd(num4, num1); - num1 = _mm_loaddup_pd(&reinterpret_cast(a)[1]); - num4 = _mm_shuffle_pd(num4, num4, 1); - num4 = _mm_mul_pd(num4, num1); - num3 = _mm_addsub_pd(num3, num4); - num2 = _mm_loadu_pd((double *)&out[row]); - num3 = _mm_add_pd(num2, num3); - _mm_storeu_pd((double *)&out[row], num3); - } -} -#elif defined(_MSC_VER) // Visual Studio no AVX -void zspmvpy(const std::complex *__restrict data, - const int *__restrict ind, const int *__restrict ptr, - const std::complex *__restrict vec, - const std::complex a, std::complex *__restrict out, - const unsigned int nrows) { - size_t row, jj; - unsigned int row_start, row_end; - std::complex dot; - for (row = 0; row < nrows; row++) { - dot = 0; - row_start = ptr[row]; - row_end = ptr[row + 1]; - for (jj = row_start; jj < row_end; jj++) { - dot += data[jj] * vec[ind[jj]]; - } - out[row] += a * dot; - } -} -#else // Everything else -void zspmvpy(const std::complex *data, const int *ind, const int *ptr, - const std::complex *vec, const std::complex a, - std::complex *out, const unsigned int nrows) { - size_t row, jj; - unsigned int row_start, row_end; - std::complex dot; - for (row = 0; row < nrows; row++) { - dot = 0; - row_start = ptr[row]; - row_end = ptr[row + 1]; - for (jj = row_start; jj < row_end; jj++) { - dot += data[jj] * vec[ind[jj]]; - } - out[row] += a * dot; - } -} -#endif diff --git a/src/open_pulse/zspmv.hpp b/src/open_pulse/zspmv.hpp deleted file mode 100644 index 9a0c68142a..0000000000 --- a/src/open_pulse/zspmv.hpp +++ /dev/null @@ -1,51 +0,0 @@ -// This file is part of QuTiP: Quantum Toolbox in Python. -// -// Copyright (c) 2011 and later, QuSTaR. -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// 1. Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright -// notice, this list of conditions and the following disclaimer in the -// documentation and/or other materials provided with the distribution. -// -// 3. Neither the name of the QuTiP: Quantum Toolbox in Python nor the names -// of its contributors may be used to endorse or promote products derived -// from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A -// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -// ############################################################################# -#include - -#ifdef __GNUC__ -void zspmvpy(const std::complex *__restrict__ data, - const int *__restrict__ ind, const int *__restrict__ ptr, - const std::complex *__restrict__ vec, - const std::complex a, - std::complex *__restrict__ out, const unsigned int nrows); -#elif defined(_MSC_VER) -void zspmvpy(const std::complex *__restrict data, - const int *__restrict ind, const int *__restrict ptr, - const std::complex *__restrict vec, - const std::complex a, std::complex *__restrict out, - const unsigned int nrows); -#else -void zspmvpy(const std::complex *data, const int *ind, const int *ptr, - const std::complex *vec, const std::complex a, - std::complex *out, const unsigned int nrows); -#endif \ No newline at end of file diff --git a/src/simulators/density_matrix/densitymatrix_state.hpp b/src/simulators/density_matrix/densitymatrix_state.hpp index dbbca5a31f..ab8e3b4fd3 100644 --- a/src/simulators/density_matrix/densitymatrix_state.hpp +++ b/src/simulators/density_matrix/densitymatrix_state.hpp @@ -564,7 +564,6 @@ template size_t State::required_memory_mb( uint_t num_qubits, const std::vector &ops) const { (void)ops; // avoid unused variable compiler warning - (void)ops; // avoid unused variable compiler warning densmat_t tmp; return tmp.required_memory_mb(2 * num_qubits); } diff --git a/src/simulators/statevector/qubitvector.hpp b/src/simulators/statevector/qubitvector.hpp index 51b4457615..4ae87ed670 100644 --- a/src/simulators/statevector/qubitvector.hpp +++ b/src/simulators/statevector/qubitvector.hpp @@ -941,6 +941,8 @@ size_t QubitVector::required_memory_mb(uint_t num_qubits) const { size_t unit = std::log2(sizeof(std::complex)); size_t shift_mb = std::max(0, num_qubits + unit - 20); + if (shift_mb >= 63) + return SIZE_MAX; size_t mem_mb = 1ULL << shift_mb; return mem_mb; } diff --git a/src/simulators/statevector/qubitvector_thrust.hpp b/src/simulators/statevector/qubitvector_thrust.hpp index 66ca583c42..06aef07eec 100644 --- a/src/simulators/statevector/qubitvector_thrust.hpp +++ b/src/simulators/statevector/qubitvector_thrust.hpp @@ -946,8 +946,9 @@ size_t QubitVectorThrust::required_memory_mb(uint_t num_qubits) const { size_t unit = std::log2(sizeof(std::complex)); size_t shift_mb = std::max(0, num_qubits + unit - 20); + if (shift_mb >= 63) + return SIZE_MAX; size_t mem_mb = 1ULL << shift_mb; - return mem_mb; } diff --git a/src/simulators/statevector/statevector_state.hpp b/src/simulators/statevector/statevector_state.hpp index 405c142376..9a257ef08d 100644 --- a/src/simulators/statevector/statevector_state.hpp +++ b/src/simulators/statevector/statevector_state.hpp @@ -70,7 +70,7 @@ const Operations::OpSet StateOpSet( "sdg", "t", "tdg", "r", "rx", "ry", "rz", "rxx", "ryy", "rzz", "rzx", "ccx", "cswap", "mcx", "mcy", "mcz", "mcu1", "mcu2", "mcu3", "mcswap", "mcphase", - "mcr", "mcrx", "mcry", "mcry", "sx", "sxdg", "csx", + "mcr", "mcrx", "mcry", "mcrz", "sx", "sxdg", "csx", "mcsx", "csxdg", "mcsxdg", "delay", "pauli", "mcx_gray", "cu", "mcu", "mcp", "ecr"}); diff --git a/src/simulators/unitary/unitary_state.hpp b/src/simulators/unitary/unitary_state.hpp index cb93f1e388..0b86625cef 100644 --- a/src/simulators/unitary/unitary_state.hpp +++ b/src/simulators/unitary/unitary_state.hpp @@ -375,12 +375,9 @@ bool State::apply_batched_op(const int_t iChunk, template size_t State::required_memory_mb( uint_t num_qubits, const std::vector &ops) const { - // An n-qubit unitary as 2^2n complex doubles - // where each complex double is 16 bytes (void)ops; // avoid unused variable compiler warning - size_t shift_mb = std::max(0, num_qubits + 4 - 20); - size_t mem_mb = 1ULL << (2 * shift_mb); - return mem_mb; + unitary_matrix_t tmp; + return tmp.required_memory_mb(2 * num_qubits); } template diff --git a/test/terra/backends/aer_simulator/test_circuit.py b/test/terra/backends/aer_simulator/test_circuit.py index 91a8f4c97a..cd4c0f7806 100644 --- a/test/terra/backends/aer_simulator/test_circuit.py +++ b/test/terra/backends/aer_simulator/test_circuit.py @@ -13,10 +13,12 @@ AerSimulator Integration Tests """ from math import sqrt +from copy import deepcopy from ddt import ddt import numpy as np -from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister -from qiskit.circuit import CircuitInstruction +from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister, assemble +from qiskit.circuit.gate import Gate +from qiskit.circuit.library.standard_gates import HGate from test.terra.reference import ref_algorithms from test.terra.backends.simulator_test_case import SimulatorTestCase, supported_methods @@ -173,6 +175,47 @@ def test_partial_result_a_single_invalid_circuit(self): self.assertTrue(hasattr(result.results[1].data, "counts")) self.assertFalse(hasattr(result.results[0].data, "counts")) + def test_metadata_protected(self): + """Test metadata is consitently viewed from users""" + + qc = QuantumCircuit(2) + qc.metadata = {"foo": "bar", "object": object} + + circuits = [qc.copy() for _ in range(5)] + + backend = self.backend() + job = backend.run(circuits) + + for circuit in circuits: + self.assertTrue("foo" in circuit.metadata) + self.assertEqual(circuit.metadata["foo"], "bar") + self.assertEqual(circuit.metadata["object"], object) + + deepcopy(job.result()) + + def test_run_qobj(self): + """Test qobj run""" + + qubits = QuantumRegister(3) + clbits = ClassicalRegister(3) + + circuit = QuantumCircuit(qubits, clbits) + circuit.h(qubits[0]) + circuit.cx(qubits[0], qubits[1]) + circuit.cx(qubits[0], qubits[2]) + + for q, c in zip(qubits, clbits): + circuit.measure(q, c) + + backend = self.backend() + + shots = 1000 + with self.assertWarns(DeprecationWarning): + result = backend.run(assemble(circuit), shots=shots).result() + + self.assertSuccess(result) + self.compare_counts(result, [circuit], [{"0x0": 500, "0x7": 500}], delta=0.05 * shots) + def test_numpy_integer_shots(self): """Test implicit cast of shot option from np.int_ to int.""" @@ -214,3 +257,28 @@ def test_floating_shots(self): shots = int(shots) self.assertSuccess(result) self.assertEqual(sum([result.get_counts()[key] for key in result.get_counts()]), shots) + + def test_invalid_parameters(self): + """Test gates with invalid parameter length.""" + + backend = self.backend() + + class Custom(Gate): + def __init__(self, label=None): + super().__init__("p", 1, [], label=label) + + def _define(self): + q = QuantumRegister(1, "q") + qc = QuantumCircuit(q, name=self.name) + qc._append(HGate(), [q[0]], []) + self.definition = qc + + qc = QuantumCircuit(1) + qc.append(Custom(), [0]) + qc.measure_all() + + try: + backend.run(qc).result() + self.fail("do not reach here") + except Exception as e: + self.assertTrue('"params" is incorrect length' in repr(e)) diff --git a/test/terra/backends/aer_simulator/test_initialize.py b/test/terra/backends/aer_simulator/test_initialize.py index a493c28e68..739a77f709 100644 --- a/test/terra/backends/aer_simulator/test_initialize.py +++ b/test/terra/backends/aer_simulator/test_initialize.py @@ -193,3 +193,34 @@ def test_initialize_with_labels(self, method, device): for q1, p1 in enumerate([1, -1j]): index = int("{}{}{}{}".format(q4, q3, q2, q1), 2) self.assertAlmostEqual(actual[index], 0.25 * p1 * p2 * p3 * p4) + + @supported_methods(SUPPORTED_METHODS) + def test_initialize_with_int(self, method, device): + """Test sampling with int""" + backend = self.backend(method=method, device=device) + + circ = QuantumCircuit(4) + circ.initialize(5, [0, 1, 2]) + circ.save_statevector() + if "tensor_network" in method: + actual = backend.run(circ, shots=50).result().get_statevector(circ) + else: + actual = backend.run(circ).result().get_statevector(circ) + + self.assertAlmostEqual(actual[5], 1) + + @supported_methods(SUPPORTED_METHODS) + def test_initialize_with_int_twice(self, method, device): + """Test sampling with int twice""" + backend = self.backend(method=method, device=device) + + circ = QuantumCircuit(4) + circ.initialize(1, [0]) + circ.initialize(1, [2]) + circ.save_statevector() + if "tensor_network" in method: + actual = backend.run(circ, shots=50).result().get_statevector(circ) + else: + actual = backend.run(circ).result().get_statevector(circ) + + self.assertAlmostEqual(actual[5], 1) diff --git a/test/terra/backends/aer_simulator/test_options.py b/test/terra/backends/aer_simulator/test_options.py index b1d0afafb8..d1e1112f5a 100644 --- a/test/terra/backends/aer_simulator/test_options.py +++ b/test/terra/backends/aer_simulator/test_options.py @@ -19,6 +19,9 @@ from test.terra.backends.simulator_test_case import SimulatorTestCase, supported_methods from qiskit.quantum_info.random import random_unitary from qiskit.quantum_info import state_fidelity +from qiskit.providers.fake_provider import FakeMontreal + +from qiskit_aer import AerSimulator @ddt @@ -283,6 +286,23 @@ def test_statevector_memory(self): ) self.assertTrue("max memory: {}".format(max_memory_mb) in result.results[0].status) + @data( + "automatic", + "stabilizer", + "statevector", + "density_matrix", + "matrix_product_state", + "extended_stabilizer", + "unitary", + "superop", + ) + def test_num_qubits(self, method): + """Test number of qubits is correctly checked""" + + num_qubits = FakeMontreal().configuration().num_qubits + backend = AerSimulator.from_backend(FakeMontreal(), method=method) + self.assertGreaterEqual(backend.configuration().num_qubits, num_qubits) + def test_mps_svd_method(self): """Test env. variabe to change MPS SVD method""" # based on test_mps_options test @@ -318,4 +338,4 @@ def test_mps_svd_method(self): self.assertAlmostEqual(state_fidelity(original_sv, lapack_sv), 1.0) # should give the same state vector - self.assertAlmostEqual(state_fidelity(original_sv, lapack_dc_sv), 1.0) + self.assertAlmostEqual(state_fidelity(original_sv, lapack_dc_sv), 1.0) \ No newline at end of file diff --git a/test/terra/backends/aer_simulator/test_save_statevector.py b/test/terra/backends/aer_simulator/test_save_statevector.py index c8f374a54d..d5353dc49c 100644 --- a/test/terra/backends/aer_simulator/test_save_statevector.py +++ b/test/terra/backends/aer_simulator/test_save_statevector.py @@ -13,10 +13,12 @@ Integration Tests for SaveStatevector instruction """ +import sys from ddt import ddt import qiskit.quantum_info as qi from qiskit import QuantumCircuit, transpile from test.terra.backends.simulator_test_case import SimulatorTestCase, supported_methods +from qiskit.qasm3 import dumps, loads @ddt @@ -201,3 +203,44 @@ def test_save_statevector_cache_blocking(self, method, device): self.assertIn(label, simdata) value = simdata[label] self.assertEqual(value, target) + + @supported_methods( + [ + "automatic", + "statevector", + "matrix_product_state", + "extended_stabilizer", + "tensor_network", + ] + ) + def test_save_statevector_for_qasm3_circuit(self, method, device): + """Test save statevector instruction""" + # qiskit_qasm3_import, which is used in qiskit.qasm3 does not support 3.7 + if sys.version_info < (3, 8): + return + + backend = self.backend(method=method, device=device) + + # Stabilizer test circuit + circ = QuantumCircuit(3) + circ.h(0) + circ.sdg(0) + circ.cx(0, 1) + circ.cx(0, 2) + + # Target statevector + target = qi.Statevector(circ) + + circ = loads(dumps(circ)) + + # Add save to circuit + label = "state" + circ.save_statevector(label=label) + + # Run + result = backend.run(transpile(circ, backend, optimization_level=0), shots=1).result() + self.assertTrue(result.success) + simdata = result.data(0) + self.assertIn(label, simdata) + value = simdata[label] + self.assertEqual(value, target) diff --git a/test/terra/backends/aer_simulator/test_standard_gates.py b/test/terra/backends/aer_simulator/test_standard_gates.py index 51e5da6914..cdf54d8b0d 100644 --- a/test/terra/backends/aer_simulator/test_standard_gates.py +++ b/test/terra/backends/aer_simulator/test_standard_gates.py @@ -20,59 +20,14 @@ from qiskit import transpile import qiskit.quantum_info as qi +# fmt: off from qiskit.circuit.library.standard_gates import ( - CXGate, - CYGate, - CZGate, - DCXGate, - HGate, - IGate, - SGate, - SXGate, - SXdgGate, - SdgGate, - SwapGate, - XGate, - YGate, - ZGate, - TGate, - TdgGate, - iSwapGate, - C3XGate, - C4XGate, - CCXGate, - CHGate, - CSXGate, - CSwapGate, - CPhaseGate, - CRXGate, - CRYGate, - CRZGate, - CUGate, - CU1Gate, - CU3Gate, - CUGate, - PhaseGate, - RC3XGate, - RCCXGate, - RGate, - RXGate, - RXXGate, - RYGate, - RYYGate, - RZGate, - RZXGate, - RZZGate, - UGate, - U1Gate, - U2Gate, - U3Gate, - UGate, - MCXGate, - MCPhaseGate, - MCXGrayCode, - ECRGate, -) + CXGate, CYGate, CZGate, DCXGate, HGate, IGate, SGate, SXGate, SXdgGate, + SdgGate, SwapGate, XGate, YGate, ZGate, TGate, TdgGate, iSwapGate, C3XGate, + C4XGate, CCXGate, CHGate, CSXGate, CSwapGate, CPhaseGate, CRXGate, CRYGate, + CRZGate, CUGate, CU1Gate, CU3Gate, CUGate, PhaseGate, RC3XGate, RCCXGate, RGate, + RXGate, RXXGate, RYGate, RYYGate, RZGate, RZXGate, RZZGate, UGate, U1Gate, U2Gate, + U3Gate, UGate, MCXGate, MCPhaseGate, MCXGrayCode, ECRGate) CLIFFORD_GATES = [ diff --git a/test/terra/backends/pulse_sim_independent.py b/test/terra/backends/pulse_sim_independent.py deleted file mode 100644 index cca1a7b5a0..0000000000 --- a/test/terra/backends/pulse_sim_independent.py +++ /dev/null @@ -1,169 +0,0 @@ -""" -Independent/manual construction and solving of DEs for verification of pulse simulator. -""" - -import numpy as np -from scipy.linalg import expm -from qiskit_aer.pulse.de.DE_Methods import ScipyODE -from qiskit_aer.pulse.de.DE_Options import DE_Options - -I = np.eye(2, dtype=complex) -X = np.array([[0.0, 1.0], [1.0, 0.0]]) -Y = np.array([[0.0, -1j], [1j, 0.0]]) -Z = np.array([[1.0, 0.0], [0.0, -1.0]]) - - -def channel_values(channel_freqs, channel_samples, dt, t): - """Computes value of channels with given frequencies, samples, sample size and current time. - - Args: - channel_freqs (array): 1d array of channel frequencies - channel_samples (array): 2d array of channel samples, the first index being time step and - the second index indexing channel - dt (float): size of each sample - t (float): current time - - Returns: - array: array of channel values at the given time - """ - - sample_idx = int(t // dt) - if sample_idx >= len(channel_samples): - sample_idx = len(channel_samples) - 1 - - sample_vals = channel_samples[sample_idx] - - return np.real(sample_vals * np.exp(1j * 2 * np.pi * channel_freqs * t)) - - -def generator(drift, control_ops, chan_vals): - """Compute the generator - g = drift + chan_vals[0] * control_ops[0] + ... + chan_vals[0] * control_ops[0] - """ - - return drift + np.tensordot(chan_vals, control_ops, axes=1) - - -def generator_in_frame(drift, control_ops, chan_vals, diag_frame, t): - """Get the generator in the frame specified by diag_frame - - Args: - drift (array): 2d drift generator - control_ops (array): 3d array representing a list of control operators - chan_vals (array): 1d array of the same length as control_ops - diag_frame (array): 1d array representing an already diagonalized frame operator - assumed to be purely imaginary - t (float): time - - Returns: - array: generator in the given frame - """ - - G = generator(drift - np.diag(diag_frame), control_ops, chan_vals) - - U = np.exp(diag_frame * t) - U_inv = U.conj() - - return np.diag(U_inv) @ G @ np.diag(U) - - -def simulate_system(y0, drift, control_ops, channel_freqs, channel_samples, dt, diag_frame): - """Simulate the DE y' = G(t) @ y, where G(t) = drift + a0(t) * A0 + ... + ak(t) Ak, where - control_ops = [A0, ..., Ak], and the aj(t) are the values of the signals specified - by channel_freqs, channel_samples, and dt - - Args: - y0 (array): initial state - drift (array): 2d drift generator - control_ops (array): 3d array representing a list of control operators - channel_freqs (array): 1d array of channel frequencies - channel_samples (array): 2d array of channel samples, the first index being time step and - the second index indexing channel - dt (float): size of each sample - diag_frame (array): 1d array representing an already diagonalized frame operator - assumed to be purely imaginary - - Returns: - array: final state of the DE - """ - - # if all channel freqs are 0 simulate using matrix exponentiation - if all(channel_freqs == 0): - yf = y0 - for t_idx in range(len(channel_samples)): - yf = expm(generator(drift, control_ops, channel_samples[t_idx]) * dt) @ yf - - return yf - - # else, simulate using standard ODE solver - else: - # set up rhs function in frame - def rhs(t, y): - chan_vals = channel_values(channel_freqs, channel_samples, dt, t) - gen = generator_in_frame(drift, control_ops, chan_vals, diag_frame, t) - return gen @ y - - de_options = DE_Options(method="RK45") - ode_method = ScipyODE(t0=0.0, y0=y0, rhs=rhs, options=de_options) - - T = len(channel_samples) * dt - ode_method.integrate(T) - yf = np.exp(diag_frame * T) * ode_method.y - - return yf - - -def simulate_1q_model(y0, q_freq, r, drive_freqs, drive_samples, dt): - """Simulate a basic 1 qubit model H(t) = 2 pi q_freq Z / 2 + 2 pi r D(t) * X / 2, - where D(t) is the drive signal given by drive_freqs, drive_samples and dt - """ - - drift = -1j * 2 * np.pi * q_freq * Z / 2 - control_ops = -1j * np.array([2 * np.pi * r * X / 2]) - - frame_op = -1j * 2 * np.pi * drive_freqs[0] * np.array([1.0, -1.0]) / 2 - - return simulate_system(y0, drift, control_ops, drive_freqs, drive_samples, dt, frame_op) - - -def simulate_2q_exchange_model(y0, q_freqs, r, j, drive_freqs, drive_samples, dt): - """Simulate a basic 2 qubit model - H(t) = 2 pi q_freq[0] Z0 / 2 + 2 pi r D0(t) * X0 / 2 - + 2 pi q_freq[1] Z1 / 2 + 2 pi r D1(t) * X1 / 2 - + 2 pi j (I0I1 + X0X1 + Y0Y1 + Z0Z1) / 2 - where D0(t) and D1(t) are the drive signals given by drive_freqs, drive_samples and dt - """ - - ZI_diag = np.kron(np.array([1.0, -1.0]), np.array([1.0, 1.0])) - IZ_diag = np.kron(np.array([1.0, 1.0]), np.array([1.0, -1.0])) - XI = np.kron(X, I) - IX = np.kron(I, X) - - HI = (np.kron(I, I) + np.kron(X, X) + np.kron(Y, Y) + np.kron(Z, Z)) / 2 - - drift_diag = -1j * 2 * np.pi * (q_freqs[0] * IZ_diag + q_freqs[1] * ZI_diag) / 2 - drift_diag = drift_diag - 1j * 2 * np.pi * j * np.diag(HI) - drift = np.diag(drift_diag) - 1j * 2 * np.pi * j * (HI - np.diag(np.diag(HI))) - - control_ops = -1j * 2 * np.pi * r * np.array([IX, XI]) / 2 - - return simulate_system(y0, drift, control_ops, drive_freqs, drive_samples, dt, drift_diag) - - -def simulate_3d_oscillator_model(y0, osc_freq, anharm, r, drive_freqs, drive_samples, dt): - """Simulate a basic duffing odscillator model truncated at 3 dimensions, with - H(t) = 2 pi osc_freq[0] a^\dagger a + 2 pi anharm (a^\dagger a)(a^dagger a - 1) - + 2 pi r D(t) (a + a^\dagger) - where D(t) is the drive signal given by drive_freqs, drive_samples and dt - """ - - drift_diag = -1j * ( - 2 * np.pi * osc_freq * np.array([0.0, 1.0, 2.0]) - + np.pi * anharm * np.array([0.0, 0.0, 2.0]) - ) - - drift = np.diag(drift_diag) - osc_X = np.array([[0.0, 1.0, 0.0], [1.0, 0.0, np.sqrt(2)], [0.0, np.sqrt(2), 0.0]]) - control_ops = -1j * np.array([2 * np.pi * r * osc_X]) - - return simulate_system(y0, drift, control_ops, drive_freqs, drive_samples, dt, drift_diag) diff --git a/test/terra/backends/test_config_pulse_simulator.py b/test/terra/backends/test_config_pulse_simulator.py deleted file mode 100644 index b905ffe785..0000000000 --- a/test/terra/backends/test_config_pulse_simulator.py +++ /dev/null @@ -1,389 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2019, 2020. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -""" -Configurable PulseSimulator Tests -""" - -import sys -import unittest -import warnings -import numpy as np -from test.terra import common - -from qiskit import QuantumCircuit, transpile, schedule -from qiskit.providers.fake_provider import FakeArmonk, FakeAthens - -from qiskit_aer.backends import PulseSimulator -from qiskit.pulse import ( - Schedule, - Play, - ShiftPhase, - SetPhase, - Delay, - Acquire, - Waveform, - DriveChannel, - ControlChannel, - AcquireChannel, - MemorySlot, -) -from qiskit_aer.aererror import AerError - -from qiskit_aer.pulse.system_models.pulse_system_model import PulseSystemModel -from qiskit_aer.pulse.system_models.hamiltonian_model import HamiltonianModel -from qiskit.providers.models.backendconfiguration import UchannelLO - - -class TestConfigPulseSimulator(common.QiskitAerTestCase): - r"""PulseSimulator tests.""" - - def test_from_backend(self): - """Test that configuration, defaults, and properties are correclty imported.""" - - athens_backend = FakeAthens() - with self.assertWarns(DeprecationWarning): - athens_sim = PulseSimulator.from_backend(athens_backend) - - self.assertEqual(athens_backend.properties(), athens_sim.properties()) - # check that configuration is correctly imported - backend_dict = athens_backend.configuration().to_dict() - sim_dict = athens_sim.configuration().to_dict() - for key in sim_dict: - if key == "backend_name": - self.assertEqual(sim_dict[key], "pulse_simulator(fake_athens)") - elif key == "description": - desc = "A Pulse-based simulator configured from the backend: fake_athens" - self.assertEqual(sim_dict[key], desc) - elif key == "simulator": - self.assertTrue(sim_dict[key]) - elif key == "local": - self.assertTrue(sim_dict[key]) - elif key == "parametric_pulses": - self.assertEqual(sim_dict[key], []) - else: - self.assertEqual(sim_dict[key], backend_dict[key]) - - backend_dict = athens_backend.defaults().to_dict() - sim_dict = athens_sim.defaults().to_dict() - for key in sim_dict: - if key == "pulse_library": - # need to compare pulse libraries directly due to containing dictionaries - for idx, entry in enumerate(sim_dict[key]): - for entry_key in entry: - if entry_key == "samples": - self.assertTrue( - np.array_equal(entry[entry_key], backend_dict[key][idx][entry_key]) - ) - else: - self.assertTrue(entry[entry_key] == backend_dict[key][idx][entry_key]) - else: - self.assertEqual(sim_dict[key], backend_dict[key]) - - def test_from_backend_system_model(self): - """Test that the system model is correctly imported from the backend.""" - - athens_backend = FakeAthens() - with self.assertWarns(DeprecationWarning): - athens_sim = PulseSimulator.from_backend(athens_backend) - - # u channel lo - athens_attr = athens_backend.configuration().u_channel_lo - sim_attr = athens_sim.configuration().u_channel_lo - model_attr = athens_sim._system_model.u_channel_lo - self.assertTrue(sim_attr == athens_attr and model_attr == athens_attr) - - # dt - athens_attr = athens_backend.configuration().dt - sim_attr = athens_sim.configuration().dt - model_attr = athens_sim._system_model.dt - self.assertTrue(sim_attr == athens_attr and model_attr == athens_attr) - - def test_set_system_model_options(self): - """Test setting of options that need to be changed in multiple places.""" - - athens_backend = FakeAthens() - with self.assertWarns(DeprecationWarning): - athens_sim = PulseSimulator.from_backend(athens_backend) - - # u channel lo - set_attr = [[UchannelLO(0, 1.0 + 0.0j)]] - athens_sim.set_options(u_channel_lo=set_attr) - sim_attr = athens_sim.configuration().u_channel_lo - model_attr = athens_sim._system_model.u_channel_lo - self.assertTrue(sim_attr == set_attr and model_attr == set_attr) - - # dt - set_attr = 5.0 - athens_sim.set_options(dt=set_attr) - sim_attr = athens_sim.configuration().dt - model_attr = athens_sim._system_model.dt - self.assertTrue(sim_attr == set_attr and model_attr == set_attr) - - def test_from_backend_parametric_pulses(self): - """Verify that the parametric_pulses parameter is overriden in the PulseSimulator. - Results don't matter, just need to check that it runs. - """ - - with self.assertWarns(DeprecationWarning): - backend = PulseSimulator.from_backend(FakeAthens()) - - qc = QuantumCircuit(2) - qc.x(0) - qc.h(1) - qc.measure_all() - - sched = schedule(transpile(qc, backend), backend) - res = backend.run(sched).result() - - def test_parametric_pulses_error(self): - """Verify error is raised if a parametric pulse makes it into the digest.""" - - fake_backend = FakeAthens() - with self.assertWarns(DeprecationWarning): - backend = PulseSimulator.from_backend(fake_backend) - - # reset parametric_pulses option - backend.set_option("parametric_pulses", fake_backend.configuration().parametric_pulses) - - qc = QuantumCircuit(2) - qc.x(0) - qc.h(1) - qc.measure_all() - sched = schedule(transpile(qc, backend), backend) - - with self.assertRaises(AerError): - res = backend.run(sched).result() - - def test_set_meas_levels(self): - """Test setting of meas_levels.""" - - athens_backend = FakeAthens() - with self.assertWarns(DeprecationWarning): - athens_sim = PulseSimulator.from_backend(athens_backend) - - # test that a warning is thrown when meas_level 0 is attempted to be set - with warnings.catch_warnings(record=True) as w: - # Cause all warnings to always be triggered. - warnings.simplefilter("always") - - athens_sim.set_options(meas_levels=[0, 1, 2]) - - self.assertEqual(len(w), 1) - self.assertTrue("Measurement level 0 not supported" in str(w[-1].message)) - self.assertEqual(athens_sim.configuration().meas_levels, [1, 2]) - - self.assertTrue(athens_sim.configuration().meas_levels == [1, 2]) - - athens_sim.set_options(meas_levels=[2]) - self.assertTrue(athens_sim.configuration().meas_levels == [2]) - - def test_set_system_model_from_backend(self): - """Test setting system model when constructing from backend.""" - - armonk_backend = FakeArmonk() - system_model = self._system_model_1Q() - - # these are 1q systems so this doesn't make sense but can still be used to test - system_model.u_channel_lo = [[UchannelLO(0, 1.0 + 0.0j)]] - - armonk_sim = None - - # construct backend and catch warning - with warnings.catch_warnings(record=True) as w: - # Cause all warnings to always be triggered. - warnings.simplefilter("always") - - armonk_sim = PulseSimulator.from_backend( - backend=armonk_backend, system_model=system_model - ) - - self.assertEqual(len(w), 2) - self.assertTrue("will be removed in a future release" in str(w[0].message)) - self.assertTrue("inconsistencies" in str(w[-1].message)) - - # check that system model properties have been imported - self.assertEqual(armonk_sim.configuration().dt, system_model.dt) - self.assertEqual(armonk_sim.configuration().u_channel_lo, system_model.u_channel_lo) - - def test_set_system_model_in_constructor(self): - """Test setting system model when constructing.""" - - system_model = self._system_model_1Q() - - # these are 1q systems so this doesn't make sense but can still be used to test - system_model.u_channel_lo = [[UchannelLO(0, 1.0 + 0.0j)]] - - # construct directly - test_sim = None - # construct backend and verify no warnings - with warnings.catch_warnings(record=True) as w: - # Cause all warnings to always be triggered. - warnings.simplefilter("always") - - test_sim = PulseSimulator(system_model=system_model) - - self.assertEqual(len(w), 1) - self.assertTrue("will be removed in a future release" in str(w[0].message)) - - # check that system model properties have been imported - self.assertEqual(test_sim.configuration().dt, system_model.dt) - self.assertEqual(test_sim.configuration().u_channel_lo, system_model.u_channel_lo) - - def test_set_system_model_after_construction(self): - """Test setting the system model after construction.""" - - system_model = self._system_model_1Q() - - # these are 1q systems so this doesn't make sense but can still be used to test - system_model.u_channel_lo = [[UchannelLO(0, 1.0 + 0.0j)]] - - # first test setting after construction with no hamiltonian - with self.assertWarns(DeprecationWarning): - test_sim = PulseSimulator() - - with warnings.catch_warnings(record=True) as w: - # Cause all warnings to always be triggered. - warnings.simplefilter("always") - - test_sim.set_options(system_model=system_model) - self.assertEqual(len(w), 0) - - # check that system model properties have been imported - self.assertEqual(test_sim._system_model, system_model) - self.assertEqual(test_sim.configuration().dt, system_model.dt) - self.assertEqual(test_sim.configuration().u_channel_lo, system_model.u_channel_lo) - - # next, construct a pulse simulator with a config containing a Hamiltonian and observe - # warnings - armonk_backend = FakeArmonk() - with self.assertWarns(DeprecationWarning): - test_sim = PulseSimulator(configuration=armonk_backend.configuration()) - - # add system model and verify warning is raised - with warnings.catch_warnings(record=True) as w: - # Cause all warnings to always be triggered. - warnings.simplefilter("always") - - armonk_sim = test_sim.set_options(system_model=system_model) - - self.assertEqual(len(w), 1) - self.assertTrue("inconsistencies" in str(w[-1].message)) - - self.assertEqual(test_sim.configuration().dt, system_model.dt) - self.assertEqual(test_sim.configuration().u_channel_lo, system_model.u_channel_lo) - - def test_validation_num_acquires(self): - """Test that validation fails if 0 or >1 acquire is given in a schedule.""" - - with self.assertWarns(DeprecationWarning): - test_sim = PulseSimulator.from_backend(FakeArmonk()) - test_sim.set_options( - meas_level=2, - qubit_lo_freq=test_sim.defaults().qubit_freq_est, - meas_return="single", - shots=256, - ) - - # check that too many acquires results in an error - sched = self._1Q_schedule(num_acquires=2) - try: - test_sim.run(sched, validate=True).result() - except AerError as error: - self.assertTrue("does not support multiple Acquire" in error.message) - - # check that no acquires results in an error - sched = self._1Q_schedule(num_acquires=0) - try: - test_sim.run(sched, validate=True).result() - except AerError as error: - self.assertTrue("requires at least one Acquire" in error.message) - - def test_run_simulation_from_backend(self): - """Construct from a backend and run a simulation.""" - armonk_backend = FakeArmonk() - - # manually override parameters to insulate from future changes to FakeArmonk - freq_est = 4.97e9 - drive_est = 6.35e7 - armonk_backend.defaults().qubit_freq_est = [freq_est] - armonk_backend.configuration().hamiltonian["h_str"] = ["wq0*0.5*(I0-Z0)", "omegad0*X0||D0"] - armonk_backend.configuration().hamiltonian["vars"] = { - "wq0": 2 * np.pi * freq_est, - "omegad0": drive_est, - } - armonk_backend.configuration().hamiltonian["qub"] = {"0": 2} - dt = 2.2222222222222221e-10 - armonk_backend.configuration().dt = dt - - with self.assertWarns(DeprecationWarning): - armonk_sim = PulseSimulator.from_backend(armonk_backend) - armonk_sim.set_options(meas_level=2, meas_return="single", shots=1) - - total_samples = 250 - amp = np.pi / (drive_est * dt * total_samples) - - sched = self._1Q_schedule(total_samples, amp) - # run and verify that a pi pulse had been done - result = armonk_sim.run(sched).result() - final_vec = result.get_statevector() - probabilities = np.abs(final_vec) ** 2 - self.assertTrue(probabilities[0] < 1e-5) - self.assertTrue(probabilities[1] > 1 - 1e-5) - - def _system_model_1Q(self, omega_0=5.0, r=0.02): - """Constructs a standard model for a 1 qubit system. - - Args: - omega_0 (float): qubit frequency - r (float): drive strength - - Returns: - PulseSystemModel: model for qubit system - """ - - hamiltonian = {} - hamiltonian["h_str"] = ["2*np.pi*omega0*0.5*Z0", "2*np.pi*r*0.5*X0||D0"] - hamiltonian["vars"] = {"omega0": omega_0, "r": r} - hamiltonian["qub"] = {"0": 2} - ham_model = HamiltonianModel.from_dict(hamiltonian) - - u_channel_lo = [] - subsystem_list = [0] - dt = 1.0 - - return PulseSystemModel( - hamiltonian=ham_model, u_channel_lo=u_channel_lo, subsystem_list=subsystem_list, dt=dt - ) - - def _1Q_schedule(self, total_samples=100, amp=1.0, num_acquires=1): - """Creates a schedule for a single qubit. - - Args: - total_samples (int): number of samples in the drive pulse - amp (complex): amplitude of drive pulse - num_acquires (int): number of acquire instructions to include in the schedule - - Returns: - schedule (pulse schedule): - """ - - schedule = Schedule() - schedule |= Play(Waveform(amp * np.ones(total_samples)), DriveChannel(0)) - for _ in range(num_acquires): - schedule |= ( - Acquire(total_samples, AcquireChannel(0), MemorySlot(0)) << schedule.duration - ) - return schedule - - -if __name__ == "__main__": - unittest.main() diff --git a/test/terra/backends/test_parameterized_qobj.py b/test/terra/backends/test_parameterized_qobj.py index 2d64003439..4ca2c809ca 100644 --- a/test/terra/backends/test_parameterized_qobj.py +++ b/test/terra/backends/test_parameterized_qobj.py @@ -426,6 +426,66 @@ def test_parameters_with_barrier(self): self.assertSuccess(res) self.assertEqual(res.get_counts(), {"111": 1024}) + def test_parameters_with_conditional(self): + """Test parameterized circuit path with conditional""" + backend = AerSimulator() + circuit = QuantumCircuit(3, 3) + theta = Parameter("theta") + phi = Parameter("phi") + circuit.rx(theta, 0).c_if(1, False) + circuit.rx(theta, 1).c_if(2, False) + circuit.rx(theta, 2).c_if(0, False) + circuit.rx(phi, 0) + circuit.rx(phi, 1) + circuit.rx(phi, 2) + circuit.measure_all() + + parameter_binds = [{theta: [pi / 2], phi: [pi / 2]}] + res = backend.run([circuit], shots=1024, parameter_binds=parameter_binds).result() + + self.assertSuccess(res) + self.assertEqual(res.get_counts(), {"111 000": 1024}) + + def test_check_parameter_binds_exist(self): + """Test parameter_binds exists to simulate parameterized circuits""" + + shots = 1000 + backend = AerSimulator() + circuit = QuantumCircuit(2) + theta = Parameter("theta") + circuit.rx(theta, 0) + circuit.cx(0, 1) + circuit.measure_all() + with self.assertRaises(AerError): + res = backend.run(circuit, shots=shots).result() + + def test_global_phase_parameters(self): + """Test parameterized global phase""" + backend = AerSimulator(method="extended_stabilizer") + + theta = Parameter("theta") + circ = QuantumCircuit(2) + circ.ry(theta, 0) + circ.ry(theta, 1) + circ.measure_all() + + circ = transpile(circ, backend) + + parameter_binds = [{theta: [1, 2, 3]}] + res = backend.run( + [circ], shots=10, parameter_binds=parameter_binds, seed_simulator=100 + ).result() + + self.assertSuccess(res) + + circs = [] + for v in [1, 2, 3]: + circs.append(circ.bind_parameters({theta: v})) + + expected = backend.run(circs, shots=10, seed_simulator=100).result() + + self.assertEqual(res.get_counts(), expected.get_counts()) + if __name__ == "__main__": unittest.main() diff --git a/test/terra/backends/test_pulse_simulator.py b/test/terra/backends/test_pulse_simulator.py deleted file mode 100644 index cedb30d18f..0000000000 --- a/test/terra/backends/test_pulse_simulator.py +++ /dev/null @@ -1,1340 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2019. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -""" -PulseSimulator Integration Tests -""" - -import sys -import unittest -import functools -from test.terra import common - -import numpy as np -from scipy.linalg import expm -from scipy.special import erf - -from qiskit_aer.backends import PulseSimulator - -from qiskit.circuit import QuantumCircuit -from qiskit.compiler import transpile -from qiskit.quantum_info import state_fidelity -from qiskit.pulse import ( - Schedule, - Play, - ShiftPhase, - SetPhase, - Delay, - Acquire, - Waveform, - DriveChannel, - ControlChannel, - AcquireChannel, - MemorySlot, - SetFrequency, - ShiftFrequency, -) -from qiskit_aer.pulse.de.DE_Methods import ScipyODE -from qiskit_aer.pulse.de.DE_Options import DE_Options -from qiskit_aer.pulse.system_models.pulse_system_model import PulseSystemModel -from qiskit_aer.pulse.system_models.hamiltonian_model import HamiltonianModel -from qiskit.providers.models.backendconfiguration import UchannelLO -from qiskit.providers.fake_provider import FakeArmonk -from qiskit_aer.aererror import AerError - -from .pulse_sim_independent import ( - simulate_1q_model, - simulate_2q_exchange_model, - simulate_3d_oscillator_model, -) - - -class TestPulseSimulator(common.QiskitAerTestCase): - r"""PulseSimulator tests.""" - - def setUp(self): - """Set configuration settings for pulse simulator""" - super().setUp() - # Get pulse simulator backend - with self.assertWarns(DeprecationWarning): - self.backend_sim = PulseSimulator() - - self.X = np.array([[0.0, 1.0], [1.0, 0.0]]) - self.Y = np.array([[0.0, -1j], [1j, 0.0]]) - self.Z = np.array([[1.0, 0.0], [0.0, -1.0]]) - - def test_circuit_conversion(self): - with self.assertWarns(DeprecationWarning): - pulse_sim = PulseSimulator.from_backend(FakeArmonk()) - qc = QuantumCircuit(1) - qc.h(0) - qc.t(0) - qc.measure_all() - tqc = transpile(qc, pulse_sim) - result = pulse_sim.run(tqc, shots=1024).result() - self.assertDictAlmostEqual(result.get_counts(0), {"0": 512, "1": 512}, delta=128) - - def test_multiple_circuit_conversion(self): - with self.assertWarns(DeprecationWarning): - pulse_sim = PulseSimulator.from_backend(FakeArmonk()) - qc = QuantumCircuit(1) - qc.h(0) - qc.t(0) - qc.measure_all() - circs = [qc] * 5 - tqc = transpile(circs, pulse_sim) - result = pulse_sim.run(tqc, shots=1024).result() - counts = result.get_counts() - self.assertEqual(5, len(counts)) - for i in range(5): - self.assertDictAlmostEqual(result.get_counts(i), {"0": 512, "1": 512}, delta=128) - - # --------------------------------------------------------------------- - # Test single qubit gates - # --------------------------------------------------------------------- - - def test_x_gate(self): - """Test a schedule for a pi pulse on a 2 level system.""" - - # qubit frequency and drive frequency - omega_0 = 1.1329824 - omega_d = omega_0 - - # drive strength and length of pulse - r = 0.01 - total_samples = 100 - - # initial state and seed - y0 = np.array([1.0, 0.0]) - seed = 9000 - - # set up simulator - with self.assertWarns(DeprecationWarning): - pulse_sim = PulseSimulator(system_model=self._system_model_1Q(omega_0, r)) - pulse_sim.set_options( - meas_level=2, meas_return="single", meas_map=[[0]], qubit_lo_freq=[omega_d], shots=256 - ) - # set up constant pulse for doing a pi pulse - sched = self._1Q_constant_sched(total_samples) - result = pulse_sim.run(sched, initial_state=y0, seed=seed).result() - pulse_sim_yf = result.get_statevector() - - # set up and run independent simulation - samples = np.ones((total_samples, 1)) - - indep_yf = simulate_1q_model(y0, omega_0, r, np.array([omega_0]), samples, 1.0) - - # approximate analytic solution - phases = np.exp(-1j * 2 * np.pi * omega_0 * total_samples * np.array([1.0, -1.0]) / 2) - approx_yf = phases * np.array([0.0, -1j]) - - # test final state - self.assertGreaterEqual(state_fidelity(pulse_sim_yf, indep_yf), 1 - 10**-5) - self.assertGreaterEqual(state_fidelity(pulse_sim_yf, approx_yf), 0.99) - - # test counts - counts = result.get_counts() - exp_counts = {"1": 256} - self.assertDictAlmostEqual(counts, exp_counts) - - def test_x_gate_rwa(self): - """Test a schedule for a pi pulse on a 2 level system in the rotating frame with a - the rotating wave approximation.""" - - # qubit frequency and drive frequency - omega_0 = 0.0 - omega_d = omega_0 - - # drive strength and length of pulse - # in rotating wave with RWA the drive strength is halved - r = 0.01 / 2 - total_samples = 100 - - # initial state and seed - y0 = np.array([1.0, 0.0]) - seed = 9000 - - # set up simulator - with self.assertWarns(DeprecationWarning): - pulse_sim = PulseSimulator(system_model=self._system_model_1Q(omega_0, r)) - pulse_sim.set_options( - meas_level=2, meas_return="single", meas_map=[[0]], qubit_lo_freq=[omega_d], shots=1 - ) - # set up constant pulse for doing a pi pulse - sched = self._1Q_constant_sched(total_samples) - result = pulse_sim.run(sched, initial_state=y0, seed=seed).result() - pulse_sim_yf = result.get_statevector() - - # expected final state - yf = np.array([0.0, -1j]) - - # test final state - self.assertGreaterEqual(state_fidelity(pulse_sim_yf, yf), 1 - 10**-5) - - def test_x_half_gate(self): - """Test a schedule for a pi/2 pulse on a 2 level system. Same setup as test_x_gate but - with half the time.""" - - # qubit frequency and drive frequency - omega_0 = 1.1329824 - omega_d = omega_0 - - # drive strength and length of pulse - r = 0.01 - total_samples = 50 - - # initial state and seed - y0 = np.array([1.0, 0.0]) - seed = 9000 - - # set up simulator - with self.assertWarns(DeprecationWarning): - pulse_sim = PulseSimulator(system_model=self._system_model_1Q(omega_0, r)) - pulse_sim.set_options( - meas_level=2, meas_return="single", meas_map=[[0]], qubit_lo_freq=[omega_d], shots=256 - ) - # set up constant pulse for doing a pi pulse - schedule = self._1Q_constant_sched(total_samples) - result = pulse_sim.run(schedule, initial_state=y0, seed=seed).result() - pulse_sim_yf = result.get_statevector() - - # set up and run independent simulation - samples = np.ones((total_samples, 1)) - - indep_yf = simulate_1q_model(y0, omega_0, r, np.array([omega_d]), samples, 1.0) - - # approximate analytic solution - phases = np.exp(-1j * 2 * np.pi * omega_0 * total_samples * np.array([1.0, -1.0]) / 2) - approx_yf = phases * (expm(-1j * (np.pi / 4) * self.X) @ y0) - - # test final state - self.assertGreaterEqual(state_fidelity(pulse_sim_yf, indep_yf), 1 - 10**-5) - self.assertGreaterEqual(state_fidelity(pulse_sim_yf, approx_yf), 0.99) - - # test counts - counts = result.get_counts() - exp_counts = {"1": 132, "0": 124} - self.assertDictAlmostEqual(counts, exp_counts) - - def test_y_half_gate(self): - """Test a schedule for a pi/2 pulse about the y axis on a 2 level system. - Same setup as test_x_half_gate but with amplitude of pulse 1j.""" - - # qubit frequency and drive frequency - omega_0 = 1.1329824 - omega_d = omega_0 - - # drive strength and length of pulse - r = 0.01 - total_samples = 50 - - # initial state and seed - y0 = np.array([1.0, 0.0]) - seed = 9000 - - # set up simulator - with self.assertWarns(DeprecationWarning): - pulse_sim = PulseSimulator(system_model=self._system_model_1Q(omega_0, r)) - pulse_sim.set_options( - meas_level=2, meas_return="single", meas_map=[[0]], qubit_lo_freq=[omega_d], shots=256 - ) - # set up constant pulse for doing a pi pulse - sched = self._1Q_constant_sched(total_samples, amp=1j) - result = pulse_sim.run(sched, initial_state=y0, seed=seed).result() - pulse_sim_yf = result.get_statevector() - - # set up and run independent simulation - samples = 1j * np.ones((total_samples, 1)) - - indep_yf = simulate_1q_model(y0, omega_0, r, np.array([omega_d]), samples, 1.0) - - # approximate analytic solution - phases = np.exp(-1j * 2 * np.pi * omega_0 * total_samples * np.array([1.0, -1.0]) / 2) - approx_yf = phases * (expm(-1j * (np.pi / 4) * self.Y) @ y0) - - # test final state - self.assertGreaterEqual(state_fidelity(pulse_sim_yf, indep_yf), 1 - 10**-5) - self.assertGreaterEqual(state_fidelity(pulse_sim_yf, approx_yf), 0.99) - - # test counts - counts = result.get_counts() - exp_counts = {"1": 131, "0": 125} - self.assertDictAlmostEqual(counts, exp_counts) - - def test_1Q_noise(self): - """Tests simulation of noise operators. Uses the same schedule as test_x_gate, but - with a high level of amplitude damping noise. - """ - - # qubit frequency and drive frequency - omega_0 = 1.1329824 - omega_d = omega_0 - - # drive strength and length of pulse - r = 0.01 - total_samples = 100 - - # initial state, seed, and noise model - y0 = np.array([1.0, 0.0]) - seed = 9000 - noise_model = {"qubit": {"0": {"Sm": 1.0}}} - - # set up simulator - with self.assertWarns(DeprecationWarning): - pulse_sim = PulseSimulator( - system_model=self._system_model_1Q(omega_0, r), noise_model=noise_model - ) - pulse_sim.set_options( - meas_level=2, meas_return="single", meas_map=[[0]], qubit_lo_freq=[omega_d], shots=10 - ) - - # set up constant pulse for doing a pi pulse - schedule = self._1Q_constant_sched(total_samples) - result = pulse_sim.run(schedule, initial_state=y0, seed=seed).result() - - # test results - # This level of noise is high enough that all counts should yield 0, - # whereas in the noiseless simulation (in test_x_gate) all counts yield 1 - counts = result.get_counts() - exp_counts = {"0": 10} - self.assertDictAlmostEqual(counts, exp_counts) - - def test_unitary_parallel(self): - """Test for parallel solving in unitary simulation. Uses same schedule as test_x_gate but - runs it twice to trigger parallel execution. - """ - # qubit frequency and drive frequency - omega_0 = 1.0 - omega_d = omega_0 - - # drive strength and length of pulse - r = 0.01 - total_samples = 50 - - # initial state and seed - y0 = np.array([1.0, 0.0]) - seed = 9000 - - with self.assertWarns(DeprecationWarning): - pulse_sim = PulseSimulator(system_model=self._system_model_1Q(omega_0, r)) - pulse_sim.set_options( - meas_level=2, meas_return="single", meas_map=[[0]], qubit_lo_freq=[omega_d], shots=256 - ) - - # set up constant pulse for doing a pi pulse - schedule = self._1Q_constant_sched(total_samples) - result = pulse_sim.run([schedule, schedule], initial_state=y0, seed=seed).result() - - # test results, checking both runs in parallel - counts = result.get_counts() - exp_counts0 = {"1": 132, "0": 124} - exp_counts1 = {"0": 147, "1": 109} - self.assertDictAlmostEqual(counts[0], exp_counts0) - self.assertDictAlmostEqual(counts[1], exp_counts1) - - def test_dt_scaling_x_gate(self): - """Test that dt is being used correctly by the solver.""" - - total_samples = 100 - - # do the same thing as test_x_gate, but scale dt and all frequency parameters - # define test case for a single scaling - def scale_test(scale): - # qubit frequency and drive frequency - omega_0 = 1.0 / scale - omega_d = omega_0 - - # drive strength and length of pulse - r = 0.01 / scale - total_samples = 100 - - # initial state and seed - y0 = np.array([1.0, 0.0]) - seed = 9000 - - # set up simulator - system_model = self._system_model_1Q(omega_0, r) - with self.assertWarns(DeprecationWarning): - pulse_sim = PulseSimulator(system_model=self._system_model_1Q(omega_0, r)) - pulse_sim.set_options(dt=scale) - pulse_sim.set_options( - meas_level=2, - meas_return="single", - meas_map=[[0]], - qubit_lo_freq=[omega_d], - shots=256, - ) - - # set up constant pulse for doing a pi pulse - schedule = self._1Q_constant_sched(total_samples) - result = pulse_sim.run(schedule, initial_state=y0, seed=seed).result() - - pulse_sim_yf = result.get_statevector() - - # set up and run independent simulation - samples = np.ones((total_samples, 1)) - - indep_yf = simulate_1q_model(y0, omega_0, r, np.array([omega_0]), samples, scale) - - # approximate analytic solution - phases = np.exp(-1j * 2 * np.pi * omega_0 * total_samples * np.array([1.0, -1.0]) / 2) - approx_yf = phases * np.array([0.0, -1j]) - - # test final state - self.assertGreaterEqual(state_fidelity(pulse_sim_yf, indep_yf), 1 - 10**-5) - self.assertGreaterEqual(state_fidelity(pulse_sim_yf, approx_yf), 0.99) - - counts = result.get_counts() - exp_counts = {"1": 256} - - self.assertDictAlmostEqual(counts, exp_counts) - - # set scales and run tests - scales = [2.0, 0.1234, 10.0**5, 10**-5] - for scale in scales: - scale_test(scale) - - def test_arbitrary_constant_drive(self): - """Test a few examples w/ arbitary drive, phase and amplitude.""" - - total_samples = 100 - num_tests = 3 - - omega_0 = 1.0 - omega_d_vals = [omega_0 + 1.0, omega_0 + 0.02, omega_0 + 0.005] - r_vals = [3 / total_samples, 5 / total_samples, 0.1] - phase_vals = [5 * np.pi / 7, 19 * np.pi / 14, np.pi / 4] - - # initial state and seed - y0 = np.array([1.0, 0.0]) - seed = 9000 - - for i in range(num_tests): - with self.subTest(i=i): - # set up simulator - with self.assertWarns(DeprecationWarning): - pulse_sim = PulseSimulator( - system_model=self._system_model_1Q(omega_0, r_vals[i]) - ) - pulse_sim.set_options( - meas_level=2, - meas_return="single", - meas_map=[[0]], - qubit_lo_freq=[omega_d_vals[i]], - shots=1, - ) - - schedule = self._1Q_constant_sched(total_samples, amp=np.exp(-1j * phase_vals[i])) - result = pulse_sim.run(schedule, initial_state=y0, seed=seed).result() - pulse_sim_yf = result.get_statevector() - - # set up and run independent simulation - samples = np.exp(-1j * phase_vals[i]) * np.ones((total_samples, 1)) - - indep_yf = simulate_1q_model( - y0, omega_0, r_vals[i], np.array([omega_d_vals[i]]), samples, 1.0 - ) - - # approximate analytic solution - phases = np.exp( - -1j * 2 * np.pi * omega_d_vals[i] * total_samples * np.array([1.0, -1.0]) / 2 - ) - detuning = omega_0 - omega_d_vals[i] - amp = np.exp(-1j * phase_vals[i]) - rwa_ham = ( - 2 - * np.pi - * ( - detuning * self.Z / 2 - + r_vals[i] * np.array([[0, amp.conj()], [amp, 0.0]]) / 4 - ) - ) - approx_yf = phases * (expm(-1j * rwa_ham * total_samples) @ y0) - - # test final state - self.assertGreaterEqual(state_fidelity(pulse_sim_yf, indep_yf), 1 - 10**-5) - self.assertGreaterEqual(state_fidelity(pulse_sim_yf, approx_yf), 0.99) - - def test_3d_oscillator(self): - """Test simulation of a duffing oscillator truncated to 3 dimensions.""" - - total_samples = 100 - - freq = 5.0 - anharm = -0.33 - - # Test pi pulse - r = 0.5 / total_samples - - # set up simulator - system_model = system_model = self._system_model_3d_oscillator(freq, anharm, r) - with self.assertWarns(DeprecationWarning): - pulse_sim = PulseSimulator(system_model=system_model) - pulse_sim.set_options( - meas_level=2, meas_return="single", meas_map=[[0]], qubit_lo_freq=[freq], shots=1 - ) - - schedule = self._1Q_constant_sched(total_samples) - result = pulse_sim.run(schedule).result() - pulse_sim_yf = result.get_statevector() - - # set up and run independent simulation - y0 = np.array([1.0, 0.0, 0.0]) - samples = np.ones((total_samples, 1)) - indep_yf = simulate_3d_oscillator_model(y0, freq, anharm, r, np.array([freq]), samples, 1.0) - - # test final state - self.assertGreaterEqual(state_fidelity(pulse_sim_yf, indep_yf), 1 - 10**-5) - - # test with different input state - y0 = np.array([0.0, 0.0, 1.0]) - - # Test some irregular value - r = 1.49815 / total_samples - system_model = self._system_model_3d_oscillator(freq, anharm, r) - pulse_sim.set_options(system_model=system_model) - pulse_sim.set_options( - meas_level=2, meas_return="single", meas_map=[[0]], qubit_lo_freq=[freq], shots=1 - ) - - schedule = self._1Q_constant_sched(total_samples) - result = pulse_sim.run(schedule, initial_state=y0).result() - pulse_sim_yf = result.get_statevector() - - samples = np.ones((total_samples, 1)) - indep_yf = simulate_3d_oscillator_model(y0, freq, anharm, r, np.array([freq]), samples, 1.0) - - # test final state - self.assertGreaterEqual(state_fidelity(pulse_sim_yf, indep_yf), 1 - 10**-5) - - def test_2Q_interaction(self): - r"""Test 2 qubit interaction via controlled operations using u channels.""" - - total_samples = 100 - - # set coupling term and drive channels to 0 frequency - j = 0.5 / total_samples - omega_d0 = 0.0 - omega_d1 = 0.0 - - y0 = np.kron(np.array([1.0, 0.0]), np.array([0.0, 1.0])) - seed = 9000 - - with self.assertWarns(DeprecationWarning): - pulse_sim = PulseSimulator(system_model=self._system_model_2Q(j)) - pulse_sim.set_options( - meas_level=2, - meas_return="single", - meas_map=[[0]], - qubit_lo_freq=[omega_d0, omega_d1], - shots=1, - ) - - schedule = self._2Q_constant_sched(total_samples) - result = pulse_sim.run(schedule, initial_state=y0, seed=seed).result() - pulse_sim_yf = result.get_statevector() - - # exact analytic solution - yf = expm(-1j * 0.5 * 2 * np.pi * np.kron(self.X, self.Z) / 4) @ y0 - - self.assertGreaterEqual(state_fidelity(pulse_sim_yf, yf), 1 - (10**-5)) - - # run with different initial state - y0 = np.kron(np.array([1.0, 0.0]), np.array([1.0, 0.0])) - - result = pulse_sim.run(schedule, initial_state=y0, seed=seed).result() - pulse_sim_yf = result.get_statevector() - - # exact analytic solution - yf = expm(-1j * 0.5 * 2 * np.pi * np.kron(self.X, self.Z) / 4) @ y0 - - self.assertGreaterEqual(state_fidelity(pulse_sim_yf, yf), 1 - (10**-5)) - - def test_subsystem_restriction(self): - r"""Test behavior of subsystem_list subsystem restriction""" - - total_samples = 100 - - # set coupling term and drive channels to 0 frequency - j = 0.5 / total_samples - omega_d = 0.0 - - subsystem_list = [0, 2] - y0 = np.kron(np.array([1.0, 0.0]), np.array([0.0, 1.0])) - - system_model = self._system_model_3Q(j, subsystem_list=subsystem_list) - with self.assertWarns(DeprecationWarning): - pulse_sim = PulseSimulator(system_model=system_model) - - schedule = self._3Q_constant_sched(total_samples, u_idx=0, subsystem_list=subsystem_list) - - result = pulse_sim.run( - [schedule], - meas_level=2, - meas_return="single", - meas_map=[[0]], - qubit_lo_freq=[omega_d, omega_d, omega_d], - shots=1, - initial_state=y0, - ).result() - - pulse_sim_yf = result.get_statevector() - - yf = expm(-1j * 0.5 * 2 * np.pi * np.kron(self.X, self.Z) / 4) @ y0 - - self.assertGreaterEqual(state_fidelity(pulse_sim_yf, yf), 1 - (10**-5)) - - y0 = np.kron(np.array([1.0, 0.0]), np.array([1.0, 0.0])) - - result = pulse_sim.run( - [schedule], - meas_level=2, - meas_return="single", - meas_map=[[0]], - qubit_lo_freq=[omega_d, omega_d, omega_d], - shots=1, - initial_state=y0, - ).result() - - pulse_sim_yf = result.get_statevector() - - yf = expm(-1j * 0.5 * 2 * np.pi * np.kron(self.X, self.Z) / 4) @ y0 - - self.assertGreaterEqual(state_fidelity(pulse_sim_yf, yf), 1 - (10**-5)) - - subsystem_list = [1, 2] - system_model = self._system_model_3Q(j, subsystem_list=subsystem_list) - - y0 = np.kron(np.array([1.0, 0.0]), np.array([0.0, 1.0])) - pulse_sim.set_options(system_model=system_model) - schedule = self._3Q_constant_sched(total_samples, u_idx=1, subsystem_list=subsystem_list) - result = pulse_sim.run( - [schedule], - meas_level=2, - meas_return="single", - meas_map=[[0]], - qubit_lo_freq=[omega_d, omega_d, omega_d], - shots=1, - initial_state=y0, - ).result() - pulse_sim_yf = result.get_statevector() - - yf = expm(-1j * 0.5 * 2 * np.pi * np.kron(self.X, self.Z) / 4) @ y0 - - self.assertGreaterEqual(state_fidelity(pulse_sim_yf, yf), 1 - (10**-5)) - - y0 = np.kron(np.array([1.0, 0.0]), np.array([1.0, 0.0])) - pulse_sim.set_options(initial_state=y0) - - result = pulse_sim.run( - [schedule], - meas_level=2, - meas_return="single", - meas_map=[[0]], - qubit_lo_freq=[omega_d, omega_d, omega_d], - shots=1, - ).result() - pulse_sim_yf = result.get_statevector() - - yf = expm(-1j * 0.5 * 2 * np.pi * np.kron(self.X, self.Z) / 4) @ y0 - - self.assertGreaterEqual(state_fidelity(pulse_sim_yf, yf), 1 - (10**-5)) - - def test_simulation_without_variables(self): - r"""Test behavior of subsystem_list subsystem restriction. - Same setup as test_x_gate, but with explicit Hamiltonian construction without - variables - """ - - ham_dict = {"h_str": ["np.pi*Z0", "0.02*np.pi*X0||D0"], "qub": {"0": 2}} - ham_model = HamiltonianModel.from_dict(ham_dict) - - u_channel_lo = [] - subsystem_list = [0] - dt = 1.0 - - system_model = PulseSystemModel( - hamiltonian=ham_model, u_channel_lo=u_channel_lo, subsystem_list=subsystem_list, dt=dt - ) - with self.assertWarns(DeprecationWarning): - pulse_sim = PulseSimulator( - system_model=system_model, initial_state=np.array([1.0, 0.0]), seed=9000 - ) - pulse_sim.set_options( - meas_level=2, meas_return="single", meas_map=[[0]], qubit_lo_freq=[1.0], shots=256 - ) - - # set up schedule and qobj - total_samples = 50 - schedule = self._1Q_constant_sched(total_samples) - # run simulation - result = pulse_sim.run(schedule).result() - - # test results - counts = result.get_counts() - exp_counts = {"1": 256} - self.assertDictAlmostEqual(counts, exp_counts) - - def test_meas_level_1(self): - """Test measurement level 1.""" - - shots = 10000 # run large number of shots for good proportions - - total_samples = 100 - omega_0 = 1.0 - omega_d = omega_0 - - # Require omega_a*time = pi to implement pi pulse (x gate) - # num of samples gives time - r = 1.0 / (2 * total_samples) - - system_model = self._system_model_1Q(omega_0, r) - - amp = np.exp(-1j * np.pi / 2) - schedule = self._1Q_constant_sched(total_samples, amp=amp) - - y0 = np.array([1.0, 0.0]) - with self.assertWarns(DeprecationWarning): - pulse_sim = PulseSimulator(system_model=system_model, initial_state=y0, seed=9000) - pulse_sim.set_options( - meas_level=1, meas_return="single", meas_map=[[0]], qubit_lo_freq=[1.0], shots=shots - ) - result = pulse_sim.run(schedule).result() - pulse_sim_yf = result.get_statevector() - - samples = amp * np.ones((total_samples, 1)) - indep_yf = simulate_1q_model(y0, omega_0, r, np.array([omega_d]), samples, 1.0) - - # test final state - self.assertGreaterEqual(state_fidelity(pulse_sim_yf, indep_yf), 1 - 10**-5) - - # Verify that (about) half the IQ vals have abs val 1 and half have abs val 0 - # (use prop for easier comparison) - mem = np.abs(result.get_memory()[:, 0]) - - iq_prop = {"0": 0, "1": 0} - for i in mem: - if i == 0: - iq_prop["0"] += 1 / shots - else: - iq_prop["1"] += 1 / shots - - exp_prop = {"0": 0.5, "1": 0.5} - - self.assertDictAlmostEqual(iq_prop, exp_prop, delta=0.01) - - def test_gaussian_drive(self): - """Test gaussian drive pulse using meas_level_2. Set omega_d0=omega_0 (drive on resonance), - phi=0, omega_a = pi/time - """ - - # set omega_0, omega_d0 equal (use qubit frequency) -> drive on resonance - total_samples = 100 - omega_0 = 1.0 - omega_d = omega_0 - - # Require omega_a*time = pi to implement pi pulse (x gate) - # num of samples gives time - r = np.pi / total_samples - - # initial state and seed - y0 = np.array([1.0, 0.0]) - seed = 9000 - - # Test gaussian drive results for a few different sigma - gauss_sigmas = [total_samples / 6, total_samples / 3, total_samples] - - # set up pulse simulator - with self.assertWarns(DeprecationWarning): - pulse_sim = PulseSimulator(system_model=self._system_model_1Q(omega_0, r)) - pulse_sim.set_options( - meas_level=2, meas_return="single", meas_map=[[0]], qubit_lo_freq=[omega_d], shots=1 - ) - - for gauss_sigma in gauss_sigmas: - with self.subTest(gauss_sigma=gauss_sigma): - times = 1.0 * np.arange(total_samples) - gaussian_samples = np.exp(-(times**2) / 2 / gauss_sigma**2) - drive_pulse = Waveform(gaussian_samples, name="drive_pulse") - - # construct schedule - schedule = Schedule() - schedule |= Play(drive_pulse, DriveChannel(0)) - schedule |= Acquire(1, AcquireChannel(0), MemorySlot(0)) << schedule.duration - - result = pulse_sim.run(schedule, initial_state=y0, seed=seed).result() - pulse_sim_yf = result.get_statevector() - - # run independent simulation - yf = simulate_1q_model(y0, omega_0, r, np.array([omega_d]), gaussian_samples, 1.0) - - # Check fidelity of statevectors - self.assertGreaterEqual(state_fidelity(pulse_sim_yf, yf), 1 - (10**-5)) - - def test_2Q_exchange(self): - r"""Test a more complicated 2q simulation""" - - q_freqs = [5.0, 5.1] - r = 0.02 - j = 0.02 - total_samples = 25 - - hamiltonian = {} - hamiltonian["h_str"] = [ - "2*np.pi*v0*0.5*Z0", - "2*np.pi*v1*0.5*Z1", - "2*np.pi*r*0.5*X0||D0", - "2*np.pi*r*0.5*X1||D1", - "2*np.pi*j*0.5*I0*I1", - "2*np.pi*j*0.5*X0*X1", - "2*np.pi*j*0.5*Y0*Y1", - "2*np.pi*j*0.5*Z0*Z1", - ] - hamiltonian["vars"] = {"v0": q_freqs[0], "v1": q_freqs[1], "r": r, "j": j} - hamiltonian["qub"] = {"0": 2, "1": 2} - ham_model = HamiltonianModel.from_dict(hamiltonian) - - # set the U0 to have frequency of drive channel 0 - u_channel_lo = [] - subsystem_list = [0, 1] - dt = 1.0 - - system_model = PulseSystemModel( - hamiltonian=ham_model, u_channel_lo=u_channel_lo, subsystem_list=subsystem_list, dt=dt - ) - - # try some random schedule - schedule = Schedule() - drive_pulse = Waveform(np.ones(total_samples)) - schedule += Play(drive_pulse, DriveChannel(0)) - schedule |= Play(drive_pulse, DriveChannel(1)) << 2 * total_samples - - schedule |= Acquire(total_samples, AcquireChannel(0), MemorySlot(0)) << 3 * total_samples - schedule |= Acquire(total_samples, AcquireChannel(1), MemorySlot(1)) << 3 * total_samples - - y0 = np.array([1.0, 0.0, 0.0, 0.0]) - with self.assertWarns(DeprecationWarning): - pulse_sim = PulseSimulator(system_model=system_model, initial_state=y0, seed=9000) - pulse_sim.set_options( - meas_level=2, meas_return="single", meas_map=[[0]], qubit_lo_freq=q_freqs, shots=1000 - ) - result = pulse_sim.run(schedule).result() - pulse_sim_yf = result.get_statevector() - - # set up and run independent simulation - d0_samps = np.concatenate((np.ones(total_samples), np.zeros(2 * total_samples))) - d1_samps = np.concatenate((np.zeros(2 * total_samples), np.ones(total_samples))) - samples = np.array([d0_samps, d1_samps]).transpose() - q_freqs = np.array(q_freqs) - yf = simulate_2q_exchange_model(y0, q_freqs, r, j, q_freqs, samples, 1.0) - - # Check fidelity of statevectors - self.assertGreaterEqual(state_fidelity(pulse_sim_yf, yf), 1 - (10**-5)) - - def test_delay_instruction(self): - """Test for delay instruction.""" - - # construct system model specifically for this - hamiltonian = {} - hamiltonian["h_str"] = ["0.5*r*X0||D0", "0.5*r*Y0||D1"] - hamiltonian["vars"] = {"r": np.pi} - hamiltonian["qub"] = {"0": 2} - ham_model = HamiltonianModel.from_dict(hamiltonian) - - u_channel_lo = [] - subsystem_list = [0] - dt = 1.0 - - system_model = PulseSystemModel( - hamiltonian=ham_model, u_channel_lo=u_channel_lo, subsystem_list=subsystem_list, dt=dt - ) - - # construct a schedule that should result in a unitary -Z if delays are correctly handled - # i.e. do a pi rotation about x, sandwiched by pi/2 rotations about y in opposite directions - # so that the x rotation is transformed into a z rotation. - # if delays are not handled correctly this process should fail - sched = Schedule() - sched += Play(Waveform([0.5]), DriveChannel(1)) - sched += Delay(1, DriveChannel(1)) - sched += Play(Waveform([-0.5]), DriveChannel(1)) - - sched += Delay(1, DriveChannel(0)) - sched += Play(Waveform([1.0]), DriveChannel(0)) - - sched |= Acquire(1, AcquireChannel(0), MemorySlot(0)) << sched.duration - - # Result of schedule should be the unitary -1j*Z, so check rotation of an X eigenstate - with self.assertWarns(DeprecationWarning): - pulse_sim = PulseSimulator( - system_model=system_model, initial_state=np.array([1.0, 1.0]) / np.sqrt(2) - ) - pulse_sim.set_options( - meas_return="single", meas_map=[[0]], qubit_lo_freq=[0.0, 0.0], shots=1 - ) - - results = pulse_sim.run(sched).result() - - statevector = results.get_statevector() - expected_vector = np.array([-1j, 1j]) / np.sqrt(2) - - self.assertGreaterEqual(state_fidelity(statevector, expected_vector), 1 - (10**-5)) - - # verify validity of simulation when no delays included - sched = Schedule() - sched += Play(Waveform([0.5]), DriveChannel(1)) - sched += Play(Waveform([-0.5]), DriveChannel(1)) - - sched += Play(Waveform([1.0]), DriveChannel(0)) - - sched |= Acquire(1, AcquireChannel(0), MemorySlot(0)) << sched.duration - - results = pulse_sim.run(sched).result() - - statevector = results.get_statevector() - U = expm(1j * np.pi * self.Y / 4) @ expm(-1j * np.pi * (self.Y / 4 + self.X / 2)) - expected_vector = U @ np.array([1.0, 1.0]) / np.sqrt(2) - - self.assertGreaterEqual(state_fidelity(statevector, expected_vector), 1 - (10**-5)) - - def test_shift_phase(self): - """Test ShiftPhase command.""" - - omega_0 = 1.123 - r = 1.0 - - system_model = self._system_model_1Q(omega_0, r) - - # run a schedule in which a shifted phase causes a pulse to cancel itself. - # Also do it in multiple phase shifts to test accumulation - sched = Schedule() - amp1 = 0.12 - sched += Play(Waveform([amp1]), DriveChannel(0)) - phi1 = 0.12374 * np.pi - sched += ShiftPhase(phi1, DriveChannel(0)) - amp2 = 0.492 - sched += Play(Waveform([amp2]), DriveChannel(0)) - phi2 = 0.5839 * np.pi - sched += ShiftPhase(phi2, DriveChannel(0)) - amp3 = 0.12 + 0.21 * 1j - sched += Play(Waveform([amp3]), DriveChannel(0)) - - sched |= Acquire(1, AcquireChannel(0), MemorySlot(0)) << sched.duration - - y0 = np.array([1.0, 0]) - - with self.assertWarns(DeprecationWarning): - pulse_sim = PulseSimulator(system_model=system_model, initial_state=y0) - pulse_sim.set_options( - meas_level=2, meas_return="single", meas_map=[[0]], qubit_lo_freq=[omega_0], shots=1 - ) - results = pulse_sim.run(sched).result() - pulse_sim_yf = results.get_statevector() - - # run independent simulation - samples = np.array( - [[amp1], [amp2 * np.exp(1j * phi1)], [amp3 * np.exp(1j * (phi1 + phi2))]] - ) - indep_yf = simulate_1q_model(y0, omega_0, r, np.array([omega_0]), samples, 1.0) - - self.assertGreaterEqual(state_fidelity(pulse_sim_yf, indep_yf), 1 - (10**-5)) - - # run another schedule with only a single shift phase to verify - sched = Schedule() - amp1 = 0.12 - sched += Play(Waveform([amp1]), DriveChannel(0)) - phi1 = 0.12374 * np.pi - sched += ShiftPhase(phi1, DriveChannel(0)) - amp2 = 0.492 - sched += Play(Waveform([amp2]), DriveChannel(0)) - sched |= Acquire(1, AcquireChannel(0), MemorySlot(0)) << sched.duration - - results = pulse_sim.run(sched).result() - pulse_sim_yf = results.get_statevector() - - # run independent simulation - samples = np.array([[amp1], [amp2 * np.exp(1j * phi1)]]) - indep_yf = simulate_1q_model(y0, omega_0, r, np.array([omega_0]), samples, 1.0) - - self.assertGreaterEqual(state_fidelity(pulse_sim_yf, indep_yf), 1 - (10**-5)) - - def test_set_phase(self): - """Test SetPhase command. Similar to the ShiftPhase test but includes a mixing of - ShiftPhase and SetPhase instructions to test relative vs absolute changes""" - - omega_0 = 1.3981 - r = 1.0 - - system_model = self._system_model_1Q(omega_0, r) - - # intermix shift and set phase instructions to verify absolute v.s. relative changes - sched = Schedule() - amp1 = 0.12 - sched += Play(Waveform([amp1]), DriveChannel(0)) - phi1 = 0.12374 * np.pi - sched += ShiftPhase(phi1, DriveChannel(0)) - amp2 = 0.492 - sched += Play(Waveform([amp2]), DriveChannel(0)) - phi2 = 0.5839 * np.pi - sched += SetPhase(phi2, DriveChannel(0)) - amp3 = 0.12 + 0.21 * 1j - sched += Play(Waveform([amp3]), DriveChannel(0)) - phi3 = 0.1 * np.pi - sched += ShiftPhase(phi3, DriveChannel(0)) - amp4 = 0.2 + 0.3 * 1j - sched += Play(Waveform([amp4]), DriveChannel(0)) - - sched |= Acquire(1, AcquireChannel(0), MemorySlot(0)) << sched.duration - - y0 = np.array([1.0, 0.0]) - with self.assertWarns(DeprecationWarning): - pulse_sim = PulseSimulator(system_model=system_model, initial_state=y0) - pulse_sim.set_options( - meas_level=2, meas_return="single", meas_map=[[0]], qubit_lo_freq=[omega_0], shots=1 - ) - results = pulse_sim.run(sched).result() - pulse_sim_yf = results.get_statevector() - - # run independent simulation - samples = np.array( - [ - [amp1], - [amp2 * np.exp(1j * phi1)], - [amp3 * np.exp(1j * phi2)], - [amp4 * np.exp(1j * (phi2 + phi3))], - ] - ) - indep_yf = simulate_1q_model(y0, omega_0, r, np.array([omega_0]), samples, 1.0) - - self.assertGreaterEqual(state_fidelity(pulse_sim_yf, indep_yf), 1 - (10**-5)) - - def test_set_phase_rwa(self): - """Test SetPhase command using an RWA approximate solution.""" - omega_0 = 5.123 - r = 0.01 - - system_model = self._system_model_1Q(omega_0, r) - - sched = Schedule() - sched += SetPhase(np.pi / 2, DriveChannel(0)) - sched += Play(Waveform(np.ones(100)), DriveChannel(0)) - - sched |= Acquire(1, AcquireChannel(0), MemorySlot(0)) << sched.duration - - y0 = np.array([1.0, 1.0]) / np.sqrt(2) - with self.assertWarns(DeprecationWarning): - pulse_sim = PulseSimulator(system_model=system_model, initial_state=y0) - pulse_sim.set_options( - meas_level=2, meas_return="single", meas_map=[[0]], qubit_lo_freq=[omega_0], shots=1 - ) - results = pulse_sim.run(sched).result() - pulse_sim_yf = results.get_statevector() - - # run independent simulation - phases = np.exp((-1j * 2 * np.pi * omega_0 * np.array([1, -1]) / 2) * 100) - approx_yf = phases * (expm(-1j * (np.pi / 2) * self.Y) @ y0) - - self.assertGreaterEqual(state_fidelity(pulse_sim_yf, approx_yf), 0.99) - - def test_frequency_error(self): - """Test that using SetFrequency and ShiftFrequency instructions raises an error.""" - - # qubit frequency and drive frequency - omega_0 = 1.1329824 - omega_d = omega_0 - - # drive strength and length of pulse - r = 0.01 - total_samples = 100 - - # set up simulator - with self.assertWarns(DeprecationWarning): - pulse_sim = PulseSimulator(system_model=self._system_model_1Q(omega_0, r)) - - # set up schedule with ShiftFrequency - drive_pulse = Waveform(1.0 * np.ones(total_samples)) - schedule = Schedule() - schedule |= Play(drive_pulse, DriveChannel(0)) - schedule += ShiftFrequency(5.0, DriveChannel(0)) - schedule += Play(drive_pulse, DriveChannel(0)) - schedule += Acquire(total_samples, AcquireChannel(0), MemorySlot(0)) << schedule.duration - - with self.assertRaises(AerError): - res = pulse_sim.run(schedule).result() - - # set up schedule with SetFrequency - drive_pulse = Waveform(1.0 * np.ones(total_samples)) - schedule = Schedule() - schedule |= Play(drive_pulse, DriveChannel(0)) - schedule += SetFrequency(5.0, DriveChannel(0)) - schedule += Play(drive_pulse, DriveChannel(0)) - schedule += Acquire(total_samples, AcquireChannel(0), MemorySlot(0)) << schedule.duration - - with self.assertRaises(AerError): - res = pulse_sim.run(schedule).result() - - def test_schedule_freqs(self): - """Test simulation when each schedule has its own frequencies.""" - - # qubit frequency and drive frequency - omega_0 = 1.1329824 - omega_d = omega_0 - - # drive strength and length of pulse - r = 0.01 - total_samples = 100 - - # initial state and seed - y0 = np.array([1.0, 0.0]) - seed = 9000 - - # set up simulator - with self.assertWarns(DeprecationWarning): - pulse_sim = PulseSimulator(system_model=self._system_model_1Q(omega_0, r)) - frequencies = np.array([0.5, 1.0, 1.5]) * omega_d - pulse_sim.set_options(shots=128) - - # set up constant pulse for doing a pi pulse - schedule = self._1Q_constant_sched(total_samples) - result = pulse_sim.run( - [schedule, schedule, schedule], - initial_state=y0, - seed=seed, - schedule_los=[{DriveChannel(0): freq} for freq in frequencies], - ).result() - - # check that the off-resonant drives fail to excite the system, - # while the on resonance drive does - self.assertDictAlmostEqual(result.get_counts(0), {"0": 128}) - self.assertDictAlmostEqual(result.get_counts(1), {"1": 128}) - self.assertDictAlmostEqual(result.get_counts(2), {"0": 128}) - - def test_3_level_measurement(self): - """Test correct measurement outcomes for a pair of 3 level systems.""" - - q_freqs = [5.0, 5.1] - r = 0.02 - j = 0.02 - total_samples = 25 - - hamiltonian = {} - hamiltonian["h_str"] = [ - "2*np.pi*v0*0.5*Z0", - "2*np.pi*v1*0.5*Z1", - "2*np.pi*r*0.5*X0||D0", - "2*np.pi*r*0.5*X1||D1", - "2*np.pi*j*0.5*I0*I1", - "2*np.pi*j*0.5*X0*X1", - "2*np.pi*j*0.5*Y0*Y1", - "2*np.pi*j*0.5*Z0*Z1", - ] - hamiltonian["vars"] = {"v0": q_freqs[0], "v1": q_freqs[1], "r": r, "j": j} - hamiltonian["qub"] = {"0": 3, "1": 3} - ham_model = HamiltonianModel.from_dict(hamiltonian) - - # set the U0 to have frequency of drive channel 0 - u_channel_lo = [] - subsystem_list = [0, 1] - dt = 1.0 - - system_model = PulseSystemModel( - hamiltonian=ham_model, u_channel_lo=u_channel_lo, subsystem_list=subsystem_list, dt=dt - ) - - schedule = Schedule() - schedule |= Acquire(total_samples, AcquireChannel(0), MemorySlot(0)) << 3 * total_samples - schedule |= Acquire(total_samples, AcquireChannel(1), MemorySlot(1)) << 3 * total_samples - - y0 = np.array([1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) / np.sqrt(2) - with self.assertWarns(DeprecationWarning): - pulse_sim = PulseSimulator(system_model=system_model, initial_state=y0, seed=50) - pulse_sim.set_options( - meas_level=2, - meas_return="single", - meas_map=[[0]], - qubit_lo_freq=q_freqs, - shots=1000, - memory_slots=2, - ) - result = pulse_sim.run(schedule).result() - counts = result.get_counts() - exp_counts = {"00": 479, "01": 502, "10": 9, "11": 10} - self.assertDictAlmostEqual(counts, exp_counts) - - def _system_model_1Q(self, omega_0, r): - """Constructs a standard model for a 1 qubit system. - - Args: - omega_0 (float): qubit frequency - r (float): drive strength - - Returns: - PulseSystemModel: model for qubit system - """ - - hamiltonian = {} - hamiltonian["h_str"] = ["2*np.pi*omega0*0.5*Z0", "2*np.pi*r*0.5*X0||D0"] - hamiltonian["vars"] = {"omega0": omega_0, "r": r} - hamiltonian["qub"] = {"0": 2} - ham_model = HamiltonianModel.from_dict(hamiltonian) - - u_channel_lo = [] - subsystem_list = [0] - dt = 1.0 - - return PulseSystemModel( - hamiltonian=ham_model, u_channel_lo=u_channel_lo, subsystem_list=subsystem_list, dt=dt - ) - - def _1Q_constant_sched(self, total_samples, amp=1.0): - """Creates a runnable schedule for 1Q with a constant drive pulse of a given length. - - Args: - total_samples (int): length of pulse - amp (float): amplitude of constant pulse (can be complex) - - Returns: - schedule (pulse schedule): schedule with a drive pulse followed by an acquire - """ - - # set up constant pulse for doing a pi pulse - drive_pulse = Waveform(amp * np.ones(total_samples)) - schedule = Schedule() - schedule |= Play(drive_pulse, DriveChannel(0)) - schedule |= Acquire(total_samples, AcquireChannel(0), MemorySlot(0)) << schedule.duration - - return schedule - - def _system_model_2Q(self, j): - """Constructs a model for a 2 qubit system with a U channel controlling coupling and - no other Hamiltonian terms. - - Args: - j (float): coupling strength - - Returns: - PulseSystemModel: model for qubit system - """ - - hamiltonian = {} - hamiltonian["h_str"] = ["a*X0||D0", "a*X0||D1", "2*np.pi*j*0.25*(Z0*X1)||U0"] - hamiltonian["vars"] = {"a": 0, "j": j} - hamiltonian["qub"] = {"0": 2, "1": 2} - ham_model = HamiltonianModel.from_dict(hamiltonian) - - # set the U0 to have frequency of drive channel 0 - u_channel_lo = [[UchannelLO(0, 1.0 + 0.0j)]] - subsystem_list = [0, 1] - dt = 1.0 - - return PulseSystemModel( - hamiltonian=ham_model, u_channel_lo=u_channel_lo, subsystem_list=subsystem_list, dt=dt - ) - - def _2Q_constant_sched(self, total_samples, amp=1.0, u_idx=0): - """Creates a runnable schedule with a single pulse on a U channel for two qubits. - - Args: - total_samples (int): length of pulse - amp (float): amplitude of constant pulse (can be complex) - u_idx (int): index of U channel - - Returns: - schedule (pulse schedule): schedule with a drive pulse followed by an acquire - """ - - # set up constant pulse for doing a pi pulse - drive_pulse = Waveform(amp * np.ones(total_samples)) - schedule = Schedule() - schedule |= Play(drive_pulse, ControlChannel(u_idx)) - schedule |= Acquire(total_samples, AcquireChannel(0), MemorySlot(0)) << total_samples - schedule |= Acquire(total_samples, AcquireChannel(1), MemorySlot(1)) << total_samples - - return schedule - - def _system_model_3Q(self, j, subsystem_list=[0, 2]): - """Constructs a model for a 3 qubit system, with the goal that the restriction to - [0, 2] and to qubits [1, 2] is the same as in _system_model_2Q - - Args: - j (float): coupling strength - subsystem_list (list): list of subsystems to include - - Returns: - PulseSystemModel: model for qubit system - """ - - hamiltonian = {} - hamiltonian["h_str"] = ["2*np.pi*j*0.25*(Z0*X2)||U0", "2*np.pi*j*0.25*(Z1*X2)||U1"] - hamiltonian["vars"] = {"j": j} - hamiltonian["qub"] = {"0": 2, "1": 2, "2": 2} - ham_model = HamiltonianModel.from_dict(hamiltonian, subsystem_list=subsystem_list) - - # set the U0 to have frequency of drive channel 0 - u_channel_lo = [[UchannelLO(0, 1.0 + 0.0j)], [UchannelLO(0, 1.0 + 0.0j)]] - dt = 1.0 - - return PulseSystemModel( - hamiltonian=ham_model, u_channel_lo=u_channel_lo, subsystem_list=subsystem_list, dt=dt - ) - - def _3Q_constant_sched(self, total_samples, amp=1.0, u_idx=0, subsystem_list=[0, 2]): - """Creates a runnable schedule for the 3Q system after the system is restricted to - 2 qubits. - - Args: - total_samples (int): length of pulse - amp (float): amplitude of constant pulse (can be complex) - u_idx (int): index of U channel - subsystem_list (list): list of qubits to restrict to - - Returns: - schedule (pulse schedule): schedule with a drive pulse followed by an acquire - """ - - # set up constant pulse for doing a pi pulse - drive_pulse = Waveform(amp * np.ones(total_samples)) - schedule = Schedule() - schedule |= Play(drive_pulse, ControlChannel(u_idx)) - for idx in subsystem_list: - schedule |= ( - Acquire(total_samples, AcquireChannel(idx), MemorySlot(idx)) << total_samples - ) - - return schedule - - def _system_model_3d_oscillator(self, freq, anharm, r): - """Model for a duffing oscillator truncated to 3 dimensions. - - Args: - freq (float): frequency of the oscillator - anharm (float): anharmonicity of the oscillator - r (float): drive strength - - Returns: - PulseSystemModel: model for oscillator system - """ - hamiltonian = {} - hamiltonian["h_str"] = ["np.pi*(2*v-alpha)*O0", "np.pi*alpha*O0*O0", "2*np.pi*r*X0||D0"] - hamiltonian["vars"] = {"v": freq, "alpha": anharm, "r": r} - hamiltonian["qub"] = {"0": 3} - ham_model = HamiltonianModel.from_dict(hamiltonian) - - u_channel_lo = [] - subsystem_list = [0] - dt = 1.0 - - return PulseSystemModel( - hamiltonian=ham_model, u_channel_lo=u_channel_lo, subsystem_list=subsystem_list, dt=dt - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/terra/noise/test_device_models.py b/test/terra/noise/test_device_models.py index 7b82553bf7..5098ab2794 100644 --- a/test/terra/noise/test_device_models.py +++ b/test/terra/noise/test_device_models.py @@ -16,6 +16,7 @@ from test.terra.common import QiskitAerTestCase +from qiskit.providers import QubitProperties from qiskit.providers.fake_provider import FakeNairobi, FakeNairobiV2 from qiskit_aer.noise.device.models import basic_device_gate_errors @@ -33,6 +34,24 @@ def test_basic_device_gate_errors_from_target(self): self.assertEqual(len(errors_on_reset), 7) self.assertEqual(len(gate_errors), 40) + def test_basic_device_gate_errors_from_target_with_non_operational_qubits(self): + """Test if no thermal relaxation errors are generated for qubits with undefined T1 and T2.""" + target = FakeNairobiV2().target + # tweak target to have non-operational qubits + faulty_qubits = (1, 2) + for q in faulty_qubits: + target.qubit_properties[q] = QubitProperties(t1=None, t2=None, frequency=0) + # build gate errors with only relaxation errors i.e. without depolarizing errors + gate_errors = basic_device_gate_errors(target=target, gate_error=False) + errors_on_sx = {qubits: error for name, qubits, error in gate_errors if name == "sx"} + errors_on_cx = {qubits: error for name, qubits, error in gate_errors if name == "cx"} + self.assertEqual(len(gate_errors), 40) + # check if no errors are added on sx gates on qubits without T1 and T2 definitions + for q in faulty_qubits: + self.assertTrue(errors_on_sx[(q,)].ideal()) + # check if no error is added on cx gate on a qubit pair without T1 and T2 definitions + self.assertTrue(errors_on_cx[faulty_qubits].ideal()) + def test_basic_device_gate_errors_from_target_and_properties(self): """Test if the device same gate errors are produced both from target and properties""" errors_from_properties = basic_device_gate_errors(properties=FakeNairobi().properties()) diff --git a/test/terra/noise/test_noise_model.py b/test/terra/noise/test_noise_model.py index 5e70e3f389..d4f5004fc6 100644 --- a/test/terra/noise/test_noise_model.py +++ b/test/terra/noise/test_noise_model.py @@ -33,6 +33,7 @@ from qiskit.circuit.library.generalized_gates import PauliGate from qiskit.circuit.library.standard_gates import IGate, XGate from qiskit.compiler import transpile +from qiskit.providers import QubitProperties from qiskit.providers.fake_provider import ( FakeBackend, FakeAlmaden, @@ -258,6 +259,25 @@ def test_noise_model_from_lagos_v2(self): result = AerSimulator().run(circ, noise_model=noise_model).result() self.assertTrue(result.success) + def test_noise_model_from_backend_v2_with_non_operational_qubits(self): + """Test if possible to create a noise model from backend with non-operational qubits. + See issues #1779 and #1815 for the details.""" + backend = FakeLagosV2() + # tweak target to have non-operational qubits + faulty_qubits = [0, 1] + for qubit in faulty_qubits: + backend.target.qubit_properties[qubit] = QubitProperties(t1=None, t2=None, frequency=0) + + noise_model = NoiseModel.from_backend(backend) + + circ = QuantumCircuit(2) + circ.h(0) + circ.cx(0, 1) + circ.measure_all() + circ = transpile(circ, backend, scheduling_method="alap") + result = AerSimulator().run(circ, noise_model=noise_model).result() + self.assertTrue(result.success) + def test_noise_model_from_invalid_t2_backend(self): """Test if silently truncate invalid T2 values when creating a noise model from backend""" from qiskit.providers.models.backendproperties import BackendProperties, Gate, Nduv diff --git a/test/terra/primitives/test_estimator.py b/test/terra/primitives/test_estimator.py index 6cf9e37537..b14a2840e0 100644 --- a/test/terra/primitives/test_estimator.py +++ b/test/terra/primitives/test_estimator.py @@ -74,6 +74,21 @@ def test_estimator(self, abelian_grouping): self.assertIsInstance(result, EstimatorResult) np.testing.assert_allclose(result.values, [1.728515625]) + with self.subTest("SparsePauliOp with another grouping"): + observable = SparsePauliOp.from_list( + [ + ("YZ", 0.39793742484318045), + ("ZI", -0.39793742484318045), + ("ZZ", -0.01128010425623538), + ("XX", 0.18093119978423156), + ] + ) + ansatz = RealAmplitudes(num_qubits=2, reps=2) + est = Estimator(abelian_grouping=abelian_grouping) + result = est.run(ansatz, observable, parameter_values=[[0] * 6], seed=15).result() + self.assertIsInstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [-0.4], rtol=0.02) + @data(True, False) def test_init_observable_from_operator(self, abelian_grouping): """test for evaluate without parameters""" @@ -272,6 +287,21 @@ def test_with_shots_option_without_approximation(self): np.testing.assert_allclose(result.values, [-1.2895828299114598]) self.assertIsInstance(result.metadata[0]["variance"], float) + def test_warn_shots_none_without_approximation(self): + """Test waning for shots=None without approximation.""" + est = Estimator(approximation=False) + with self.assertWarns(RuntimeWarning): + result = est.run( + self.ansatz, + self.observable, + parameter_values=[[0, 1, 1, 2, 3, 5]], + shots=None, + seed=15, + ).result() + self.assertIsInstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [-1.313831587508902]) + self.assertIsInstance(result.metadata[0]["variance"], float) + if __name__ == "__main__": unittest.main() diff --git a/test/terra/pulse/__init__.py b/test/terra/pulse/__init__.py deleted file mode 100644 index 7db3645a62..0000000000 --- a/test/terra/pulse/__init__.py +++ /dev/null @@ -1,32 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2019, 2020. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -""" -Terra tests -""" - -import os - - -def load_tests(loader, standard_tests, pattern): - """ - test suite for unittest discovery - """ - this_dir = os.path.dirname(__file__) - if pattern in ["test*.py", "*_test.py"]: - package_tests = loader.discover(start_dir=this_dir, pattern=pattern) - standard_tests.addTests(package_tests) - elif pattern in ["profile*.py", "*_profile.py"]: - loader.testMethodPrefix = "profile" - package_tests = loader.discover(start_dir=this_dir, pattern="test*.py") - standard_tests.addTests(package_tests) - return standard_tests diff --git a/test/terra/pulse/controllers/__init__.py b/test/terra/pulse/controllers/__init__.py deleted file mode 100644 index fbf7236957..0000000000 --- a/test/terra/pulse/controllers/__init__.py +++ /dev/null @@ -1,32 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -""" -Pulse simulator controllers tests -""" - -import os - - -def load_tests(loader, standard_tests, pattern): - """ - test suite for unittest discovery - """ - this_dir = os.path.dirname(__file__) - if pattern in ["test*.py", "*_test.py"]: - package_tests = loader.discover(start_dir=this_dir, pattern=pattern) - standard_tests.addTests(package_tests) - elif pattern in ["profile*.py", "*_profile.py"]: - loader.testMethodPrefix = "profile" - package_tests = loader.discover(start_dir=this_dir, pattern="test*.py") - standard_tests.addTests(package_tests) - return standard_tests diff --git a/test/terra/pulse/controllers/test_pulse_controller.py b/test/terra/pulse/controllers/test_pulse_controller.py deleted file mode 100644 index 161bb469e9..0000000000 --- a/test/terra/pulse/controllers/test_pulse_controller.py +++ /dev/null @@ -1,62 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""tests for pulse_controller.py""" - -import unittest -import numpy as np -from qiskit_aer.pulse.controllers.pulse_controller import setup_rhs_dict_freqs - -from ...common import QiskitAerTestCase - - -class TestSetupRHSDictFreqs(QiskitAerTestCase): - """Tests for setup_rhs_dict_freqs""" - - def setUp(self): - super().setUp() - self.default_dict = {"freqs": [1.0, 2.0, 3.0]} - - def calculate_channel_frequencies(qubit_lo_freq): - return { - "D0": qubit_lo_freq[0], - "U0": qubit_lo_freq[0] - qubit_lo_freq[1], - "D1": qubit_lo_freq[1], - } - - self.calculate_channel_frequencies = calculate_channel_frequencies - - def test_without_override(self): - """Test maintenance of default values if no frequencies specified in exp.""" - - output_dict = setup_rhs_dict_freqs( - self.default_dict, {}, self.calculate_channel_frequencies - ) - - self.assertAlmostEqual(np.array(output_dict["freqs"]), np.array(self.default_dict["freqs"])) - - output_dict = setup_rhs_dict_freqs( - self.default_dict, {"qubit_lo_freq": None}, self.calculate_channel_frequencies - ) - - self.assertAlmostEqual(np.array(output_dict["freqs"]), np.array(self.default_dict["freqs"])) - - def test_with_override(self): - """Test overriding of default values with qubit_lo_freq in exp.""" - - output_dict = setup_rhs_dict_freqs( - self.default_dict, {"qubit_lo_freq": [5, 11]}, self.calculate_channel_frequencies - ) - self.assertAlmostEqual(np.array(output_dict["freqs"]), np.array([5, -6, 11])) - - def assertAlmostEqual(self, A, B, tol=10**-15): - self.assertTrue(np.abs(A - B).max() < tol) diff --git a/test/terra/pulse/de/__init__.py b/test/terra/pulse/de/__init__.py deleted file mode 100644 index 7db3645a62..0000000000 --- a/test/terra/pulse/de/__init__.py +++ /dev/null @@ -1,32 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2019, 2020. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -""" -Terra tests -""" - -import os - - -def load_tests(loader, standard_tests, pattern): - """ - test suite for unittest discovery - """ - this_dir = os.path.dirname(__file__) - if pattern in ["test*.py", "*_test.py"]: - package_tests = loader.discover(start_dir=this_dir, pattern=pattern) - standard_tests.addTests(package_tests) - elif pattern in ["profile*.py", "*_profile.py"]: - loader.testMethodPrefix = "profile" - package_tests = loader.discover(start_dir=this_dir, pattern="test*.py") - standard_tests.addTests(package_tests) - return standard_tests diff --git a/test/terra/pulse/de/test_de_methods.py b/test/terra/pulse/de/test_de_methods.py deleted file mode 100644 index a5cc9b0926..0000000000 --- a/test/terra/pulse/de/test_de_methods.py +++ /dev/null @@ -1,157 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020, 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""tests for DE_Methods.py""" - -import unittest -import numpy as np -from scipy.linalg import expm -from qiskit_aer.pulse.de.DE_Options import DE_Options -from qiskit_aer.pulse.de.DE_Methods import ( - ODE_Method, - RK4, - ScipyODE, - QiskitZVODE, - method_from_string, -) - -from ...common import QiskitAerTestCase - - -class TestDE_Methods(QiskitAerTestCase): - def setUp(self): - super().setUp() - # set up a 2d test problem - self.t0 = 0.0 - self.y0 = np.eye(2) - - self.X = np.array([[0.0, 1.0], [1.0, 0.0]], dtype=complex) - self.Y = np.array([[0.0, -1j], [1j, 0.0]], dtype=complex) - self.Z = np.array([[1.0, 0.0], [0.0, -1.0]], dtype=complex) - - # define rhs in terms of constant a constant generator - def generator(t): - return -1j * 2 * np.pi * self.X / 2 - - def rhs(t, y): - return generator(t) @ y - - self.rhs = {"rhs": rhs} - - def test_method_from_string(self): - """Test method_from_string""" - - method = method_from_string("scipy-RK45") - self.assertTrue(method == ScipyODE) - - method = method_from_string("zvode-adams") - self.assertTrue(method == QiskitZVODE) - - def test_ScipyODE_options_and_defaults(self): - """Test option handling for ScipyODE solver.""" - - options = DE_Options(method="scipy-RK45") - solver = ScipyODE(options=options) - - # test restructuring/default handling for this solver - self.assertTrue(solver.options.method == "RK45") - self.assertTrue(solver.options.max_step == np.inf) - - def test_QiskitZVODE_options_and_defaults(self): - """Test option handling for QiskitZVODE solver.""" - - options = DE_Options(method="zvode-adams") - solver = QiskitZVODE(t0=0.0, y0=np.array([1.0]), rhs=self.rhs, options=options) - - # test restructuring/default handling for this solver - self.assertTrue(solver.options.method == "adams") - self.assertTrue(solver.options.first_step == 0) - self.assertTrue(solver.options.max_step == 0) - self.assertTrue(solver.options.min_step == 0) - - def test_QiskitZVODE_instantiation_error(self): - """Test option handling for QiskitZVODE solver.""" - - expected_message = "QiskitZVODE solver requires t0, y0, and rhs at instantiation." - - options = DE_Options(method="zvode-adams") - try: - solver = QiskitZVODE(options=options) - except Exception as exception: - self.assertEqual(str(exception), expected_message) - - def test_standard_problems_ScipyODE_RK45(self): - """Run standard variable step tests for scipy-RK45.""" - self._test_variable_step_method("scipy-RK45") - - def test_standard_problems_ScipyODE_RK23(self): - """Run standard variable step tests for scipy-RK23.""" - self._test_variable_step_method("scipy-RK23") - - def test_standard_problems_ScipyODE_BDF(self): - """Run standard variable step tests for scipy-BDF.""" - self._test_variable_step_method("scipy-BDF") - - def test_standard_problems_QiskitZVODE_adams(self): - """Run standard variable step tests for zvode-adams.""" - self._test_variable_step_method("zvode-adams") - - def _test_variable_step_method(self, method_str): - """Some tests for a variable step method.""" - - # get method and set general options - ode_method = method_from_string(method_str) - options = DE_Options(method=method_str, atol=10**-10, rtol=10**-10) - - # run on matrix problem - solver = ode_method(t0=self.t0, y0=self.y0, rhs=self.rhs, options=options) - solver.integrate(1.0) - expected = expm(-1j * np.pi * self.X) - - # set the comparison tolerance to be somewhat lenient - self.assertAlmostEqual(solver.y, expected, tol=10**-8) - - # test with an arbitrary problem - def rhs(t, y): - return np.array([t**2]) - - solver = ode_method(t0=0.0, y0=np.array(0.0), rhs={"rhs": rhs}, options=options) - - solver.integrate(1.0) - expected = 1.0 / 3 - self.assertAlmostEqual(solver.y, expected, tol=10**-9) - - def test_RK4(self): - """Run tests on RK4 fixed-step solver.""" - ode_method = method_from_string("RK4") - options = DE_Options(max_dt=10**-3) - - # run on matrix problem - solver = ode_method(t0=self.t0, y0=self.y0, rhs=self.rhs, options=options) - solver.integrate(1.0) - expected = expm(-1j * np.pi * self.X) - - # set the comparison tolerance to be somewhat lenient - self.assertAlmostEqual(solver.y, expected, tol=10**-8) - - # test with an arbitrary problem - def rhs(t, y): - return np.array([t**2]) - - solver = ode_method(t0=0.0, y0=np.array(0.0), rhs={"rhs": rhs}, options=options) - - solver.integrate(1.0) - expected = 1.0 / 3 - self.assertAlmostEqual(solver.y, expected, tol=10**-8) - - def assertAlmostEqual(self, A, B, tol=10**-15): - self.assertTrue(np.abs(A - B).max() < tol) diff --git a/test/terra/pulse/de/test_type_utils.py b/test/terra/pulse/de/test_type_utils.py deleted file mode 100644 index 0092df5e25..0000000000 --- a/test/terra/pulse/de/test_type_utils.py +++ /dev/null @@ -1,132 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020, 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""tests for type_utils.py""" - -import numpy as np -from qiskit_aer.pulse.de.type_utils import ( - convert_state, - type_spec_from_instance, - StateTypeConverter, -) - -from ...common import QiskitAerTestCase - - -class TestTypeUtils(QiskitAerTestCase): - def test_convert_state(self): - """Test convert_state""" - - type_spec = {"type": "array", "shape": (4,)} - y = np.array([[1, 2], [3, 4]]) - expected = np.array([1, 2, 3, 4]) - - self.assertAlmostEqual(convert_state(y, type_spec), expected) - - type_spec = {"type": "array"} - y = [[1, 2], [3, 4]] - expected = np.array([[1, 2], [3, 4]]) - - self.assertAlmostEqual(convert_state(y, type_spec), expected) - - def test_type_spec_from_instance(self): - """Test type_spec_from_instance""" - - y = np.array([1, 2, 3, 4]) - type_spec = type_spec_from_instance(y) - - self.assertEqual(type_spec, {"type": "array", "shape": (4,)}) - - y = np.array([[1, 2], [3, 4], [5, 6]]) - type_spec = type_spec_from_instance(y) - - self.assertEqual(type_spec, {"type": "array", "shape": (3, 2)}) - - def test_converter_inner_outer(self): - """Test standard constructor of StateTypeConverter along with basic state conversion - functions""" - - inner_spec = {"type": "array", "shape": (4,)} - outer_spec = {"type": "array", "shape": (2, 2)} - converter = StateTypeConverter(inner_spec, outer_spec) - - y_in = np.array([1, 2, 3, 4]) - y_out = np.array([[1, 2], [3, 4]]) - - convert_out = converter.inner_to_outer(y_in) - convert_in = converter.outer_to_inner(y_out) - - self.assertAlmostEqual(convert_out, y_out) - self.assertAlmostEqual(convert_in, y_in) - - def test_from_instances(self): - """Test from_instances constructor""" - - inner_y = np.array([1, 2, 3, 4]) - outer_y = np.array([[1, 2], [3, 4]]) - - converter = StateTypeConverter.from_instances(inner_y, outer_y) - - self.assertEqual(converter.inner_type_spec, {"type": "array", "shape": (4,)}) - self.assertEqual(converter.outer_type_spec, {"type": "array", "shape": (2, 2)}) - - converter = StateTypeConverter.from_instances(inner_y) - - self.assertEqual(converter.inner_type_spec, {"type": "array", "shape": (4,)}) - self.assertEqual(converter.outer_type_spec, {"type": "array", "shape": (4,)}) - - def test_from_outer_instance_inner_type_spec(self): - """Test from_outer_instance_inner_type_spec constructor""" - - # test case for inner type spec with 1d array - inner_type_spec = {"type": "array", "ndim": 1} - outer_y = np.array([[1, 2], [3, 4]]) - - converter = StateTypeConverter.from_outer_instance_inner_type_spec(outer_y, inner_type_spec) - - self.assertEqual(converter.inner_type_spec, {"type": "array", "shape": (4,)}) - self.assertEqual(converter.outer_type_spec, {"type": "array", "shape": (2, 2)}) - - # inner type spec is a generic array - inner_type_spec = {"type": "array"} - outer_y = np.array([[1, 2], [3, 4]]) - - converter = StateTypeConverter.from_outer_instance_inner_type_spec(outer_y, inner_type_spec) - - self.assertEqual(converter.inner_type_spec, {"type": "array", "shape": (2, 2)}) - self.assertEqual(converter.outer_type_spec, {"type": "array", "shape": (2, 2)}) - - def test_transform_rhs_funcs(self): - """Test rhs function conversion""" - - inner_spec = {"type": "array", "shape": (4,)} - outer_spec = {"type": "array", "shape": (2, 2)} - converter = StateTypeConverter(inner_spec, outer_spec) - - # do matrix multiplication (a truly '2d' operation) - def rhs(t, y): - return t * (y @ y) - - rhs_funcs = {"rhs": rhs} - new_rhs_funcs = converter.transform_rhs_funcs(rhs_funcs) - - test_t = np.pi - y_2d = np.array([[1, 2], [3, 4]]) - y_1d = y_2d.flatten() - - expected_output = rhs(test_t, y_2d).flatten() - output = new_rhs_funcs["rhs"](test_t, y_1d) - - self.assertAlmostEqual(output, expected_output) - - def assertAlmostEqual(self, A, B, tol=10**-15): - self.assertTrue(np.abs(A - B).max() < tol) diff --git a/test/terra/pulse/test_duffing_model_generators.py b/test/terra/pulse/test_duffing_model_generators.py deleted file mode 100644 index 760d6d30d9..0000000000 --- a/test/terra/pulse/test_duffing_model_generators.py +++ /dev/null @@ -1,684 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2019. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -""" -Tests for pulse system generator functions -""" - -import unittest -from numpy import array, array_equal, kron -from test.terra.common import QiskitAerTestCase -from qiskit_aer.pulse.system_models.pulse_system_model import PulseSystemModel -from qiskit_aer.pulse.system_models.hamiltonian_model import HamiltonianModel -from qiskit_aer.pulse.system_models import duffing_model_generators as model_gen -from qiskit_aer.pulse.system_models.string_model_parser.operator_generators import get_oper -from qiskit.providers.models.backendconfiguration import UchannelLO -from qiskit.quantum_info.operators.operator import Operator - - -class TestDuffingModelGenerators(QiskitAerTestCase): - """Tests for functions in duffing_model_generators.py""" - - def test_duffing_system_model1(self): - """First test of duffing_system_model, 2 qubits, 2 dimensional""" - - dim_oscillators = 2 - oscillator_freqs = [5.0, 5.1] - anharm_freqs = [-0.33, -0.33] - drive_strengths = [1.1, 1.2] - coupling_dict = {(0, 1): 0.02} - dt = 1.3 - - system_model = model_gen.duffing_system_model( - dim_oscillators, oscillator_freqs, anharm_freqs, drive_strengths, coupling_dict, dt - ) - cr_idx_dict = {label: idx for idx, label in enumerate(system_model.control_channel_labels)} - - # check basic parameters - self.assertEqual(system_model.subsystem_list, [0, 1]) - self.assertEqual(system_model.dt, 1.3) - - # check that cr_idx_dict is correct - self.assertEqual(cr_idx_dict, {(0, 1): 0, (1, 0): 1}) - self.assertEqual(system_model.control_channel_index((0, 1)), 0) - - # check u_channel_lo is correct - self.assertEqual( - system_model.u_channel_lo, [[UchannelLO(1, 1.0 + 0.0j)], [UchannelLO(0, 1.0 + 0.0j)]] - ) - - # check consistency of system_model.u_channel_lo with cr_idx_dict - # this should in principle be redundant with the above two checks - for q_pair, idx in cr_idx_dict.items(): - self.assertEqual(system_model.u_channel_lo[idx], [UchannelLO(q_pair[1], 1.0 + 0.0j)]) - - # check correct hamiltonian - ham_model = system_model.hamiltonian - expected_vars = { - "v0": 5.0, - "v1": 5.1, - "alpha0": -0.33, - "alpha1": -0.33, - "r0": 1.1, - "r1": 1.2, - "j01": 0.02, - } - self.assertEqual(ham_model._variables, expected_vars) - self.assertEqual(ham_model._subsystem_dims, {0: 2, 1: 2}) - self._compare_str_lists(list(ham_model._channels), ["D0", "D1", "U0", "U1"]) - - # check that Hamiltonian terms have been imported correctly - # constructing the expected_terms requires some knowledge of how the strings get generated - # and then parsed - O0 = self._operator_array_from_str(2, ["I", "O"]) - O1 = self._operator_array_from_str(2, ["O", "I"]) - OO0 = O0 & O0 - OO1 = O1 & O1 - X0 = self._operator_array_from_str(2, ["I", "X"]) - X1 = self._operator_array_from_str(2, ["X", "I"]) - exchange = self._operator_array_from_str(2, ["Sm", "Sp"]) + self._operator_array_from_str( - 2, ["Sp", "Sm"] - ) - expected_terms = [ - ("np.pi*(2*v0-alpha0)", O0), - ("np.pi*(2*v1-alpha1)", O1), - ("np.pi*alpha0", OO0), - ("np.pi*alpha1", OO1), - ("2*np.pi*r0*D0", X0), - ("2*np.pi*r1*D1", X1), - ("2*np.pi*j01", exchange), - ("2*np.pi*r0*U0", X0), - ("2*np.pi*r1*U1", X1), - ] - - # first check the number of terms is correct, then loop through - # each expected term and verify that it is present and consistent - self.assertEqual(len(ham_model._system), len(expected_terms)) - for expected_string, expected_op in expected_terms: - idx = 0 - found = False - while idx < len(ham_model._system) and found is False: - op, string = ham_model._system[idx] - if expected_string == string: - found = True - self.assertTrue(array_equal(expected_op, op)) - idx += 1 - self.assertTrue(found) - - def test_duffing_system_model2(self): - """Second test of duffing_system_model, 3 qubits, 3 dimensional""" - - # do similar tests for different model - dim_oscillators = 3 - oscillator_freqs = [5.0, 5.1, 5.2] - anharm_freqs = [-0.33, -0.33, -0.32] - drive_strengths = [1.1, 1.2, 1.3] - coupling_dict = {(1, 2): 0.03, (0, 1): 0.02} - dt = 1.3 - - system_model = model_gen.duffing_system_model( - dim_oscillators, oscillator_freqs, anharm_freqs, drive_strengths, coupling_dict, dt - ) - cr_idx_dict = {label: idx for idx, label in enumerate(system_model.control_channel_labels)} - - # check basic parameters - self.assertEqual(system_model.subsystem_list, [0, 1, 2]) - self.assertEqual(system_model.dt, 1.3) - - # check that cr_idx_dict is correct - self.assertEqual(cr_idx_dict, {(0, 1): 0, (1, 0): 1, (1, 2): 2, (2, 1): 3}) - self.assertEqual(system_model.control_channel_index((1, 2)), 2) - - # check u_channel_lo is correct - self.assertEqual( - system_model.u_channel_lo, - [ - [UchannelLO(1, 1.0 + 0.0j)], - [UchannelLO(0, 1.0 + 0.0j)], - [UchannelLO(2, 1.0 + 0.0j)], - [UchannelLO(1, 1.0 + 0.0j)], - ], - ) - - # check consistency of system_model.u_channel_lo with cr_idx_dict - # this should in principle be redundant with the above two checks - for q_pair, idx in cr_idx_dict.items(): - self.assertEqual(system_model.u_channel_lo[idx], [UchannelLO(q_pair[1], 1.0 + 0.0j)]) - - # check correct hamiltonian - ham_model = system_model.hamiltonian - expected_vars = { - "v0": 5.0, - "v1": 5.1, - "v2": 5.2, - "alpha0": -0.33, - "alpha1": -0.33, - "alpha2": -0.32, - "r0": 1.1, - "r1": 1.2, - "r2": 1.3, - "j01": 0.02, - "j12": 0.03, - } - self.assertEqual(ham_model._variables, expected_vars) - self.assertEqual(ham_model._subsystem_dims, {0: 3, 1: 3, 2: 3}) - self._compare_str_lists( - list(ham_model._channels), ["D0", "D1", "D3", "U0", "U1", "U2", "U3"] - ) - - # check that Hamiltonian terms have been imported correctly - # constructing the expected_terms requires some knowledge of how the strings get generated - # and then parsed - O0 = self._operator_array_from_str(3, ["I", "I", "O"]) - O1 = self._operator_array_from_str(3, ["I", "O", "I"]) - O2 = self._operator_array_from_str(3, ["O", "I", "I"]) - OO0 = O0 & O0 - OO1 = O1 & O1 - OO2 = O2 & O2 - X0 = self._operator_array_from_str(3, ["I", "I", "A"]) + self._operator_array_from_str( - 3, ["I", "I", "C"] - ) - X1 = self._operator_array_from_str(3, ["I", "A", "I"]) + self._operator_array_from_str( - 3, ["I", "C", "I"] - ) - X2 = self._operator_array_from_str(3, ["A", "I", "I"]) + self._operator_array_from_str( - 3, ["C", "I", "I"] - ) - exchange01 = self._operator_array_from_str( - 3, ["I", "Sm", "Sp"] - ) + self._operator_array_from_str(3, ["I", "Sp", "Sm"]) - exchange12 = self._operator_array_from_str( - 3, ["Sm", "Sp", "I"] - ) + self._operator_array_from_str(3, ["Sp", "Sm", "I"]) - expected_terms = [ - ("np.pi*(2*v0-alpha0)", O0), - ("np.pi*(2*v1-alpha1)", O1), - ("np.pi*(2*v2-alpha2)", O2), - ("np.pi*alpha0", OO0), - ("np.pi*alpha1", OO1), - ("np.pi*alpha2", OO2), - ("2*np.pi*r0*D0", X0), - ("2*np.pi*r1*D1", X1), - ("2*np.pi*r2*D2", X2), - ("2*np.pi*j01", exchange01), - ("2*np.pi*j12", exchange12), - ("2*np.pi*r0*U0", X0), - ("2*np.pi*r1*U1", X1), - ("2*np.pi*r1*U2", X1), - ("2*np.pi*r2*U3", X2), - ] - - # first check the number of terms is correct, then loop through - # each expected term and verify that it is present and consistent - self.assertEqual(len(ham_model._system), len(expected_terms)) - for expected_string, expected_op in expected_terms: - idx = 0 - found = False - while idx < len(ham_model._system) and found is False: - op, string = ham_model._system[idx] - if expected_string == string: - found = True - self.assertTrue(array_equal(expected_op, op)) - idx += 1 - self.assertTrue(found) - - def test_duffing_system_model3(self): - """Third test of duffing_system_model, 4 qubits, 2 dimensional""" - - # do similar tests for different model - dim_oscillators = 2 - oscillator_freqs = [5.0, 5.1, 5.2, 5.3] - anharm_freqs = [-0.33, -0.33, -0.32, -0.31] - drive_strengths = [1.1, 1.2, 1.3, 1.4] - coupling_dict = {(0, 2): 0.03, (1, 0): 0.02, (0, 3): 0.14, (3, 1): 0.18, (1, 2): 0.33} - dt = 1.3 - - system_model = model_gen.duffing_system_model( - dim_oscillators, oscillator_freqs, anharm_freqs, drive_strengths, coupling_dict, dt - ) - cr_idx_dict = {label: idx for idx, label in enumerate(system_model.control_channel_labels)} - - # check basic parameters - self.assertEqual(system_model.subsystem_list, [0, 1, 2, 3]) - self.assertEqual(system_model.dt, 1.3) - - # check that cr_idx_dict is correct - self.assertEqual( - cr_idx_dict, - { - (0, 1): 0, - (1, 0): 1, - (0, 2): 2, - (2, 0): 3, - (0, 3): 4, - (3, 0): 5, - (1, 2): 6, - (2, 1): 7, - (1, 3): 8, - (3, 1): 9, - }, - ) - self.assertEqual(system_model.control_channel_index((1, 2)), 6) - - # check u_channel_lo is correct - self.assertEqual( - system_model.u_channel_lo, - [ - [UchannelLO(1, 1.0 + 0.0j)], - [UchannelLO(0, 1.0 + 0.0j)], - [UchannelLO(2, 1.0 + 0.0j)], - [UchannelLO(0, 1.0 + 0.0j)], - [UchannelLO(3, 1.0 + 0.0j)], - [UchannelLO(0, 1.0 + 0.0j)], - [UchannelLO(2, 1.0 + 0.0j)], - [UchannelLO(1, 1.0 + 0.0j)], - [UchannelLO(3, 1.0 + 0.0j)], - [UchannelLO(1, 1.0 + 0.0j)], - ], - ) - - # check consistency of system_model.u_channel_lo with cr_idx_dict - # this should in principle be redundant with the above two checks - for q_pair, idx in cr_idx_dict.items(): - self.assertEqual(system_model.u_channel_lo[idx], [UchannelLO(q_pair[1], 1.0 + 0.0j)]) - - # check correct hamiltonian - ham_model = system_model.hamiltonian - expected_vars = { - "v0": 5.0, - "v1": 5.1, - "v2": 5.2, - "v3": 5.3, - "alpha0": -0.33, - "alpha1": -0.33, - "alpha2": -0.32, - "alpha3": -0.31, - "r0": 1.1, - "r1": 1.2, - "r2": 1.3, - "r3": 1.4, - "j01": 0.02, - "j02": 0.03, - "j03": 0.14, - "j12": 0.33, - "j13": 0.18, - } - self.assertEqual(ham_model._variables, expected_vars) - self.assertEqual(ham_model._subsystem_dims, {0: 2, 1: 2, 2: 2, 3: 2}) - self._compare_str_lists( - list(ham_model._channels), - ["D0", "D1", "D3", "D4", "U0", "U1", "U2", "U3", "U4", "U5", "U6", "U7", "U8", "U9"], - ) - - # check that Hamiltonian terms have been imported correctly - # constructing the expected_terms requires some knowledge of how the strings get generated - # and then parsed - O0 = self._operator_array_from_str(2, ["I", "I", "I", "O"]) - O1 = self._operator_array_from_str(2, ["I", "I", "O", "I"]) - O2 = self._operator_array_from_str(2, ["I", "O", "I", "I"]) - O3 = self._operator_array_from_str(2, ["O", "I", "I", "I"]) - OO0 = O0 & O0 - OO1 = O1 & O1 - OO2 = O2 & O2 - OO3 = O3 & O3 - X0 = self._operator_array_from_str(2, ["I", "I", "I", "A"]) + self._operator_array_from_str( - 2, ["I", "I", "I", "C"] - ) - X1 = self._operator_array_from_str(2, ["I", "I", "A", "I"]) + self._operator_array_from_str( - 2, ["I", "I", "C", "I"] - ) - X2 = self._operator_array_from_str(2, ["I", "A", "I", "I"]) + self._operator_array_from_str( - 2, ["I", "C", "I", "I"] - ) - X3 = self._operator_array_from_str(2, ["A", "I", "I", "I"]) + self._operator_array_from_str( - 2, ["C", "I", "I", "I"] - ) - exchange01 = self._operator_array_from_str( - 2, ["I", "I", "Sm", "Sp"] - ) + self._operator_array_from_str(2, ["I", "I", "Sp", "Sm"]) - exchange02 = self._operator_array_from_str( - 2, ["I", "Sm", "I", "Sp"] - ) + self._operator_array_from_str(2, ["I", "Sp", "I", "Sm"]) - exchange03 = self._operator_array_from_str( - 2, ["Sm", "I", "I", "Sp"] - ) + self._operator_array_from_str(2, ["Sp", "I", "I", "Sm"]) - exchange12 = self._operator_array_from_str( - 2, ["I", "Sm", "Sp", "I"] - ) + self._operator_array_from_str(2, ["I", "Sp", "Sm", "I"]) - exchange13 = self._operator_array_from_str( - 2, ["Sm", "I", "Sp", "I"] - ) + self._operator_array_from_str(2, ["Sp", "I", "Sm", "I"]) - expected_terms = [ - ("np.pi*(2*v0-alpha0)", O0), - ("np.pi*(2*v1-alpha1)", O1), - ("np.pi*(2*v2-alpha2)", O2), - ("np.pi*(2*v3-alpha3)", O3), - ("np.pi*alpha0", OO0), - ("np.pi*alpha1", OO1), - ("np.pi*alpha2", OO2), - ("np.pi*alpha3", OO3), - ("2*np.pi*r0*D0", X0), - ("2*np.pi*r1*D1", X1), - ("2*np.pi*r2*D2", X2), - ("2*np.pi*r3*D3", X3), - ("2*np.pi*j01", exchange01), - ("2*np.pi*j02", exchange02), - ("2*np.pi*j03", exchange03), - ("2*np.pi*j12", exchange12), - ("2*np.pi*j13", exchange13), - ("2*np.pi*r0*U0", X0), - ("2*np.pi*r1*U1", X1), - ("2*np.pi*r0*U2", X0), - ("2*np.pi*r2*U3", X2), - ("2*np.pi*r0*U4", X0), - ("2*np.pi*r3*U5", X3), - ("2*np.pi*r1*U6", X1), - ("2*np.pi*r2*U7", X2), - ("2*np.pi*r1*U8", X1), - ("2*np.pi*r3*U9", X3), - ] - - # first check the number of terms is correct, then loop through - # each expected term and verify that it is present and consistent - self.assertEqual(len(ham_model._system), len(expected_terms)) - for expected_string, expected_op in expected_terms: - idx = 0 - found = False - while idx < len(ham_model._system) and found is False: - op, string = ham_model._system[idx] - if expected_string == string: - found = True - self.assertTrue(array_equal(expected_op, op)) - idx += 1 - self.assertTrue(found) - - def test_duffing_hamiltonian_dict(self): - """Test _duffing_hamiltonian_dict""" - - oscillators = [0, 1] - oscillator_dims = [2, 2] - oscillator_freqs = [5.0, 5.1] - freq_symbols = ["v0", "v1"] - anharm_freqs = [-0.33, -0.33] - anharm_symbols = ["a0", "a1"] - drive_strengths = [1.1, 1.2] - drive_symbols = ["r0", "r1"] - ordered_coupling_edges = [(0, 1)] - coupling_strengths = [0.02] - coupling_symbols = ["j"] - cr_idx_dict = {(0, 1): 0} - - expected = { - "h_str": [ - "np.pi*(2*v0-a0)*O0", - "np.pi*(2*v1-a1)*O1", - "np.pi*a0*O0*O0", - "np.pi*a1*O1*O1", - "2*np.pi*r0*X0||D0", - "2*np.pi*r1*X1||D1", - "2*np.pi*j*(Sp0*Sm1+Sm0*Sp1)", - "2*np.pi*r0*X0||U0", - ], - "vars": { - "v0": 5.0, - "v1": 5.1, - "a0": -0.33, - "a1": -0.33, - "r0": 1.1, - "r1": 1.2, - "j": 0.02, - }, - "qub": {"0": 2, "1": 2}, - } - output = model_gen._duffing_hamiltonian_dict( - oscillators, - oscillator_dims, - oscillator_freqs, - freq_symbols, - anharm_freqs, - anharm_symbols, - drive_strengths, - drive_symbols, - ordered_coupling_edges, - coupling_strengths, - coupling_symbols, - cr_idx_dict, - ) - self._compare_str_lists(output["h_str"], expected["h_str"]) - self.assertEqual(output["vars"], expected["vars"]) - self.assertEqual(output["qub"], expected["qub"]) - - # test 3 oscillators with mixed up inputs - oscillators = [0, 1, 2] - oscillator_dims = [2, 2, 3] - oscillator_freqs = [5.0, 5.1, 4.9] - freq_symbols = ["v0", "v1", "x3"] - anharm_freqs = [-0.33, -0.33, 1.0] - anharm_symbols = ["a0", "a1", "z4"] - drive_strengths = [1.1, 1.2, 0.98] - drive_symbols = ["r0", "r1", "sa"] - ordered_coupling_edges = [(0, 1), (1, 2), (2, 0)] - coupling_strengths = [0.02, 0.1, 0.33] - coupling_symbols = ["j", "s", "r"] - cr_idx_dict = {(0, 1): 0, (2, 0): 1, (1, 2): 2} - - expected = { - "h_str": [ - "np.pi*(2*v0-a0)*O0", - "np.pi*(2*v1-a1)*O1", - "np.pi*(2*x3-z4)*O2", - "np.pi*a0*O0*O0", - "np.pi*a1*O1*O1", - "np.pi*z4*O2*O2", - "2*np.pi*r0*X0||D0", - "2*np.pi*r1*X1||D1", - "2*np.pi*sa*X2||D2", - "2*np.pi*j*(Sp0*Sm1+Sm0*Sp1)", - "2*np.pi*s*(Sp1*Sm2+Sm1*Sp2)", - "2*np.pi*r*(Sp0*Sm2+Sm0*Sp2)", - "2*np.pi*r0*X0||U0", - "2*np.pi*sa*X2||U1", - "2*np.pi*r1*X1||U2", - ], - "vars": { - "v0": 5.0, - "v1": 5.1, - "x3": 4.9, - "a0": -0.33, - "a1": -0.33, - "z4": 1.0, - "r0": 1.1, - "r1": 1.2, - "sa": 0.98, - "j": 0.02, - "s": 0.1, - "r": 0.33, - }, - "qub": {"0": 2, "1": 2, "2": 3}, - } - output = model_gen._duffing_hamiltonian_dict( - oscillators, - oscillator_dims, - oscillator_freqs, - freq_symbols, - anharm_freqs, - anharm_symbols, - drive_strengths, - drive_symbols, - ordered_coupling_edges, - coupling_strengths, - coupling_symbols, - cr_idx_dict, - ) - self._compare_str_lists(output["h_str"], expected["h_str"]) - self.assertEqual(output["vars"], expected["vars"]) - self.assertEqual(output["qub"], expected["qub"]) - - def test_calculate_channel_frequencies(self): - """test calculate_channel_frequencies of resulting PulseSystemModel objects""" - - dim_oscillators = 2 - oscillator_freqs = [5.0, 5.1] - anharm_freqs = [-0.33, -0.33] - drive_strengths = [1.1, 1.2] - coupling_dict = {(0, 1): 0.0} - dt = 1.3 - - system_model = model_gen.duffing_system_model( - dim_oscillators, oscillator_freqs, anharm_freqs, drive_strengths, coupling_dict, dt - ) - - channel_freqs = system_model.calculate_channel_frequencies([5.0, 5.1]) - expected = {"D0": 5.0, "D1": 5.1, "U0": 5.1, "U1": 5.0} - self.assertEqual(dict(channel_freqs), expected) - - def test_cr_lo_list(self): - """Test _cr_lo_list""" - - cr_dict = {(0, 1): 0, (1, 0): 1, (3, 4): 2} - expected = [ - [UchannelLO(1, 1.0 + 0.0j)], - [UchannelLO(0, 1.0 + 0.0j)], - [UchannelLO(4, 1.0 + 0.0j)], - ] - self.assertEqual(model_gen._cr_lo_list(cr_dict), expected) - - cr_dict = {(0, 1): 0, (3, 4): 2, (1, 0): 1} - expected = [ - [UchannelLO(1, 1.0 + 0.0j)], - [UchannelLO(0, 1.0 + 0.0j)], - [UchannelLO(4, 1.0 + 0.0j)], - ] - self.assertEqual(model_gen._cr_lo_list(cr_dict), expected) - - def test_single_term_generators(self): - """Test various functions for individual terms: - _single_duffing_drift_terms, _drive_terms, _exchange_coupling_terms, _cr_terms - """ - - # single duffing terms - self.assertEqual( - model_gen._single_duffing_drift_terms( - freq_symbols="v", anharm_symbols="a", system_list=0 - ), - ["np.pi*(2*v-a)*O0", "np.pi*a*O0*O0"], - ) - self.assertEqual( - model_gen._single_duffing_drift_terms( - freq_symbols=["v0", "v1"], anharm_symbols=["a0", "a1"], system_list=[2, 3] - ), - ["np.pi*(2*v0-a0)*O2", "np.pi*(2*v1-a1)*O3", "np.pi*a0*O2*O2", "np.pi*a1*O3*O3"], - ) - - # drive terms - self.assertEqual( - model_gen._drive_terms(drive_symbols="r", system_list=0), ["2*np.pi*r*X0||D0"] - ) - self.assertEqual( - model_gen._drive_terms(drive_symbols=["r0", "r1"], system_list=[1, 2]), - ["2*np.pi*r0*X1||D1", "2*np.pi*r1*X2||D2"], - ) - - # exchange coupling - symbols = "j" - edges = [(0, 1)] - expected = ["2*np.pi*j*(Sp0*Sm1+Sm0*Sp1)"] - self.assertEqual(model_gen._exchange_coupling_terms(symbols, edges), expected) - symbols = ["j", "k"] - edges = [(0, 1), (3, 2)] - expected = ["2*np.pi*j*(Sp0*Sm1+Sm0*Sp1)", "2*np.pi*k*(Sp3*Sm2+Sm3*Sp2)"] - self.assertEqual(model_gen._exchange_coupling_terms(symbols, edges), expected) - - # cross resonance terms - symbols = "r" - driven_indices = 0 - u_channel_indices = [1] - expected = ["2*np.pi*r*X0||U1"] - self.assertEqual(model_gen._cr_terms(symbols, driven_indices, u_channel_indices), expected) - symbols = ["r", "s"] - driven_indices = [0, 3] - u_channel_indices = [1, 1] - expected = ["2*np.pi*r*X0||U1", "2*np.pi*s*X3||U1"] - self.assertEqual(model_gen._cr_terms(symbols, driven_indices, u_channel_indices), expected) - - def test_str_list_generator(self): - """Test _str_list_generator""" - - # test one argument - template = "First: {0}" - self.assertEqual(model_gen._str_list_generator(template, "a"), ["First: a"]) - self.assertEqual( - model_gen._str_list_generator(template, ["a1", "a2"]), ["First: a1", "First: a2"] - ) - - # test multiple arguments - template = "First: {0}, Second: {1}" - self.assertEqual(model_gen._str_list_generator(template, "a", "b"), ["First: a, Second: b"]) - self.assertEqual( - model_gen._str_list_generator(template, ["a1", "a2"], ["b1", "b2"]), - ["First: a1, Second: b1", "First: a2, Second: b2"], - ) - - def test_arg_to_iterable(self): - """Test _arg_to_iterable.""" - - self.assertEqual(model_gen._arg_to_iterable("a"), ["a"]) - self.assertEqual(model_gen._arg_to_iterable(["a"]), ["a"]) - self.assertEqual(model_gen._arg_to_iterable(("a", "b")), ("a", "b")) - self.assertEqual(model_gen._arg_to_iterable({"a", "b"}), {"a", "b"}) - - def test_CouplingGraph(self): - """Test CouplingGraph class.""" - - coupling_graph = model_gen.CouplingGraph([(0, 1), (1, 0), (3, 2), (1, 2)]) - - # test constructor, including catching of duplicate entries - self.assertEqual(len(coupling_graph.graph), 3) - self.assertEqual(coupling_graph.sorted_graph, [(0, 1), (1, 2), (2, 3)]) - self.assertEqual( - coupling_graph.sorted_two_way_graph, [(0, 1), (1, 0), (1, 2), (2, 1), (2, 3), (3, 2)] - ) - self.assertEqual( - coupling_graph.two_way_graph_dict, - {(0, 1): 0, (1, 0): 1, (1, 2): 2, (2, 1): 3, (2, 3): 4, (3, 2): 5}, - ) - - # test sorted_edge_index, and that it treats (1,2) and (2,1) as the same edge - self.assertEqual(coupling_graph.sorted_edge_index((1, 2)), 1) - self.assertEqual(coupling_graph.sorted_edge_index((2, 1)), 1) - - # test two_way_edge_index, and that it treats (1,2) and (2,1) as different - self.assertEqual(coupling_graph.two_way_edge_index((1, 2)), 2) - self.assertEqual(coupling_graph.two_way_edge_index((2, 1)), 3) - - def _compare_str_lists(self, list1, list2): - """Helper function for checking that the contents of string lists are the same when order - doesn't matter. - - Args: - list1 (list): A list of strings - list2 (list): A list of strings - """ - - list1_copy = list1.copy() - list2_copy = list1.copy() - self.assertEqual(len(list1_copy), len(list2_copy)) - list1_copy.sort() - list2_copy.sort() - for str1, str2 in zip(list1_copy, list2_copy): - self.assertEqual(str1, str2) - - def _operator_array_from_str(self, dim, op_str_list): - op = get_oper(op_str_list[0], dim) - for c in op_str_list[1:]: - op = op.tensor(get_oper(c, dim)) - - return op diff --git a/test/terra/pulse/test_system_models.py b/test/terra/pulse/test_system_models.py deleted file mode 100644 index c48b309b4c..0000000000 --- a/test/terra/pulse/test_system_models.py +++ /dev/null @@ -1,341 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2019. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -""" -Tests for PulseSystemModel and HamiltonianModel functionality -""" - -import unittest -import warnings -import numpy as np -from numpy.linalg import norm -from test.terra.common import QiskitAerTestCase -from qiskit.providers.fake_provider import FakeOpenPulse2Q -from qiskit.providers.fake_provider import FakeArmonk -from qiskit_aer.pulse.system_models.pulse_system_model import PulseSystemModel -from qiskit_aer.pulse.system_models.hamiltonian_model import HamiltonianModel -from qiskit.providers.models.backendconfiguration import UchannelLO - - -class BaseTestPulseSystemModel(QiskitAerTestCase): - """Tests for PulseSystemModel""" - - def setUp(self): - super().setUp() - self._default_qubit_lo_freq = [4.9, 5.0] - self._u_channel_lo = [] - self._u_channel_lo.append([UchannelLO(0, 1.0 + 0.0j)]) - self._u_channel_lo.append([UchannelLO(0, -1.0 + 0.0j), UchannelLO(1, 1.0 + 0.0j)]) - - def _simple_system_model(self, v0=5.0, v1=5.1, j=0.01, r=0.02, alpha0=-0.33, alpha1=-0.33): - hamiltonian = {} - hamiltonian["h_str"] = [ - "np.pi*(2*v0-alpha0)*O0", - "np.pi*alpha0*O0*O0", - "2*np.pi*r*X0||D0", - "2*np.pi*r*X0||U1", - "2*np.pi*r*X1||U0", - "np.pi*(2*v1-alpha1)*O1", - "np.pi*alpha1*O1*O1", - "2*np.pi*r*X1||D1", - "2*np.pi*j*(Sp0*Sm1+Sm0*Sp1)", - ] - hamiltonian["qub"] = {"0": 3, "1": 3} - hamiltonian["vars"] = { - "v0": v0, - "v1": v1, - "j": j, - "r": r, - "alpha0": alpha0, - "alpha1": alpha1, - } - ham_model = HamiltonianModel.from_dict(hamiltonian) - - subsystem_list = [0, 1] - dt = 1.0 - - return PulseSystemModel( - hamiltonian=ham_model, - u_channel_lo=self._u_channel_lo, - subsystem_list=subsystem_list, - dt=dt, - ) - - -class TestPulseSystemModel(BaseTestPulseSystemModel): - r"""Tests for Hamiltonian options and processing.""" - - def test_control_channel_index(self): - """Test PulseSystemModel.control_channel_index().""" - - # get the model with no control channel dict yet - test_model = self._simple_system_model() - - # test that it gives a warning when a key has no corresponding control channel - with warnings.catch_warnings(record=True) as w: - # Cause all warnings to always be triggered. - warnings.simplefilter("always") - - ctrl_idx = test_model.control_channel_index("no_key") - - self.assertEqual(len(w), 1) - self.assertTrue("ControlChannel" in str(w[-1].message)) - - # control channel labels - test_model.control_channel_labels = [(0, 1)] - - self.assertEqual(test_model.control_channel_index((0, 1)), 0) - - # test that it still correctly gives a warning for nonexistant indices - with warnings.catch_warnings(record=True) as w: - # Cause all warnings to always be triggered. - warnings.simplefilter("always") - - ctrl_idx = test_model.control_channel_index((1, 0)) - - self.assertEqual(len(w), 1) - self.assertTrue("ControlChannel" in str(w[-1].message)) - - def test_control_channel_labels_from_backend(self): - """Test correct importing of backend control channel description.""" - backend = FakeOpenPulse2Q() - - system_model = PulseSystemModel.from_backend(backend) - expected = [ - {"driven_q": 1, "freq": "(1+0j)q0"}, - {"driven_q": 0, "freq": "(-1+0j)q0 + (1+0j)q1"}, - ] - - self.assertEqual(system_model.control_channel_labels, expected) - - def test_qubit_lo_from_hamiltonian(self): - """Test computation of qubit_lo_freq from the hamiltonian itself.""" - test_model = self._simple_system_model() - - qubit_lo_from_hamiltonian = test_model.hamiltonian.get_qubit_lo_from_drift() - freqs = test_model.calculate_channel_frequencies(qubit_lo_from_hamiltonian) - self.assertAlmostEqual(freqs["D0"], 4.999009804864) - self.assertAlmostEqual(freqs["D1"], 5.100990195135) - self.assertAlmostEqual(freqs["U0"], 4.999009804864) - self.assertAlmostEqual(freqs["U1"], 0.101980390271) - - # test again with different parameters - test_model = self._simple_system_model(v0=5.1, v1=4.9, j=0.02) - qubit_lo_from_hamiltonian = test_model.hamiltonian.get_qubit_lo_from_drift() - freqs = test_model.calculate_channel_frequencies(qubit_lo_from_hamiltonian) - self.assertAlmostEqual(freqs["D0"], 5.101980390271) - self.assertAlmostEqual(freqs["D1"], 4.898019609728) - self.assertAlmostEqual(freqs["U0"], 5.101980390271) - self.assertAlmostEqual(freqs["U1"], -0.203960780543) - - def test_qubit_lo_from_configurable_backend(self): - """Test computation of qubit_lo_freq from configurable backend.""" - backend = FakeArmonk() - test_model = PulseSystemModel.from_backend(backend) - qubit_lo_from_hamiltonian = test_model.hamiltonian.get_qubit_lo_from_drift() - freqs = test_model.calculate_channel_frequencies(qubit_lo_from_hamiltonian) - expected = getattr(backend.configuration(), "hamiltonian")["vars"]["wq0"] / (2 * np.pi) - self.assertAlmostEqual(freqs["D0"], expected, places=5) - - def _compute_u_lo_freqs(self, qubit_lo_freq): - """ - Given qubit_lo_freq, return the computed u_channel_lo. - """ - u_lo_freqs = [] - for scales in self._u_channel_lo: - u_lo_freq = 0 - for u_lo_idx in scales: - qfreq = qubit_lo_freq[u_lo_idx.q] - qscale = u_lo_idx.scale.real - u_lo_freq += qfreq * qscale - u_lo_freqs.append(u_lo_freq) - return u_lo_freqs - - -class TestHamiltonianModel(QiskitAerTestCase): - """Tests for HamiltonianModel""" - - def test_subsystem_list_from_dict(self): - """Test correct restriction of a Hamiltonian dict to a subset of systems""" - - # construct 2 duffing oscillator hamiltonian - v0 = 5.0 - v1 = 5.1 - j = 0.01 - r = 0.02 - alpha0 = -0.33 - alpha1 = -0.33 - - hamiltonian = {} - hamiltonian["h_str"] = [ - "np.pi*(2*v0-alpha0)*O0", - "np.pi*alpha0*O0*O0", - "2*np.pi*r*X0||D0", - "2*np.pi*r*X0||U1", - "2*np.pi*r*X1||U0", - "np.pi*(2*v1-alpha1)*O1", - "np.pi*alpha1*O1*O1", - "2*np.pi*r*X1||D1", - "2*np.pi*j*(Sp0*Sm1+Sm0*Sp1)", - ] - hamiltonian["qub"] = {"0": 3, "1": 3} - hamiltonian["vars"] = { - "v0": v0, - "v1": v1, - "j": j, - "r": r, - "alpha0": alpha0, - "alpha1": alpha1, - } - - # restrict to qubit 0 and verify some properties - ham_model0 = HamiltonianModel.from_dict(hamiltonian, subsystem_list=[0]) - evals_expected0 = np.array( - [ - 0, - np.pi * (2 * v0 - alpha0) + np.pi * alpha0, - (2 * np.pi * (2 * v0 - alpha0)) + (4 * np.pi * alpha0), - ] - ) - eval_diff = norm(evals_expected0 - ham_model0._evals) - self.assertAlmostEqual(eval_diff, 0) - - channel_labels0 = ham_model0._channels.keys() - for key in ["D0", "U1"]: - self.assertTrue(key in channel_labels0) - self.assertEqual(len(channel_labels0), 2) - - qubit_lo_freq0 = ham_model0.get_qubit_lo_from_drift() - expected_freq0 = np.array([(np.pi * (2 * v0 - alpha0) + np.pi * alpha0) / (2 * np.pi)]) - self.assertAlmostEqual(norm(qubit_lo_freq0 - expected_freq0), 0) - - # restrict to qubit 1 and verify some properties - ham_model1 = HamiltonianModel.from_dict(hamiltonian, subsystem_list=[1]) - evals_expected1 = np.array( - [ - 0, - np.pi * (2 * v1 - alpha1) + np.pi * alpha1, - (2 * np.pi * (2 * v1 - alpha1)) + (4 * np.pi * alpha1), - ] - ) - eval_diff = norm(evals_expected1 - ham_model1._evals) - self.assertAlmostEqual(eval_diff, 0) - - channel_labels1 = ham_model1._channels.keys() - for key in ["D1", "U0"]: - self.assertTrue(key in channel_labels1) - self.assertEqual(len(channel_labels1), 2) - - qubit_lo_freq1 = ham_model1.get_qubit_lo_from_drift() - expected_freq1 = np.array([0, (np.pi * (2 * v1 - alpha1) + np.pi * alpha1) / (2 * np.pi)]) - self.assertAlmostEqual(norm(qubit_lo_freq1 - expected_freq1), 0) - - def test_eigen_sorting(self): - """Test estate mappings""" - - X = np.array([[0, 1], [1, 0]]) - Y = np.array([[0, -1j], [1j, 0]]) - Z = np.array([[1, 0], [0, -1]]) - - simple_ham = { - "h_str": ["a*X0", "b*Y0", "c*Z0", "d*X0||D0"], - "vars": {"a": 0.1, "b": 0.1, "c": 1, "d": 0.0}, - "qub": {"0": 2}, - } - - ham_model = HamiltonianModel.from_dict(simple_ham) - - # check norm - for estate in ham_model._estates: - self.assertAlmostEqual(norm(estate), 1) - - # check actually an eigenstate - mat = 0.1 * X + 0.1 * Y + 1 * Z - for idx, eval in enumerate(ham_model._evals): - diff = mat @ ham_model._estates[:, idx] - eval * ham_model._estates[:, idx] - self.assertAlmostEqual(norm(diff), 0) - - # Same test but with strongly off-diagonal hamiltonian, which should raise warning - simple_ham = { - "h_str": ["a*X0", "b*Y0", "c*Z0", "d*X0||D0"], - "vars": {"a": 100, "b": 32.1, "c": 0.12, "d": 0.0}, - "qub": {"0": 2}, - } - - ham_model = HamiltonianModel.from_dict(simple_ham) - - # check norm - for estate in ham_model._estates: - self.assertAlmostEqual(norm(estate), 1) - - # check actually an eigenstate - mat = 100 * X + 32.1 * Y + 0.12 * Z - for idx, eval in enumerate(ham_model._evals): - diff = mat @ ham_model._estates[:, idx] - eval * ham_model._estates[:, idx] - self.assertAlmostEqual(norm(diff), 0) - - def test_no_variables(self): - """Test successful construction of Hamiltonian without variables""" - - # fully specified hamiltonian without variables - ham_dict = {"h_str": ["2*np.pi*O0", "2*np.pi*X0||D0"], "qub": {"0": 2}} - ham_model = HamiltonianModel.from_dict(ham_dict) - - qubit_lo = ham_model.get_qubit_lo_from_drift() - self.assertAlmostEqual(norm(qubit_lo - np.array([1.0])), 0) - - def test_empty_hamiltonian_string_exception(self): - """Test exception raising for empty hamiltonian string""" - - message = "Hamiltonian dict requires a non-empty 'h_str' entry." - - ham_dict = {} - self.assert_hamiltonian_parse_exception(ham_dict, message) - - ham_dict = {"h_str": [""]} - self.assert_hamiltonian_parse_exception(ham_dict, message) - - def test_empty_qub_exception(self): - """Test exception raising for empty qub""" - - message = "Hamiltonian dict requires non-empty 'qub' entry with subsystem dimensions." - - ham_dict = {"h_str": "X0"} - self.assert_hamiltonian_parse_exception(ham_dict, message) - - ham_dict = {"h_str": "X0", "qub": {}} - self.assert_hamiltonian_parse_exception(ham_dict, message) - - def test_no_channels_exception(self): - """Test exception raising for a hamiltonian with no channels""" - - message = "HamiltonianModel must contain channels to simulate." - - # hamiltonian with no channels at all - ham_dict = {"h_str": ["X0"], "qub": {"0": 2}} - self.assert_hamiltonian_parse_exception(ham_dict, message) - - # hamiltonian with channels that get removed by parsing - ham_dict = {"h_str": ["X1||D1"], "qub": {"0": 2}} - self.assert_hamiltonian_parse_exception(ham_dict, message) - - def assert_hamiltonian_parse_exception(self, ham_dict, message): - """Test that an attempt to parse a given ham_dict results in an exception with - the given message. - """ - try: - ham_model = HamiltonianModel.from_dict(ham_dict) - except Exception as exception: - self.assertEqual(exception.message, message) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/terra/test_python_to_cpp.py b/test/terra/test_python_to_cpp.py deleted file mode 100644 index 0c0611d614..0000000000 --- a/test/terra/test_python_to_cpp.py +++ /dev/null @@ -1,70 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2019. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -import unittest -import numpy as np -from qiskit_aer.pulse.controllers.test_python_to_cpp import ( - test_py_list_to_cpp_vec, - test_py_list_of_lists_to_cpp_vector_of_vectors, - test_py_dict_string_numeric_to_cpp_map_string_numeric, - test_py_dict_string_list_of_list_of_doubles_to_cpp_map_string_vec_of_vecs_of_doubles, - test_np_array_of_doubles, - test_np_2D_array_of_doubles, - test_evaluate_hamiltonians, - test_py_ordered_map, -) - -from .common import QiskitAerTestCase - - -class TestPythonToCpp(QiskitAerTestCase): - """Test Pyhton C API wrappers we have for dealing with Python data structures - in C++ code.""" - - def test_py_list_to_cpp_vec(self): - arg = [1.0, 2.0, 3.0] - self.assertTrue(test_py_list_to_cpp_vec(arg)) - - def test_py_list_of_lists_to_cpp_vector_of_vectors(self): - arg = [[1.0, 2.0, 3.0]] - self.assertTrue(test_py_list_of_lists_to_cpp_vector_of_vectors(arg)) - - def test_py_dict_string_numeric_to_cpp_map_string_numeric(self): - arg = {"key": 1} - self.assertTrue(test_py_dict_string_numeric_to_cpp_map_string_numeric(arg)) - - def test_py_dict_string_list_of_list_of_doubles_to_cpp_map_string_vec_of_vecs_of_doubles( - self, - ): - arg = {"key": [[1.0, 2.0, 3.0]]} - self.assertTrue( - test_py_dict_string_list_of_list_of_doubles_to_cpp_map_string_vec_of_vecs_of_doubles( - arg - ) - ) - - def test_np_array_of_doubles(self): - arg = np.array([0.0, 1.0, 2.0, 3.0]) - self.assertTrue(test_np_array_of_doubles(arg)) - - def test_np_2D_array_of_doubles(self): - arg = np.array([[0.0, 1.0, 2.0, 3.0], [10.0, 20.0, 30.0, 40.0]]) - self.assertTrue(test_np_2D_array_of_doubles(arg)) - - def test_evaluate_hamiltonians(self): - """TODO: Evaluate different hamiltoninan expressions?""" - self.assertEqual(True, True) - - def test_py_ordered_map(self): - # Since Python 3.6 dict insertion order is guaranted - arg = {"D0": 1, "U0": 2, "D1": 3, "U1": 4} - self.assertTrue(test_py_ordered_map(arg)) diff --git a/tools/verify_wheels.py b/tools/verify_wheels.py index f1228372de..5b7e461463 100644 --- a/tools/verify_wheels.py +++ b/tools/verify_wheels.py @@ -15,14 +15,10 @@ from qiskit.quantum_info import Operator, Statevector from qiskit.quantum_info.operators.predicates import matrix_equal -from qiskit_aer.pulse.system_models.duffing_model_generators import duffing_system_model -from qiskit.pulse import Schedule, Play, Acquire, Waveform, DriveChannel, AcquireChannel, MemorySlot - from qiskit_aer import AerSimulator from qiskit_aer import QasmSimulator from qiskit_aer import StatevectorSimulator from qiskit_aer import UnitarySimulator -from qiskit_aer import PulseSimulator # Backwards compatibility for Terra <= 0.13 if not hasattr(QuantumCircuit, "i"): @@ -370,30 +366,6 @@ def compare_unitary(result, circuits, targets, ignore_phase=False, atol=1e-8, rt raise Exception(msg) -def model_and_pi_schedule(): - """Return a simple model and schedule for pulse simulation""" - - # construct model - model = duffing_system_model( - dim_oscillators=2, - oscillator_freqs=[5.0], - anharm_freqs=[0], - drive_strengths=[0.01], - coupling_dict={}, - dt=1.0, - ) - - # note: parameters set so that area under curve is 1/4 - sample_pulse = Waveform(np.ones(50)) - - # construct schedule - schedule = Schedule(name="test_sched") - schedule |= Play(sample_pulse, DriveChannel(0)) - schedule += Acquire(10, AcquireChannel(0), MemorySlot(0)) << schedule.duration - - return model, schedule - - if __name__ == "__main__": # Run Aer simulator shots = 4000 @@ -429,19 +401,3 @@ def model_and_pi_schedule(): assert result.status == "COMPLETED" assert result.success is True compare_unitary(result, circuits, targets) - - # Run pulse simulator - system_model, schedule = model_and_pi_schedule() - backend_sim = PulseSimulator() - qobj = assemble( - [schedule], - backend=backend_sim, - qubit_lo_freq=[5.0], - meas_level=1, - meas_return="avg", - shots=1, - ) - results = backend_sim.run(qobj, system_model=system_model).result() - state = results.get_statevector(0) - assertAlmostEqual(state[0], 0, delta=10**-3) - assertAlmostEqual(state[1], -1j, delta=10**-3) diff --git a/tox.ini b/tox.ini index 5e42476b4d..8f418f383c 100644 --- a/tox.ini +++ b/tox.ini @@ -54,3 +54,9 @@ commands = python -I -m build --wheel -C=--build-option=-- -C=--build-option=-- -C=--build-option=-j4 pip install --find-links={toxinidir}/dist qiskit_aer sphinx-build -W -b html docs/ docs/_build/html -j auto {posargs} + +[testenv:docs-clean] +skip_install = true +deps = +allowlist_externals = rm +commands = rm -rf {toxinidir}/docs/stubs/ {toxinidir}/docs/_build