Spaces:
Paused
Paused
harishaseebat92
commited on
Commit
·
6d8ed8c
1
Parent(s):
c8de2be
Fix: Add adapt-aqc as regular files, not submodule
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- utils/adapt-aqc +0 -1
- utils/adapt-aqc/.gitignore +364 -0
- utils/adapt-aqc/LICENSE +203 -0
- utils/adapt-aqc/README.md +111 -0
- utils/adapt-aqc/adaptaqc/__init__.py +10 -0
- utils/adapt-aqc/adaptaqc/backends/__init__.py +10 -0
- utils/adapt-aqc/adaptaqc/backends/aer_mps_backend.py +93 -0
- utils/adapt-aqc/adaptaqc/backends/aer_sv_backend.py +59 -0
- utils/adapt-aqc/adaptaqc/backends/aqc_backend.py +29 -0
- utils/adapt-aqc/adaptaqc/backends/itensor_backend.py +62 -0
- utils/adapt-aqc/adaptaqc/backends/julia_default_backends.py +13 -0
- utils/adapt-aqc/adaptaqc/backends/python_default_backends.py +19 -0
- utils/adapt-aqc/adaptaqc/backends/qiskit_sampling_backend.py +108 -0
- utils/adapt-aqc/adaptaqc/compilers/__init__.py +13 -0
- utils/adapt-aqc/adaptaqc/compilers/adapt/adapt_compiler.py +1163 -0
- utils/adapt-aqc/adaptaqc/compilers/adapt/adapt_config.py +97 -0
- utils/adapt-aqc/adaptaqc/compilers/adapt/adapt_result.py +70 -0
- utils/adapt-aqc/adaptaqc/compilers/approximate_compiler.py +527 -0
- utils/adapt-aqc/adaptaqc/utils/__init__.py +11 -0
- utils/adapt-aqc/adaptaqc/utils/ansatzes.py +100 -0
- utils/adapt-aqc/adaptaqc/utils/circuit_operations/__init__.py +17 -0
- utils/adapt-aqc/adaptaqc/utils/circuit_operations/circuit_operations_basic.py +262 -0
- utils/adapt-aqc/adaptaqc/utils/circuit_operations/circuit_operations_circuit_division.py +144 -0
- utils/adapt-aqc/adaptaqc/utils/circuit_operations/circuit_operations_full_circuit.py +465 -0
- utils/adapt-aqc/adaptaqc/utils/circuit_operations/circuit_operations_optimisation.py +231 -0
- utils/adapt-aqc/adaptaqc/utils/circuit_operations/circuit_operations_pauli_ops.py +127 -0
- utils/adapt-aqc/adaptaqc/utils/circuit_operations/circuit_operations_running.py +139 -0
- utils/adapt-aqc/adaptaqc/utils/circuit_operations/circuit_operations_variational.py +84 -0
- utils/adapt-aqc/adaptaqc/utils/constants.py +131 -0
- utils/adapt-aqc/adaptaqc/utils/cost_minimiser.py +418 -0
- utils/adapt-aqc/adaptaqc/utils/entanglement_measures.py +370 -0
- utils/adapt-aqc/adaptaqc/utils/fixed_ansatz_circuits.py +126 -0
- utils/adapt-aqc/adaptaqc/utils/gate_tomography.py +104 -0
- utils/adapt-aqc/adaptaqc/utils/gradients.py +224 -0
- utils/adapt-aqc/adaptaqc/utils/hamiltonians.py +85 -0
- utils/adapt-aqc/adaptaqc/utils/utilityfunctions.py +481 -0
- utils/adapt-aqc/docs/future_heuristic_ideas.md +66 -0
- utils/adapt-aqc/docs/running_options_explained.md +296 -0
- utils/adapt-aqc/examples/advanced_mps_example.py +66 -0
- utils/adapt-aqc/examples/advanced_sv_example.py +61 -0
- utils/adapt-aqc/examples/readme_example.py +64 -0
- utils/adapt-aqc/examples/simple_mps_example.py +33 -0
- utils/adapt-aqc/examples/simple_sv_example.py +25 -0
- utils/adapt-aqc/requirements.txt +1 -0
- utils/adapt-aqc/setup.py +28 -0
- utils/adapt-aqc/test/__init__.py +0 -0
- utils/adapt-aqc/test/recompilers/__init__.py +0 -0
- utils/adapt-aqc/test/recompilers/test_adapt_compiler.py +1543 -0
- utils/adapt-aqc/test/recompilers/test_approximate_compiler.py +170 -0
- utils/adapt-aqc/test/utils/__init__.py +0 -0
utils/adapt-aqc
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
Subproject commit da9bf5895b1b694b167f7eaecae358b670ea29d9
|
|
|
|
|
|
utils/adapt-aqc/.gitignore
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**/__pycache__/*
|
| 2 |
+
.vscode/
|
| 3 |
+
.idea*
|
| 4 |
+
*.egg-info/
|
| 5 |
+
=======
|
| 6 |
+
# Created by https://www.toptal.com/developers/gitignore/api/python,pycharm,visualstudiocode,osx,macos
|
| 7 |
+
# Edit at https://www.toptal.com/developers/gitignore?templates=python,pycharm,visualstudiocode,osx,macos
|
| 8 |
+
|
| 9 |
+
## Misc ##
|
| 10 |
+
|
| 11 |
+
*.npy
|
| 12 |
+
*.pdf
|
| 13 |
+
*.png
|
| 14 |
+
.idea
|
| 15 |
+
|
| 16 |
+
### macOS ###
|
| 17 |
+
# General
|
| 18 |
+
.DS_Store
|
| 19 |
+
.AppleDouble
|
| 20 |
+
.LSOverride
|
| 21 |
+
|
| 22 |
+
# Icon must end with two \r
|
| 23 |
+
Icon
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
# Thumbnails
|
| 27 |
+
._*
|
| 28 |
+
|
| 29 |
+
# Files that might appear in the root of a volume
|
| 30 |
+
.DocumentRevisions-V100
|
| 31 |
+
.fseventsd
|
| 32 |
+
.Spotlight-V100
|
| 33 |
+
.TemporaryItems
|
| 34 |
+
.Trashes
|
| 35 |
+
.VolumeIcon.icns
|
| 36 |
+
.com.apple.timemachine.donotpresent
|
| 37 |
+
|
| 38 |
+
# Directories potentially created on remote AFP share
|
| 39 |
+
.AppleDB
|
| 40 |
+
.AppleDesktop
|
| 41 |
+
Network Trash Folder
|
| 42 |
+
Temporary Items
|
| 43 |
+
.apdisk
|
| 44 |
+
|
| 45 |
+
### macOS Patch ###
|
| 46 |
+
# iCloud generated files
|
| 47 |
+
*.icloud
|
| 48 |
+
|
| 49 |
+
### OSX ###
|
| 50 |
+
# General
|
| 51 |
+
|
| 52 |
+
# Icon must end with two \r
|
| 53 |
+
|
| 54 |
+
# Thumbnails
|
| 55 |
+
|
| 56 |
+
# Files that might appear in the root of a volume
|
| 57 |
+
|
| 58 |
+
# Directories potentially created on remote AFP share
|
| 59 |
+
|
| 60 |
+
### PyCharm ###
|
| 61 |
+
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
| 62 |
+
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
| 63 |
+
|
| 64 |
+
# User-specific stuff
|
| 65 |
+
.idea/**/workspace.xml
|
| 66 |
+
.idea/**/tasks.xml
|
| 67 |
+
.idea/**/usage.statistics.xml
|
| 68 |
+
.idea/**/dictionaries
|
| 69 |
+
.idea/**/shelf
|
| 70 |
+
|
| 71 |
+
# AWS User-specific
|
| 72 |
+
.idea/**/aws.xml
|
| 73 |
+
|
| 74 |
+
# Generated files
|
| 75 |
+
.idea/**/contentModel.xml
|
| 76 |
+
|
| 77 |
+
# Sensitive or high-churn files
|
| 78 |
+
.idea/**/dataSources/
|
| 79 |
+
.idea/**/dataSources.ids
|
| 80 |
+
.idea/**/dataSources.local.xml
|
| 81 |
+
.idea/**/sqlDataSources.xml
|
| 82 |
+
.idea/**/dynamic.xml
|
| 83 |
+
.idea/**/uiDesigner.xml
|
| 84 |
+
.idea/**/dbnavigator.xml
|
| 85 |
+
|
| 86 |
+
# Gradle
|
| 87 |
+
.idea/**/gradle.xml
|
| 88 |
+
.idea/**/libraries
|
| 89 |
+
|
| 90 |
+
# Gradle and Maven with auto-import
|
| 91 |
+
# When using Gradle or Maven with auto-import, you should exclude module files,
|
| 92 |
+
# since they will be recreated, and may cause churn. Uncomment if using
|
| 93 |
+
# auto-import.
|
| 94 |
+
# .idea/artifacts
|
| 95 |
+
# .idea/compiler.xml
|
| 96 |
+
# .idea/jarRepositories.xml
|
| 97 |
+
# .idea/modules.xml
|
| 98 |
+
# .idea/*.iml
|
| 99 |
+
# .idea/modules
|
| 100 |
+
# *.iml
|
| 101 |
+
# *.ipr
|
| 102 |
+
|
| 103 |
+
# CMake
|
| 104 |
+
cmake-build-*/
|
| 105 |
+
|
| 106 |
+
# Mongo Explorer plugin
|
| 107 |
+
.idea/**/mongoSettings.xml
|
| 108 |
+
|
| 109 |
+
# File-based project format
|
| 110 |
+
*.iws
|
| 111 |
+
|
| 112 |
+
# IntelliJ
|
| 113 |
+
out/
|
| 114 |
+
|
| 115 |
+
# mpeltonen/sbt-idea plugin
|
| 116 |
+
.idea_modules/
|
| 117 |
+
|
| 118 |
+
# JIRA plugin
|
| 119 |
+
atlassian-ide-plugin.xml
|
| 120 |
+
|
| 121 |
+
# Cursive Clojure plugin
|
| 122 |
+
.idea/replstate.xml
|
| 123 |
+
|
| 124 |
+
# SonarLint plugin
|
| 125 |
+
.idea/sonarlint/
|
| 126 |
+
|
| 127 |
+
# Crashlytics plugin (for Android Studio and IntelliJ)
|
| 128 |
+
com_crashlytics_export_strings.xml
|
| 129 |
+
crashlytics.properties
|
| 130 |
+
crashlytics-build.properties
|
| 131 |
+
fabric.properties
|
| 132 |
+
|
| 133 |
+
# Editor-based Rest Client
|
| 134 |
+
.idea/httpRequests
|
| 135 |
+
|
| 136 |
+
# Android studio 3.1+ serialized cache file
|
| 137 |
+
.idea/caches/build_file_checksums.ser
|
| 138 |
+
|
| 139 |
+
### PyCharm Patch ###
|
| 140 |
+
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
|
| 141 |
+
|
| 142 |
+
# *.iml
|
| 143 |
+
# modules.xml
|
| 144 |
+
# .idea/misc.xml
|
| 145 |
+
# *.ipr
|
| 146 |
+
|
| 147 |
+
# Sonarlint plugin
|
| 148 |
+
# https://plugins.jetbrains.com/plugin/7973-sonarlint
|
| 149 |
+
.idea/**/sonarlint/
|
| 150 |
+
|
| 151 |
+
# SonarQube Plugin
|
| 152 |
+
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
|
| 153 |
+
.idea/**/sonarIssues.xml
|
| 154 |
+
|
| 155 |
+
# Markdown Navigator plugin
|
| 156 |
+
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
|
| 157 |
+
.idea/**/markdown-navigator.xml
|
| 158 |
+
.idea/**/markdown-navigator-enh.xml
|
| 159 |
+
.idea/**/markdown-navigator/
|
| 160 |
+
|
| 161 |
+
# Cache file creation bug
|
| 162 |
+
# See https://youtrack.jetbrains.com/issue/JBR-2257
|
| 163 |
+
.idea/$CACHE_FILE$
|
| 164 |
+
|
| 165 |
+
# CodeStream plugin
|
| 166 |
+
# https://plugins.jetbrains.com/plugin/12206-codestream
|
| 167 |
+
.idea/codestream.xml
|
| 168 |
+
|
| 169 |
+
# Azure Toolkit for IntelliJ plugin
|
| 170 |
+
# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
|
| 171 |
+
.idea/**/azureSettings.xml
|
| 172 |
+
|
| 173 |
+
### Python ###
|
| 174 |
+
# Byte-compiled / optimized / DLL files
|
| 175 |
+
__pycache__/
|
| 176 |
+
*.py[cod]
|
| 177 |
+
*$py.class
|
| 178 |
+
|
| 179 |
+
# C extensions
|
| 180 |
+
*.so
|
| 181 |
+
|
| 182 |
+
# Distribution / packaging
|
| 183 |
+
.Python
|
| 184 |
+
build/
|
| 185 |
+
develop-eggs/
|
| 186 |
+
dist/
|
| 187 |
+
downloads/
|
| 188 |
+
eggs/
|
| 189 |
+
.eggs/
|
| 190 |
+
lib/
|
| 191 |
+
lib64/
|
| 192 |
+
parts/
|
| 193 |
+
sdist/
|
| 194 |
+
var/
|
| 195 |
+
wheels/
|
| 196 |
+
share/python-wheels/
|
| 197 |
+
*.egg-info/
|
| 198 |
+
.installed.cfg
|
| 199 |
+
*.egg
|
| 200 |
+
MANIFEST
|
| 201 |
+
|
| 202 |
+
# PyInstaller
|
| 203 |
+
# Usually these files are written by a python script from a template
|
| 204 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
| 205 |
+
*.manifest
|
| 206 |
+
*.spec
|
| 207 |
+
|
| 208 |
+
# Installer logs
|
| 209 |
+
pip-log.txt
|
| 210 |
+
pip-delete-this-directory.txt
|
| 211 |
+
|
| 212 |
+
# Unit test / coverage reports
|
| 213 |
+
htmlcov/
|
| 214 |
+
.tox/
|
| 215 |
+
.nox/
|
| 216 |
+
.coverage
|
| 217 |
+
.coverage.*
|
| 218 |
+
.cache
|
| 219 |
+
nosetests.xml
|
| 220 |
+
coverage.xml
|
| 221 |
+
*.cover
|
| 222 |
+
*.py,cover
|
| 223 |
+
.hypothesis/
|
| 224 |
+
.pytest_cache/
|
| 225 |
+
cover/
|
| 226 |
+
|
| 227 |
+
# Translations
|
| 228 |
+
*.mo
|
| 229 |
+
*.pot
|
| 230 |
+
|
| 231 |
+
# Django stuff:
|
| 232 |
+
*.log
|
| 233 |
+
local_settings.py
|
| 234 |
+
db.sqlite3
|
| 235 |
+
db.sqlite3-journal
|
| 236 |
+
|
| 237 |
+
# Flask stuff:
|
| 238 |
+
instance/
|
| 239 |
+
.webassets-cache
|
| 240 |
+
|
| 241 |
+
# Scrapy stuff:
|
| 242 |
+
.scrapy
|
| 243 |
+
|
| 244 |
+
# Sphinx documentation
|
| 245 |
+
docs/_build/
|
| 246 |
+
|
| 247 |
+
# PyBuilder
|
| 248 |
+
.pybuilder/
|
| 249 |
+
target/
|
| 250 |
+
|
| 251 |
+
# Jupyter Notebook
|
| 252 |
+
.ipynb_checkpoints
|
| 253 |
+
|
| 254 |
+
# IPython
|
| 255 |
+
profile_default/
|
| 256 |
+
ipython_config.py
|
| 257 |
+
|
| 258 |
+
# pyenv
|
| 259 |
+
# For a library or package, you might want to ignore these files since the code is
|
| 260 |
+
# intended to run in multiple environments; otherwise, check them in:
|
| 261 |
+
# .python-version
|
| 262 |
+
|
| 263 |
+
# pipenv
|
| 264 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
| 265 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
| 266 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
| 267 |
+
# install all needed dependencies.
|
| 268 |
+
#Pipfile.lock
|
| 269 |
+
|
| 270 |
+
# poetry
|
| 271 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
| 272 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 273 |
+
# commonly ignored for libraries.
|
| 274 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
| 275 |
+
#poetry.lock
|
| 276 |
+
|
| 277 |
+
# pdm
|
| 278 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
| 279 |
+
#pdm.lock
|
| 280 |
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
| 281 |
+
# in version control.
|
| 282 |
+
# https://pdm.fming.dev/#use-with-ide
|
| 283 |
+
.pdm.toml
|
| 284 |
+
|
| 285 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
| 286 |
+
__pypackages__/
|
| 287 |
+
|
| 288 |
+
# Celery stuff
|
| 289 |
+
celerybeat-schedule
|
| 290 |
+
celerybeat.pid
|
| 291 |
+
|
| 292 |
+
# SageMath parsed files
|
| 293 |
+
*.sage.py
|
| 294 |
+
|
| 295 |
+
# Environments
|
| 296 |
+
.env
|
| 297 |
+
.venv
|
| 298 |
+
env/
|
| 299 |
+
venv/
|
| 300 |
+
ENV/
|
| 301 |
+
env.bak/
|
| 302 |
+
venv.bak/
|
| 303 |
+
|
| 304 |
+
# Spyder project settings
|
| 305 |
+
.spyderproject
|
| 306 |
+
.spyproject
|
| 307 |
+
|
| 308 |
+
# Rope project settings
|
| 309 |
+
.ropeproject
|
| 310 |
+
|
| 311 |
+
# mkdocs documentation
|
| 312 |
+
/site
|
| 313 |
+
|
| 314 |
+
# mypy
|
| 315 |
+
.mypy_cache/
|
| 316 |
+
.dmypy.json
|
| 317 |
+
dmypy.json
|
| 318 |
+
|
| 319 |
+
# Pyre type checker
|
| 320 |
+
.pyre/
|
| 321 |
+
|
| 322 |
+
# pytype static type analyzer
|
| 323 |
+
.pytype/
|
| 324 |
+
|
| 325 |
+
# Cython debug symbols
|
| 326 |
+
cython_debug/
|
| 327 |
+
|
| 328 |
+
# PyCharm
|
| 329 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
| 330 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
| 331 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
| 332 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
| 333 |
+
#.idea/
|
| 334 |
+
|
| 335 |
+
### Python Patch ###
|
| 336 |
+
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
|
| 337 |
+
poetry.toml
|
| 338 |
+
|
| 339 |
+
# ruff
|
| 340 |
+
.ruff_cache/
|
| 341 |
+
|
| 342 |
+
# LSP config files
|
| 343 |
+
pyrightconfig.json
|
| 344 |
+
|
| 345 |
+
### VisualStudioCode ###
|
| 346 |
+
.vscode/*
|
| 347 |
+
!.vscode/settings.json
|
| 348 |
+
!.vscode/tasks.json
|
| 349 |
+
!.vscode/launch.json
|
| 350 |
+
!.vscode/extensions.json
|
| 351 |
+
!.vscode/*.code-snippets
|
| 352 |
+
|
| 353 |
+
# Local History for Visual Studio Code
|
| 354 |
+
.history/
|
| 355 |
+
|
| 356 |
+
# Built Visual Studio Code Extensions
|
| 357 |
+
*.vsix
|
| 358 |
+
|
| 359 |
+
### VisualStudioCode Patch ###
|
| 360 |
+
# Ignore all local history of files
|
| 361 |
+
.history
|
| 362 |
+
.ionide
|
| 363 |
+
|
| 364 |
+
# End of https://www.toptal.com/developers/gitignore/api/python,pycharm,visualstudiocode,osx,macos
|
utils/adapt-aqc/LICENSE
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Copyright 2025 IBM and its contributors
|
| 2 |
+
|
| 3 |
+
Apache License
|
| 4 |
+
Version 2.0, January 2004
|
| 5 |
+
http://www.apache.org/licenses/
|
| 6 |
+
|
| 7 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 8 |
+
|
| 9 |
+
1. Definitions.
|
| 10 |
+
|
| 11 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 12 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 13 |
+
|
| 14 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 15 |
+
the copyright owner that is granting the License.
|
| 16 |
+
|
| 17 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 18 |
+
other entities that control, are controlled by, or are under common
|
| 19 |
+
control with that entity. For the purposes of this definition,
|
| 20 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 21 |
+
direction or management of such entity, whether by contract or
|
| 22 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 23 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 24 |
+
|
| 25 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 26 |
+
exercising permissions granted by this License.
|
| 27 |
+
|
| 28 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 29 |
+
including but not limited to software source code, documentation
|
| 30 |
+
source, and configuration files.
|
| 31 |
+
|
| 32 |
+
"Object" form shall mean any form resulting from mechanical
|
| 33 |
+
transformation or translation of a Source form, including but
|
| 34 |
+
not limited to compiled object code, generated documentation,
|
| 35 |
+
and conversions to other media types.
|
| 36 |
+
|
| 37 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 38 |
+
Object form, made available under the License, as indicated by a
|
| 39 |
+
copyright notice that is included in or attached to the work
|
| 40 |
+
(an example is provided in the Appendix below).
|
| 41 |
+
|
| 42 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 43 |
+
form, that is based on (or derived from) the Work and for which the
|
| 44 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 45 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 46 |
+
of this License, Derivative Works shall not include works that remain
|
| 47 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 48 |
+
the Work and Derivative Works thereof.
|
| 49 |
+
|
| 50 |
+
"Contribution" shall mean any work of authorship, including
|
| 51 |
+
the original version of the Work and any modifications or additions
|
| 52 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 53 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 54 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 55 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 56 |
+
means any form of electronic, verbal, or written communication sent
|
| 57 |
+
to the Licensor or its representatives, including but not limited to
|
| 58 |
+
communication on electronic mailing lists, source code control systems,
|
| 59 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 60 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 61 |
+
excluding communication that is conspicuously marked or otherwise
|
| 62 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 63 |
+
|
| 64 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 65 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 66 |
+
subsequently incorporated within the Work.
|
| 67 |
+
|
| 68 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 69 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 70 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 71 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 72 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 73 |
+
Work and such Derivative Works in Source or Object form.
|
| 74 |
+
|
| 75 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 76 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 77 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 78 |
+
(except as stated in this section) patent license to make, have made,
|
| 79 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 80 |
+
where such license applies only to those patent claims licensable
|
| 81 |
+
by such Contributor that are necessarily infringed by their
|
| 82 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 83 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 84 |
+
institute patent litigation against any entity (including a
|
| 85 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 86 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 87 |
+
or contributory patent infringement, then any patent licenses
|
| 88 |
+
granted to You under this License for that Work shall terminate
|
| 89 |
+
as of the date such litigation is filed.
|
| 90 |
+
|
| 91 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 92 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 93 |
+
modifications, and in Source or Object form, provided that You
|
| 94 |
+
meet the following conditions:
|
| 95 |
+
|
| 96 |
+
(a) You must give any other recipients of the Work or
|
| 97 |
+
Derivative Works a copy of this License; and
|
| 98 |
+
|
| 99 |
+
(b) You must cause any modified files to carry prominent notices
|
| 100 |
+
stating that You changed the files; and
|
| 101 |
+
|
| 102 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 103 |
+
that You distribute, all copyright, patent, trademark, and
|
| 104 |
+
attribution notices from the Source form of the Work,
|
| 105 |
+
excluding those notices that do not pertain to any part of
|
| 106 |
+
the Derivative Works; and
|
| 107 |
+
|
| 108 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 109 |
+
distribution, then any Derivative Works that You distribute must
|
| 110 |
+
include a readable copy of the attribution notices contained
|
| 111 |
+
within such NOTICE file, excluding those notices that do not
|
| 112 |
+
pertain to any part of the Derivative Works, in at least one
|
| 113 |
+
of the following places: within a NOTICE text file distributed
|
| 114 |
+
as part of the Derivative Works; within the Source form or
|
| 115 |
+
documentation, if provided along with the Derivative Works; or,
|
| 116 |
+
within a display generated by the Derivative Works, if and
|
| 117 |
+
wherever such third-party notices normally appear. The contents
|
| 118 |
+
of the NOTICE file are for informational purposes only and
|
| 119 |
+
do not modify the License. You may add Your own attribution
|
| 120 |
+
notices within Derivative Works that You distribute, alongside
|
| 121 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 122 |
+
that such additional attribution notices cannot be construed
|
| 123 |
+
as modifying the License.
|
| 124 |
+
|
| 125 |
+
You may add Your own copyright statement to Your modifications and
|
| 126 |
+
may provide additional or different license terms and conditions
|
| 127 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 128 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 129 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 130 |
+
the conditions stated in this License.
|
| 131 |
+
|
| 132 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 133 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 134 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 135 |
+
this License, without any additional terms or conditions.
|
| 136 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 137 |
+
the terms of any separate license agreement you may have executed
|
| 138 |
+
with Licensor regarding such Contributions.
|
| 139 |
+
|
| 140 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 141 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 142 |
+
except as required for reasonable and customary use in describing the
|
| 143 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 144 |
+
|
| 145 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 146 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 147 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 148 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 149 |
+
implied, including, without limitation, any warranties or conditions
|
| 150 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 151 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 152 |
+
appropriateness of using or redistributing the Work and assume any
|
| 153 |
+
risks associated with Your exercise of permissions under this License.
|
| 154 |
+
|
| 155 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 156 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 157 |
+
unless required by applicable law (such as deliberate and grossly
|
| 158 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 159 |
+
liable to You for damages, including any direct, indirect, special,
|
| 160 |
+
incidental, or consequential damages of any character arising as a
|
| 161 |
+
result of this License or out of the use or inability to use the
|
| 162 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 163 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 164 |
+
other commercial damages or losses), even if such Contributor
|
| 165 |
+
has been advised of the possibility of such damages.
|
| 166 |
+
|
| 167 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 168 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 169 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 170 |
+
or other liability obligations and/or rights consistent with this
|
| 171 |
+
License. However, in accepting such obligations, You may act only
|
| 172 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 173 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 174 |
+
defend, and hold each Contributor harmless for any liability
|
| 175 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 176 |
+
of your accepting any such warranty or additional liability.
|
| 177 |
+
|
| 178 |
+
END OF TERMS AND CONDITIONS
|
| 179 |
+
|
| 180 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 181 |
+
|
| 182 |
+
To apply the Apache License to your work, attach the following
|
| 183 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 184 |
+
replaced with your own identifying information. (Don't include
|
| 185 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 186 |
+
comment syntax for the file format. We also recommend that a
|
| 187 |
+
file or class name and description of purpose be included on the
|
| 188 |
+
same "printed page" as the copyright notice for easier
|
| 189 |
+
identification within third-party archives.
|
| 190 |
+
|
| 191 |
+
Copyright 2017 IBM and its contributors.
|
| 192 |
+
|
| 193 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 194 |
+
you may not use this file except in compliance with the License.
|
| 195 |
+
You may obtain a copy of the License at
|
| 196 |
+
|
| 197 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 198 |
+
|
| 199 |
+
Unless required by applicable law or agreed to in writing, software
|
| 200 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 201 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 202 |
+
See the License for the specific language governing permissions and
|
| 203 |
+
limitations under the License.
|
utils/adapt-aqc/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Adaptive approximate quantum compiling
|
| 2 |
+
|
| 3 |
+
An open-source implementation of ADAPT-AQC [1], an approximate quantum compiling (AQC) and matrix
|
| 4 |
+
product state (MPS) preparation algorithm.
|
| 5 |
+
As opposed to assuming any particular ansatz structure, ADAPT-AQC adaptively builds
|
| 6 |
+
an ansatz, adding a new two-qubit unitary every iteration.
|
| 7 |
+
|
| 8 |
+
ADAPT-AQC is the successor to ISL [2], using much of the same core code, routine and optimiser. The
|
| 9 |
+
most significant difference
|
| 10 |
+
however is its use of MPS simulators. This allows it to compile circuits at 50+ qubits, as well as
|
| 11 |
+
directly prepare MPSs.
|
| 12 |
+
|
| 13 |
+
[1] https://arxiv.org/abs/2503.09683 \
|
| 14 |
+
[2] https://github.com/abhishekagarwal2301/isl
|
| 15 |
+
|
| 16 |
+
## Installation
|
| 17 |
+
|
| 18 |
+
This repository can be easily installed using `pip`. You have two options:
|
| 19 |
+
|
| 20 |
+
Use a stable version based on the last commit to `master`
|
| 21 |
+
|
| 22 |
+
```
|
| 23 |
+
pip install git+ssh://git@github.com/qiskit-community/adapt-aqc.git
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
Use an editable local version (after already cloning this repository)
|
| 27 |
+
|
| 28 |
+
```
|
| 29 |
+
pip install -e PATH_TO_LOCAL_CLONE
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
## Contributing
|
| 33 |
+
|
| 34 |
+
To make changes to ADAPT-AQC, first clone the repository.
|
| 35 |
+
Then navigate to your local copy, create a Python environment and install the required dependencies
|
| 36 |
+
|
| 37 |
+
```
|
| 38 |
+
pip install .
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
You can check your development environment is ready by successfully running the scripts
|
| 42 |
+
in `/examples/`.
|
| 43 |
+
|
| 44 |
+
## Minimal examples
|
| 45 |
+
|
| 46 |
+
### Compiling with statevector simulator
|
| 47 |
+
|
| 48 |
+
A circuit can be compiled and the result accessed with only 3 lines if using the
|
| 49 |
+
default settings.
|
| 50 |
+
|
| 51 |
+
```python
|
| 52 |
+
from adaptaqc.compilers import AdaptCompiler
|
| 53 |
+
from qiskit import QuantumCircuit
|
| 54 |
+
|
| 55 |
+
# Setup the circuit
|
| 56 |
+
qc = QuantumCircuit(3)
|
| 57 |
+
qc.rx(1.23, 0)
|
| 58 |
+
qc.cx(0, 1)
|
| 59 |
+
qc.ry(2.5, 1)
|
| 60 |
+
qc.rx(-1.6, 2)
|
| 61 |
+
qc.ccx(2, 1, 0)
|
| 62 |
+
|
| 63 |
+
# Compile
|
| 64 |
+
compiler = AdaptCompiler(qc)
|
| 65 |
+
result = compiler.compile()
|
| 66 |
+
compiled_circuit = result.circuit
|
| 67 |
+
|
| 68 |
+
# See the compiled output
|
| 69 |
+
print(compiled_circuit)
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
### Compiling matrix product states
|
| 73 |
+
|
| 74 |
+
Circuits beyond the size accessible to statevector simulators can be compiled via their
|
| 75 |
+
representation as matrix product states. To give a very simple example where most the qubits are
|
| 76 |
+
left in the $|0\rangle$ state.
|
| 77 |
+
|
| 78 |
+
```python
|
| 79 |
+
from qiskit import QuantumCircuit
|
| 80 |
+
|
| 81 |
+
from adaptaqc.backends.aer_mps_backend import AerMPSBackend
|
| 82 |
+
from adaptaqc.compilers import AdaptCompiler
|
| 83 |
+
|
| 84 |
+
n = 50
|
| 85 |
+
qc = QuantumCircuit(n)
|
| 86 |
+
qc.h(0)
|
| 87 |
+
qc.cx(0, 1)
|
| 88 |
+
qc.h(2)
|
| 89 |
+
qc.cx(2, 3)
|
| 90 |
+
qc.h(range(4, n))
|
| 91 |
+
|
| 92 |
+
# Create compiler with the default MPS simulator, which has very minimal truncation.
|
| 93 |
+
adapt_compiler = AdaptCompiler(qc, backend=AerMPSBackend())
|
| 94 |
+
|
| 95 |
+
result = adapt_compiler.compile()
|
| 96 |
+
print(f"Overlap between circuits is {result.overlap}")
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
### Specifying additional configuration
|
| 100 |
+
|
| 101 |
+
For more advanced examples, please see `examples/advanced_mps_example.py` and
|
| 102 |
+
`advanced_sv_example.py`.
|
| 103 |
+
For a full overview of the different configuration options, in addition to the documentation, see
|
| 104 |
+
`docs/running_options_explained.md`.
|
| 105 |
+
|
| 106 |
+
## Citing usage
|
| 107 |
+
|
| 108 |
+
We respectfully ask any publication, project or whitepaper using ADAPT-AQC to cite the following
|
| 109 |
+
work:
|
| 110 |
+
|
| 111 |
+
[Jaderberg, B., Pennington, G., Marshall, K.V., Anderson, L.W., Agarwal, A., Lindoy, L.P., Rungger, I., Mensa, S. and Crain, J., 2025. Variational preparation of normal matrix product states on quantum computers. arXiv preprint arXiv:2503.09683](https://arxiv.org/abs/2503.09683)
|
utils/adapt-aqc/adaptaqc/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# (C) Copyright IBM 2025.
|
| 2 |
+
#
|
| 3 |
+
# This code is licensed under the Apache License, Version 2.0. You may
|
| 4 |
+
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
| 5 |
+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
| 6 |
+
#
|
| 7 |
+
# Any modifications or derivative works of this code must retain this
|
| 8 |
+
# copyright notice, and modified files need to carry a notice indicating
|
| 9 |
+
# that they have been altered from the originals.
|
| 10 |
+
|
utils/adapt-aqc/adaptaqc/backends/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# (C) Copyright IBM 2025.
|
| 2 |
+
#
|
| 3 |
+
# This code is licensed under the Apache License, Version 2.0. You may
|
| 4 |
+
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
| 5 |
+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
| 6 |
+
#
|
| 7 |
+
# Any modifications or derivative works of this code must retain this
|
| 8 |
+
# copyright notice, and modified files need to carry a notice indicating
|
| 9 |
+
# that they have been altered from the originals.
|
| 10 |
+
|
utils/adapt-aqc/adaptaqc/backends/aer_mps_backend.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# (C) Copyright IBM 2025.
|
| 2 |
+
#
|
| 3 |
+
# This code is licensed under the Apache License, Version 2.0. You may
|
| 4 |
+
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
| 5 |
+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
| 6 |
+
#
|
| 7 |
+
# Any modifications or derivative works of this code must retain this
|
| 8 |
+
# copyright notice, and modified files need to carry a notice indicating
|
| 9 |
+
# that they have been altered from the originals.
|
| 10 |
+
|
| 11 |
+
import logging
|
| 12 |
+
|
| 13 |
+
import numpy as np
|
| 14 |
+
from aqc_research.mps_operations import (
|
| 15 |
+
mps_from_circuit,
|
| 16 |
+
mps_dot,
|
| 17 |
+
mps_expectation,
|
| 18 |
+
extract_amplitude,
|
| 19 |
+
)
|
| 20 |
+
from qiskit_aer import AerSimulator
|
| 21 |
+
|
| 22 |
+
from adaptaqc.backends.aqc_backend import AQCBackend
|
| 23 |
+
|
| 24 |
+
logger = logging.getLogger(__name__)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def mps_sim_with_args(mps_truncation_threshold=1e-16, max_chi=None, mps_log_data=False):
|
| 28 |
+
"""
|
| 29 |
+
:param mps_truncation_threshold: truncation threshold to use in AerSimulator
|
| 30 |
+
:param max_chi: maximum bond dimension to use in AerSimulator
|
| 31 |
+
:param mps_log_data: same as corresponding argument in AerSimulator. Setting to true will
|
| 32 |
+
massively reduce performance and should only be done for debugging
|
| 33 |
+
|
| 34 |
+
:return: instance of AerSimulator using MPS method and parameters specified above
|
| 35 |
+
"""
|
| 36 |
+
logger.info(f"Using Aer MPS Simulator with truncation {mps_truncation_threshold}")
|
| 37 |
+
return AerSimulator(
|
| 38 |
+
method="matrix_product_state",
|
| 39 |
+
matrix_product_state_truncation_threshold=mps_truncation_threshold,
|
| 40 |
+
matrix_product_state_max_bond_dimension=max_chi,
|
| 41 |
+
mps_log_data=mps_log_data,
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class AerMPSBackend(AQCBackend):
|
| 46 |
+
def __init__(self, simulator=mps_sim_with_args()):
|
| 47 |
+
self.simulator = simulator
|
| 48 |
+
|
| 49 |
+
def evaluate_global_cost(self, compiler):
|
| 50 |
+
circ_mps = self.evaluate_circuit(compiler)
|
| 51 |
+
global_cost = (
|
| 52 |
+
1
|
| 53 |
+
- np.absolute(
|
| 54 |
+
mps_dot(circ_mps, compiler.zero_mps, already_preprocessed=True)
|
| 55 |
+
)
|
| 56 |
+
** 2
|
| 57 |
+
)
|
| 58 |
+
if not compiler.soften_global_cost:
|
| 59 |
+
return global_cost
|
| 60 |
+
else:
|
| 61 |
+
previous_cost = (
|
| 62 |
+
compiler.global_cost_history[-1]
|
| 63 |
+
if len(compiler.global_cost_history) > 0
|
| 64 |
+
else 1
|
| 65 |
+
)
|
| 66 |
+
alpha = abs(previous_cost - compiler.adapt_config.sufficient_cost)
|
| 67 |
+
hamming_weight_one_overlaps = self.evaluate_hamming_weight_one_overlaps(
|
| 68 |
+
circ_mps
|
| 69 |
+
)
|
| 70 |
+
return global_cost - alpha * sum(hamming_weight_one_overlaps)
|
| 71 |
+
|
| 72 |
+
def evaluate_local_cost(self, compiler):
|
| 73 |
+
evals = self.measure_qubit_expectation_values(compiler)
|
| 74 |
+
return 0.5 * (1 - np.mean(evals))
|
| 75 |
+
|
| 76 |
+
def evaluate_circuit(self, compiler):
|
| 77 |
+
circ = compiler.full_circuit.copy()
|
| 78 |
+
return mps_from_circuit(circ, return_preprocessed=True, sim=self.simulator)
|
| 79 |
+
|
| 80 |
+
def measure_qubit_expectation_values(self, compiler):
|
| 81 |
+
mps = self.evaluate_circuit(compiler)
|
| 82 |
+
expectation_values = [
|
| 83 |
+
(mps_expectation(mps, "Z", i, already_preprocessed=True))
|
| 84 |
+
for i in range(compiler.full_circuit.num_qubits)
|
| 85 |
+
]
|
| 86 |
+
return expectation_values
|
| 87 |
+
|
| 88 |
+
def evaluate_hamming_weight_one_overlaps(self, mps):
|
| 89 |
+
overlaps = [
|
| 90 |
+
abs(extract_amplitude(mps, 2**i, already_preprocessed=True)) ** 2
|
| 91 |
+
for i in range(len(mps))
|
| 92 |
+
]
|
| 93 |
+
return overlaps
|
utils/adapt-aqc/adaptaqc/backends/aer_sv_backend.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# (C) Copyright IBM 2025.
|
| 2 |
+
#
|
| 3 |
+
# This code is licensed under the Apache License, Version 2.0. You may
|
| 4 |
+
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
| 5 |
+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
| 6 |
+
#
|
| 7 |
+
# Any modifications or derivative works of this code must retain this
|
| 8 |
+
# copyright notice, and modified files need to carry a notice indicating
|
| 9 |
+
# that they have been altered from the originals.
|
| 10 |
+
|
| 11 |
+
import os
|
| 12 |
+
|
| 13 |
+
import numpy as np
|
| 14 |
+
from qiskit_aer import Aer
|
| 15 |
+
|
| 16 |
+
from adaptaqc.backends.aqc_backend import AQCBackend
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class AerSVBackend(AQCBackend):
|
| 20 |
+
def __init__(self, simulator=Aer.get_backend("statevector_simulator")):
|
| 21 |
+
self.simulator = simulator
|
| 22 |
+
|
| 23 |
+
def evaluate_global_cost(self, compiler):
|
| 24 |
+
if compiler.soften_global_cost:
|
| 25 |
+
raise NotImplementedError(
|
| 26 |
+
"soften_global_cost is currently only implemented for AerMPSBackend"
|
| 27 |
+
)
|
| 28 |
+
sv = self.evaluate_circuit(compiler)
|
| 29 |
+
cost = 1 - (np.absolute(sv[0])) ** 2
|
| 30 |
+
return cost
|
| 31 |
+
|
| 32 |
+
def evaluate_local_cost(self, compiler):
|
| 33 |
+
e_vals = self.measure_qubit_expectation_values(compiler)
|
| 34 |
+
cost = 0.5 * (1 - np.mean(e_vals))
|
| 35 |
+
return cost
|
| 36 |
+
|
| 37 |
+
def evaluate_circuit(self, compiler):
|
| 38 |
+
# Don't parallelise shots if ADAPT-AQC is already being run in parallel
|
| 39 |
+
already_in_parallel = os.environ["QISKIT_IN_PARALLEL"] == "TRUE"
|
| 40 |
+
backend_options = {} if already_in_parallel else compiler.backend_options
|
| 41 |
+
|
| 42 |
+
job = self.simulator.run(
|
| 43 |
+
compiler.full_circuit, **backend_options, **compiler.execute_kwargs
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
result = job.result()
|
| 47 |
+
return result.get_statevector()
|
| 48 |
+
|
| 49 |
+
def measure_qubit_expectation_values(self, compiler):
|
| 50 |
+
sv = self.evaluate_circuit(compiler)
|
| 51 |
+
expectation_values = []
|
| 52 |
+
n_qubits = sv.num_qubits
|
| 53 |
+
for i in range(n_qubits):
|
| 54 |
+
if i >= n_qubits:
|
| 55 |
+
raise ValueError("qubit_index outside of register range")
|
| 56 |
+
[p0, p1] = sv.probabilities([i])
|
| 57 |
+
exp_val = p0 - p1
|
| 58 |
+
expectation_values.append(exp_val)
|
| 59 |
+
return expectation_values
|
utils/adapt-aqc/adaptaqc/backends/aqc_backend.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# (C) Copyright IBM 2025.
|
| 2 |
+
#
|
| 3 |
+
# This code is licensed under the Apache License, Version 2.0. You may
|
| 4 |
+
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
| 5 |
+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
| 6 |
+
#
|
| 7 |
+
# Any modifications or derivative works of this code must retain this
|
| 8 |
+
# copyright notice, and modified files need to carry a notice indicating
|
| 9 |
+
# that they have been altered from the originals.
|
| 10 |
+
|
| 11 |
+
import abc
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class AQCBackend(abc.ABC):
|
| 15 |
+
@abc.abstractmethod
|
| 16 |
+
def evaluate_global_cost(self, compiler):
|
| 17 |
+
pass
|
| 18 |
+
|
| 19 |
+
@abc.abstractmethod
|
| 20 |
+
def evaluate_local_cost(self, compiler):
|
| 21 |
+
pass
|
| 22 |
+
|
| 23 |
+
@abc.abstractmethod
|
| 24 |
+
def evaluate_circuit(self, compiler):
|
| 25 |
+
pass
|
| 26 |
+
|
| 27 |
+
@abc.abstractmethod
|
| 28 |
+
def measure_qubit_expectation_values(self, compiler):
|
| 29 |
+
pass
|
utils/adapt-aqc/adaptaqc/backends/itensor_backend.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# (C) Copyright IBM 2025.
|
| 2 |
+
#
|
| 3 |
+
# This code is licensed under the Apache License, Version 2.0. You may
|
| 4 |
+
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
| 5 |
+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
| 6 |
+
#
|
| 7 |
+
# Any modifications or derivative works of this code must retain this
|
| 8 |
+
# copyright notice, and modified files need to carry a notice indicating
|
| 9 |
+
# that they have been altered from the originals.
|
| 10 |
+
|
| 11 |
+
import logging
|
| 12 |
+
|
| 13 |
+
from adaptaqc.backends.aqc_backend import AQCBackend
|
| 14 |
+
from adaptaqc.utils.circuit_operations import extract_inner_circuit
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class ITensorBackend(AQCBackend):
|
| 18 |
+
def __init__(self, chi=10_000, cutoff=1e-14):
|
| 19 |
+
from itensornetworks_qiskit.utils import qiskit_circ_to_it_circ
|
| 20 |
+
|
| 21 |
+
self.qiskit_circ_to_it_circ = qiskit_circ_to_it_circ
|
| 22 |
+
try:
|
| 23 |
+
from juliacall import Main as jl
|
| 24 |
+
from juliacall import JuliaError
|
| 25 |
+
|
| 26 |
+
self.jl = jl
|
| 27 |
+
self.jl.seval("using ITensorNetworksQiskit")
|
| 28 |
+
except JuliaError as e:
|
| 29 |
+
logging.error("ITensor backend installation not found")
|
| 30 |
+
raise e
|
| 31 |
+
self.chi = chi
|
| 32 |
+
self.cutoff = cutoff
|
| 33 |
+
|
| 34 |
+
def evaluate_global_cost(self, compiler):
|
| 35 |
+
if compiler.soften_global_cost:
|
| 36 |
+
raise NotImplementedError(
|
| 37 |
+
"soften_global_cost is currently only implemented for AerMPSBackend"
|
| 38 |
+
)
|
| 39 |
+
psi = self.evaluate_circuit(compiler)
|
| 40 |
+
|
| 41 |
+
n = compiler.total_num_qubits
|
| 42 |
+
return 1 - self.jl.overlap_with_zero_itensors(n, psi, compiler.itensor_sites)
|
| 43 |
+
|
| 44 |
+
def evaluate_local_cost(self, compiler):
|
| 45 |
+
raise NotImplementedError()
|
| 46 |
+
|
| 47 |
+
def evaluate_circuit(self, compiler):
|
| 48 |
+
ansatz_circ = extract_inner_circuit(
|
| 49 |
+
compiler.full_circuit, compiler.ansatz_range()
|
| 50 |
+
)
|
| 51 |
+
gates = self.qiskit_circ_to_it_circ(ansatz_circ)
|
| 52 |
+
psi = self.jl.mps_from_circuit_and_mps_itensors(
|
| 53 |
+
compiler.itensor_target,
|
| 54 |
+
gates,
|
| 55 |
+
self.chi,
|
| 56 |
+
self.cutoff,
|
| 57 |
+
compiler.itensor_sites,
|
| 58 |
+
)
|
| 59 |
+
return psi
|
| 60 |
+
|
| 61 |
+
def measure_qubit_expectation_values(self, compiler):
|
| 62 |
+
raise NotImplementedError()
|
utils/adapt-aqc/adaptaqc/backends/julia_default_backends.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# (C) Copyright IBM 2025.
|
| 2 |
+
#
|
| 3 |
+
# This code is licensed under the Apache License, Version 2.0. You may
|
| 4 |
+
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
| 5 |
+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
| 6 |
+
#
|
| 7 |
+
# Any modifications or derivative works of this code must retain this
|
| 8 |
+
# copyright notice, and modified files need to carry a notice indicating
|
| 9 |
+
# that they have been altered from the originals.
|
| 10 |
+
|
| 11 |
+
from adaptaqc.backends.itensor_backend import ITensorBackend
|
| 12 |
+
|
| 13 |
+
ITENSOR_SIM = ITensorBackend()
|
utils/adapt-aqc/adaptaqc/backends/python_default_backends.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# (C) Copyright IBM 2025.
|
| 2 |
+
#
|
| 3 |
+
# This code is licensed under the Apache License, Version 2.0. You may
|
| 4 |
+
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
| 5 |
+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
| 6 |
+
#
|
| 7 |
+
# Any modifications or derivative works of this code must retain this
|
| 8 |
+
# copyright notice, and modified files need to carry a notice indicating
|
| 9 |
+
# that they have been altered from the originals.
|
| 10 |
+
|
| 11 |
+
from adaptaqc.backends.aer_mps_backend import AerMPSBackend
|
| 12 |
+
from adaptaqc.backends.aer_sv_backend import AerSVBackend
|
| 13 |
+
from adaptaqc.backends.qiskit_sampling_backend import QiskitSamplingBackend
|
| 14 |
+
|
| 15 |
+
# These constants are generally used in testing and are a relic from before we had custom classes
|
| 16 |
+
# for each type of backend.
|
| 17 |
+
QASM_SIM = QiskitSamplingBackend()
|
| 18 |
+
SV_SIM = AerSVBackend()
|
| 19 |
+
MPS_SIM = AerMPSBackend()
|
utils/adapt-aqc/adaptaqc/backends/qiskit_sampling_backend.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# (C) Copyright IBM 2025.
|
| 2 |
+
#
|
| 3 |
+
# This code is licensed under the Apache License, Version 2.0. You may
|
| 4 |
+
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
| 5 |
+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
| 6 |
+
#
|
| 7 |
+
# Any modifications or derivative works of this code must retain this
|
| 8 |
+
# copyright notice, and modified files need to carry a notice indicating
|
| 9 |
+
# that they have been altered from the originals.
|
| 10 |
+
|
| 11 |
+
import os
|
| 12 |
+
|
| 13 |
+
import numpy as np
|
| 14 |
+
from qiskit_aer import Aer
|
| 15 |
+
from qiskit_aer.backends.aerbackend import AerBackend
|
| 16 |
+
|
| 17 |
+
from adaptaqc.backends.aqc_backend import AQCBackend
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class QiskitSamplingBackend(AQCBackend):
|
| 21 |
+
def __init__(self, simulator=Aer.get_backend("qasm_simulator")):
|
| 22 |
+
self.simulator = simulator
|
| 23 |
+
|
| 24 |
+
def evaluate_global_cost(self, compiler):
|
| 25 |
+
if compiler.soften_global_cost:
|
| 26 |
+
raise NotImplementedError(
|
| 27 |
+
"soften_global_cost is currently only implemented for AerMPSBackend"
|
| 28 |
+
)
|
| 29 |
+
counts = self.evaluate_circuit(compiler)
|
| 30 |
+
total_qubits = (
|
| 31 |
+
2 * compiler.total_num_qubits
|
| 32 |
+
if compiler.general_initial_state
|
| 33 |
+
else compiler.total_num_qubits
|
| 34 |
+
)
|
| 35 |
+
all_zero_string = "".join(str(int(e)) for e in np.zeros(total_qubits))
|
| 36 |
+
total_shots = sum([each_count for _, each_count in counts.items()])
|
| 37 |
+
# '00...00' might not be present in counts if no shot resulted in
|
| 38 |
+
# the ground state
|
| 39 |
+
if all_zero_string in counts:
|
| 40 |
+
overlap = counts[all_zero_string] / total_shots
|
| 41 |
+
else:
|
| 42 |
+
overlap = 0
|
| 43 |
+
cost = 1 - overlap
|
| 44 |
+
return cost
|
| 45 |
+
|
| 46 |
+
def evaluate_local_cost(self, compiler):
|
| 47 |
+
qubit_costs = np.zeros(compiler.total_num_qubits)
|
| 48 |
+
for i in range(compiler.total_num_qubits):
|
| 49 |
+
if compiler.general_initial_state:
|
| 50 |
+
compiler.full_circuit.measure(i, 0)
|
| 51 |
+
compiler.full_circuit.measure(i + compiler.total_num_qubits, 1)
|
| 52 |
+
counts = self.evaluate_circuit(compiler)
|
| 53 |
+
del compiler.full_circuit.data[-1]
|
| 54 |
+
del compiler.full_circuit.data[-1]
|
| 55 |
+
total_shots = sum([each_count for _, each_count in counts.items()])
|
| 56 |
+
# '00...00' might not be present in counts if no shot
|
| 57 |
+
# resulted in the ground state
|
| 58 |
+
if "00" in counts:
|
| 59 |
+
overlap = counts["00"] / total_shots
|
| 60 |
+
else:
|
| 61 |
+
overlap = 0
|
| 62 |
+
qubit_costs[i] = 1 - overlap
|
| 63 |
+
else:
|
| 64 |
+
compiler.full_circuit.measure(i, 0)
|
| 65 |
+
counts = self.evaluate_circuit(compiler)
|
| 66 |
+
del compiler.full_circuit.data[-1]
|
| 67 |
+
total_shots = sum([each_count for _, each_count in counts.items()])
|
| 68 |
+
# '00...00' might not be present in counts if no shot
|
| 69 |
+
# resulted in the ground state
|
| 70 |
+
if "0" in counts:
|
| 71 |
+
overlap = counts["0"] / total_shots
|
| 72 |
+
else:
|
| 73 |
+
overlap = 0
|
| 74 |
+
qubit_costs[i] = 1 - overlap
|
| 75 |
+
cost = np.mean(qubit_costs)
|
| 76 |
+
return cost
|
| 77 |
+
|
| 78 |
+
def evaluate_circuit(self, compiler):
|
| 79 |
+
# Don't parallelise shots if ADAPT-AQC is already being run in parallel
|
| 80 |
+
already_in_parallel = os.environ["QISKIT_IN_PARALLEL"] == "TRUE"
|
| 81 |
+
backend_options = None if already_in_parallel else compiler.backend_options
|
| 82 |
+
|
| 83 |
+
if backend_options is None or not isinstance(self.simulator, AerBackend):
|
| 84 |
+
backend_options = {}
|
| 85 |
+
job = self.simulator.run(
|
| 86 |
+
compiler.full_circuit, **backend_options, **compiler.execute_kwargs
|
| 87 |
+
)
|
| 88 |
+
result = job.result()
|
| 89 |
+
return result.get_counts()
|
| 90 |
+
|
| 91 |
+
def measure_qubit_expectation_values(self, compiler):
|
| 92 |
+
counts = self.evaluate_circuit(compiler)
|
| 93 |
+
n_qubits = len(list(counts)[0])
|
| 94 |
+
|
| 95 |
+
expectation_values = []
|
| 96 |
+
for i in range(n_qubits):
|
| 97 |
+
if i >= n_qubits:
|
| 98 |
+
raise ValueError("qubit_index outside of register range")
|
| 99 |
+
reverse_index = n_qubits - (i + 1)
|
| 100 |
+
exp_val = 0
|
| 101 |
+
total_counts = 0
|
| 102 |
+
for bitstring in list(counts):
|
| 103 |
+
exp_val += (1 if bitstring[reverse_index] == "0" else -1) * counts[
|
| 104 |
+
bitstring
|
| 105 |
+
]
|
| 106 |
+
total_counts += counts[bitstring]
|
| 107 |
+
expectation_values.append(exp_val / total_counts)
|
| 108 |
+
return expectation_values
|
utils/adapt-aqc/adaptaqc/compilers/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# (C) Copyright IBM 2025.
|
| 2 |
+
#
|
| 3 |
+
# This code is licensed under the Apache License, Version 2.0. You may
|
| 4 |
+
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
| 5 |
+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
| 6 |
+
#
|
| 7 |
+
# Any modifications or derivative works of this code must retain this
|
| 8 |
+
# copyright notice, and modified files need to carry a notice indicating
|
| 9 |
+
# that they have been altered from the originals.
|
| 10 |
+
|
| 11 |
+
from adaptaqc.compilers.adapt.adapt_compiler import AdaptCompiler
|
| 12 |
+
from adaptaqc.compilers.adapt.adapt_config import AdaptConfig
|
| 13 |
+
from adaptaqc.compilers.adapt.adapt_result import AdaptResult
|
utils/adapt-aqc/adaptaqc/compilers/adapt/adapt_compiler.py
ADDED
|
@@ -0,0 +1,1163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# (C) Copyright IBM 2025.
|
| 2 |
+
#
|
| 3 |
+
# This code is licensed under the Apache License, Version 2.0. You may
|
| 4 |
+
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
| 5 |
+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
| 6 |
+
#
|
| 7 |
+
# Any modifications or derivative works of this code must retain this
|
| 8 |
+
# copyright notice, and modified files need to carry a notice indicating
|
| 9 |
+
# that they have been altered from the originals.
|
| 10 |
+
|
| 11 |
+
"""Contains AdaptCompiler"""
|
| 12 |
+
|
| 13 |
+
import logging
|
| 14 |
+
import os
|
| 15 |
+
import pickle
|
| 16 |
+
import timeit
|
| 17 |
+
from pathlib import Path
|
| 18 |
+
|
| 19 |
+
import aqc_research.mps_operations as mpsops
|
| 20 |
+
import numpy as np
|
| 21 |
+
from qiskit import QuantumCircuit, qasm2
|
| 22 |
+
from qiskit.compiler import transpile
|
| 23 |
+
|
| 24 |
+
import adaptaqc.utils.ansatzes as ans
|
| 25 |
+
import adaptaqc.utils.constants as vconstants
|
| 26 |
+
from adaptaqc.backends.aer_sv_backend import AerSVBackend
|
| 27 |
+
from adaptaqc.backends.aqc_backend import AQCBackend
|
| 28 |
+
from adaptaqc.backends.itensor_backend import ITensorBackend
|
| 29 |
+
from adaptaqc.compilers.adapt.adapt_config import AdaptConfig
|
| 30 |
+
from adaptaqc.compilers.adapt.adapt_result import AdaptResult
|
| 31 |
+
from adaptaqc.compilers.approximate_compiler import ApproximateCompiler
|
| 32 |
+
from adaptaqc.utils import circuit_operations as co
|
| 33 |
+
from adaptaqc.utils import gradients as gr
|
| 34 |
+
from adaptaqc.utils.constants import CMAP_FULL, generate_coupling_map
|
| 35 |
+
from adaptaqc.utils.entanglement_measures import (
|
| 36 |
+
EM_TOMOGRAPHY_CONCURRENCE,
|
| 37 |
+
calculate_entanglement_measure,
|
| 38 |
+
)
|
| 39 |
+
from adaptaqc.utils.utilityfunctions import (
|
| 40 |
+
has_stopped_improving,
|
| 41 |
+
remove_permutations_from_coupling_map,
|
| 42 |
+
multi_qubit_gate_depth,
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
logger = logging.getLogger(__name__)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class AdaptCompiler(ApproximateCompiler):
|
| 49 |
+
"""
|
| 50 |
+
Structure learning algorithm that incrementally builds a circuit that
|
| 51 |
+
has the same result when acting on |0> state
|
| 52 |
+
(computational basis) as the given circuit.
|
| 53 |
+
"""
|
| 54 |
+
|
| 55 |
+
def __init__(
|
| 56 |
+
self,
|
| 57 |
+
target,
|
| 58 |
+
entanglement_measure=EM_TOMOGRAPHY_CONCURRENCE,
|
| 59 |
+
backend: AQCBackend = AerSVBackend(),
|
| 60 |
+
execute_kwargs=None,
|
| 61 |
+
coupling_map=None,
|
| 62 |
+
adapt_config: AdaptConfig = None,
|
| 63 |
+
general_initial_state=False,
|
| 64 |
+
custom_layer_2q_gate=None,
|
| 65 |
+
save_circuit_history=False,
|
| 66 |
+
starting_circuit=None,
|
| 67 |
+
use_roto_algos=True,
|
| 68 |
+
use_rotoselect=True,
|
| 69 |
+
use_advanced_transpilation=False,
|
| 70 |
+
rotosolve_fraction=1.0,
|
| 71 |
+
perform_final_minimisation=False,
|
| 72 |
+
optimise_local_cost=False,
|
| 73 |
+
soften_global_cost=False,
|
| 74 |
+
debug_log_full_ansatz=False,
|
| 75 |
+
initial_single_qubit_layer=False,
|
| 76 |
+
itensor_chi=None,
|
| 77 |
+
itensor_cutoff=None,
|
| 78 |
+
):
|
| 79 |
+
"""
|
| 80 |
+
:param target: Circuit or MPS that is to be compiled
|
| 81 |
+
:param entanglement_measure: The entanglement measurement method to
|
| 82 |
+
use for quantifying local entanglement. Valid options are defined in
|
| 83 |
+
entanglement_measures.py.
|
| 84 |
+
:param backend: Backend to run circuits on. Valid options are defined in
|
| 85 |
+
circuit_operations_running.py.
|
| 86 |
+
:param execute_kwargs: keyword arguments passed onto AerBackend.run
|
| 87 |
+
:param coupling_map: 2-qubit gate coupling map to use
|
| 88 |
+
:param adapt_config: AdaptConfig object
|
| 89 |
+
:param general_initial_state: Compile circuit for an arbitrary
|
| 90 |
+
initial state.
|
| 91 |
+
:param custom_layer_2q_gate: A two-qubit QuantumCircuit which will be used as the ansatz
|
| 92 |
+
layers.
|
| 93 |
+
:param save_circuit_history: Option to regularly save circuit output as a QASM string to
|
| 94 |
+
results object each time a block is added and optimised
|
| 95 |
+
:param starting_circuit: This circuit will be used as a set of initial fixed gates for the
|
| 96 |
+
compiled solution. Importantly, the string "tenpy_product_state" can also be passed here.
|
| 97 |
+
In this case, TenPy will be used to find the best χ=1 compression of the target MPS/circuit
|
| 98 |
+
and start the compiled solution with the single-qubit gates that prepare this state.
|
| 99 |
+
:param use_roto_algos: Whether to use rotoselect and rotosolve for cost minimisation.
|
| 100 |
+
Disable if custom_layer_2q_gate does not support rotosolve
|
| 101 |
+
:param use_rotoselect: Whether to use rotoselect for cost minimisation. Disable if
|
| 102 |
+
not appropriate for chosen ansatz.
|
| 103 |
+
:param use_advanced_transpilation: Whether to use optimization_level=2 transpilation on
|
| 104 |
+
variational circuit before each call to rotosolve. This should result in fewer redundant
|
| 105 |
+
layers in the compiled circuit and faster optimisations.
|
| 106 |
+
:param rotosolve_fraction: During each rotosolve cycle, modify a random sample of the
|
| 107 |
+
available gates. Set to 1 to modify all available gates, 0.5 to modify half, etc.
|
| 108 |
+
:param perform_final_minimisation: Perform a final cost minimisation
|
| 109 |
+
once ADAPT-AQC has ended
|
| 110 |
+
:param optimise_local_cost: Choose the cost function with which to optimise layers:
|
| 111 |
+
- True: 'local' cost function: C_l = 1/2 * (1 - sum_i(<Z_i>)/n) (arXiv:1908.04416, eq. 11)
|
| 112 |
+
- False: 'global' cost function: C_g = 1 - |<0|ψ>|^2 (arXiv:1908.04416, eq. 9)
|
| 113 |
+
ADAPT-AQC will still use the global cost function when deciding if compiling is completed.
|
| 114 |
+
:param soften_global_cost: Set to True to modify the global cost to:
|
| 115 |
+
C_ɑ = C_g - ɑ * sum_i(|<0|X_i|ψ>|^2) (arXiv:2301.08609, eq. 8). ɑ is chosen to be:
|
| 116 |
+
ɑ = |C' - C_s| where C' is the cost, C_ɑ, reached during optimisation of the previous layer,
|
| 117 |
+
and C_s is the sufficient cost.
|
| 118 |
+
:param debug_log_full_ansatz: When True, debug logging will print the entire ansatz at
|
| 119 |
+
every step, as opposed to just the most recently optimised layer.
|
| 120 |
+
:param initial_single_qubit_layer: When True, the first layer of the ADAPT-AQC ansatz will be
|
| 121 |
+
a trainable single-qubit rotation on each qubit.
|
| 122 |
+
"""
|
| 123 |
+
super().__init__(
|
| 124 |
+
target=target,
|
| 125 |
+
initial_state=None,
|
| 126 |
+
backend=backend,
|
| 127 |
+
execute_kwargs=execute_kwargs,
|
| 128 |
+
general_initial_state=general_initial_state,
|
| 129 |
+
starting_circuit=starting_circuit,
|
| 130 |
+
optimise_local_cost=optimise_local_cost,
|
| 131 |
+
itensor_chi=itensor_chi,
|
| 132 |
+
itensor_cutoff=itensor_cutoff,
|
| 133 |
+
rotosolve_fraction=rotosolve_fraction,
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
self.save_circuit_history = save_circuit_history
|
| 137 |
+
self.entanglement_measure_method = entanglement_measure
|
| 138 |
+
self.adapt_config = adapt_config if adapt_config is not None else AdaptConfig()
|
| 139 |
+
|
| 140 |
+
if coupling_map is None:
|
| 141 |
+
coupling_map = generate_coupling_map(
|
| 142 |
+
self.total_num_qubits, CMAP_FULL, False, False
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
# If custom layer gate is provided, do not remove gate during ADAPT-AQC
|
| 146 |
+
# because individual gates
|
| 147 |
+
# might depend on each other.
|
| 148 |
+
self.remove_unnecessary_gates_during_adapt = custom_layer_2q_gate is None
|
| 149 |
+
self.use_roto_algos = use_roto_algos
|
| 150 |
+
self.use_rotoselect = use_rotoselect
|
| 151 |
+
self.use_advanced_transpilation = use_advanced_transpilation
|
| 152 |
+
if self.use_advanced_transpilation:
|
| 153 |
+
logger.warning(
|
| 154 |
+
"Using advanced qiskit transpilation (optimization_level=2) for variational circuit. This behaviour can be unpredicable with caching. You can turn this off in settings with use_advanced_transpilation=False."
|
| 155 |
+
)
|
| 156 |
+
if self.use_rotoselect and custom_layer_2q_gate in [
|
| 157 |
+
ans.u4(),
|
| 158 |
+
ans.fully_dressed_cnot(),
|
| 159 |
+
ans.heisenberg(),
|
| 160 |
+
]:
|
| 161 |
+
logger.warning(
|
| 162 |
+
"For ansatz designed to perform physically motivated or universal operations Rotoselect may "
|
| 163 |
+
"cause change from expected behaviour"
|
| 164 |
+
)
|
| 165 |
+
if not self.use_rotoselect and (
|
| 166 |
+
custom_layer_2q_gate == ans.thinly_dressed_cnot()
|
| 167 |
+
or custom_layer_2q_gate == ans.identity_resolvable()
|
| 168 |
+
or custom_layer_2q_gate is None
|
| 169 |
+
):
|
| 170 |
+
logger.warning("Rotoselect is necessary for convergence of chosen ansatz")
|
| 171 |
+
self.perform_final_minimisation = perform_final_minimisation
|
| 172 |
+
self.layer_2q_gate = self.construct_layer_2q_gate(custom_layer_2q_gate)
|
| 173 |
+
|
| 174 |
+
# Remove permutations so that ADAPT-AQC is not stuck on the same pair of
|
| 175 |
+
# qubits
|
| 176 |
+
self.coupling_map = remove_permutations_from_coupling_map(coupling_map)
|
| 177 |
+
self.coupling_map = [
|
| 178 |
+
(q1, q2)
|
| 179 |
+
for (q1, q2) in self.coupling_map
|
| 180 |
+
if q1 in self.qubit_subset_to_compile and q2 in self.qubit_subset_to_compile
|
| 181 |
+
]
|
| 182 |
+
# Used to avoid adding thinly dressed CNOTs to the same qubit pair
|
| 183 |
+
self.qubit_pair_history = []
|
| 184 |
+
# Avoid adding CNOTs to these qubit pairs
|
| 185 |
+
self.bad_qubit_pairs = []
|
| 186 |
+
# Used to keep track of whether ADAPT-AQC/expectation method was used
|
| 187 |
+
self.pair_selection_method_history = []
|
| 188 |
+
self.entanglement_measures_history = []
|
| 189 |
+
self.e_val_history = []
|
| 190 |
+
self.general_gradient_history = []
|
| 191 |
+
self.time_taken = None
|
| 192 |
+
self.debug_log_full_ansatz = debug_log_full_ansatz
|
| 193 |
+
|
| 194 |
+
self.initial_single_qubit_layer = initial_single_qubit_layer
|
| 195 |
+
|
| 196 |
+
if self.is_aer_mps_backend:
|
| 197 |
+
# As variational gates will be absorbed into one large MPS instruction, we need to
|
| 198 |
+
# separately keep track of ansatz gates to return a compiled solution.
|
| 199 |
+
self.layers_saved_to_mps = self.full_circuit.copy()
|
| 200 |
+
del self.layers_saved_to_mps.data[1:]
|
| 201 |
+
|
| 202 |
+
# Keep track of which layers have not been absorbed into the MPS
|
| 203 |
+
self.layers_as_gates = []
|
| 204 |
+
|
| 205 |
+
self.resume_from_layer = None
|
| 206 |
+
self.prev_checkpoint_time_taken = None
|
| 207 |
+
|
| 208 |
+
if self.adapt_config.method == "general_gradient":
|
| 209 |
+
if not self.is_aer_mps_backend:
|
| 210 |
+
raise ValueError(
|
| 211 |
+
"general_gradient method is only implemented for Aer MPS backend"
|
| 212 |
+
)
|
| 213 |
+
self.generators, self.degeneracies = gr.get_generators_and_degeneracies(
|
| 214 |
+
self.layer_2q_gate, use_rotoselect, inverse=True
|
| 215 |
+
)
|
| 216 |
+
self.inverse_zero_ansatz = transpile(self.layer_2q_gate).inverse()
|
| 217 |
+
|
| 218 |
+
self.soften_global_cost = soften_global_cost
|
| 219 |
+
if self.soften_global_cost and self.optimise_local_cost:
|
| 220 |
+
raise ValueError(
|
| 221 |
+
"soften_global_cost must be False when optimising local cost"
|
| 222 |
+
)
|
| 223 |
+
|
| 224 |
+
def construct_layer_2q_gate(self, custom_layer_2q_gate):
|
| 225 |
+
if custom_layer_2q_gate is None:
|
| 226 |
+
qc = QuantumCircuit(2)
|
| 227 |
+
if self.general_initial_state:
|
| 228 |
+
co.add_dressed_cnot(qc, 0, 1, True)
|
| 229 |
+
co.add_dressed_cnot(qc, 0, 1, True, v1=False, v2=False)
|
| 230 |
+
else:
|
| 231 |
+
co.add_dressed_cnot(qc, 0, 1, True)
|
| 232 |
+
return qc
|
| 233 |
+
else:
|
| 234 |
+
for i, circ_instr in enumerate(custom_layer_2q_gate):
|
| 235 |
+
gate = circ_instr.operation
|
| 236 |
+
if gate.label is None and gate.name in co.SUPPORTED_1Q_GATES:
|
| 237 |
+
gate.label = gate.name
|
| 238 |
+
custom_layer_2q_gate.data[i] = circ_instr
|
| 239 |
+
return custom_layer_2q_gate
|
| 240 |
+
|
| 241 |
+
def get_layer_2q_gate(self, layer_index):
|
| 242 |
+
qc = self.layer_2q_gate.copy()
|
| 243 |
+
co.add_subscript_to_all_variables(qc, layer_index)
|
| 244 |
+
return qc
|
| 245 |
+
|
| 246 |
+
def compile(
|
| 247 |
+
self,
|
| 248 |
+
initial_ansatz: QuantumCircuit = None,
|
| 249 |
+
optimise_initial_ansatz=True,
|
| 250 |
+
checkpoint_every=0,
|
| 251 |
+
checkpoint_dir="checkpoint/",
|
| 252 |
+
delete_prev_chkpt=False,
|
| 253 |
+
freeze_prev_layers=False,
|
| 254 |
+
):
|
| 255 |
+
"""
|
| 256 |
+
Perform recompilation algorithm.
|
| 257 |
+
:param initial_ansatz: A trial ansatz to start the recompilation
|
| 258 |
+
with instead of starting from scratch
|
| 259 |
+
:param modify_initial_ansatz: If True, optimise the parameters of initial_ansatz when
|
| 260 |
+
intially adding it to the circuit. NOTE: the parameters of initial ansatz will be fixed for
|
| 261 |
+
the rest of recompilation.
|
| 262 |
+
:param checkpoint_every: If checkpoint_every = n != 0, compiler object will be saved to a
|
| 263 |
+
file after layers 0, n, 2n, ... have been added.
|
| 264 |
+
:param checkpoint_dir: Directory to place checkpoints in. Will be created if not already
|
| 265 |
+
existing.
|
| 266 |
+
:param delete_prev_chkpt: Delete the last checkpoint each time a new one is made.
|
| 267 |
+
:param freeze_prev_layers: When resuming compilation from a checkpoint, set to True to not
|
| 268 |
+
modify the parameters of any layers added before the checkpoint.
|
| 269 |
+
Termination criteria: SUFFICIENT_COST reached; max_layers reached;
|
| 270 |
+
std(last_5_costs)/avg(last_5_costs) < TOL
|
| 271 |
+
:return: AdaptResult object
|
| 272 |
+
"""
|
| 273 |
+
|
| 274 |
+
start_time = timeit.default_timer()
|
| 275 |
+
if self.resume_from_layer is None:
|
| 276 |
+
self.time_taken = 0
|
| 277 |
+
start_point = 0
|
| 278 |
+
logger.info("ADAPT-AQC started")
|
| 279 |
+
logger.debug(f"ADAPT-AQC coupling map {self.coupling_map}")
|
| 280 |
+
self.cost_evaluation_counter = 0
|
| 281 |
+
self.global_cost, self.local_cost = None, None
|
| 282 |
+
num_1q_gates, num_2q_gates, self.cnot_depth = None, None, None
|
| 283 |
+
|
| 284 |
+
self.global_cost_history = []
|
| 285 |
+
if self.optimise_local_cost:
|
| 286 |
+
self.local_cost_history = []
|
| 287 |
+
self.circuit_history = []
|
| 288 |
+
self.cnot_depth_history = []
|
| 289 |
+
self.g_range = self.variational_circuit_range
|
| 290 |
+
self.original_lhs_gate_count = self.lhs_gate_count
|
| 291 |
+
|
| 292 |
+
if freeze_prev_layers:
|
| 293 |
+
logger.warning(
|
| 294 |
+
"freeze_prev_layers only applies when resuming from a checkpoint"
|
| 295 |
+
)
|
| 296 |
+
|
| 297 |
+
# If an initial ansatz has been provided, add that and run minimization
|
| 298 |
+
self.initial_ansatz_already_successful = False
|
| 299 |
+
if initial_ansatz is not None:
|
| 300 |
+
self._add_initial_ansatz(initial_ansatz, optimise_initial_ansatz)
|
| 301 |
+
|
| 302 |
+
else:
|
| 303 |
+
start_point = self.resume_from_layer
|
| 304 |
+
self.time_taken = self.prev_checkpoint_time_taken
|
| 305 |
+
logger.info(f"ADAPT-AQC resuming from layer: {start_point}")
|
| 306 |
+
if initial_ansatz is not None:
|
| 307 |
+
logger.warning(
|
| 308 |
+
"An initial ansatz will be ignored when resuming recompilation from a checkpoint"
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
if freeze_prev_layers:
|
| 312 |
+
if self.is_aer_mps_backend:
|
| 313 |
+
# Absorb all gates, apart from starting_circuit, into MPS and add gates to ref_circuit_as_gates
|
| 314 |
+
num_gates = len(self.full_circuit) - self.rhs_gate_count - 1
|
| 315 |
+
gates_absorbed = self._absorb_n_gates_into_mps(n=num_gates)
|
| 316 |
+
co.add_to_circuit(self.layers_saved_to_mps, gates_absorbed)
|
| 317 |
+
self._update_reference_circuit()
|
| 318 |
+
else:
|
| 319 |
+
# Make lhs_gate_count include all layers added before checkpoint
|
| 320 |
+
self.lhs_gate_count = self.variational_circuit_range()[1]
|
| 321 |
+
|
| 322 |
+
if checkpoint_every > 0:
|
| 323 |
+
Path(checkpoint_dir).mkdir(parents=True, exist_ok=True)
|
| 324 |
+
|
| 325 |
+
for layer_count in range(start_point, self.adapt_config.max_layers):
|
| 326 |
+
if self.initial_ansatz_already_successful:
|
| 327 |
+
break
|
| 328 |
+
|
| 329 |
+
logger.info(f"Global cost before adding layer: {self.global_cost}")
|
| 330 |
+
logger.info(f"CNOT depth before adding layer: {self.cnot_depth}")
|
| 331 |
+
if self.optimise_local_cost:
|
| 332 |
+
logger.info(f"Local cost before adding layer: {self.local_cost}")
|
| 333 |
+
self.local_cost = self._add_layer(layer_count)
|
| 334 |
+
self.global_cost = self.backend.evaluate_global_cost(self)
|
| 335 |
+
self.local_cost_history.append(self.local_cost)
|
| 336 |
+
else:
|
| 337 |
+
self.global_cost = self._add_layer(layer_count)
|
| 338 |
+
self.global_cost_history.append(self.global_cost)
|
| 339 |
+
self.record_cnot_depth()
|
| 340 |
+
|
| 341 |
+
# Caching layers as MPS requires that the number of gates remain constant
|
| 342 |
+
if (
|
| 343 |
+
self.remove_unnecessary_gates_during_adapt
|
| 344 |
+
and not self.is_aer_mps_backend
|
| 345 |
+
):
|
| 346 |
+
co.remove_unnecessary_gates_from_circuit(
|
| 347 |
+
self.full_circuit, False, False, gate_range=self.g_range()
|
| 348 |
+
)
|
| 349 |
+
|
| 350 |
+
num_2q_gates, num_1q_gates = co.find_num_gates(
|
| 351 |
+
circuit=self.ref_circuit_as_gates
|
| 352 |
+
if self.is_aer_mps_backend
|
| 353 |
+
else self.full_circuit,
|
| 354 |
+
gate_range=self.g_range(
|
| 355 |
+
self.ref_circuit_as_gates if self.is_aer_mps_backend else None
|
| 356 |
+
),
|
| 357 |
+
)
|
| 358 |
+
|
| 359 |
+
if self.save_circuit_history:
|
| 360 |
+
if not self.is_aer_mps_backend:
|
| 361 |
+
circuit_qasm_string = qasm2.dumps(self.full_circuit)
|
| 362 |
+
else:
|
| 363 |
+
circuit_copy = self.full_circuit.copy()
|
| 364 |
+
del circuit_copy.data[0]
|
| 365 |
+
circuit_qasm_string = qasm2.dumps(circuit_copy)
|
| 366 |
+
self.circuit_history.append(circuit_qasm_string)
|
| 367 |
+
|
| 368 |
+
cinl = self.adapt_config.cost_improvement_num_layers
|
| 369 |
+
cit = self.adapt_config.cost_improvement_tol
|
| 370 |
+
if len(self.global_cost_history) >= cinl and has_stopped_improving(
|
| 371 |
+
self.global_cost_history[-1 * cinl :], cit
|
| 372 |
+
):
|
| 373 |
+
logger.warning("ADAPT-AQC stopped improving")
|
| 374 |
+
self.compiling_finished = True
|
| 375 |
+
break
|
| 376 |
+
|
| 377 |
+
if self.global_cost < self.adapt_config.sufficient_cost:
|
| 378 |
+
logger.info("ADAPT-AQC successfully found approximate circuit")
|
| 379 |
+
self.compiling_finished = True
|
| 380 |
+
break
|
| 381 |
+
elif num_2q_gates >= self.adapt_config.max_2q_gates:
|
| 382 |
+
logger.warning(
|
| 383 |
+
"ADAPT-AQC MAX_2Q_GATES reached. Using ROTOSOLVE one last time"
|
| 384 |
+
)
|
| 385 |
+
# NOTE this may need changing to use a different stop_val when using local cost
|
| 386 |
+
self.minimizer.minimize_cost(
|
| 387 |
+
algorithm_kind=vconstants.ALG_ROTOSOLVE,
|
| 388 |
+
max_cycles=10,
|
| 389 |
+
tol=1e-5,
|
| 390 |
+
stop_val=self.adapt_config.sufficient_cost,
|
| 391 |
+
)
|
| 392 |
+
self.compiling_finished = True
|
| 393 |
+
break
|
| 394 |
+
|
| 395 |
+
if checkpoint_every > 0 and layer_count % checkpoint_every == 0:
|
| 396 |
+
self.checkpoint(
|
| 397 |
+
checkpoint_every,
|
| 398 |
+
checkpoint_dir,
|
| 399 |
+
delete_prev_chkpt,
|
| 400 |
+
layer_count,
|
| 401 |
+
start_time,
|
| 402 |
+
)
|
| 403 |
+
|
| 404 |
+
# Perform a final optimisation
|
| 405 |
+
if self.perform_final_minimisation:
|
| 406 |
+
self.minimizer.minimize_cost(
|
| 407 |
+
algorithm_kind=vconstants.ALG_PYBOBYQA,
|
| 408 |
+
alg_kwargs={"seek_global_minimum": False},
|
| 409 |
+
)
|
| 410 |
+
|
| 411 |
+
if self.is_aer_mps_backend:
|
| 412 |
+
# Replace full_circuit with ref_circuit_as_gates, otherwise no way to remove unnecessary gates
|
| 413 |
+
self.full_circuit = self.ref_circuit_as_gates
|
| 414 |
+
|
| 415 |
+
else:
|
| 416 |
+
# Reset lhs_gate_count to what it was at the start of compiling
|
| 417 |
+
self.lhs_gate_count = self.original_lhs_gate_count
|
| 418 |
+
|
| 419 |
+
co.remove_unnecessary_gates_from_circuit(
|
| 420 |
+
self.full_circuit, True, True, gate_range=self.g_range()
|
| 421 |
+
)
|
| 422 |
+
|
| 423 |
+
# Calculate the final global cost, as 1 - |<solution|target>|^2
|
| 424 |
+
if self.soften_global_cost:
|
| 425 |
+
self.soften_global_cost = False
|
| 426 |
+
final_global_cost = self.backend.evaluate_global_cost(self)
|
| 427 |
+
self.soften_global_cost = True
|
| 428 |
+
else:
|
| 429 |
+
final_global_cost = self.backend.evaluate_global_cost(self)
|
| 430 |
+
logger.info(f"Final global cost: {final_global_cost}")
|
| 431 |
+
self.global_cost_history.append(final_global_cost)
|
| 432 |
+
if checkpoint_every > 0:
|
| 433 |
+
self.checkpoint(
|
| 434 |
+
checkpoint_every,
|
| 435 |
+
checkpoint_dir,
|
| 436 |
+
delete_prev_chkpt,
|
| 437 |
+
len(self.qubit_pair_history) - 1,
|
| 438 |
+
start_time,
|
| 439 |
+
)
|
| 440 |
+
compiled_circuit = self.get_compiled_circuit()
|
| 441 |
+
|
| 442 |
+
num_2q_gates, num_1q_gates = co.find_num_gates(compiled_circuit)
|
| 443 |
+
final_cnot_depth = multi_qubit_gate_depth(compiled_circuit)
|
| 444 |
+
logger.info(f"Final CNOT depth: {final_cnot_depth}")
|
| 445 |
+
self.cnot_depth_history.append(final_cnot_depth)
|
| 446 |
+
|
| 447 |
+
exact_overlap = "Not computable without SV backend"
|
| 448 |
+
if self.is_statevector_backend:
|
| 449 |
+
exact_overlap = co.calculate_overlap_between_circuits(
|
| 450 |
+
self.circuit_to_compile,
|
| 451 |
+
co.make_quantum_only_circuit(compiled_circuit),
|
| 452 |
+
)
|
| 453 |
+
|
| 454 |
+
result = AdaptResult(
|
| 455 |
+
circuit=compiled_circuit,
|
| 456 |
+
overlap=1 - final_global_cost,
|
| 457 |
+
exact_overlap=exact_overlap,
|
| 458 |
+
num_1q_gates=num_1q_gates,
|
| 459 |
+
num_2q_gates=num_2q_gates,
|
| 460 |
+
cnot_depth_history=self.cnot_depth_history,
|
| 461 |
+
global_cost_history=self.global_cost_history,
|
| 462 |
+
local_cost_history=self.local_cost_history
|
| 463 |
+
if self.optimise_local_cost
|
| 464 |
+
else None,
|
| 465 |
+
circuit_history=self.circuit_history,
|
| 466 |
+
entanglement_measures_history=self.entanglement_measures_history,
|
| 467 |
+
e_val_history=self.e_val_history,
|
| 468 |
+
qubit_pair_history=self.qubit_pair_history,
|
| 469 |
+
method_history=self.pair_selection_method_history,
|
| 470 |
+
time_taken=self.time_taken + (timeit.default_timer() - start_time),
|
| 471 |
+
cost_evaluations=self.cost_evaluation_counter,
|
| 472 |
+
coupling_map=self.coupling_map,
|
| 473 |
+
circuit_qasm=qasm2.dumps(compiled_circuit),
|
| 474 |
+
)
|
| 475 |
+
|
| 476 |
+
if self.save_circuit_history and self.is_aer_mps_backend:
|
| 477 |
+
logger.warning(
|
| 478 |
+
"When using MPS backend, circuit history will not contain the"
|
| 479 |
+
" set_matrix_product_state instruction at the start of the circuit"
|
| 480 |
+
)
|
| 481 |
+
logger.info("ADAPT-AQC completed")
|
| 482 |
+
return result
|
| 483 |
+
|
| 484 |
+
def checkpoint(
|
| 485 |
+
self,
|
| 486 |
+
checkpoint_every,
|
| 487 |
+
checkpoint_dir,
|
| 488 |
+
delete_prev_chkpt,
|
| 489 |
+
layer_count,
|
| 490 |
+
start_time,
|
| 491 |
+
):
|
| 492 |
+
self.resume_from_layer = layer_count + 1
|
| 493 |
+
current_chkpt_time_taken = timeit.default_timer() - start_time
|
| 494 |
+
self.prev_checkpoint_time_taken = self.time_taken + current_chkpt_time_taken
|
| 495 |
+
file_name = f"{layer_count}.pkl"
|
| 496 |
+
with open(os.path.join(checkpoint_dir, file_name), "wb") as f:
|
| 497 |
+
pickle.dump(self, f)
|
| 498 |
+
if delete_prev_chkpt:
|
| 499 |
+
try:
|
| 500 |
+
os.remove(
|
| 501 |
+
os.path.join(
|
| 502 |
+
checkpoint_dir, f"{layer_count - checkpoint_every}.pkl"
|
| 503 |
+
)
|
| 504 |
+
)
|
| 505 |
+
except FileNotFoundError:
|
| 506 |
+
pass
|
| 507 |
+
|
| 508 |
+
def _debug_log_optimised_layer(self, layer_count):
|
| 509 |
+
if logger.getEffectiveLevel() == 10:
|
| 510 |
+
logger.debug(f"Qubit pair history: \n{self.qubit_pair_history}")
|
| 511 |
+
|
| 512 |
+
if self.debug_log_full_ansatz:
|
| 513 |
+
if self.is_aer_mps_backend:
|
| 514 |
+
ansatz = self.ref_circuit_as_gates.copy()
|
| 515 |
+
else:
|
| 516 |
+
ansatz = self.full_circuit.copy()
|
| 517 |
+
del ansatz.data[: len(self.circuit_to_compile.data)]
|
| 518 |
+
logger.debug(f"Optimised ansatz after layer added: \n{ansatz}")
|
| 519 |
+
|
| 520 |
+
layer_added = self._get_layer_added(layer_count)
|
| 521 |
+
if self.initial_single_qubit_layer == True and layer_count == 0:
|
| 522 |
+
logger.debug(f"Optimised layer added: \n{layer_added}")
|
| 523 |
+
else:
|
| 524 |
+
# Remove all qubits apart from the pair acted on in the current layer
|
| 525 |
+
for qubit in range(layer_added.num_qubits - 1, -1, -1):
|
| 526 |
+
if qubit not in self.qubit_pair_history[-1]:
|
| 527 |
+
del layer_added.qubits[qubit]
|
| 528 |
+
try:
|
| 529 |
+
logger.debug(f"Optimised layer added: \n{layer_added}")
|
| 530 |
+
except ValueError:
|
| 531 |
+
logging.error(
|
| 532 |
+
"Final ansatz layer logging not implemented for custom ansatz or functionalities "
|
| 533 |
+
"placing more gates after trainable ansatz"
|
| 534 |
+
)
|
| 535 |
+
|
| 536 |
+
def _add_initial_ansatz(self, initial_ansatz, optimise_initial_ansatz):
|
| 537 |
+
# Label ansatz gates to work with rotosolve
|
| 538 |
+
for gate in initial_ansatz:
|
| 539 |
+
if gate[0].label is None and gate[0].name in co.SUPPORTED_1Q_GATES:
|
| 540 |
+
gate[0].label = gate[0].name
|
| 541 |
+
|
| 542 |
+
co.add_to_circuit(
|
| 543 |
+
self.full_circuit,
|
| 544 |
+
co.circuit_by_inverting_circuit(initial_ansatz),
|
| 545 |
+
self.variational_circuit_range()[1],
|
| 546 |
+
)
|
| 547 |
+
if optimise_initial_ansatz:
|
| 548 |
+
if self.use_roto_algos:
|
| 549 |
+
cost = self.minimizer.minimize_cost(
|
| 550 |
+
algorithm_kind=vconstants.ALG_ROTOSOLVE,
|
| 551 |
+
tol=1e-3,
|
| 552 |
+
stop_val=0
|
| 553 |
+
if self.optimise_local_cost
|
| 554 |
+
else self.adapt_config.sufficient_cost,
|
| 555 |
+
indexes_to_modify=self.variational_circuit_range(),
|
| 556 |
+
)
|
| 557 |
+
else:
|
| 558 |
+
cost = self.minimizer.minimize_cost(
|
| 559 |
+
algorithm_kind=vconstants.ALG_PYBOBYQA,
|
| 560 |
+
alg_kwargs={"seek_global_minimum": True},
|
| 561 |
+
)
|
| 562 |
+
else:
|
| 563 |
+
cost = self.evaluate_cost()
|
| 564 |
+
|
| 565 |
+
self.global_cost = (
|
| 566 |
+
self.backend.evaluate_global_cost() if self.optimise_local_cost else cost
|
| 567 |
+
)
|
| 568 |
+
self.cnot_depth = multi_qubit_gate_depth(initial_ansatz)
|
| 569 |
+
|
| 570 |
+
if self.global_cost < self.adapt_config.sufficient_cost:
|
| 571 |
+
self.initial_ansatz_already_successful = True
|
| 572 |
+
logger.debug(
|
| 573 |
+
"ADAPT-AQC successfully found approximate circuit using provided ansatz only"
|
| 574 |
+
)
|
| 575 |
+
|
| 576 |
+
if self.is_aer_mps_backend:
|
| 577 |
+
# Absorb optimised initial_ansatz into MPS and add gates to ref_circuit_as_gates
|
| 578 |
+
gates_absorbed = self._absorb_n_gates_into_mps(n=len(initial_ansatz))
|
| 579 |
+
co.add_to_circuit(self.layers_saved_to_mps, gates_absorbed)
|
| 580 |
+
self._update_reference_circuit()
|
| 581 |
+
else:
|
| 582 |
+
# Ensure initial_ansatz is not modified again
|
| 583 |
+
self.lhs_gate_count = self.variational_circuit_range()[1]
|
| 584 |
+
|
| 585 |
+
def _add_layer(self, index):
|
| 586 |
+
"""
|
| 587 |
+
Adds a dressed CNOT gate or other ansatz layer to the qubits with the
|
| 588 |
+
highest local entanglement. If all qubit pairs have no
|
| 589 |
+
local entanglement, adds a dressed CNOT gate to the qubit pair with
|
| 590 |
+
the highest sum of expectation values
|
| 591 |
+
(computational basis).
|
| 592 |
+
:return: New cost
|
| 593 |
+
"""
|
| 594 |
+
|
| 595 |
+
ansatz_start_index = self.variational_circuit_range()[0]
|
| 596 |
+
# Define first layer differently when initial_single_qubit_layer=True
|
| 597 |
+
if self.initial_single_qubit_layer and index == 0:
|
| 598 |
+
logger.debug(
|
| 599 |
+
"Starting with first layer comprising of only single qubit rotations"
|
| 600 |
+
)
|
| 601 |
+
layer_added_optimisation_indexes = self._add_rotation_to_all_qubits()
|
| 602 |
+
else:
|
| 603 |
+
layer_added_optimisation_indexes = self._add_entangling_layer(index)
|
| 604 |
+
|
| 605 |
+
if self.optimise_local_cost:
|
| 606 |
+
stop_val = 0
|
| 607 |
+
else:
|
| 608 |
+
stop_val = self.adapt_config.sufficient_cost
|
| 609 |
+
|
| 610 |
+
if self.use_roto_algos:
|
| 611 |
+
# Optimise layer currently being added
|
| 612 |
+
# For normal layers, use Rotoselect/Rotosolve if self.use_rotoselect=True/False
|
| 613 |
+
# For the initial_single_qubit_layer, use Rotoselect
|
| 614 |
+
|
| 615 |
+
if self.use_rotoselect or (self.initial_single_qubit_layer and index == 0):
|
| 616 |
+
ALG = vconstants.ALG_ROTOSELECT
|
| 617 |
+
else:
|
| 618 |
+
ALG = vconstants.ALG_ROTOSOLVE
|
| 619 |
+
|
| 620 |
+
cost = self.minimizer.minimize_cost(
|
| 621 |
+
algorithm_kind=ALG,
|
| 622 |
+
tol=self.adapt_config.rotoselect_tol,
|
| 623 |
+
stop_val=stop_val,
|
| 624 |
+
indexes_to_modify=layer_added_optimisation_indexes,
|
| 625 |
+
)
|
| 626 |
+
# Do Rotosolve on previous max_layers_to_modify layers, when appropriate
|
| 627 |
+
if (
|
| 628 |
+
self.adapt_config.rotosolve_frequency != 0
|
| 629 |
+
and index > 0
|
| 630 |
+
and index % self.adapt_config.rotosolve_frequency == 0
|
| 631 |
+
):
|
| 632 |
+
multi_layer_optimisation_indexes = (
|
| 633 |
+
self._calculate_multi_layer_optimisation_indices(ansatz_start_index)
|
| 634 |
+
)
|
| 635 |
+
if self.use_advanced_transpilation:
|
| 636 |
+
# Now do optimization_level=2 transpilation on variational circuit before calling rotosolve
|
| 637 |
+
variational_circuit = co.extract_inner_circuit(
|
| 638 |
+
self.full_circuit, self.variational_circuit_range()
|
| 639 |
+
)
|
| 640 |
+
transpiled_variational_circuit = co.advanced_circuit_transpilation(
|
| 641 |
+
variational_circuit, self.coupling_map
|
| 642 |
+
)
|
| 643 |
+
co.replace_inner_circuit(
|
| 644 |
+
self.full_circuit,
|
| 645 |
+
transpiled_variational_circuit,
|
| 646 |
+
self.variational_circuit_range(),
|
| 647 |
+
)
|
| 648 |
+
if self.is_aer_mps_backend:
|
| 649 |
+
self._update_reference_circuit()
|
| 650 |
+
cost = self.minimizer.minimize_cost(
|
| 651 |
+
algorithm_kind=vconstants.ALG_ROTOSOLVE,
|
| 652 |
+
tol=self.adapt_config.rotosolve_tol,
|
| 653 |
+
stop_val=stop_val,
|
| 654 |
+
indexes_to_modify=multi_layer_optimisation_indexes,
|
| 655 |
+
)
|
| 656 |
+
else:
|
| 657 |
+
cost = self.minimizer.minimize_cost(
|
| 658 |
+
algorithm_kind=vconstants.ALG_PYBOBYQA,
|
| 659 |
+
alg_kwargs={"seek_global_minimum": True},
|
| 660 |
+
)
|
| 661 |
+
|
| 662 |
+
if self.is_aer_mps_backend:
|
| 663 |
+
self.layers_as_gates.append(index)
|
| 664 |
+
|
| 665 |
+
num_layers_to_absorb = self._calculate_num_layers_to_absorb(index)
|
| 666 |
+
|
| 667 |
+
# Absorb appropriate layers into MPS, and add their gates to layers_saved_to_mps
|
| 668 |
+
if num_layers_to_absorb > 0:
|
| 669 |
+
includes_isql = (
|
| 670 |
+
self.layers_as_gates[0] == 0 and self.initial_single_qubit_layer
|
| 671 |
+
)
|
| 672 |
+
|
| 673 |
+
# Absorb layers into MPS, then add those layers to layers_saved_to_mps
|
| 674 |
+
num_gates_to_absorb = self._get_num_gates_to_cache(
|
| 675 |
+
n=num_layers_to_absorb, includes_isql=includes_isql
|
| 676 |
+
)
|
| 677 |
+
if self.is_aer_mps_backend:
|
| 678 |
+
gates_absorbed = self._absorb_n_gates_into_mps(num_gates_to_absorb)
|
| 679 |
+
co.add_to_circuit(self.layers_saved_to_mps, gates_absorbed)
|
| 680 |
+
|
| 681 |
+
# Update layers_as_gates
|
| 682 |
+
del self.layers_as_gates[:num_layers_to_absorb]
|
| 683 |
+
|
| 684 |
+
if self.is_aer_mps_backend:
|
| 685 |
+
self._update_reference_circuit()
|
| 686 |
+
|
| 687 |
+
self._debug_log_optimised_layer(index)
|
| 688 |
+
|
| 689 |
+
return cost
|
| 690 |
+
|
| 691 |
+
def _calculate_num_layers_to_absorb(self, index):
|
| 692 |
+
layers_since_solve = index % self.adapt_config.rotosolve_frequency
|
| 693 |
+
layers_to_next_solve = (
|
| 694 |
+
self.adapt_config.rotosolve_frequency - layers_since_solve
|
| 695 |
+
)
|
| 696 |
+
next_rotosolve_layer = index + layers_to_next_solve
|
| 697 |
+
|
| 698 |
+
# Compute the index of the leftmost layer to be modified in the next Rotosolve
|
| 699 |
+
lowest_index = next_rotosolve_layer - self.adapt_config.max_layers_to_modify + 1
|
| 700 |
+
|
| 701 |
+
# All layers with indices below lowest_index can be absorbed
|
| 702 |
+
num_layers_to_absorb = len(
|
| 703 |
+
[i for i in self.layers_as_gates if i < lowest_index]
|
| 704 |
+
)
|
| 705 |
+
|
| 706 |
+
return num_layers_to_absorb
|
| 707 |
+
|
| 708 |
+
def _update_reference_circuit(self):
|
| 709 |
+
# These are the layers now in circuit form, which are needed to update the reference circuit
|
| 710 |
+
layers_not_saved_to_mps = self.full_circuit.copy()
|
| 711 |
+
del layers_not_saved_to_mps.data[0]
|
| 712 |
+
|
| 713 |
+
# Update ref_circuit_as_gates = layers_saved_to_mps + layers_not_saved_to_mps
|
| 714 |
+
self.ref_circuit_as_gates = self.layers_saved_to_mps.copy()
|
| 715 |
+
co.add_to_circuit(self.ref_circuit_as_gates, layers_not_saved_to_mps)
|
| 716 |
+
|
| 717 |
+
def _calculate_multi_layer_optimisation_indices(self, ansatz_start_index):
|
| 718 |
+
num_entangling_layers = self.adapt_config.max_layers_to_modify - int(
|
| 719 |
+
self.initial_single_qubit_layer
|
| 720 |
+
)
|
| 721 |
+
# This assumes first layer has n gates
|
| 722 |
+
num_gates_in_non_entangling_layer = self.full_circuit.num_qubits * int(
|
| 723 |
+
self.initial_single_qubit_layer
|
| 724 |
+
)
|
| 725 |
+
# The earliest layer Rotosolve acts on is defined by the user. Calculating the
|
| 726 |
+
# index requires taking into account the first layer potentially being different
|
| 727 |
+
rotosolve_gate_start_index = max(
|
| 728 |
+
ansatz_start_index,
|
| 729 |
+
self.variational_circuit_range()[1]
|
| 730 |
+
- len(self.layer_2q_gate.data) * num_entangling_layers
|
| 731 |
+
- num_gates_in_non_entangling_layer,
|
| 732 |
+
)
|
| 733 |
+
# Don't modify only a fraction of the first layer gates
|
| 734 |
+
first_layer_end_index = ansatz_start_index + num_gates_in_non_entangling_layer
|
| 735 |
+
if ansatz_start_index < rotosolve_gate_start_index < first_layer_end_index:
|
| 736 |
+
rotosolve_gate_start_index = first_layer_end_index
|
| 737 |
+
multi_layer_optimisation_indexes = (
|
| 738 |
+
rotosolve_gate_start_index,
|
| 739 |
+
(self.variational_circuit_range()[1]),
|
| 740 |
+
)
|
| 741 |
+
return multi_layer_optimisation_indexes
|
| 742 |
+
|
| 743 |
+
def _add_entangling_layer(self, index):
|
| 744 |
+
logger.debug("Finding best qubit pair")
|
| 745 |
+
control, target = self._find_appropriate_qubit_pair()
|
| 746 |
+
logger.debug(f"Best qubit pair found {(control, target)}")
|
| 747 |
+
co.add_to_circuit(
|
| 748 |
+
self.full_circuit,
|
| 749 |
+
self.get_layer_2q_gate(index),
|
| 750 |
+
self.variational_circuit_range()[1],
|
| 751 |
+
qubit_subset=[control, target],
|
| 752 |
+
)
|
| 753 |
+
self.qubit_pair_history.append((control, target))
|
| 754 |
+
# Rotoselect or Rotosolve is applied to most recent layer
|
| 755 |
+
layer_added_optimisation_indexes = (
|
| 756 |
+
self.variational_circuit_range()[1] - len(self.layer_2q_gate.data),
|
| 757 |
+
(self.variational_circuit_range()[1]),
|
| 758 |
+
)
|
| 759 |
+
return layer_added_optimisation_indexes
|
| 760 |
+
|
| 761 |
+
def _add_rotation_to_all_qubits(self):
|
| 762 |
+
first_layer = QuantumCircuit(self.full_circuit.num_qubits)
|
| 763 |
+
first_layer.ry(0, range(self.full_circuit.num_qubits))
|
| 764 |
+
co.add_to_circuit(
|
| 765 |
+
self.full_circuit, first_layer, self.variational_circuit_range()[1]
|
| 766 |
+
)
|
| 767 |
+
self._first_layer_increment_results_dict()
|
| 768 |
+
# Gate indices in the initial layer
|
| 769 |
+
initial_layer_optimisation_indexes = (
|
| 770 |
+
self.variational_circuit_range()[1] - self.full_circuit.num_qubits,
|
| 771 |
+
(self.variational_circuit_range()[1]),
|
| 772 |
+
)
|
| 773 |
+
return initial_layer_optimisation_indexes
|
| 774 |
+
|
| 775 |
+
def _find_appropriate_qubit_pair(self):
|
| 776 |
+
if self.adapt_config.method == "random":
|
| 777 |
+
rand_index = np.random.randint(len(self.coupling_map))
|
| 778 |
+
self.pair_selection_method_history.append(f"random")
|
| 779 |
+
return self.coupling_map[rand_index]
|
| 780 |
+
|
| 781 |
+
if self.adapt_config.method == "basic":
|
| 782 |
+
# Choose the qubit pair with the highest reuse priority
|
| 783 |
+
self.pair_selection_method_history.append(f"basic")
|
| 784 |
+
reuse_priorities = self._get_all_qubit_pair_reuse_priorities(1)
|
| 785 |
+
return self.coupling_map[np.argmax(reuse_priorities)]
|
| 786 |
+
|
| 787 |
+
if self.adapt_config.method == "expectation":
|
| 788 |
+
return self._find_best_expectation_qubit_pair()
|
| 789 |
+
|
| 790 |
+
if self.adapt_config.method == "ISL":
|
| 791 |
+
logger.debug("Computing entanglement of pairs")
|
| 792 |
+
ems = self._get_all_qubit_pair_entanglement_measures()
|
| 793 |
+
self.entanglement_measures_history.append(ems)
|
| 794 |
+
return self._find_best_entanglement_qubit_pair(ems)
|
| 795 |
+
|
| 796 |
+
if self.adapt_config.method == "general_gradient":
|
| 797 |
+
logger.debug("Computing gradients of pairs")
|
| 798 |
+
gradients = self._get_all_qubit_pair_gradients()
|
| 799 |
+
self.general_gradient_history.append(gradients)
|
| 800 |
+
self.pair_selection_method_history.append(f"general_gradient")
|
| 801 |
+
return self._find_best_gradient_qubit_pair(gradients)
|
| 802 |
+
|
| 803 |
+
if self.adapt_config.method == "brickwall":
|
| 804 |
+
n = self.full_circuit.num_qubits
|
| 805 |
+
if n < 2:
|
| 806 |
+
raise ValueError(
|
| 807 |
+
"Cannot pick a pair if there are fewer than two qubits"
|
| 808 |
+
)
|
| 809 |
+
if (
|
| 810 |
+
len(self.qubit_pair_history) == 0 # This is the first layer
|
| 811 |
+
or n == 2 # There are only two qubits
|
| 812 |
+
or self.qubit_pair_history[-1][0]
|
| 813 |
+
is None # The first layer was single-qubit-layer
|
| 814 |
+
):
|
| 815 |
+
return (0, 1)
|
| 816 |
+
|
| 817 |
+
previous_pair = self.qubit_pair_history[-1]
|
| 818 |
+
next_pair = (previous_pair[0] + 2, previous_pair[1] + 2)
|
| 819 |
+
n_odd = n % 2
|
| 820 |
+
if next_pair == (n, n + 1):
|
| 821 |
+
return (1 - n_odd, 2 - n_odd)
|
| 822 |
+
if next_pair == (n - 1, n):
|
| 823 |
+
return (0 + n_odd, 1 + n_odd)
|
| 824 |
+
else:
|
| 825 |
+
return next_pair
|
| 826 |
+
|
| 827 |
+
raise ValueError(
|
| 828 |
+
f"Invalid compiling method {self.adapt_config.method}. "
|
| 829 |
+
f"Method must be one of ISL, expectation, random, basic, general_gradient, brickwall"
|
| 830 |
+
)
|
| 831 |
+
|
| 832 |
+
def _find_best_gradient_qubit_pair(self, gradients):
|
| 833 |
+
reuse_priorities = self._get_all_qubit_pair_reuse_priorities(
|
| 834 |
+
self.adapt_config.reuse_exponent
|
| 835 |
+
)
|
| 836 |
+
combined_priority = np.multiply(gradients, reuse_priorities)
|
| 837 |
+
return self.coupling_map[np.argmax(combined_priority)]
|
| 838 |
+
|
| 839 |
+
def _get_all_qubit_pair_gradients(self):
|
| 840 |
+
# Get the full_circuit without starting_circuit
|
| 841 |
+
if self.starting_circuit is not None:
|
| 842 |
+
range = (0, len(self.full_circuit) - len(self.starting_circuit))
|
| 843 |
+
else:
|
| 844 |
+
range = (0, len(self.full_circuit))
|
| 845 |
+
circuit = co.extract_inner_circuit(self.full_circuit, range)
|
| 846 |
+
gradients = gr.general_grad_of_pairs(
|
| 847 |
+
circuit,
|
| 848 |
+
self.inverse_zero_ansatz,
|
| 849 |
+
self.generators,
|
| 850 |
+
self.degeneracies,
|
| 851 |
+
self.coupling_map,
|
| 852 |
+
self.starting_circuit,
|
| 853 |
+
self.backend,
|
| 854 |
+
)
|
| 855 |
+
logger.debug(f"Gradient of all pairs: {gradients}")
|
| 856 |
+
return gradients
|
| 857 |
+
|
| 858 |
+
def _find_best_entanglement_qubit_pair(self, entanglement_measures):
|
| 859 |
+
"""
|
| 860 |
+
Returns the qubit pair with the largest entanglement multiplied by the reuse priority of
|
| 861 |
+
that pair.
|
| 862 |
+
"""
|
| 863 |
+
reuse_priorities = self._get_all_qubit_pair_reuse_priorities(
|
| 864 |
+
self.adapt_config.reuse_exponent
|
| 865 |
+
)
|
| 866 |
+
|
| 867 |
+
# First check if the previous qubit pair was 'bad'
|
| 868 |
+
if len(self.entanglement_measures_history) >= 2 + int(
|
| 869 |
+
self.initial_single_qubit_layer
|
| 870 |
+
):
|
| 871 |
+
prev_qp_index = self.coupling_map.index(self.qubit_pair_history[-1])
|
| 872 |
+
pre_em = self.entanglement_measures_history[-2][prev_qp_index]
|
| 873 |
+
post_em = self.entanglement_measures_history[-1][prev_qp_index]
|
| 874 |
+
if post_em >= pre_em:
|
| 875 |
+
logger.debug(
|
| 876 |
+
f"Entanglement did not reduce for previous pair {self.coupling_map[prev_qp_index]}. "
|
| 877 |
+
f"Adding to bad qubit pairs list."
|
| 878 |
+
)
|
| 879 |
+
self.bad_qubit_pairs.append(self.coupling_map[prev_qp_index])
|
| 880 |
+
if len(self.bad_qubit_pairs) > self.adapt_config.bad_qubit_pair_memory:
|
| 881 |
+
# Maintain max size of bad_qubit_pairs
|
| 882 |
+
logger.debug(
|
| 883 |
+
f"Max size of bad qubit pairs reached. Removing {self.bad_qubit_pairs[0]} from list."
|
| 884 |
+
)
|
| 885 |
+
del self.bad_qubit_pairs[0]
|
| 886 |
+
|
| 887 |
+
logger.debug(f"Entanglement of all pairs: {entanglement_measures}")
|
| 888 |
+
|
| 889 |
+
# Combine entanglement value with reuse priority
|
| 890 |
+
filtered_ems = [
|
| 891 |
+
entanglement_measure * reuse_priority
|
| 892 |
+
for (entanglement_measure, reuse_priority) in zip(
|
| 893 |
+
entanglement_measures, reuse_priorities
|
| 894 |
+
)
|
| 895 |
+
]
|
| 896 |
+
|
| 897 |
+
for qp in set(self.bad_qubit_pairs):
|
| 898 |
+
# Find the number of times this qubit pair has occurred recently
|
| 899 |
+
reps = len(
|
| 900 |
+
[
|
| 901 |
+
x
|
| 902 |
+
for x in self.qubit_pair_history[
|
| 903 |
+
-1 * self.adapt_config.bad_qubit_pair_memory :
|
| 904 |
+
]
|
| 905 |
+
if x == qp
|
| 906 |
+
]
|
| 907 |
+
)
|
| 908 |
+
if reps >= 1:
|
| 909 |
+
filtered_ems[self.coupling_map.index(qp)] = -1
|
| 910 |
+
|
| 911 |
+
logger.debug(f"Combined priority of all pairs: {filtered_ems}")
|
| 912 |
+
if max(filtered_ems) <= self.adapt_config.entanglement_threshold:
|
| 913 |
+
# No local entanglement detected in non-bad qubit pairs;
|
| 914 |
+
# defer to using 'basic' method
|
| 915 |
+
logger.info("No local entanglement detected in non-bad qubit pairs")
|
| 916 |
+
return self._find_best_expectation_qubit_pair()
|
| 917 |
+
else:
|
| 918 |
+
self.pair_selection_method_history.append(f"ISL")
|
| 919 |
+
# Add 'None' to e_val_history if no expectation values were needed
|
| 920 |
+
self.e_val_history.append(None)
|
| 921 |
+
return self.coupling_map[np.argmax(filtered_ems)]
|
| 922 |
+
|
| 923 |
+
def _find_best_expectation_qubit_pair(self):
|
| 924 |
+
"""
|
| 925 |
+
Choose the qubit pair to be the one with the largest expectation value priority multiplied by the reuse
|
| 926 |
+
priority of that pair.
|
| 927 |
+
@return: The pair of qubits with the highest multiplied e_val priority and reuse priority.
|
| 928 |
+
"""
|
| 929 |
+
reuse_priorities = self._get_all_qubit_pair_reuse_priorities(
|
| 930 |
+
self.adapt_config.reuse_exponent
|
| 931 |
+
)
|
| 932 |
+
|
| 933 |
+
e_vals = self.backend.measure_qubit_expectation_values(self)
|
| 934 |
+
self.e_val_history.append(e_vals)
|
| 935 |
+
|
| 936 |
+
e_val_sums = self._get_all_qubit_pair_e_val_sums(e_vals)
|
| 937 |
+
logger.debug(f"Summed σ_z expectation values of pairs {e_val_sums}")
|
| 938 |
+
|
| 939 |
+
# Mapping from the σz expectation values {1, -1} to the range {0, 2} to make an expectation value based
|
| 940 |
+
# priority. This ensures that the argmax of the list favours qubits close to the |1> state (eigenvalue -1)
|
| 941 |
+
# to apply the next layer to.
|
| 942 |
+
e_val_priorities = [2 - e_val for e_val in e_val_sums]
|
| 943 |
+
|
| 944 |
+
logger.debug(f"σ_z expectation value priorities of pairs {e_val_priorities}")
|
| 945 |
+
combined_priorities = [
|
| 946 |
+
e_val_priority * reuse_priority
|
| 947 |
+
for (e_val_priority, reuse_priority) in zip(
|
| 948 |
+
e_val_priorities, reuse_priorities
|
| 949 |
+
)
|
| 950 |
+
]
|
| 951 |
+
logger.debug(f"Combined priorities of pairs {combined_priorities}")
|
| 952 |
+
self.pair_selection_method_history.append(f"expectation")
|
| 953 |
+
return self.coupling_map[np.argmax(combined_priorities)]
|
| 954 |
+
|
| 955 |
+
def _get_all_qubit_pair_entanglement_measures(self):
|
| 956 |
+
entanglement_measures = []
|
| 957 |
+
# Generate MPS from circuit once if using MPS backend
|
| 958 |
+
if isinstance(self.backend, ITensorBackend):
|
| 959 |
+
raise NotImplementedError("ISL mode not supported for ITensor")
|
| 960 |
+
if self.is_aer_mps_backend:
|
| 961 |
+
self.circ_mps = self.backend.evaluate_circuit(self)
|
| 962 |
+
else:
|
| 963 |
+
self.circ_mps = None
|
| 964 |
+
for control, target in self.coupling_map:
|
| 965 |
+
this_entanglement_measure = calculate_entanglement_measure(
|
| 966 |
+
self.entanglement_measure_method,
|
| 967 |
+
self.full_circuit,
|
| 968 |
+
control,
|
| 969 |
+
target,
|
| 970 |
+
self.backend,
|
| 971 |
+
self.backend_options,
|
| 972 |
+
self.execute_kwargs,
|
| 973 |
+
self.circ_mps,
|
| 974 |
+
)
|
| 975 |
+
entanglement_measures.append(this_entanglement_measure)
|
| 976 |
+
return entanglement_measures
|
| 977 |
+
|
| 978 |
+
def _get_all_qubit_pair_e_val_sums(self, e_vals):
|
| 979 |
+
e_val_sums = []
|
| 980 |
+
for control, target in self.coupling_map:
|
| 981 |
+
e_val_sums.append(e_vals[control] + e_vals[target])
|
| 982 |
+
return e_val_sums
|
| 983 |
+
|
| 984 |
+
def _get_all_qubit_pair_reuse_priorities(self, k):
|
| 985 |
+
if not len(self.qubit_pair_history):
|
| 986 |
+
return [1 for _ in range(len(self.coupling_map))]
|
| 987 |
+
priorities = []
|
| 988 |
+
for qp in self.coupling_map:
|
| 989 |
+
if self.adapt_config.reuse_priority_mode == "pair":
|
| 990 |
+
priorities.append(self._get_pair_reuse_priority(qp, k))
|
| 991 |
+
elif self.adapt_config.reuse_priority_mode == "qubit":
|
| 992 |
+
priorities.append(self._get_qubit_reuse_priority(qp, k))
|
| 993 |
+
else:
|
| 994 |
+
raise ValueError(
|
| 995 |
+
f"Reuse priority mode must be one of: {['pair', 'qubit']}"
|
| 996 |
+
)
|
| 997 |
+
logger.debug(f"Reuse priorities of pairs: {priorities}")
|
| 998 |
+
return priorities
|
| 999 |
+
|
| 1000 |
+
def _find_last_use_of_qubit(self, qubit_pairs, qubit):
|
| 1001 |
+
for index, tup in enumerate(qubit_pairs):
|
| 1002 |
+
if qubit in tup:
|
| 1003 |
+
return index
|
| 1004 |
+
return np.inf
|
| 1005 |
+
|
| 1006 |
+
def _get_qubit_reuse_priority(self, qubit_pair, k):
|
| 1007 |
+
"""
|
| 1008 |
+
Priority system based on how recently either of the qubits in a given pair were acted on.
|
| 1009 |
+
The priority of a qubit pair (a,b) is given by:
|
| 1010 |
+
1. -1 if (a,b) was the last pair acted on
|
| 1011 |
+
2. 1 if k=0
|
| 1012 |
+
3. 1 if a and b have never been acted on
|
| 1013 |
+
4. min[1-2^(-(la+1)/k), 1-2^(-(lb+1)/k)] where la (lb) is the number of layers since
|
| 1014 |
+
qubit a (b) has been acted on.
|
| 1015 |
+
|
| 1016 |
+
@param qubit_pair: Tuple where each element is the index of a qubit
|
| 1017 |
+
@param k: Constant controlling how heavily recent pairs are disfavoured
|
| 1018 |
+
"""
|
| 1019 |
+
# Hard code that previous pair has priority -1
|
| 1020 |
+
if (
|
| 1021 |
+
len(self.qubit_pair_history) > 0 + int(self.initial_single_qubit_layer)
|
| 1022 |
+
and qubit_pair == self.qubit_pair_history[-1]
|
| 1023 |
+
):
|
| 1024 |
+
return -1
|
| 1025 |
+
# If not previous pair, then use exponential disfavouring
|
| 1026 |
+
elif k == 0:
|
| 1027 |
+
return 1
|
| 1028 |
+
else:
|
| 1029 |
+
qubit_pairs_reversed = self.qubit_pair_history[::-1]
|
| 1030 |
+
locs = [
|
| 1031 |
+
self._find_last_use_of_qubit(qubit_pairs_reversed, qubit)
|
| 1032 |
+
for qubit in qubit_pair
|
| 1033 |
+
]
|
| 1034 |
+
priorities = [1 - np.exp2(-(loc + 1) / k) for loc in locs]
|
| 1035 |
+
return np.min(priorities)
|
| 1036 |
+
|
| 1037 |
+
def _get_pair_reuse_priority(self, qubit_pair, k):
|
| 1038 |
+
"""
|
| 1039 |
+
Priority system based on how recently a specific pair of qubits were acted on.
|
| 1040 |
+
The priority of a qubit pair (a,b) is given by:
|
| 1041 |
+
1. -1 if (a,b) was the last pair acted on
|
| 1042 |
+
2. 1 if k=0
|
| 1043 |
+
3. 1 if (a,b) has never been acted on
|
| 1044 |
+
4. 1-2^(-l/k) l is the number of layers since the pair (a,b) has been acted on.
|
| 1045 |
+
|
| 1046 |
+
@param qubit_pair: Tuple where each element is the index of a qubit
|
| 1047 |
+
@param k: Constant controlling how heavily recent pairs are disfavoured
|
| 1048 |
+
"""
|
| 1049 |
+
# Hard code that previous pair has priority -1
|
| 1050 |
+
if (
|
| 1051 |
+
len(self.qubit_pair_history) > 0 + int(self.initial_single_qubit_layer)
|
| 1052 |
+
and qubit_pair == self.qubit_pair_history[-1]
|
| 1053 |
+
):
|
| 1054 |
+
return -1
|
| 1055 |
+
# If not previous pair, then use exponential disfavouring
|
| 1056 |
+
elif k == 0:
|
| 1057 |
+
return 1
|
| 1058 |
+
else:
|
| 1059 |
+
qubit_pairs_reversed = self.qubit_pair_history[::-1]
|
| 1060 |
+
try:
|
| 1061 |
+
loc = qubit_pairs_reversed.index(qubit_pair)
|
| 1062 |
+
priority = 1 - np.exp2(-loc / k)
|
| 1063 |
+
return priority
|
| 1064 |
+
except ValueError:
|
| 1065 |
+
return 1
|
| 1066 |
+
|
| 1067 |
+
def _first_layer_increment_results_dict(self):
|
| 1068 |
+
self.entanglement_measures_history.append([None])
|
| 1069 |
+
self.e_val_history.append(None)
|
| 1070 |
+
self.general_gradient_history.append(None)
|
| 1071 |
+
self.qubit_pair_history.append((None, None))
|
| 1072 |
+
self.pair_selection_method_history.append(None)
|
| 1073 |
+
|
| 1074 |
+
def _get_layer_added(self, layer_count):
|
| 1075 |
+
layer_added = (
|
| 1076 |
+
self.ref_circuit_as_gates.copy()
|
| 1077 |
+
if self.is_aer_mps_backend
|
| 1078 |
+
else self.full_circuit.copy()
|
| 1079 |
+
)
|
| 1080 |
+
len_layer_added = len(self.layer_2q_gate)
|
| 1081 |
+
# Remove starting_circuit from end of ansatz, if there is one
|
| 1082 |
+
if self.starting_circuit is not None:
|
| 1083 |
+
del layer_added.data[-len(self.starting_circuit.data) :]
|
| 1084 |
+
if self.initial_single_qubit_layer == True and layer_count == 0:
|
| 1085 |
+
del layer_added.data[: -layer_added.num_qubits]
|
| 1086 |
+
return layer_added
|
| 1087 |
+
else:
|
| 1088 |
+
# Delete all gates apart from the 5 from the added layer
|
| 1089 |
+
del layer_added.data[:-len_layer_added]
|
| 1090 |
+
return layer_added
|
| 1091 |
+
|
| 1092 |
+
def _get_num_gates_to_cache(self, n, includes_isql=False):
|
| 1093 |
+
return len(self.layer_2q_gate) * (
|
| 1094 |
+
n - int(includes_isql)
|
| 1095 |
+
) + self.full_circuit.num_qubits * int(includes_isql)
|
| 1096 |
+
|
| 1097 |
+
def _absorb_n_gates_into_mps(self, n):
|
| 1098 |
+
"""
|
| 1099 |
+
Takes full_circuit, which consists of a set_matrix_product_state instruction, followed by some number of ADAPT-AQC
|
| 1100 |
+
gates and absorbs the first n of these gates (immediately after set_matrix_product_state) into the
|
| 1101 |
+
set_matrix_product_state instruction. Also returns a copy of the gates absorbed as a QuantumCircuit.
|
| 1102 |
+
|
| 1103 |
+
In other words it converts full_circuit from this:
|
| 1104 |
+
-|0>--|mps(V†(k)U|0>)|--|N ADAPT-AQC gates |--|starting_circuit_inverse|-
|
| 1105 |
+
To this:
|
| 1106 |
+
-|0>--|mps(V†(k+n)U|0>)|--|N-n ADAPT-AQC gates|--|starting_circuit_inverse|-
|
| 1107 |
+
|
| 1108 |
+
Where mps(V†(k)U|0>) is the set_matrix_product_state instruction representing the state after k gates
|
| 1109 |
+
have been added.
|
| 1110 |
+
|
| 1111 |
+
:param n: Number of gates to absorb.
|
| 1112 |
+
:return: QuantumCircuit containing a copy of the gates which were absorbed.
|
| 1113 |
+
|
| 1114 |
+
:param n: Number of gates to absorb.
|
| 1115 |
+
:return: QuantumCircuit containing a copy of the gates which were absorbed.
|
| 1116 |
+
"""
|
| 1117 |
+
# +1 to include the initial set_matrix_product_state
|
| 1118 |
+
num_gates_to_absorb = n + 1
|
| 1119 |
+
|
| 1120 |
+
# Get full_circuit up to and including gates to be absorbed
|
| 1121 |
+
circ_to_absorb = self.full_circuit.copy()
|
| 1122 |
+
del circ_to_absorb.data[num_gates_to_absorb:]
|
| 1123 |
+
|
| 1124 |
+
# Keep a copy of what was absorbed to add to the reference circuit
|
| 1125 |
+
gates_absorbed = circ_to_absorb.copy()
|
| 1126 |
+
del gates_absorbed.data[0]
|
| 1127 |
+
|
| 1128 |
+
# Get MPS of circ_to_absorb
|
| 1129 |
+
circ_to_absorb_mps = mpsops.mps_from_circuit(
|
| 1130 |
+
circ_to_absorb, sim=self.backend.simulator
|
| 1131 |
+
)
|
| 1132 |
+
|
| 1133 |
+
# Create circuit with MPS instruction found above, with same registers as full_circuit
|
| 1134 |
+
mps_circuit = QuantumCircuit(self.full_circuit.qregs[0])
|
| 1135 |
+
mps_circuit.set_matrix_product_state(circ_to_absorb_mps)
|
| 1136 |
+
|
| 1137 |
+
# Replace absorbed part of full_circuit with its MPS instruction
|
| 1138 |
+
num_gates_not_absorbed = len(self.full_circuit.data) - num_gates_to_absorb
|
| 1139 |
+
if num_gates_not_absorbed != 0:
|
| 1140 |
+
del self.full_circuit.data[:-num_gates_not_absorbed]
|
| 1141 |
+
else:
|
| 1142 |
+
del self.full_circuit.data[:]
|
| 1143 |
+
self.full_circuit.data.insert(0, mps_circuit.data[0])
|
| 1144 |
+
|
| 1145 |
+
return gates_absorbed
|
| 1146 |
+
|
| 1147 |
+
def record_cnot_depth(self):
|
| 1148 |
+
if self.is_aer_mps_backend:
|
| 1149 |
+
ansatz_circ = co.extract_inner_circuit(
|
| 1150 |
+
self.ref_circuit_as_gates,
|
| 1151 |
+
gate_range=(1, len(self.ref_circuit_as_gates)),
|
| 1152 |
+
)
|
| 1153 |
+
else:
|
| 1154 |
+
# Make sure initial ansatz and any "frozen" layers are included
|
| 1155 |
+
ansatz_circ = co.extract_inner_circuit(
|
| 1156 |
+
self.full_circuit,
|
| 1157 |
+
gate_range=(
|
| 1158 |
+
self.original_lhs_gate_count,
|
| 1159 |
+
self.variational_circuit_range()[1],
|
| 1160 |
+
),
|
| 1161 |
+
)
|
| 1162 |
+
self.cnot_depth = multi_qubit_gate_depth(ansatz_circ)
|
| 1163 |
+
self.cnot_depth_history.append(self.cnot_depth)
|
utils/adapt-aqc/adaptaqc/compilers/adapt/adapt_config.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# (C) Copyright IBM 2025.
|
| 2 |
+
#
|
| 3 |
+
# This code is licensed under the Apache License, Version 2.0. You may
|
| 4 |
+
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
| 5 |
+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
| 6 |
+
#
|
| 7 |
+
# Any modifications or derivative works of this code must retain this
|
| 8 |
+
# copyright notice, and modified files need to carry a notice indicating
|
| 9 |
+
# that they have been altered from the originals.
|
| 10 |
+
|
| 11 |
+
"""Contains AdaptConfig"""
|
| 12 |
+
|
| 13 |
+
import adaptaqc.utils.constants as vconstants
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class AdaptConfig:
|
| 17 |
+
def __init__(
|
| 18 |
+
self,
|
| 19 |
+
max_layers: int = int(1e5),
|
| 20 |
+
sufficient_cost=vconstants.DEFAULT_SUFFICIENT_COST,
|
| 21 |
+
max_2q_gates=1e4,
|
| 22 |
+
cost_improvement_num_layers=10,
|
| 23 |
+
cost_improvement_tol=1e-2,
|
| 24 |
+
max_layers_to_modify=100,
|
| 25 |
+
method="ISL",
|
| 26 |
+
bad_qubit_pair_memory=10,
|
| 27 |
+
reuse_exponent=0,
|
| 28 |
+
reuse_priority_mode="pair",
|
| 29 |
+
rotosolve_frequency=1,
|
| 30 |
+
rotoselect_tol=1e-5,
|
| 31 |
+
rotosolve_tol=1e-3,
|
| 32 |
+
entanglement_threshold=1e-8,
|
| 33 |
+
):
|
| 34 |
+
"""
|
| 35 |
+
ADAPT-AQC termination criteria.
|
| 36 |
+
:param max_layers: ADAPT-AQC will terminate if the number of ansatz layers reaches this value.
|
| 37 |
+
:param sufficient_cost: ADAPT-AQC will terminate if the cost reaches below this value.
|
| 38 |
+
:param max_2q_gates: ADAPT-AQC will terminate if the number of 2 qubit gates reaches this value.
|
| 39 |
+
:param cost_improvement_num_layers: The number of layer costs to consider when evaluating
|
| 40 |
+
if the cost is decreasing fast enough.
|
| 41 |
+
:param cost_improvement_tol: ADAPT-AQC will terminate if in the last cost_improvement_num_layers,
|
| 42 |
+
the cost has not decreased by this value on average per layer.
|
| 43 |
+
|
| 44 |
+
Add layer criteria:
|
| 45 |
+
:param max_layers_to_modify: The number of layers to modify, counting from the back of
|
| 46 |
+
the ansatz, when Rotosolve is used.
|
| 47 |
+
:param method: Method by which a qubit pair is prioritised for the next layer. One of:
|
| 48 |
+
'ISL' - Largest pairwise entanglement as defined by AdaptCompiler.entanglement_measure
|
| 49 |
+
'expectation' - Smallest combined σz expectation values (i.e., closest to min value of -2)
|
| 50 |
+
'basic' - Pair not picked in the longest time
|
| 51 |
+
'random' - Pair selected randomly
|
| 52 |
+
'general_gradient' - Pair with the largest Euclidean norm of the global cost gradient with
|
| 53 |
+
respect to all parameters (θ) in the layer ansatz, evaluated at θ=0. This is the setting
|
| 54 |
+
used in https://arxiv.org/abs/2503.09683.
|
| 55 |
+
:param bad_qubit_pair_memory: For the ISL method, if acting on a qubit pair leads to
|
| 56 |
+
entanglement increasing, it is labelled a "bad pair". After this, for a number of layers
|
| 57 |
+
corresponding to the bad_qubit_pair_memory, this pair will not be selected.
|
| 58 |
+
:param reuse_exponent: For entanglement, expectation or general_gradient method, this
|
| 59 |
+
controls how much priority should be given to picking qubits not recently acted on. If 0,
|
| 60 |
+
the priority system is turned off and all qubits have the same reuse priority when adding
|
| 61 |
+
a new layer. Note ADAPT-AQC never reuses the same pair of qubits regardless of this setting.
|
| 62 |
+
:param reuse_priority_mode: For the priority system, given qubit pair (q1, q2) has been used
|
| 63 |
+
before, should priority be given to:
|
| 64 |
+
(a) not reusing the same pair of qubits (q1, q2) (set param to "pair")
|
| 65 |
+
(b) not reusing the qubits q1 OR q2 (set param to "qubit")
|
| 66 |
+
|
| 67 |
+
Other parameters:
|
| 68 |
+
:param rotosolve_frequency: How often Rotosolve is used (if n, rotosolve will be used after
|
| 69 |
+
every n layers).
|
| 70 |
+
:param rotoselect_tol: How much does the cost need to decrease by each iteration to continue
|
| 71 |
+
Rotoselect.
|
| 72 |
+
:param rotosolve_tol: How much does the cost need to decrease by each iteration to continue
|
| 73 |
+
Rotosolve.
|
| 74 |
+
:param entanglement_threshold: For the ISL method, entanglement below this value is treated
|
| 75 |
+
as zero in terms of picking the next layer.
|
| 76 |
+
"""
|
| 77 |
+
self.bad_qubit_pair_memory = bad_qubit_pair_memory
|
| 78 |
+
self.max_layers = max_layers
|
| 79 |
+
self.sufficient_cost = sufficient_cost
|
| 80 |
+
self.max_2q_gates = max_2q_gates
|
| 81 |
+
self.cost_improvement_tol = cost_improvement_tol
|
| 82 |
+
self.cost_improvement_num_layers = int(cost_improvement_num_layers)
|
| 83 |
+
self.max_layers_to_modify = max_layers_to_modify
|
| 84 |
+
self.method = method
|
| 85 |
+
self.rotosolve_frequency = rotosolve_frequency
|
| 86 |
+
self.rotoselect_tol = rotoselect_tol
|
| 87 |
+
self.rotosolve_tol = rotosolve_tol
|
| 88 |
+
self.entanglement_threshold = entanglement_threshold
|
| 89 |
+
self.reuse_exponent = reuse_exponent
|
| 90 |
+
self.reuse_priority_mode = reuse_priority_mode.lower()
|
| 91 |
+
|
| 92 |
+
def __repr__(self):
|
| 93 |
+
representation_str = f"{self.__class__.__name__}("
|
| 94 |
+
for k, v in self.__dict__.items():
|
| 95 |
+
representation_str += f"{k}={v!r}, "
|
| 96 |
+
representation_str += ")"
|
| 97 |
+
return representation_str
|
utils/adapt-aqc/adaptaqc/compilers/adapt/adapt_result.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# (C) Copyright IBM 2025.
|
| 2 |
+
#
|
| 3 |
+
# This code is licensed under the Apache License, Version 2.0. You may
|
| 4 |
+
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
| 5 |
+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
| 6 |
+
#
|
| 7 |
+
# Any modifications or derivative works of this code must retain this
|
| 8 |
+
# copyright notice, and modified files need to carry a notice indicating
|
| 9 |
+
# that they have been altered from the originals.
|
| 10 |
+
|
| 11 |
+
"""Contains AdaptResult"""
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class AdaptResult:
|
| 15 |
+
def __init__(
|
| 16 |
+
self,
|
| 17 |
+
circuit,
|
| 18 |
+
overlap,
|
| 19 |
+
exact_overlap,
|
| 20 |
+
num_1q_gates,
|
| 21 |
+
num_2q_gates,
|
| 22 |
+
cnot_depth_history,
|
| 23 |
+
global_cost_history,
|
| 24 |
+
local_cost_history,
|
| 25 |
+
circuit_history,
|
| 26 |
+
entanglement_measures_history,
|
| 27 |
+
e_val_history,
|
| 28 |
+
qubit_pair_history,
|
| 29 |
+
method_history,
|
| 30 |
+
time_taken,
|
| 31 |
+
cost_evaluations,
|
| 32 |
+
coupling_map,
|
| 33 |
+
circuit_qasm,
|
| 34 |
+
):
|
| 35 |
+
"""
|
| 36 |
+
:param circuit: Resulting circuit.
|
| 37 |
+
:param overlap: 1 - final_global_cost.
|
| 38 |
+
:param exact_overlap: Only computable with SV backend.
|
| 39 |
+
:param num_1q_gates: Number of rotation gates in circuit.
|
| 40 |
+
:param num_2q_gates: Number of entangling gates in circuit.
|
| 41 |
+
:param cnot_depth_history: Depth of ansatz after each layer when only considering 2-qubit gates.
|
| 42 |
+
:param global_cost_history: List of global costs after each layer.
|
| 43 |
+
:param local_cost_history: List of local costs after each layer (if applicable).
|
| 44 |
+
:param circuit_history: List of circuits as qasm strings after each layer (if applicable).
|
| 45 |
+
:param entanglement_measures_history: List of pairwise entanglements after each layer.
|
| 46 |
+
:param e_val_history: List of single-qubit sigma_z expectation values after each layer.
|
| 47 |
+
:param qubit_pair_history: List of qubit pair acted on in each layer.
|
| 48 |
+
:param method_history: List of methods used to select qubit pairs for each layer.
|
| 49 |
+
:param time_taken: Total time taken for recompilation.
|
| 50 |
+
:param cost_evaluations: Total number of cost evalutions during recompilation.
|
| 51 |
+
:param coupling_map: List of allowed qubit connections.
|
| 52 |
+
:param circuit_qasm: QASM string of the resulting circuit.
|
| 53 |
+
"""
|
| 54 |
+
self.circuit = circuit
|
| 55 |
+
self.overlap = overlap
|
| 56 |
+
self.exact_overlap = exact_overlap
|
| 57 |
+
self.num_1q_gates = num_1q_gates
|
| 58 |
+
self.num_2q_gates = num_2q_gates
|
| 59 |
+
self.cnot_depth_history = cnot_depth_history
|
| 60 |
+
self.global_cost_history = global_cost_history
|
| 61 |
+
self.local_cost_history = local_cost_history
|
| 62 |
+
self.circuit_history = circuit_history
|
| 63 |
+
self.entanglement_measures_history = entanglement_measures_history
|
| 64 |
+
self.e_val_history = e_val_history
|
| 65 |
+
self.qubit_pair_history = qubit_pair_history
|
| 66 |
+
self.method_history = method_history
|
| 67 |
+
self.time_taken = time_taken
|
| 68 |
+
self.cost_evaluations = cost_evaluations
|
| 69 |
+
self.coupling_map = coupling_map
|
| 70 |
+
self.circuit_qasm = circuit_qasm
|
utils/adapt-aqc/adaptaqc/compilers/approximate_compiler.py
ADDED
|
@@ -0,0 +1,527 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# (C) Copyright IBM 2025.
|
| 2 |
+
#
|
| 3 |
+
# This code is licensed under the Apache License, Version 2.0. You may
|
| 4 |
+
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
| 5 |
+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
| 6 |
+
#
|
| 7 |
+
# Any modifications or derivative works of this code must retain this
|
| 8 |
+
# copyright notice, and modified files need to carry a notice indicating
|
| 9 |
+
# that they have been altered from the originals.
|
| 10 |
+
|
| 11 |
+
"""
|
| 12 |
+
Contains ApproximateCcompiler
|
| 13 |
+
"""
|
| 14 |
+
import logging
|
| 15 |
+
import multiprocessing
|
| 16 |
+
import os
|
| 17 |
+
import timeit
|
| 18 |
+
from abc import ABC, abstractmethod
|
| 19 |
+
|
| 20 |
+
from aqc_research.mps_operations import mps_from_circuit, check_mps
|
| 21 |
+
from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister
|
| 22 |
+
from qiskit.providers import Backend
|
| 23 |
+
|
| 24 |
+
from adaptaqc.backends.aer_mps_backend import AerMPSBackend
|
| 25 |
+
from adaptaqc.backends.aqc_backend import AQCBackend
|
| 26 |
+
from adaptaqc.backends.itensor_backend import ITensorBackend
|
| 27 |
+
from adaptaqc.backends.python_default_backends import QASM_SIM
|
| 28 |
+
from adaptaqc.backends.qiskit_sampling_backend import QiskitSamplingBackend
|
| 29 |
+
from adaptaqc.utils import circuit_operations as co
|
| 30 |
+
from adaptaqc.utils.circuit_operations.circuit_operations_full_circuit import (
|
| 31 |
+
remove_classical_operations,
|
| 32 |
+
)
|
| 33 |
+
from adaptaqc.utils.constants import QiskitMPS
|
| 34 |
+
from adaptaqc.utils.cost_minimiser import CostMinimiser
|
| 35 |
+
from adaptaqc.utils.utilityfunctions import (
|
| 36 |
+
is_statevector_backend,
|
| 37 |
+
qiskit_to_tenpy_mps,
|
| 38 |
+
tenpy_chi_1_mps_to_circuit,
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
logger = logging.getLogger(__name__)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class CompileInPartsResult:
|
| 45 |
+
def __init__(
|
| 46 |
+
self,
|
| 47 |
+
circuit,
|
| 48 |
+
overlap,
|
| 49 |
+
individual_results,
|
| 50 |
+
time_taken,
|
| 51 |
+
):
|
| 52 |
+
"""
|
| 53 |
+
:param circuit: Resulting circuit.
|
| 54 |
+
:param overlap: 1 - final_global_cost.
|
| 55 |
+
:param individual_results: List of result objects for each sub-recompilation.
|
| 56 |
+
:param time_taken: Total time taken for recompilation.
|
| 57 |
+
"""
|
| 58 |
+
self.circuit = circuit
|
| 59 |
+
self.overlap = overlap
|
| 60 |
+
self.individual_results = individual_results
|
| 61 |
+
self.time_taken = time_taken
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
class ApproximateCompiler(ABC):
|
| 65 |
+
"""
|
| 66 |
+
Variational hybrid quantum-classical algorithm that compiles a given
|
| 67 |
+
circuit into another circuit. The new circuit
|
| 68 |
+
has the same result when acting on the given input state as the given
|
| 69 |
+
circuit.
|
| 70 |
+
"""
|
| 71 |
+
|
| 72 |
+
full_circuit: QuantumCircuit
|
| 73 |
+
|
| 74 |
+
def __init__(
|
| 75 |
+
self,
|
| 76 |
+
target: QuantumCircuit | QiskitMPS,
|
| 77 |
+
backend: AQCBackend,
|
| 78 |
+
execute_kwargs=None,
|
| 79 |
+
initial_state=None,
|
| 80 |
+
qubit_subset=None,
|
| 81 |
+
general_initial_state=False,
|
| 82 |
+
starting_circuit=None,
|
| 83 |
+
optimise_local_cost=False,
|
| 84 |
+
soften_global_cost=False,
|
| 85 |
+
itensor_chi=None,
|
| 86 |
+
itensor_cutoff=None,
|
| 87 |
+
rotosolve_fraction=1.0,
|
| 88 |
+
):
|
| 89 |
+
"""
|
| 90 |
+
:param target: Circuit or MPS that is to be compiled
|
| 91 |
+
:param backend: Backend that is to be used
|
| 92 |
+
:param execute_kwargs: keyword arguments passed down to Qiskit AerBackend.run
|
| 93 |
+
e.g. {'noise_model:NoiseModel, shots=10000}
|
| 94 |
+
|
| 95 |
+
:param initial_state: Can be used to define an initial state to compile with respect to
|
| 96 |
+
(as opposed to the default of the |0..0> state). Effectively redefines the cost function as
|
| 97 |
+
C = 1 - |<init|V†U|init>|^2. Similar functionality can be achieved for AdaptCompiler with
|
| 98 |
+
the `starting_circuit` param, but here the solution won't prepare the initial state - it
|
| 99 |
+
assumes the initial state is already prepared. Can be a circuit (QuantumCircuit/Instruction)
|
| 100 |
+
or vector (list/np.ndarray) or None
|
| 101 |
+
|
| 102 |
+
:param qubit_subset: The subset of qubits (relative to initial state
|
| 103 |
+
circuit) that target acts
|
| 104 |
+
on. If None, it will be assumed that target and
|
| 105 |
+
initial_state circuit have the same qubits
|
| 106 |
+
:param general_initial_state: Whether recompilation should be for a
|
| 107 |
+
general initial state
|
| 108 |
+
"""
|
| 109 |
+
self.target = target
|
| 110 |
+
self.original_circuit_classical_ops = None
|
| 111 |
+
self.backend = backend if backend is not None else QASM_SIM
|
| 112 |
+
self.is_statevector_backend = is_statevector_backend(self.backend)
|
| 113 |
+
self.is_aer_mps_backend = isinstance(self.backend, AerMPSBackend)
|
| 114 |
+
if isinstance(self.backend, ITensorBackend):
|
| 115 |
+
logger.warning(
|
| 116 |
+
"ITensor is an experimental backend with many missing features"
|
| 117 |
+
)
|
| 118 |
+
self.itensor_target = None
|
| 119 |
+
self.itensor_chi = itensor_chi
|
| 120 |
+
self.itensor_cutoff = itensor_cutoff
|
| 121 |
+
if check_mps(self.target) and not self.is_aer_mps_backend:
|
| 122 |
+
raise Exception("Aer MPS backend must be used when target is an Aer MPS")
|
| 123 |
+
self.circuit_to_compile = self.prepare_circuit()
|
| 124 |
+
self.execute_kwargs = self.parse_default_execute_kwargs(execute_kwargs)
|
| 125 |
+
self.backend_options = self.parse_default_backend_options()
|
| 126 |
+
self.initial_state_circuit = co.initial_state_to_circuit(initial_state)
|
| 127 |
+
self.total_num_qubits = self.calculate_total_num_qubits()
|
| 128 |
+
self.qubit_subset_to_compile = (
|
| 129 |
+
qubit_subset if qubit_subset else list(range(self.total_num_qubits))
|
| 130 |
+
)
|
| 131 |
+
self.general_initial_state = general_initial_state
|
| 132 |
+
self.starting_circuit = self.prepare_starting_circuit(starting_circuit)
|
| 133 |
+
self.zero_mps = mps_from_circuit(
|
| 134 |
+
QuantumCircuit(self.total_num_qubits), return_preprocessed=True
|
| 135 |
+
)
|
| 136 |
+
self.optimise_local_cost = optimise_local_cost
|
| 137 |
+
self.soften_global_cost = soften_global_cost
|
| 138 |
+
|
| 139 |
+
if initial_state is not None and general_initial_state:
|
| 140 |
+
raise ValueError(
|
| 141 |
+
"Can't compile for general initial state when specific "
|
| 142 |
+
"initial state is provided"
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
(
|
| 146 |
+
self.full_circuit,
|
| 147 |
+
self.lhs_gate_count,
|
| 148 |
+
self.rhs_gate_count,
|
| 149 |
+
) = self._prepare_full_circuit()
|
| 150 |
+
if 0 < rotosolve_fraction <= 1:
|
| 151 |
+
self.minimizer = CostMinimiser(
|
| 152 |
+
self.evaluate_cost,
|
| 153 |
+
self.variational_circuit_range,
|
| 154 |
+
self.full_circuit,
|
| 155 |
+
rotosolve_fraction,
|
| 156 |
+
)
|
| 157 |
+
else:
|
| 158 |
+
raise ValueError("rotosolve_fraction must be in the range (0,1]")
|
| 159 |
+
|
| 160 |
+
# Count number of cost evaluations
|
| 161 |
+
self.cost_evaluation_counter = 0
|
| 162 |
+
|
| 163 |
+
self.compiling_finished = False
|
| 164 |
+
|
| 165 |
+
def prepare_circuit(self):
|
| 166 |
+
"""
|
| 167 |
+
Constructs a circuit from the target which will then be compiled. This is composed of four
|
| 168 |
+
possible parts:
|
| 169 |
+
1. Remove classical operations from circuit
|
| 170 |
+
2. Transpile circuit to BASIS_GATES
|
| 171 |
+
3. Find MPS representation of target circuit
|
| 172 |
+
4. Create circuit with set_matrix_product_state instruction generating the MPS found in 3.
|
| 173 |
+
|
| 174 |
+
For the four combinations of (target, backend), prepare_circuit performs:
|
| 175 |
+
(circuit, non-mps): 1 -> 2 -> return circuit
|
| 176 |
+
(circuit, mps): 1 -> 2 -> 3 -> 4 -> return circuit
|
| 177 |
+
(mps, non-mps): exception will have been raised already
|
| 178 |
+
(mps, mps): 4 -> return circuit
|
| 179 |
+
"""
|
| 180 |
+
# Check if target is Aer MPS
|
| 181 |
+
if check_mps(self.target):
|
| 182 |
+
target_mps = self.target
|
| 183 |
+
target_mps_circuit = QuantumCircuit(len(target_mps[0]))
|
| 184 |
+
target_mps_circuit.set_matrix_product_state(target_mps)
|
| 185 |
+
return target_mps_circuit
|
| 186 |
+
else:
|
| 187 |
+
target_copy = self.target.copy()
|
| 188 |
+
self.original_circuit_classical_ops = remove_classical_operations(
|
| 189 |
+
target_copy
|
| 190 |
+
)
|
| 191 |
+
qc2 = QuantumCircuit(len(self.target.qubits))
|
| 192 |
+
qc2.append(
|
| 193 |
+
co.make_quantum_only_circuit(target_copy).to_instruction(), qc2.qregs[0]
|
| 194 |
+
)
|
| 195 |
+
prepared_circuit = co.unroll_to_basis_gates(qc2)
|
| 196 |
+
if self.is_aer_mps_backend:
|
| 197 |
+
logger.info("Pre-computing target circuit as MPS using AerSimulator")
|
| 198 |
+
target_mps = mps_from_circuit(
|
| 199 |
+
prepared_circuit, sim=self.backend.simulator
|
| 200 |
+
)
|
| 201 |
+
target_mps_circuit = QuantumCircuit(prepared_circuit.num_qubits)
|
| 202 |
+
target_mps_circuit.set_matrix_product_state(target_mps)
|
| 203 |
+
# Return a circuit with the target MPS embedded inside
|
| 204 |
+
return target_mps_circuit
|
| 205 |
+
if isinstance(self.backend, ITensorBackend):
|
| 206 |
+
from itensornetworks_qiskit.utils import qiskit_circ_to_it_circ
|
| 207 |
+
from juliacall import Main as jl
|
| 208 |
+
|
| 209 |
+
jl.seval("using ITensorNetworksQiskit")
|
| 210 |
+
logger.info("Pre-computing target circuit as MPS using ITensor")
|
| 211 |
+
gates = qiskit_circ_to_it_circ(prepared_circuit)
|
| 212 |
+
n = self.target.num_qubits
|
| 213 |
+
self.itensor_sites = jl.generate_siteindices_itensors(n)
|
| 214 |
+
self.itensor_target = jl.mps_from_circuit_itensors(
|
| 215 |
+
n, gates, self.itensor_chi, self.itensor_cutoff, self.itensor_sites
|
| 216 |
+
)
|
| 217 |
+
return prepared_circuit
|
| 218 |
+
|
| 219 |
+
def prepare_starting_circuit(self, starting_circuit):
|
| 220 |
+
if starting_circuit is None or isinstance(starting_circuit, QuantumCircuit):
|
| 221 |
+
return starting_circuit
|
| 222 |
+
elif starting_circuit == "tenpy_product_state":
|
| 223 |
+
if isinstance(self.backend, AerMPSBackend):
|
| 224 |
+
trunc_thr = (
|
| 225 |
+
self.backend.simulator.options.matrix_product_state_truncation_threshold
|
| 226 |
+
)
|
| 227 |
+
else:
|
| 228 |
+
trunc_thr = 1e-8
|
| 229 |
+
tenpy_mps = qiskit_to_tenpy_mps(
|
| 230 |
+
mps_from_circuit(self.circuit_to_compile.copy(), trunc_thr=trunc_thr)
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
compression_options = {
|
| 234 |
+
"compression_method": "variational",
|
| 235 |
+
"trunc_params": {"chi_max": 1},
|
| 236 |
+
"max_trunc_err": 1,
|
| 237 |
+
"max_sweeps": 50,
|
| 238 |
+
"min_sweeps": 10,
|
| 239 |
+
}
|
| 240 |
+
tenpy_mps.compress(compression_options)
|
| 241 |
+
|
| 242 |
+
return tenpy_chi_1_mps_to_circuit(tenpy_mps)
|
| 243 |
+
else:
|
| 244 |
+
raise ValueError(
|
| 245 |
+
"starting_circuit must be a QuantumCircuit, None, or string: 'tenpy_product_state'"
|
| 246 |
+
)
|
| 247 |
+
|
| 248 |
+
def parse_default_execute_kwargs(self, execute_kwargs):
|
| 249 |
+
kwargs = {} if execute_kwargs is None else dict(execute_kwargs)
|
| 250 |
+
if "shots" not in kwargs:
|
| 251 |
+
if isinstance(self.backend, QiskitSamplingBackend):
|
| 252 |
+
kwargs["shots"] = 8192
|
| 253 |
+
else:
|
| 254 |
+
kwargs["shots"] = 1
|
| 255 |
+
if "optimization_level" not in kwargs:
|
| 256 |
+
kwargs["optimization_level"] = 0
|
| 257 |
+
return kwargs
|
| 258 |
+
|
| 259 |
+
def parse_default_backend_options(self):
|
| 260 |
+
backend_options = {}
|
| 261 |
+
if (
|
| 262 |
+
"noise_model" in self.execute_kwargs
|
| 263 |
+
and self.execute_kwargs["noise_model"] is not None
|
| 264 |
+
):
|
| 265 |
+
backend_options["method"] = "automatic"
|
| 266 |
+
else:
|
| 267 |
+
backend_options["method"] = "automatic"
|
| 268 |
+
|
| 269 |
+
try:
|
| 270 |
+
if os.environ["QISKIT_IN_PARALLEL"] == "TRUE":
|
| 271 |
+
# Already in parallel
|
| 272 |
+
backend_options["max_parallel_experiments"] = 1
|
| 273 |
+
else:
|
| 274 |
+
num_threads = multiprocessing.cpu_count()
|
| 275 |
+
backend_options["max_parallel_experiments"] = num_threads
|
| 276 |
+
logger.debug(
|
| 277 |
+
"Circuits will be evaluated with {} experiments in "
|
| 278 |
+
"parallel".format(num_threads)
|
| 279 |
+
)
|
| 280 |
+
os.environ["KMP_WARNINGS"] = "0"
|
| 281 |
+
|
| 282 |
+
except KeyError:
|
| 283 |
+
logger.debug(
|
| 284 |
+
"No OMP number of threads defined. Qiskit will autodiscover "
|
| 285 |
+
"the number of parallel shots to run"
|
| 286 |
+
)
|
| 287 |
+
return backend_options
|
| 288 |
+
|
| 289 |
+
def calculate_total_num_qubits(self):
|
| 290 |
+
if self.initial_state_circuit is None:
|
| 291 |
+
total_num_qubits = self.circuit_to_compile.num_qubits
|
| 292 |
+
else:
|
| 293 |
+
total_num_qubits = self.initial_state_circuit.num_qubits
|
| 294 |
+
return total_num_qubits
|
| 295 |
+
|
| 296 |
+
def variational_circuit_range(self, circuit=None):
|
| 297 |
+
if circuit == None:
|
| 298 |
+
circuit = self.full_circuit
|
| 299 |
+
return self.lhs_gate_count, len(circuit.data) - self.rhs_gate_count
|
| 300 |
+
|
| 301 |
+
def ansatz_range(self):
|
| 302 |
+
return self.lhs_gate_count, len(self.full_circuit.data)
|
| 303 |
+
|
| 304 |
+
def _starting_circuit_range(self):
|
| 305 |
+
end = len(self.full_circuit.data)
|
| 306 |
+
start = end - self.rhs_gate_count
|
| 307 |
+
return start, end
|
| 308 |
+
|
| 309 |
+
@abstractmethod
|
| 310 |
+
def compile(self):
|
| 311 |
+
"""
|
| 312 |
+
Run the recompilation algorithm
|
| 313 |
+
:return: Result object (AdaptResult, FixedAnsatzResult, RotoselectResult) containing the
|
| 314 |
+
resulting circuit, the overlap between original and resulting circuit, and other optional
|
| 315 |
+
entries (such as circuit parameters).
|
| 316 |
+
"""
|
| 317 |
+
raise NotImplementedError(
|
| 318 |
+
"A compiler must provide implementation for the compile() " "method"
|
| 319 |
+
)
|
| 320 |
+
|
| 321 |
+
def compile_in_parts(self, max_depth_per_block=10):
|
| 322 |
+
"""
|
| 323 |
+
Compiles the circuit using the following procedure: First break
|
| 324 |
+
the circuit into n subcircuits.
|
| 325 |
+
Then iteratively find an approximation recompilation for the first
|
| 326 |
+
m+1 subcircuits by finding an approximate
|
| 327 |
+
of (approx_circuit_for_first_m_subcircuits + (m+1)th subcircuit)
|
| 328 |
+
:param max_depth_per_block: The maximum allowed depth of each of the
|
| 329 |
+
n subcircuits
|
| 330 |
+
:return: CompileInPartsResult object
|
| 331 |
+
"""
|
| 332 |
+
logger.info("Started partial recompilation")
|
| 333 |
+
start_time = timeit.default_timer()
|
| 334 |
+
|
| 335 |
+
all_subcircuits = co.vertically_divide_circuit(
|
| 336 |
+
self.circuit_to_compile.copy(), max_depth_per_block
|
| 337 |
+
)
|
| 338 |
+
|
| 339 |
+
logger.info(
|
| 340 |
+
f"Circuit was split into {len(all_subcircuits)} parts to compile sequentially"
|
| 341 |
+
)
|
| 342 |
+
|
| 343 |
+
last_compiled_subcircuit = None
|
| 344 |
+
individual_results = []
|
| 345 |
+
for subcircuit in all_subcircuits:
|
| 346 |
+
co.replace_inner_circuit(
|
| 347 |
+
self.full_circuit,
|
| 348 |
+
last_compiled_subcircuit,
|
| 349 |
+
self.variational_circuit_range(),
|
| 350 |
+
True,
|
| 351 |
+
{"backend": self.backend.simulator},
|
| 352 |
+
)
|
| 353 |
+
co.add_to_circuit(
|
| 354 |
+
self.full_circuit,
|
| 355 |
+
subcircuit,
|
| 356 |
+
self.variational_circuit_range()[1],
|
| 357 |
+
True,
|
| 358 |
+
{"backend": self.backend.simulator},
|
| 359 |
+
)
|
| 360 |
+
partial_recompilation_result = self.compile()
|
| 361 |
+
last_compiled_subcircuit = partial_recompilation_result.circuit
|
| 362 |
+
partial_recompilation_result.circuit = None
|
| 363 |
+
individual_results.append(partial_recompilation_result)
|
| 364 |
+
percentage = (
|
| 365 |
+
100 * (1 + all_subcircuits.index(subcircuit)) / len(all_subcircuits)
|
| 366 |
+
)
|
| 367 |
+
logger.info(f"Completed {percentage}% of recompilation")
|
| 368 |
+
|
| 369 |
+
end_time = timeit.default_timer()
|
| 370 |
+
|
| 371 |
+
result = CompileInPartsResult(
|
| 372 |
+
circuit=last_compiled_subcircuit,
|
| 373 |
+
overlap=co.calculate_overlap_between_circuits(
|
| 374 |
+
last_compiled_subcircuit,
|
| 375 |
+
self.circuit_to_compile,
|
| 376 |
+
self.initial_state_circuit,
|
| 377 |
+
self.qubit_subset_to_compile,
|
| 378 |
+
),
|
| 379 |
+
individual_results=individual_results,
|
| 380 |
+
time_taken=end_time - start_time,
|
| 381 |
+
)
|
| 382 |
+
|
| 383 |
+
return result
|
| 384 |
+
|
| 385 |
+
def get_compiled_circuit(self):
|
| 386 |
+
compiled_circuit = co.circuit_by_inverting_circuit(
|
| 387 |
+
co.extract_inner_circuit(
|
| 388 |
+
self.full_circuit, self.variational_circuit_range()
|
| 389 |
+
)
|
| 390 |
+
)
|
| 391 |
+
if self.starting_circuit is not None:
|
| 392 |
+
transpile_kwargs = (
|
| 393 |
+
{"backend": self.backend}
|
| 394 |
+
if (isinstance(self.backend, Backend))
|
| 395 |
+
else None
|
| 396 |
+
)
|
| 397 |
+
co.add_to_circuit(
|
| 398 |
+
compiled_circuit,
|
| 399 |
+
self.starting_circuit,
|
| 400 |
+
0,
|
| 401 |
+
transpile_before_adding=True,
|
| 402 |
+
transpile_kwargs=transpile_kwargs,
|
| 403 |
+
)
|
| 404 |
+
final_circuit = QuantumCircuit(
|
| 405 |
+
*self.circuit_to_compile.qregs, *self.circuit_to_compile.cregs
|
| 406 |
+
)
|
| 407 |
+
qubit_map = {
|
| 408 |
+
full_circ_index: subset_index
|
| 409 |
+
for subset_index, full_circ_index in enumerate(self.qubit_subset_to_compile)
|
| 410 |
+
}
|
| 411 |
+
co.add_to_circuit(final_circuit, compiled_circuit, qubit_subset=qubit_map)
|
| 412 |
+
|
| 413 |
+
# If self.target is a QuantumCircuit object, this ensures the quantum and classical registers of the compiled
|
| 414 |
+
# circuit are the same as those of the target. If self.target is an MPS, there were no registers in the first
|
| 415 |
+
# place, so this makes a QuantumCircuit with the default register names
|
| 416 |
+
if isinstance(self.target, QuantumCircuit):
|
| 417 |
+
final_circuit_original_regs = QuantumCircuit(
|
| 418 |
+
*self.target.qregs, *self.target.cregs
|
| 419 |
+
)
|
| 420 |
+
else:
|
| 421 |
+
final_circuit_original_regs = QuantumCircuit(
|
| 422 |
+
self.circuit_to_compile.num_qubits
|
| 423 |
+
)
|
| 424 |
+
|
| 425 |
+
final_circuit_original_regs.append(
|
| 426 |
+
final_circuit.to_instruction(), final_circuit_original_regs.qubits
|
| 427 |
+
)
|
| 428 |
+
circuit_no_classical_ops = co.unroll_to_basis_gates(final_circuit_original_regs)
|
| 429 |
+
if self.original_circuit_classical_ops is not None:
|
| 430 |
+
co.add_classical_operations(
|
| 431 |
+
circuit_no_classical_ops, self.original_circuit_classical_ops
|
| 432 |
+
)
|
| 433 |
+
return circuit_no_classical_ops
|
| 434 |
+
|
| 435 |
+
def _prepare_full_circuit(self):
|
| 436 |
+
"""Circuit is of form:
|
| 437 |
+
-|0>--|initial_state|--|circuit_to_compile
|
| 438 |
+
|--|variational_circuit|--|initial_state_inverse|--|(measure)|
|
| 439 |
+
With this circuit, the overlap between circuit_to_compile and
|
| 440 |
+
inverse of full_circuit
|
| 441 |
+
w.r.t initial_state is just the probability of resulting state
|
| 442 |
+
being in all zero |00...00> state
|
| 443 |
+
If self.general_initial_state is true, circuit takes a different
|
| 444 |
+
form described in the papers below.
|
| 445 |
+
(refer to arXiv:1811.03147, arXiv:1908.04416)
|
| 446 |
+
"""
|
| 447 |
+
total_qubits = (
|
| 448 |
+
2 * self.total_num_qubits
|
| 449 |
+
if self.general_initial_state
|
| 450 |
+
else self.total_num_qubits
|
| 451 |
+
)
|
| 452 |
+
qr = QuantumRegister(total_qubits)
|
| 453 |
+
qc = QuantumCircuit(qr)
|
| 454 |
+
|
| 455 |
+
# TODO update this to use new custom backend class
|
| 456 |
+
transpile_kwargs = (
|
| 457 |
+
{"backend": self.backend} if (isinstance(self.backend, Backend)) else None
|
| 458 |
+
)
|
| 459 |
+
|
| 460 |
+
if self.initial_state_circuit is not None:
|
| 461 |
+
co.add_to_circuit(
|
| 462 |
+
qc,
|
| 463 |
+
self.initial_state_circuit,
|
| 464 |
+
transpile_before_adding=True,
|
| 465 |
+
transpile_kwargs=transpile_kwargs,
|
| 466 |
+
)
|
| 467 |
+
elif self.general_initial_state:
|
| 468 |
+
for qubit in range(self.total_num_qubits):
|
| 469 |
+
qc.h(qubit)
|
| 470 |
+
qc.cx(qubit, qubit + self.total_num_qubits)
|
| 471 |
+
|
| 472 |
+
co.add_to_circuit(
|
| 473 |
+
qc,
|
| 474 |
+
self.circuit_to_compile,
|
| 475 |
+
transpile_before_adding=False,
|
| 476 |
+
qubit_subset=self.qubit_subset_to_compile,
|
| 477 |
+
)
|
| 478 |
+
|
| 479 |
+
lhs_gate_count = len(qc.data)
|
| 480 |
+
|
| 481 |
+
if self.initial_state_circuit is not None:
|
| 482 |
+
isc = co.unroll_to_basis_gates(self.initial_state_circuit)
|
| 483 |
+
co.remove_reset_gates(isc)
|
| 484 |
+
co.add_to_circuit(
|
| 485 |
+
qc,
|
| 486 |
+
isc.inverse(),
|
| 487 |
+
transpile_before_adding=True,
|
| 488 |
+
transpile_kwargs=transpile_kwargs,
|
| 489 |
+
)
|
| 490 |
+
if self.starting_circuit is not None:
|
| 491 |
+
co.add_to_circuit(
|
| 492 |
+
qc,
|
| 493 |
+
self.starting_circuit.inverse(),
|
| 494 |
+
transpile_before_adding=True,
|
| 495 |
+
transpile_kwargs=transpile_kwargs,
|
| 496 |
+
)
|
| 497 |
+
elif self.general_initial_state:
|
| 498 |
+
for qubit in range(self.total_num_qubits - 1, -1, -1):
|
| 499 |
+
qc.cx(qubit, qubit + self.total_num_qubits)
|
| 500 |
+
qc.h(qubit)
|
| 501 |
+
|
| 502 |
+
if self.backend == QASM_SIM:
|
| 503 |
+
if self.optimise_local_cost:
|
| 504 |
+
register_size = 2 if self.general_initial_state else 1
|
| 505 |
+
qc.add_register(ClassicalRegister(register_size, name="compiler_creg"))
|
| 506 |
+
else:
|
| 507 |
+
qc.add_register(ClassicalRegister(total_qubits, name="compiler_creg"))
|
| 508 |
+
[qc.measure(i, i) for i in range(total_qubits)]
|
| 509 |
+
|
| 510 |
+
rhs_gate_count = len(qc.data) - lhs_gate_count
|
| 511 |
+
|
| 512 |
+
return qc, lhs_gate_count, rhs_gate_count
|
| 513 |
+
|
| 514 |
+
def evaluate_cost(self):
|
| 515 |
+
"""
|
| 516 |
+
Run the full circuit and evaluate the overlap.
|
| 517 |
+
The cost function is the Loschmidt Echo Test as defined in arXiv:1908.04416.
|
| 518 |
+
"Global" and "local" cost functions refer to equations 9 and 11 respectively,
|
| 519 |
+
(also illustrated in Figure 2 (a) and (b) respectively)
|
| 520 |
+
:return: Cost (float)
|
| 521 |
+
"""
|
| 522 |
+
self.cost_evaluation_counter += 1
|
| 523 |
+
|
| 524 |
+
if self.optimise_local_cost:
|
| 525 |
+
return self.backend.evaluate_local_cost(self)
|
| 526 |
+
else:
|
| 527 |
+
return self.backend.evaluate_global_cost(self)
|
utils/adapt-aqc/adaptaqc/utils/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# (C) Copyright IBM 2025.
|
| 2 |
+
#
|
| 3 |
+
# This code is licensed under the Apache License, Version 2.0. You may
|
| 4 |
+
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
| 5 |
+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
| 6 |
+
#
|
| 7 |
+
# Any modifications or derivative works of this code must retain this
|
| 8 |
+
# copyright notice, and modified files need to carry a notice indicating
|
| 9 |
+
# that they have been altered from the originals.
|
| 10 |
+
|
| 11 |
+
from adaptaqc.utils.circuit_operations import *
|
utils/adapt-aqc/adaptaqc/utils/ansatzes.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# (C) Copyright IBM 2025.
|
| 2 |
+
#
|
| 3 |
+
# This code is licensed under the Apache License, Version 2.0. You may
|
| 4 |
+
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
| 5 |
+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
| 6 |
+
#
|
| 7 |
+
# Any modifications or derivative works of this code must retain this
|
| 8 |
+
# copyright notice, and modified files need to carry a notice indicating
|
| 9 |
+
# that they have been altered from the originals.
|
| 10 |
+
|
| 11 |
+
from qiskit import QuantumCircuit
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def u4():
|
| 15 |
+
"""
|
| 16 |
+
U(4) ansatz from Fig. 6 of
|
| 17 |
+
Vatan, Farrokh, and Colin Williams. "Optimal quantum circuits for general two-qubit gates."
|
| 18 |
+
Physical Review A 69.3 (2004): 032315.
|
| 19 |
+
"""
|
| 20 |
+
qc = QuantumCircuit(2)
|
| 21 |
+
qc.rz(0, 0)
|
| 22 |
+
qc.ry(0, 0)
|
| 23 |
+
qc.rz(0, 0)
|
| 24 |
+
qc.rz(0, 1)
|
| 25 |
+
qc.ry(0, 1)
|
| 26 |
+
qc.rz(0, 1)
|
| 27 |
+
qc.cx(1, 0)
|
| 28 |
+
qc.rz(0, 0)
|
| 29 |
+
qc.ry(0, 1)
|
| 30 |
+
qc.cx(0, 1)
|
| 31 |
+
qc.ry(0, 1)
|
| 32 |
+
qc.cx(1, 0)
|
| 33 |
+
qc.rz(0, 0)
|
| 34 |
+
qc.ry(0, 0)
|
| 35 |
+
qc.rz(0, 0)
|
| 36 |
+
qc.rz(0, 1)
|
| 37 |
+
qc.ry(0, 1)
|
| 38 |
+
qc.rz(0, 1)
|
| 39 |
+
return qc
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def thinly_dressed_cnot():
|
| 43 |
+
qc = QuantumCircuit(2)
|
| 44 |
+
qc.rx(0, 0)
|
| 45 |
+
qc.rx(0, 1)
|
| 46 |
+
qc.cx(0, 1)
|
| 47 |
+
qc.rx(0, 0)
|
| 48 |
+
qc.rx(0, 1)
|
| 49 |
+
return qc
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def fully_dressed_cnot():
|
| 53 |
+
qc = QuantumCircuit(2)
|
| 54 |
+
qc.rz(0, 0)
|
| 55 |
+
qc.ry(0, 0)
|
| 56 |
+
qc.rz(0, 0)
|
| 57 |
+
qc.rz(0, 1)
|
| 58 |
+
qc.ry(0, 1)
|
| 59 |
+
qc.rz(0, 1)
|
| 60 |
+
qc.cx(0, 1)
|
| 61 |
+
qc.rz(0, 0)
|
| 62 |
+
qc.ry(0, 0)
|
| 63 |
+
qc.rz(0, 0)
|
| 64 |
+
qc.rz(0, 1)
|
| 65 |
+
qc.ry(0, 1)
|
| 66 |
+
qc.rz(0, 1)
|
| 67 |
+
return qc
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def identity_resolvable():
|
| 71 |
+
qc = QuantumCircuit(2)
|
| 72 |
+
qc.rx(0, 0)
|
| 73 |
+
qc.rx(0, 1)
|
| 74 |
+
qc.cx(0, 1)
|
| 75 |
+
qc.rx(0, 0)
|
| 76 |
+
qc.rx(0, 1)
|
| 77 |
+
qc.cx(0, 1)
|
| 78 |
+
qc.rx(0, 0)
|
| 79 |
+
qc.rx(0, 1)
|
| 80 |
+
return qc
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def heisenberg():
|
| 84 |
+
"""
|
| 85 |
+
Based on fig 2. from N. Robertson et al. "Approximate Quantum Compiling for Quantum Simulation: A Tensor Network
|
| 86 |
+
based approach" arxiv:2301.08609, which gives circuit representing two site evolution operator
|
| 87 |
+
e^(iαXX + iβYY + iγZZ) corresponding to XYZ Heisenberg model with no field. Here, we additionally allow for the Rz
|
| 88 |
+
gates applied (at the end) to the first qubit and (at the start) to the second qubit to be trainable, to mimic
|
| 89 |
+
additional (learnable) evolution under an external field (effectively a first order trotter expansion).
|
| 90 |
+
"""
|
| 91 |
+
qc = QuantumCircuit(2)
|
| 92 |
+
qc.rz(0.0, 1)
|
| 93 |
+
qc.cx(1, 0)
|
| 94 |
+
qc.rz(0.0, 0)
|
| 95 |
+
qc.ry(0.0, 1)
|
| 96 |
+
qc.cx(0, 1)
|
| 97 |
+
qc.ry(0.0, 1)
|
| 98 |
+
qc.cx(1, 0)
|
| 99 |
+
qc.rz(0.0, 0)
|
| 100 |
+
return qc
|
utils/adapt-aqc/adaptaqc/utils/circuit_operations/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# (C) Copyright IBM 2025.
|
| 2 |
+
#
|
| 3 |
+
# This code is licensed under the Apache License, Version 2.0. You may
|
| 4 |
+
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
| 5 |
+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
| 6 |
+
#
|
| 7 |
+
# Any modifications or derivative works of this code must retain this
|
| 8 |
+
# copyright notice, and modified files need to carry a notice indicating
|
| 9 |
+
# that they have been altered from the originals.
|
| 10 |
+
|
| 11 |
+
from adaptaqc.utils.circuit_operations.circuit_operations_basic import *
|
| 12 |
+
from adaptaqc.utils.circuit_operations.circuit_operations_circuit_division import *
|
| 13 |
+
from adaptaqc.utils.circuit_operations.circuit_operations_full_circuit import *
|
| 14 |
+
from adaptaqc.utils.circuit_operations.circuit_operations_optimisation import *
|
| 15 |
+
from adaptaqc.utils.circuit_operations.circuit_operations_pauli_ops import *
|
| 16 |
+
from adaptaqc.utils.circuit_operations.circuit_operations_running import *
|
| 17 |
+
from adaptaqc.utils.circuit_operations.circuit_operations_variational import *
|
utils/adapt-aqc/adaptaqc/utils/circuit_operations/circuit_operations_basic.py
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# (C) Copyright IBM 2025.
|
| 2 |
+
#
|
| 3 |
+
# This code is licensed under the Apache License, Version 2.0. You may
|
| 4 |
+
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
| 5 |
+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
| 6 |
+
#
|
| 7 |
+
# Any modifications or derivative works of this code must retain this
|
| 8 |
+
# copyright notice, and modified files need to carry a notice indicating
|
| 9 |
+
# that they have been altered from the originals.
|
| 10 |
+
|
| 11 |
+
import random
|
| 12 |
+
|
| 13 |
+
import numpy as np
|
| 14 |
+
from qiskit import QuantumCircuit
|
| 15 |
+
from qiskit.circuit import Gate, CircuitInstruction
|
| 16 |
+
from qiskit.circuit.library import RXGate, RYGate, RZGate, CZGate, CXGate
|
| 17 |
+
from sympy import parse_expr
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def create_1q_gate(gate_name, angle):
|
| 21 |
+
"""
|
| 22 |
+
Create 1 qubit rotation gate with given name and angle
|
| 23 |
+
:param gate_name: Name of rotation gate ('rx','ry','rz')
|
| 24 |
+
:param angle: Angle of rotation
|
| 25 |
+
:return: New gate
|
| 26 |
+
"""
|
| 27 |
+
if gate_name == "rx":
|
| 28 |
+
return RXGate(angle, label="rx")
|
| 29 |
+
elif gate_name == "ry":
|
| 30 |
+
return RYGate(angle, label="ry")
|
| 31 |
+
elif gate_name == "rz":
|
| 32 |
+
return RZGate(angle, label="rz")
|
| 33 |
+
else:
|
| 34 |
+
raise ValueError(f"Unsupported gate {gate_name}")
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def create_2q_gate(gate_name):
|
| 38 |
+
"""
|
| 39 |
+
Create 2 qubit gate with given name
|
| 40 |
+
:param gate_name: Name of rotation gate ('cx','cz')
|
| 41 |
+
:return: New gate
|
| 42 |
+
"""
|
| 43 |
+
if gate_name == "cx":
|
| 44 |
+
return CXGate()
|
| 45 |
+
elif gate_name == "cz":
|
| 46 |
+
return CZGate()
|
| 47 |
+
else:
|
| 48 |
+
raise ValueError("Unsupported gate")
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def add_gate(
|
| 52 |
+
circuit: QuantumCircuit,
|
| 53 |
+
gate,
|
| 54 |
+
gate_index=None,
|
| 55 |
+
qubit_indexes=None,
|
| 56 |
+
clbit_indexes=None,
|
| 57 |
+
):
|
| 58 |
+
if gate_index is None:
|
| 59 |
+
gate_index = len(circuit.data)
|
| 60 |
+
qubits = (
|
| 61 |
+
[circuit.qubits[i] for i in qubit_indexes] if qubit_indexes is not None else []
|
| 62 |
+
)
|
| 63 |
+
clbits = (
|
| 64 |
+
[circuit.clbits[i] for i in clbit_indexes] if clbit_indexes is not None else []
|
| 65 |
+
)
|
| 66 |
+
circ_instr = CircuitInstruction(operation=gate, qubits=qubits, clbits=clbits)
|
| 67 |
+
circuit.data.insert(gate_index, circ_instr)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def replace_1q_gate(circuit, gate_index, gate_name, angle):
|
| 71 |
+
"""
|
| 72 |
+
Replace the gate at the specified index of circuit
|
| 73 |
+
:param circuit: QuantumCircuit
|
| 74 |
+
:param gate_index: The index of the gate that is to be replaced
|
| 75 |
+
:param gate_name: New gate name
|
| 76 |
+
:param angle: New gate angle
|
| 77 |
+
"""
|
| 78 |
+
if gate_name is None:
|
| 79 |
+
return
|
| 80 |
+
circ_instr = circuit.data[gate_index]
|
| 81 |
+
qargs = circ_instr.qubits
|
| 82 |
+
cargs = circ_instr.clbits
|
| 83 |
+
if "#" in gate_name:
|
| 84 |
+
circuit.data[gate_index] = CircuitInstruction(
|
| 85 |
+
operation=create_independent_parameterised_gate(
|
| 86 |
+
*gate_name.split("#"), angle
|
| 87 |
+
),
|
| 88 |
+
qubits=qargs,
|
| 89 |
+
clbits=cargs,
|
| 90 |
+
)
|
| 91 |
+
reevaluate_dependent_parameterised_gates(
|
| 92 |
+
circuit, calculate_independent_variable_values(circuit)
|
| 93 |
+
)
|
| 94 |
+
elif "@" in gate_name:
|
| 95 |
+
raise ValueError("Cant replace dependent parameterised gate")
|
| 96 |
+
else:
|
| 97 |
+
circuit.data[gate_index] = CircuitInstruction(
|
| 98 |
+
operation=create_1q_gate(gate_name, angle), qubits=qargs, clbits=cargs
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def replace_2q_gate(circuit, gate_index, control, target, gate_name="cx"):
|
| 103 |
+
"""
|
| 104 |
+
Replace the gate at the specified index of circuit
|
| 105 |
+
:param circuit: QuantumCircuit
|
| 106 |
+
:param gate_index: The index of the gate that is to be replaced
|
| 107 |
+
:param control: New gate control qubit
|
| 108 |
+
:param target: New gate target qubit
|
| 109 |
+
:param gate_name: New gate name
|
| 110 |
+
"""
|
| 111 |
+
circ_instr = circuit.data[gate_index]
|
| 112 |
+
old_qargs = circ_instr.qubits
|
| 113 |
+
cargs = circ_instr.clbits
|
| 114 |
+
|
| 115 |
+
qr = old_qargs[0]._register
|
| 116 |
+
new_qargs = [qr[control], qr[target]]
|
| 117 |
+
new_gate = create_2q_gate(gate_name)
|
| 118 |
+
circuit.data[gate_index] = CircuitInstruction(
|
| 119 |
+
operation=new_gate, qubits=new_qargs, clbits=cargs
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def is_supported_1q_gate(gate):
|
| 124 |
+
if not isinstance(gate, Gate):
|
| 125 |
+
return False
|
| 126 |
+
gate_name = gate.label if gate.label is not None else gate.name
|
| 127 |
+
|
| 128 |
+
if "@" in gate_name:
|
| 129 |
+
return False
|
| 130 |
+
if "#" in gate_name:
|
| 131 |
+
gate_name = gate_name.split("#")[0]
|
| 132 |
+
return gate_name in SUPPORTED_1Q_GATES
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def add_appropriate_gates(circuit, qubit, thinly_dressed, loc):
|
| 136 |
+
ry_gate = create_1q_gate("ry", 0)
|
| 137 |
+
rz_gate = create_1q_gate("rz", 0)
|
| 138 |
+
add_gate(circuit, rz_gate.copy(), loc, [qubit])
|
| 139 |
+
loc += 1
|
| 140 |
+
if not thinly_dressed:
|
| 141 |
+
add_gate(circuit, ry_gate.copy(), loc, [qubit])
|
| 142 |
+
loc += 1
|
| 143 |
+
add_gate(circuit, rz_gate.copy(), loc, [qubit])
|
| 144 |
+
loc += 1
|
| 145 |
+
return loc
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def add_dressed_cnot(
|
| 149 |
+
circuit: QuantumCircuit,
|
| 150 |
+
control,
|
| 151 |
+
target,
|
| 152 |
+
thinly_dressed=False,
|
| 153 |
+
gate_index=None,
|
| 154 |
+
v1=True,
|
| 155 |
+
v2=True,
|
| 156 |
+
v3=True,
|
| 157 |
+
v4=True,
|
| 158 |
+
):
|
| 159 |
+
"""
|
| 160 |
+
Add a dressed cnot gate (cx surrounded by 4 general-rotation(rzryrz
|
| 161 |
+
decomposition) gates)
|
| 162 |
+
:param circuit: QuantumCircuit
|
| 163 |
+
:param control: Control qubit
|
| 164 |
+
:param target: Target qubit
|
| 165 |
+
:param thinly_dressed: Whether only a single rz gate should be added
|
| 166 |
+
instead of the 3 gate rzryrz decomposition
|
| 167 |
+
:param gate_index: The location of the dressed CNOT gate in circuit.data
|
| 168 |
+
(gates are added to the end if None)
|
| 169 |
+
:param v1: Whether there should be rotation gates before control qubit
|
| 170 |
+
:param v2: Whether there should be rotation gates before target qubit
|
| 171 |
+
:param v3: Whether there should be rotation gates after control qubit
|
| 172 |
+
:param v4: Whether there should be rotation gates after target qubit
|
| 173 |
+
"""
|
| 174 |
+
if gate_index is None:
|
| 175 |
+
gate_index = len(circuit.data)
|
| 176 |
+
|
| 177 |
+
cx_gate = create_2q_gate("cx")
|
| 178 |
+
|
| 179 |
+
if v1:
|
| 180 |
+
gate_index = add_appropriate_gates(circuit, control, thinly_dressed, gate_index)
|
| 181 |
+
if v2:
|
| 182 |
+
gate_index = add_appropriate_gates(circuit, target, thinly_dressed, gate_index)
|
| 183 |
+
|
| 184 |
+
add_gate(circuit, cx_gate.copy(), gate_index, [control, target])
|
| 185 |
+
gate_index += 1
|
| 186 |
+
if v3:
|
| 187 |
+
gate_index = add_appropriate_gates(circuit, control, thinly_dressed, gate_index)
|
| 188 |
+
if v4:
|
| 189 |
+
add_appropriate_gates(circuit, target, thinly_dressed, gate_index)
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def random_1q_gate():
|
| 193 |
+
"""
|
| 194 |
+
Create rotation gate with random angle and axis randomly chosen from x,y,z
|
| 195 |
+
:return: New gate
|
| 196 |
+
"""
|
| 197 |
+
return create_1q_gate(
|
| 198 |
+
random.choice(SUPPORTED_1Q_GATES), random.uniform(-np.pi, np.pi)
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
SUPPORTED_1Q_GATES = ["rx", "ry", "rz"]
|
| 203 |
+
SUPPORTED_2Q_GATES = ["cx", "cz"]
|
| 204 |
+
BASIS_GATES = ["u3", "cx", "cz", "rx", "ry", "rz", "x", "y", "z", "XY", "ZZ", "h"]
|
| 205 |
+
DEFAULT_GATES = ["rz", "rx", "ry", "u1", "u2", "u3", "cx", "id", "measure", "reset"]
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def create_independent_parameterised_gate(gate_type, variable_name, angle=0):
|
| 209 |
+
gate = create_1q_gate(gate_type, angle)
|
| 210 |
+
gate.label = f"{gate.label}#{variable_name}"
|
| 211 |
+
return gate
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def create_dependent_parameterised_gate(gate_type, equation_string, angle=0):
|
| 215 |
+
gate = create_1q_gate(gate_type, angle)
|
| 216 |
+
gate.label = f"{gate.label}@{equation_string}"
|
| 217 |
+
return gate
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
def calculate_independent_variable_values(circuit: QuantumCircuit):
|
| 221 |
+
variable_dict = {}
|
| 222 |
+
for circ_instr in circuit.data:
|
| 223 |
+
gate = circ_instr.operation
|
| 224 |
+
if gate.label is not None and "#" in gate.label:
|
| 225 |
+
variable_name = gate.label.split("#")[1]
|
| 226 |
+
variable_value = gate.params[0]
|
| 227 |
+
variable_dict[variable_name] = variable_value
|
| 228 |
+
return variable_dict
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
def reevaluate_dependent_parameterised_gates(
|
| 232 |
+
circuit: QuantumCircuit, independent_variable_values
|
| 233 |
+
):
|
| 234 |
+
for i, circ_instr in enumerate(circuit.data):
|
| 235 |
+
gate = circ_instr.operation
|
| 236 |
+
if gate.label is not None and "@" in gate.label:
|
| 237 |
+
equation = gate.label.split("@")[1]
|
| 238 |
+
result = parse_expr(equation, independent_variable_values)
|
| 239 |
+
angle = float(result)
|
| 240 |
+
gate.params[0] = angle
|
| 241 |
+
circuit.data[i] = circ_instr
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def add_subscript_to_all_variables(circuit: QuantumCircuit, subscript_value):
|
| 245 |
+
substitution_dict = {}
|
| 246 |
+
for i, circ_instr in enumerate(circuit.data):
|
| 247 |
+
gate = circ_instr.operation
|
| 248 |
+
if gate.label is not None and "#" in gate.label:
|
| 249 |
+
gate_type, variable_name = gate.label.split("#")
|
| 250 |
+
gate.label = f"{gate_type}#{variable_name}_{subscript_value}"
|
| 251 |
+
circuit.data[i] = circ_instr
|
| 252 |
+
|
| 253 |
+
substitution_dict[variable_name] = f"{variable_name}_{subscript_value}"
|
| 254 |
+
|
| 255 |
+
for i, circ_instr in enumerate(circuit.data):
|
| 256 |
+
gate = circ_instr.operation
|
| 257 |
+
if gate.label is not None and "@" in gate.label:
|
| 258 |
+
gate_type, equation = gate.label.split("@")
|
| 259 |
+
for old_name, new_name in substitution_dict.items():
|
| 260 |
+
equation = equation.replace(old_name, new_name)
|
| 261 |
+
gate.label = f"{gate_type}@{equation}"
|
| 262 |
+
circuit.data[i] = circ_instr
|
utils/adapt-aqc/adaptaqc/utils/circuit_operations/circuit_operations_circuit_division.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# (C) Copyright IBM 2025.
|
| 2 |
+
#
|
| 3 |
+
# This code is licensed under the Apache License, Version 2.0. You may
|
| 4 |
+
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
| 5 |
+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
| 6 |
+
#
|
| 7 |
+
# Any modifications or derivative works of this code must retain this
|
| 8 |
+
# copyright notice, and modified files need to carry a notice indicating
|
| 9 |
+
# that they have been altered from the originals.
|
| 10 |
+
|
| 11 |
+
from qiskit import QuantumCircuit
|
| 12 |
+
from qiskit.circuit import Clbit, Instruction, Qubit
|
| 13 |
+
|
| 14 |
+
from adaptaqc.utils.circuit_operations.circuit_operations_full_circuit import (
|
| 15 |
+
unroll_to_basis_gates,
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def find_previous_gate_on_qubit(circuit, gate_index):
|
| 20 |
+
"""
|
| 21 |
+
Find the gate just before specified gate that acts on at least 1 same
|
| 22 |
+
qubit as the specified gate
|
| 23 |
+
:param circuit: QuantumCircuit
|
| 24 |
+
:param gate_index: The index of the specified gate
|
| 25 |
+
:return: (previous_gate_object, index) (or (None,None) if no such gate)
|
| 26 |
+
"""
|
| 27 |
+
# circuit.data has form list[CircuitInstruction], with:
|
| 28 |
+
# gate_object = CircuitInstruction.operation
|
| 29 |
+
# [(register, qubit)] = CircuitInstruction.qubits
|
| 30 |
+
# cargs = CircuitInstruction.clbits
|
| 31 |
+
required_qubits = set(circuit.data[gate_index].qubits)
|
| 32 |
+
index = gate_index - 1
|
| 33 |
+
while index >= 0:
|
| 34 |
+
circ_instr = circuit.data[index]
|
| 35 |
+
gate = circ_instr.operation
|
| 36 |
+
qargs = circ_instr.qubits
|
| 37 |
+
# If at least one of the qargs (register, qubit) of the gate is the
|
| 38 |
+
# same as the qargs of the specified circuit
|
| 39 |
+
if len(required_qubits & set(qargs)) > 0:
|
| 40 |
+
return gate, index
|
| 41 |
+
index -= 1
|
| 42 |
+
return None, None
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def index_of_bit_in_circuit(bit, circuit):
|
| 46 |
+
"""
|
| 47 |
+
Calculate the index of the qubit/clbit in the circuit.
|
| 48 |
+
Qubit/clbit index is relative to Quantum/ClassicalRegister
|
| 49 |
+
:param bit: Qubit or Clbit
|
| 50 |
+
:param circuit: QuantumCircuit
|
| 51 |
+
:return:
|
| 52 |
+
"""
|
| 53 |
+
if isinstance(bit, Qubit):
|
| 54 |
+
return circuit.qubits.index(bit)
|
| 55 |
+
elif isinstance(bit, Clbit):
|
| 56 |
+
return circuit.clbits.index(bit)
|
| 57 |
+
else:
|
| 58 |
+
raise TypeError(f"{bit} not a Qubit or Clbit")
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def calculate_next_gate_indexes(
|
| 62 |
+
current_next_gate_indexes, circuit, gate_qargs, gate_cargs
|
| 63 |
+
):
|
| 64 |
+
"""
|
| 65 |
+
Pre-emptively calculates the index at which gates yet to applied will
|
| 66 |
+
be placed in the circuit. For n > 1 bit gates, all bits involved in
|
| 67 |
+
that gate action will have the same index, calculated as 1 + the
|
| 68 |
+
largest position for the list of relevant bits.
|
| 69 |
+
:param circuit: QuantumCircuit
|
| 70 |
+
:param current_next_gate_indexes: Current next_gate_indexes
|
| 71 |
+
:param gate_qargs: Qubits the gate acts on
|
| 72 |
+
:param gate_cargs: Clbits the gate acts on
|
| 73 |
+
:return: New next_gate_indexes
|
| 74 |
+
"""
|
| 75 |
+
qubit_indexes = [index_of_bit_in_circuit(qubit, circuit) for qubit in gate_qargs]
|
| 76 |
+
clbit_indexes = [
|
| 77 |
+
len(circuit.qubits) + index_of_bit_in_circuit(clbit, circuit)
|
| 78 |
+
for clbit in gate_cargs
|
| 79 |
+
]
|
| 80 |
+
|
| 81 |
+
largest_index = max(
|
| 82 |
+
current_next_gate_indexes[i] for i in qubit_indexes + clbit_indexes
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
resulting_next_gate_indexes = list(current_next_gate_indexes)
|
| 86 |
+
for i in qubit_indexes + clbit_indexes:
|
| 87 |
+
resulting_next_gate_indexes[i] = largest_index + 1
|
| 88 |
+
|
| 89 |
+
return resulting_next_gate_indexes
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def vertically_divide_circuit(circuit, max_depth_per_division=10):
|
| 93 |
+
"""
|
| 94 |
+
----------|----|----|---|---------
|
| 95 |
+
----- ____|____|____|____|__ -----
|
| 96 |
+
-----| | | | | |-----
|
| 97 |
+
-----|____|____|____|____|__|-----
|
| 98 |
+
----------|----|----|---|---------
|
| 99 |
+
:param circuit: Circuit to divide (QuantumCircuit/Instruction)
|
| 100 |
+
:param max_depth_per_division: Upper limit of depth of each of the
|
| 101 |
+
subcircuits resulting from the division
|
| 102 |
+
:return List of subcircuits [QuantumCircuit]
|
| 103 |
+
"""
|
| 104 |
+
if isinstance(circuit, Instruction):
|
| 105 |
+
if circuit.num_clbits > 0:
|
| 106 |
+
remaining_circuit = QuantumCircuit(circuit.num_qubits, circuit.num_clbits)
|
| 107 |
+
else:
|
| 108 |
+
remaining_circuit = QuantumCircuit(circuit.num_qubits)
|
| 109 |
+
remaining_circuit.append(
|
| 110 |
+
circuit, remaining_circuit.qubits, remaining_circuit.clbits
|
| 111 |
+
)
|
| 112 |
+
else:
|
| 113 |
+
remaining_circuit = circuit.copy()
|
| 114 |
+
|
| 115 |
+
remaining_circuit = unroll_to_basis_gates(remaining_circuit)
|
| 116 |
+
all_subcircuits = []
|
| 117 |
+
while len(remaining_circuit) > 0:
|
| 118 |
+
subcircuit = QuantumCircuit(*remaining_circuit.qregs, *remaining_circuit.cregs)
|
| 119 |
+
gate_indexes_to_remove = []
|
| 120 |
+
next_gate_indexes = [0] * (
|
| 121 |
+
len(remaining_circuit.qubits) + len(remaining_circuit.clbits)
|
| 122 |
+
)
|
| 123 |
+
for i in range(len(remaining_circuit.data)):
|
| 124 |
+
circ_instr = remaining_circuit.data[i]
|
| 125 |
+
instr = circ_instr.operation
|
| 126 |
+
qargs = circ_instr.qubits
|
| 127 |
+
cargs = circ_instr.clbits
|
| 128 |
+
|
| 129 |
+
next_gate_indexes = calculate_next_gate_indexes(
|
| 130 |
+
next_gate_indexes, remaining_circuit, qargs, cargs
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
if max(next_gate_indexes) <= max_depth_per_division:
|
| 134 |
+
subcircuit.append(instr, qargs, cargs)
|
| 135 |
+
gate_indexes_to_remove.append(i)
|
| 136 |
+
elif min(next_gate_indexes) >= max_depth_per_division:
|
| 137 |
+
break
|
| 138 |
+
|
| 139 |
+
for j in reversed(gate_indexes_to_remove):
|
| 140 |
+
del remaining_circuit.data[j]
|
| 141 |
+
|
| 142 |
+
all_subcircuits.append(subcircuit)
|
| 143 |
+
|
| 144 |
+
return all_subcircuits
|
utils/adapt-aqc/adaptaqc/utils/circuit_operations/circuit_operations_full_circuit.py
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# (C) Copyright IBM 2025.
|
| 2 |
+
#
|
| 3 |
+
# This code is licensed under the Apache License, Version 2.0. You may
|
| 4 |
+
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
| 5 |
+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
| 6 |
+
#
|
| 7 |
+
# Any modifications or derivative works of this code must retain this
|
| 8 |
+
# copyright notice, and modified files need to carry a notice indicating
|
| 9 |
+
# that they have been altered from the originals.
|
| 10 |
+
|
| 11 |
+
import multiprocessing
|
| 12 |
+
import random
|
| 13 |
+
from typing import Union
|
| 14 |
+
|
| 15 |
+
import numpy as np
|
| 16 |
+
from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister
|
| 17 |
+
from qiskit import transpile as qiskit_transpile
|
| 18 |
+
from qiskit.circuit import Clbit, Gate, Instruction, Qubit, Reset, CircuitInstruction
|
| 19 |
+
from qiskit.quantum_info import random_statevector, Statevector
|
| 20 |
+
|
| 21 |
+
from adaptaqc.utils.circuit_operations import (
|
| 22 |
+
BASIS_GATES,
|
| 23 |
+
DEFAULT_GATES,
|
| 24 |
+
SUPPORTED_1Q_GATES,
|
| 25 |
+
SUPPORTED_2Q_GATES,
|
| 26 |
+
)
|
| 27 |
+
from adaptaqc.utils.circuit_operations.circuit_operations_basic import (
|
| 28 |
+
add_gate,
|
| 29 |
+
create_1q_gate,
|
| 30 |
+
create_2q_gate,
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def find_register(circuit, bit):
|
| 35 |
+
for reg in circuit.qregs + circuit.cregs:
|
| 36 |
+
if bit in reg:
|
| 37 |
+
return reg
|
| 38 |
+
return None
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def find_bit_index(reg, bit):
|
| 42 |
+
for i, reg_bit in enumerate(reg):
|
| 43 |
+
if bit == reg_bit:
|
| 44 |
+
return i
|
| 45 |
+
return None
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def create_random_circuit(
|
| 49 |
+
num_qubits, depth=5, one_qubit_gates=None, two_qubit_gates=None
|
| 50 |
+
):
|
| 51 |
+
qc = QuantumCircuit(num_qubits)
|
| 52 |
+
if one_qubit_gates is None:
|
| 53 |
+
one_qubit_gates = SUPPORTED_1Q_GATES
|
| 54 |
+
if two_qubit_gates is None:
|
| 55 |
+
two_qubit_gates = SUPPORTED_2Q_GATES
|
| 56 |
+
rs = np.random.RandomState(multiprocessing.current_process().pid)
|
| 57 |
+
while qc.depth() < depth:
|
| 58 |
+
random_gate = rs.choice(one_qubit_gates + two_qubit_gates)
|
| 59 |
+
if random_gate in one_qubit_gates:
|
| 60 |
+
qubits = rs.choice(list(range(num_qubits)), [1])
|
| 61 |
+
add_gate(
|
| 62 |
+
qc,
|
| 63 |
+
create_1q_gate(random_gate, random.uniform(-np.pi, np.pi)),
|
| 64 |
+
qubit_indexes=qubits,
|
| 65 |
+
)
|
| 66 |
+
elif random_gate in two_qubit_gates:
|
| 67 |
+
qubits = rs.choice(list(range(num_qubits)), [2], replace=False)
|
| 68 |
+
add_gate(qc, create_2q_gate(random_gate), qubit_indexes=qubits)
|
| 69 |
+
return qc
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def are_circuits_identical(
|
| 73 |
+
qc1: QuantumCircuit, qc2: QuantumCircuit, match_labels=False, match_registers=False
|
| 74 |
+
):
|
| 75 |
+
if len(qc1.data) != len(qc2.data):
|
| 76 |
+
return False
|
| 77 |
+
for (gate1, qargs1, cargs1), (gate2, qargs2, cargs2) in zip(qc1.data, qc2.data):
|
| 78 |
+
# Checks that gates match
|
| 79 |
+
gate1_name = gate1.label if gate1.label is not None else gate1.name
|
| 80 |
+
gate2_name = gate2.label if gate2.label is not None else gate2.name
|
| 81 |
+
|
| 82 |
+
if gate1_name != gate2_name:
|
| 83 |
+
return False
|
| 84 |
+
|
| 85 |
+
if len(gate1.params) != len(gate2.params) and gate1_name in ["rx", "ry", "rz"]:
|
| 86 |
+
gate1_params = [gate1.params[0]]
|
| 87 |
+
gate2_params = [gate2.params[0]]
|
| 88 |
+
else:
|
| 89 |
+
gate1_params = gate1.params
|
| 90 |
+
gate2_params = gate2.params
|
| 91 |
+
|
| 92 |
+
if gate1_params != gate2_params:
|
| 93 |
+
return False
|
| 94 |
+
|
| 95 |
+
if match_labels and gate1.label != gate2.label:
|
| 96 |
+
return False
|
| 97 |
+
|
| 98 |
+
# Check that qargs match
|
| 99 |
+
for qubit1, qubit2 in zip(qargs1, qargs2):
|
| 100 |
+
if match_registers and qubit1 != qubit2:
|
| 101 |
+
return False
|
| 102 |
+
if qubit1._index != qubit2._index:
|
| 103 |
+
return False
|
| 104 |
+
|
| 105 |
+
# Check that cargs match
|
| 106 |
+
for clbit1, clbit2 in zip(cargs1, cargs2):
|
| 107 |
+
if match_registers and clbit1 != clbit2:
|
| 108 |
+
return False
|
| 109 |
+
if clbit1._index != clbit2._index:
|
| 110 |
+
return False
|
| 111 |
+
return True
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def change_circuit_register(
|
| 115 |
+
circuit: QuantumCircuit,
|
| 116 |
+
new_circuit_reg: Union[QuantumRegister, ClassicalRegister],
|
| 117 |
+
bit_mapping=None,
|
| 118 |
+
):
|
| 119 |
+
"""
|
| 120 |
+
Only supports 1 quantum/classical register circuits
|
| 121 |
+
:param circuit:
|
| 122 |
+
:param new_circuit_reg:
|
| 123 |
+
:param bit_mapping:
|
| 124 |
+
"""
|
| 125 |
+
change_quantum = isinstance(new_circuit_reg, QuantumRegister)
|
| 126 |
+
if change_quantum:
|
| 127 |
+
old_reg = circuit.qregs[0]
|
| 128 |
+
# If qregs used in multiple circuits (e.g. if circuit is copied)
|
| 129 |
+
# then don't affect other circuits
|
| 130 |
+
circuit.qregs = circuit.qregs.copy()
|
| 131 |
+
num_bits = circuit.num_qubits
|
| 132 |
+
else:
|
| 133 |
+
old_reg = circuit.cregs[0]
|
| 134 |
+
# If cregs used in multiple circuits (e.g. if circuit is copied)
|
| 135 |
+
# then don't affect other circuits
|
| 136 |
+
circuit.cregs = circuit.cregs.copy()
|
| 137 |
+
num_bits = circuit.num_clbits
|
| 138 |
+
bit_mapping = {} if bit_mapping is None else bit_mapping
|
| 139 |
+
for bit in range(num_bits):
|
| 140 |
+
if bit not in bit_mapping:
|
| 141 |
+
bit_mapping[bit] = bit
|
| 142 |
+
|
| 143 |
+
# Add new register to circuit if necessary
|
| 144 |
+
if new_circuit_reg not in circuit.qregs + circuit.cregs:
|
| 145 |
+
if change_quantum:
|
| 146 |
+
circuit.qregs = []
|
| 147 |
+
circuit.add_register(new_circuit_reg)
|
| 148 |
+
else:
|
| 149 |
+
circuit.cregs = []
|
| 150 |
+
circuit.add_register(new_circuit_reg)
|
| 151 |
+
|
| 152 |
+
for index, circ_instr in enumerate(circuit.data):
|
| 153 |
+
if change_quantum:
|
| 154 |
+
new_qargs = [
|
| 155 |
+
Qubit(new_circuit_reg, bit_mapping[find_bit_index(old_reg, qubit)])
|
| 156 |
+
for qubit in circ_instr.qubits
|
| 157 |
+
]
|
| 158 |
+
circuit.data[index] = CircuitInstruction(
|
| 159 |
+
operation=circ_instr.operation,
|
| 160 |
+
qubits=new_qargs,
|
| 161 |
+
clbits=circ_instr.clbits,
|
| 162 |
+
)
|
| 163 |
+
else:
|
| 164 |
+
new_cargs = [
|
| 165 |
+
Clbit(new_circuit_reg, bit_mapping[find_bit_index(old_reg, clbit)])
|
| 166 |
+
for clbit in circ_instr.clbits
|
| 167 |
+
]
|
| 168 |
+
circuit.data[index] = CircuitInstruction(
|
| 169 |
+
operation=circ_instr.operation,
|
| 170 |
+
qubits=circ_instr.qubits,
|
| 171 |
+
clbits=new_cargs,
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def add_to_circuit(
|
| 176 |
+
original_circuit: QuantumCircuit,
|
| 177 |
+
circuit_to_be_added: QuantumCircuit,
|
| 178 |
+
location=None,
|
| 179 |
+
transpile_before_adding=False,
|
| 180 |
+
transpile_kwargs=None,
|
| 181 |
+
qubit_subset=None,
|
| 182 |
+
clbit_subset=None,
|
| 183 |
+
):
|
| 184 |
+
"""
|
| 185 |
+
Only supports 1 quantum register circuits
|
| 186 |
+
:param original_circuit:
|
| 187 |
+
:param circuit_to_be_added:
|
| 188 |
+
:param location:
|
| 189 |
+
:param transpile_before_adding:
|
| 190 |
+
:param transpile_kwargs:
|
| 191 |
+
:param qubit_subset:
|
| 192 |
+
:param clbit_subset:
|
| 193 |
+
"""
|
| 194 |
+
circuit_to_be_added_copy = circuit_to_be_added.copy()
|
| 195 |
+
if location is None:
|
| 196 |
+
location = len(original_circuit.data)
|
| 197 |
+
if transpile_before_adding:
|
| 198 |
+
circuit_to_be_added_copy = unroll_to_basis_gates(
|
| 199 |
+
circuit_to_be_added_copy, DEFAULT_GATES
|
| 200 |
+
)
|
| 201 |
+
if transpile_kwargs is not None:
|
| 202 |
+
circuit_to_be_added_copy = transpile(
|
| 203 |
+
circuit_to_be_added_copy, **transpile_kwargs
|
| 204 |
+
)
|
| 205 |
+
qubit_mapping = None
|
| 206 |
+
if qubit_subset is not None:
|
| 207 |
+
qubit_mapping = (
|
| 208 |
+
{index: value for index, value in enumerate(qubit_subset)}
|
| 209 |
+
if isinstance(qubit_subset, list)
|
| 210 |
+
else qubit_subset
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
clbit_mapping = None
|
| 214 |
+
if clbit_subset is not None:
|
| 215 |
+
clbit_mapping = {index: value for index, value in enumerate(clbit_subset)}
|
| 216 |
+
|
| 217 |
+
# Change quantum register
|
| 218 |
+
change_circuit_register(
|
| 219 |
+
circuit_to_be_added_copy,
|
| 220 |
+
find_register(original_circuit, original_circuit.qubits[0]),
|
| 221 |
+
qubit_mapping,
|
| 222 |
+
)
|
| 223 |
+
|
| 224 |
+
# Change classical register if present
|
| 225 |
+
if len(circuit_to_be_added_copy.clbits) > 0 and len(original_circuit.clbits) > 0:
|
| 226 |
+
change_circuit_register(
|
| 227 |
+
circuit_to_be_added_copy,
|
| 228 |
+
find_register(original_circuit, original_circuit.clbits[0]),
|
| 229 |
+
clbit_mapping,
|
| 230 |
+
)
|
| 231 |
+
|
| 232 |
+
for gate in circuit_to_be_added_copy:
|
| 233 |
+
original_circuit.data.insert(location, gate)
|
| 234 |
+
location += 1
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
def remove_inner_circuit(circuit: QuantumCircuit, gate_range_to_remove):
|
| 238 |
+
for index in list(range(*gate_range_to_remove))[::-1]:
|
| 239 |
+
del circuit.data[index]
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def extract_inner_circuit(circuit: QuantumCircuit, gate_range):
|
| 243 |
+
inner_circuit = QuantumCircuit()
|
| 244 |
+
[inner_circuit.add_register(qreg) for qreg in circuit.qregs]
|
| 245 |
+
[inner_circuit.add_register(creg) for creg in circuit.cregs]
|
| 246 |
+
for gate_index in range(*gate_range):
|
| 247 |
+
circ_instr = circuit.data[gate_index]
|
| 248 |
+
inner_circuit.data.append(circ_instr)
|
| 249 |
+
return inner_circuit
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
def replace_inner_circuit(
|
| 253 |
+
circuit: QuantumCircuit,
|
| 254 |
+
inner_circuit_replacement,
|
| 255 |
+
gate_range,
|
| 256 |
+
transpile_before_adding=False,
|
| 257 |
+
transpile_kwargs=None,
|
| 258 |
+
):
|
| 259 |
+
remove_inner_circuit(circuit, gate_range)
|
| 260 |
+
if (
|
| 261 |
+
inner_circuit_replacement is not None
|
| 262 |
+
and len(inner_circuit_replacement.data) > 0
|
| 263 |
+
):
|
| 264 |
+
add_to_circuit(
|
| 265 |
+
circuit,
|
| 266 |
+
inner_circuit_replacement,
|
| 267 |
+
gate_range[0],
|
| 268 |
+
transpile_before_adding=transpile_before_adding,
|
| 269 |
+
transpile_kwargs=transpile_kwargs,
|
| 270 |
+
)
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
def find_num_gates(
|
| 274 |
+
circuit, transpile_before_counting=False, transpile_kwargs=None, gate_range=None
|
| 275 |
+
):
|
| 276 |
+
"""
|
| 277 |
+
Find the number of 2 qubit and 1 qubit (non classical) gates in circuit
|
| 278 |
+
:param circuit: QuantumCircuit
|
| 279 |
+
:param transpile_before_counting: Whether circuit should be transpiled
|
| 280 |
+
before counting
|
| 281 |
+
:param transpile_kwargs: transpile kwargs (e.g {'backend':backend})
|
| 282 |
+
:param gate_range: The range of gates to include in search space (full
|
| 283 |
+
circuit if None)
|
| 284 |
+
:return: (num_2q_gates, num_1q_gates)
|
| 285 |
+
"""
|
| 286 |
+
if circuit is None:
|
| 287 |
+
return 0, 0
|
| 288 |
+
if transpile_before_counting:
|
| 289 |
+
if transpile_kwargs is None:
|
| 290 |
+
circuit = unroll_to_basis_gates(circuit)
|
| 291 |
+
else:
|
| 292 |
+
circuit = transpile(circuit, **transpile_kwargs)
|
| 293 |
+
if gate_range is None:
|
| 294 |
+
gate_range = (0, len(circuit.data))
|
| 295 |
+
num_2q_gates = 0
|
| 296 |
+
num_1q_gates = 0
|
| 297 |
+
for gate_index in range(*gate_range):
|
| 298 |
+
if (
|
| 299 |
+
len(circuit.data[gate_index].qubits) == 1
|
| 300 |
+
and len(circuit.data[gate_index].clbits) == 0
|
| 301 |
+
):
|
| 302 |
+
num_1q_gates += 1
|
| 303 |
+
elif (
|
| 304 |
+
len(circuit.data[gate_index].qubits) == 2
|
| 305 |
+
and len(circuit.data[gate_index].clbits) == 0
|
| 306 |
+
):
|
| 307 |
+
num_2q_gates += 1
|
| 308 |
+
return num_2q_gates, num_1q_gates
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
def transpile(circuit, **transpile_kwargs):
|
| 312 |
+
if transpile_kwargs is None:
|
| 313 |
+
transpile_kwargs = {}
|
| 314 |
+
|
| 315 |
+
return qiskit_transpile(circuit, **transpile_kwargs)
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
def unroll_to_basis_gates(circuit, basis_gates=None):
|
| 319 |
+
"""
|
| 320 |
+
Create circuit by unrolling given circuit to basis_gates
|
| 321 |
+
:param circuit: Circuit to unroll
|
| 322 |
+
:param basis_gates: Basis gate set to unroll to (BASIS_GATES by default)
|
| 323 |
+
:return: Transpiled circuit
|
| 324 |
+
"""
|
| 325 |
+
basis_gates = basis_gates if basis_gates is not None else BASIS_GATES
|
| 326 |
+
return transpile(circuit, basis_gates=basis_gates, optimization_level=0)
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
def append_to_instruction(main_ins, ins_to_append):
|
| 330 |
+
qc = QuantumCircuit(main_ins.num_qubits)
|
| 331 |
+
if main_ins.definition is not None and len(main_ins.definition) > 0:
|
| 332 |
+
qc.append(main_ins, qc.qubits)
|
| 333 |
+
if ins_to_append.definition is not None and len(ins_to_append.definition) > 0:
|
| 334 |
+
qc.append(ins_to_append, qc.qubits)
|
| 335 |
+
return qc.to_instruction()
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
def remove_classical_operations(circuit: QuantumCircuit):
|
| 339 |
+
gates_and_locations = []
|
| 340 |
+
for index, circ_instr in list(enumerate(circuit.data))[::-1]:
|
| 341 |
+
if len(circ_instr.clbits) > 0:
|
| 342 |
+
gates_and_locations.append((index, circ_instr))
|
| 343 |
+
del circuit.data[index]
|
| 344 |
+
return gates_and_locations[::-1]
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
def add_classical_operations(circuit: QuantumCircuit, gates_and_locations):
|
| 348 |
+
for index, circ_instr in gates_and_locations:
|
| 349 |
+
circuit.data.insert(index, circ_instr)
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
def make_quantum_only_circuit(circuit: QuantumCircuit):
|
| 353 |
+
new_qc = QuantumCircuit(*circuit.qregs)
|
| 354 |
+
no_classical_circuit = circuit.copy()
|
| 355 |
+
remove_classical_operations(no_classical_circuit)
|
| 356 |
+
for i in no_classical_circuit.data:
|
| 357 |
+
new_qc.data.append(i)
|
| 358 |
+
# remove_classical_operations(new_qc)
|
| 359 |
+
# new_qc.cregs = []
|
| 360 |
+
# new_qc.clbits = []
|
| 361 |
+
return new_qc
|
| 362 |
+
|
| 363 |
+
|
| 364 |
+
def circuit_by_inverting_circuit(circuit: QuantumCircuit):
|
| 365 |
+
new_circuit = QuantumCircuit(*circuit.qregs, *circuit.cregs)
|
| 366 |
+
|
| 367 |
+
for circ_instr in circuit.data[::-1]:
|
| 368 |
+
gate = circ_instr.operation
|
| 369 |
+
if not isinstance(gate, Gate):
|
| 370 |
+
new_circuit.data.append(circ_instr)
|
| 371 |
+
continue
|
| 372 |
+
if gate.label not in ["rx", "ry", "rz"]:
|
| 373 |
+
inverted_gate = gate.inverse().to_mutable()
|
| 374 |
+
else:
|
| 375 |
+
inverted_gate = gate.copy()
|
| 376 |
+
inverted_gate.params[0] *= -1
|
| 377 |
+
inverted_gate.label = gate.label
|
| 378 |
+
inverted_circ_instr = CircuitInstruction(
|
| 379 |
+
operation=inverted_gate, qubits=circ_instr.qubits, clbits=circ_instr.clbits
|
| 380 |
+
)
|
| 381 |
+
new_circuit.data.append(inverted_circ_instr)
|
| 382 |
+
return new_circuit
|
| 383 |
+
|
| 384 |
+
|
| 385 |
+
def initial_state_to_circuit(initial_state):
|
| 386 |
+
"""
|
| 387 |
+
Convert to QuantumCircuit
|
| 388 |
+
:param initial_state: Can either be a circuit (
|
| 389 |
+
QuantumCircuit/Instruction) or vector (list/np.ndarray) or None
|
| 390 |
+
:return: QuantumCircuit or None
|
| 391 |
+
"""
|
| 392 |
+
if initial_state is None:
|
| 393 |
+
return None
|
| 394 |
+
elif isinstance(initial_state, (list, np.ndarray)):
|
| 395 |
+
num_qubits = int(np.log2(len(initial_state)))
|
| 396 |
+
qc = QuantumCircuit(num_qubits)
|
| 397 |
+
qc.initialize(initial_state, qc.qubits)
|
| 398 |
+
# Unrolling will remove 'reset' gates from circuit
|
| 399 |
+
qc = unroll_to_basis_gates(qc)
|
| 400 |
+
remove_reset_gates(qc)
|
| 401 |
+
return qc
|
| 402 |
+
elif isinstance(initial_state, Instruction):
|
| 403 |
+
num_qubits = initial_state.num_qubits
|
| 404 |
+
qc = QuantumCircuit(num_qubits)
|
| 405 |
+
qc.append(initial_state, qc.qubits)
|
| 406 |
+
return qc
|
| 407 |
+
elif isinstance(initial_state, QuantumCircuit):
|
| 408 |
+
return initial_state.copy()
|
| 409 |
+
else:
|
| 410 |
+
raise TypeError("Invalid type of initial_state provided")
|
| 411 |
+
|
| 412 |
+
|
| 413 |
+
def calculate_overlap_between_circuits(
|
| 414 |
+
circuit1, circuit2, initial_state=None, qubit_subset=None
|
| 415 |
+
):
|
| 416 |
+
initial_state_circuit = initial_state_to_circuit(initial_state)
|
| 417 |
+
if initial_state_circuit is None:
|
| 418 |
+
total_num_qubits = circuit1.num_qubits
|
| 419 |
+
else:
|
| 420 |
+
total_num_qubits = initial_state_circuit.num_qubits
|
| 421 |
+
|
| 422 |
+
qubit_subset_to_compile = (
|
| 423 |
+
qubit_subset if qubit_subset else list(range(total_num_qubits))
|
| 424 |
+
)
|
| 425 |
+
qr1 = QuantumRegister(total_num_qubits)
|
| 426 |
+
qr2 = QuantumRegister(total_num_qubits)
|
| 427 |
+
qc1 = QuantumCircuit(qr1)
|
| 428 |
+
qc2 = QuantumCircuit(qr2)
|
| 429 |
+
|
| 430 |
+
if initial_state_circuit is not None:
|
| 431 |
+
add_to_circuit(qc1, initial_state_circuit)
|
| 432 |
+
add_to_circuit(qc2, initial_state_circuit)
|
| 433 |
+
qc1.append(circuit1, [qr1[i] for i in qubit_subset_to_compile])
|
| 434 |
+
qc2.append(circuit2, [qr2[i] for i in qubit_subset_to_compile])
|
| 435 |
+
|
| 436 |
+
sv1 = Statevector(qc1)
|
| 437 |
+
sv2 = Statevector(qc2)
|
| 438 |
+
return np.absolute(np.vdot(sv1, sv2)) ** 2
|
| 439 |
+
|
| 440 |
+
|
| 441 |
+
def create_random_initial_state_circuit(
|
| 442 |
+
num_qubits, return_statevector=False, seed=None
|
| 443 |
+
):
|
| 444 |
+
seed = seed() if None else seed
|
| 445 |
+
rand_state = random_statevector(2**num_qubits, seed).data
|
| 446 |
+
qc = QuantumCircuit(num_qubits)
|
| 447 |
+
qc.initialize(rand_state, qc.qubits)
|
| 448 |
+
qc = unroll_to_basis_gates(qc)
|
| 449 |
+
|
| 450 |
+
# Delete reset gates
|
| 451 |
+
for i in range(len(qc.data) - 1, -1, -1):
|
| 452 |
+
gate = qc.data[i].operation
|
| 453 |
+
if isinstance(gate, Reset):
|
| 454 |
+
del qc.data[i]
|
| 455 |
+
|
| 456 |
+
if return_statevector:
|
| 457 |
+
return qc, rand_state
|
| 458 |
+
else:
|
| 459 |
+
return qc
|
| 460 |
+
|
| 461 |
+
|
| 462 |
+
def remove_reset_gates(circuit: QuantumCircuit):
|
| 463 |
+
for i, circ_instr in list(enumerate(circuit.data))[::-1]:
|
| 464 |
+
if isinstance(circ_instr.operation, Reset):
|
| 465 |
+
del circuit.data[i]
|
utils/adapt-aqc/adaptaqc/utils/circuit_operations/circuit_operations_optimisation.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# (C) Copyright IBM 2025.
|
| 2 |
+
#
|
| 3 |
+
# This code is licensed under the Apache License, Version 2.0. You may
|
| 4 |
+
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
| 5 |
+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
| 6 |
+
#
|
| 7 |
+
# Any modifications or derivative works of this code must retain this
|
| 8 |
+
# copyright notice, and modified files need to carry a notice indicating
|
| 9 |
+
# that they have been altered from the originals.
|
| 10 |
+
|
| 11 |
+
import numpy as np
|
| 12 |
+
from qiskit import QuantumCircuit
|
| 13 |
+
from qiskit.compiler import transpile
|
| 14 |
+
from qiskit.synthesis import OneQubitEulerDecomposer
|
| 15 |
+
|
| 16 |
+
from adaptaqc.utils.circuit_operations.circuit_operations_basic import (
|
| 17 |
+
is_supported_1q_gate,
|
| 18 |
+
replace_1q_gate,
|
| 19 |
+
)
|
| 20 |
+
from adaptaqc.utils.circuit_operations.circuit_operations_circuit_division import (
|
| 21 |
+
find_previous_gate_on_qubit,
|
| 22 |
+
)
|
| 23 |
+
from adaptaqc.utils.constants import (
|
| 24 |
+
get_initial_layout,
|
| 25 |
+
convert_cmap_to_qiskit_format,
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
MINIMUM_ROTATION_ANGLE = 1e-3
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def remove_unnecessary_gates_from_circuit(
|
| 32 |
+
circuit: QuantumCircuit,
|
| 33 |
+
remove_zero_gates=True,
|
| 34 |
+
remove_small_gates=False,
|
| 35 |
+
gate_range=None,
|
| 36 |
+
):
|
| 37 |
+
"""
|
| 38 |
+
Remove unnecessary gates from circuit by merging adjacent gates of same
|
| 39 |
+
kind, converting 3+ consecutive single
|
| 40 |
+
qubit gates on a single qubit to an rzryrz decomposition, removing
|
| 41 |
+
similar consecutive cx, cz gates.
|
| 42 |
+
:param circuit: Circuit from which gates are to be removed
|
| 43 |
+
:param remove_zero_gates: If true, single qubit gates with 0 angle will
|
| 44 |
+
be removed
|
| 45 |
+
:param remove_small_gates: If true, single qubit gates with angle less
|
| 46 |
+
than MINIMUM_ROTATION_ANGLE will be removed
|
| 47 |
+
:param gate_range: If provided, only gates in that range relative to
|
| 48 |
+
circuit.data will be modified
|
| 49 |
+
"""
|
| 50 |
+
if gate_range is None:
|
| 51 |
+
gate_range = [0, len(circuit.data)]
|
| 52 |
+
else:
|
| 53 |
+
gate_range = list(gate_range)
|
| 54 |
+
|
| 55 |
+
last_circuit_length = len(circuit.data)
|
| 56 |
+
i = 0
|
| 57 |
+
while True:
|
| 58 |
+
if i == 0:
|
| 59 |
+
remove_unnecessary_1q_gates_from_circuit(
|
| 60 |
+
circuit, remove_zero_gates, remove_small_gates, gate_range
|
| 61 |
+
)
|
| 62 |
+
i = 1
|
| 63 |
+
else:
|
| 64 |
+
remove_unnecessary_2q_gates_from_circuit(circuit, gate_range)
|
| 65 |
+
i = 0
|
| 66 |
+
new_circuit_length = len(circuit.data)
|
| 67 |
+
if new_circuit_length != last_circuit_length:
|
| 68 |
+
# Update the gate range maximum to account for the shortened
|
| 69 |
+
# circuit
|
| 70 |
+
gate_range[1] -= last_circuit_length - new_circuit_length
|
| 71 |
+
last_circuit_length = new_circuit_length
|
| 72 |
+
elif i == 0:
|
| 73 |
+
return
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def remove_unnecessary_1q_gates_from_circuit(
|
| 77 |
+
circuit,
|
| 78 |
+
remove_zero_gates=True,
|
| 79 |
+
remove_small_gates=False,
|
| 80 |
+
gate_range=None,
|
| 81 |
+
min_rotation_angle=MINIMUM_ROTATION_ANGLE,
|
| 82 |
+
):
|
| 83 |
+
"""
|
| 84 |
+
Remove unnecessary 1-qubit gates from circuit by converting 3+
|
| 85 |
+
consecutive single qubit gates on a single qubit
|
| 86 |
+
to an rzryrz decomposition
|
| 87 |
+
:param circuit: Circuit from which gates are to be removed
|
| 88 |
+
:param remove_zero_gates: If true, single qubit gates with 0 angle will
|
| 89 |
+
be removed
|
| 90 |
+
:param remove_small_gates: If true, single qubit gates with angle less
|
| 91 |
+
than MINIMUM_ROTATION_ANGLE will be removed
|
| 92 |
+
:param gate_range: If provided, only gates in that range relative to
|
| 93 |
+
circuit.data will be modified
|
| 94 |
+
(lower index is inclusive and upper index is exclusive)
|
| 95 |
+
:param min_rotation_angle: If remove_small_gates, rotation gates
|
| 96 |
+
with angles smaller than min_rotation_angle will be removed
|
| 97 |
+
"""
|
| 98 |
+
if gate_range is None:
|
| 99 |
+
gate_range = (0, len(circuit.data))
|
| 100 |
+
|
| 101 |
+
indexes_to_remove = []
|
| 102 |
+
indexes_dealt_with = []
|
| 103 |
+
|
| 104 |
+
# Reverse iterate over all gates
|
| 105 |
+
for gate_index in range(gate_range[1] - 1, gate_range[0] - 1, -1):
|
| 106 |
+
gate = circuit.data[gate_index].operation
|
| 107 |
+
if (
|
| 108 |
+
gate_index in indexes_to_remove
|
| 109 |
+
or gate_index in indexes_dealt_with
|
| 110 |
+
or not is_supported_1q_gate(gate)
|
| 111 |
+
):
|
| 112 |
+
continue
|
| 113 |
+
remove_because_zero = remove_zero_gates and gate.params[0] == 0
|
| 114 |
+
remove_because_small = (
|
| 115 |
+
remove_small_gates and np.absolute(gate.params[0]) < min_rotation_angle
|
| 116 |
+
)
|
| 117 |
+
if remove_because_zero or remove_because_small:
|
| 118 |
+
indexes_to_remove += [gate_index]
|
| 119 |
+
continue
|
| 120 |
+
|
| 121 |
+
# Any single qubit operation can be reduced to phase * Rz(phi) * Ry(
|
| 122 |
+
# theta) * Rz(lambda)
|
| 123 |
+
# RXGate, RYGate, RZGate do not implement to_matrix() but their
|
| 124 |
+
# definitions (U3Gate or U1Gate) do
|
| 125 |
+
matrix = circuit.data[gate_index].operation.to_matrix()
|
| 126 |
+
prev_gate_indexes = [gate_index]
|
| 127 |
+
prev_gate, prev_gate_index = find_previous_gate_on_qubit(circuit, gate_index)
|
| 128 |
+
|
| 129 |
+
# Get all previous gates on qubit (until end or non rx/rz/rz gate is
|
| 130 |
+
# met)
|
| 131 |
+
while (
|
| 132 |
+
prev_gate is not None
|
| 133 |
+
and is_supported_1q_gate(prev_gate)
|
| 134 |
+
and prev_gate_index >= gate_range[0]
|
| 135 |
+
):
|
| 136 |
+
# If that gate is small, add it to indexes_to_remove and do not
|
| 137 |
+
# add it in decomposition
|
| 138 |
+
remove_because_zero = remove_zero_gates and prev_gate.params[0] == 0
|
| 139 |
+
remove_because_small = (
|
| 140 |
+
remove_small_gates
|
| 141 |
+
and np.absolute(prev_gate.params[0]) < min_rotation_angle
|
| 142 |
+
)
|
| 143 |
+
if remove_because_zero or remove_because_small:
|
| 144 |
+
indexes_to_remove += [prev_gate_index]
|
| 145 |
+
else:
|
| 146 |
+
prev_gate_indexes += [prev_gate_index]
|
| 147 |
+
prev_gate_matrix = circuit.data[prev_gate_index].operation.to_matrix()
|
| 148 |
+
matrix = np.matmul(matrix, prev_gate_matrix)
|
| 149 |
+
prev_gate, prev_gate_index = find_previous_gate_on_qubit(
|
| 150 |
+
circuit, prev_gate_index
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
if len(prev_gate_indexes) > 3:
|
| 154 |
+
theta, phi, lam = OneQubitEulerDecomposer().angles(matrix)
|
| 155 |
+
replace_1q_gate(circuit, prev_gate_indexes[0], "rz", phi)
|
| 156 |
+
replace_1q_gate(circuit, prev_gate_indexes[1], "ry", theta)
|
| 157 |
+
replace_1q_gate(circuit, prev_gate_indexes[2], "rz", lam)
|
| 158 |
+
# replace_1q_gate(circuit, prev_gate_indexes[3], 'ph', phase)
|
| 159 |
+
indexes_dealt_with += [prev_gate_indexes[1], prev_gate_indexes[2]]
|
| 160 |
+
indexes_to_remove += prev_gate_indexes[3:]
|
| 161 |
+
else:
|
| 162 |
+
indexes_dealt_with += prev_gate_indexes
|
| 163 |
+
for index in sorted(indexes_to_remove, reverse=True):
|
| 164 |
+
del circuit.data[index]
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def remove_unnecessary_2q_gates_from_circuit(circuit, gate_range=None):
|
| 168 |
+
"""
|
| 169 |
+
Remove unnecessary 2-qubit gates from circuit by removing pairs of
|
| 170 |
+
consecutive CX/CZ gates
|
| 171 |
+
:param circuit: Circuit from which gates are to be removed
|
| 172 |
+
:param gate_range: If provided, only gates in that range relative to
|
| 173 |
+
circuit.data will be modified
|
| 174 |
+
(lower index is inclusive and upper index is exclusive)
|
| 175 |
+
"""
|
| 176 |
+
if gate_range is None:
|
| 177 |
+
gate_range = (0, len(circuit.data))
|
| 178 |
+
|
| 179 |
+
indexes_to_remove = []
|
| 180 |
+
indexes_dealt_with = []
|
| 181 |
+
|
| 182 |
+
# Reverse iterate over all gates
|
| 183 |
+
for gate_index in range(gate_range[1] - 1, gate_range[0] - 1, -1):
|
| 184 |
+
circ_instr = circuit.data[gate_index]
|
| 185 |
+
gate = circ_instr.operation
|
| 186 |
+
qargs = circ_instr.qubits
|
| 187 |
+
if gate.name not in ["cx", "cy", "cz"]:
|
| 188 |
+
continue
|
| 189 |
+
if gate_index in indexes_to_remove or gate_index in indexes_dealt_with:
|
| 190 |
+
continue
|
| 191 |
+
prev_gate, prev_gate_index = find_previous_gate_on_qubit(circuit, gate_index)
|
| 192 |
+
if prev_gate is None or prev_gate.name != gate.name:
|
| 193 |
+
continue
|
| 194 |
+
if prev_gate_index < gate_range[0]:
|
| 195 |
+
continue
|
| 196 |
+
if (
|
| 197 |
+
prev_gate_index in indexes_to_remove
|
| 198 |
+
or prev_gate_index in indexes_dealt_with
|
| 199 |
+
):
|
| 200 |
+
continue
|
| 201 |
+
if circuit.data[prev_gate_index].qubits == qargs:
|
| 202 |
+
indexes_to_remove += [gate_index, prev_gate_index]
|
| 203 |
+
for index in sorted(indexes_to_remove, reverse=True):
|
| 204 |
+
del circuit.data[index]
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
def advanced_circuit_transpilation(
|
| 208 |
+
circuit,
|
| 209 |
+
c_map,
|
| 210 |
+
optimization_level=2,
|
| 211 |
+
basis_gates=["cx", "rx", "ry", "rz"],
|
| 212 |
+
):
|
| 213 |
+
"""
|
| 214 |
+
Advanced circuit transpilation with chosen optimization_level.
|
| 215 |
+
:param circuit: Circuit to transpile
|
| 216 |
+
:param c_map: Directed coupling map for qiskit transpiler to target in mapping.
|
| 217 |
+
:param optimization_level: Order of optimization for transpiler to apply
|
| 218 |
+
Generally indicates how aggressively circuit transpilation will be done
|
| 219 |
+
Default = 2
|
| 220 |
+
:param basis_gates: Basis gates to transpile to.
|
| 221 |
+
Default = ["cx", "rx", "ry", "rz"]
|
| 222 |
+
"""
|
| 223 |
+
return transpile(
|
| 224 |
+
circuit,
|
| 225 |
+
basis_gates=basis_gates,
|
| 226 |
+
coupling_map=convert_cmap_to_qiskit_format(c_map),
|
| 227 |
+
# ensure qubits are not re-ordered
|
| 228 |
+
layout_method="trivial",
|
| 229 |
+
initial_layout=get_initial_layout(circuit),
|
| 230 |
+
optimization_level=optimization_level,
|
| 231 |
+
)
|
utils/adapt-aqc/adaptaqc/utils/circuit_operations/circuit_operations_pauli_ops.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# (C) Copyright IBM 2025.
|
| 2 |
+
#
|
| 3 |
+
# This code is licensed under the Apache License, Version 2.0. You may
|
| 4 |
+
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
| 5 |
+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
| 6 |
+
#
|
| 7 |
+
# Any modifications or derivative works of this code must retain this
|
| 8 |
+
# copyright notice, and modified files need to carry a notice indicating
|
| 9 |
+
# that they have been altered from the originals.
|
| 10 |
+
|
| 11 |
+
import numpy as np
|
| 12 |
+
from openfermion import QubitOperator
|
| 13 |
+
from qiskit import ClassicalRegister, QuantumCircuit
|
| 14 |
+
from qiskit.circuit.library import UGate, PhaseGate
|
| 15 |
+
from qiskit.quantum_info import Pauli
|
| 16 |
+
|
| 17 |
+
from adaptaqc.utils.circuit_operations.circuit_operations_full_circuit import (
|
| 18 |
+
add_classical_operations,
|
| 19 |
+
add_to_circuit,
|
| 20 |
+
remove_classical_operations,
|
| 21 |
+
remove_inner_circuit,
|
| 22 |
+
)
|
| 23 |
+
from adaptaqc.utils.circuit_operations.circuit_operations_running import (
|
| 24 |
+
run_circuit_without_transpilation,
|
| 25 |
+
)
|
| 26 |
+
from adaptaqc.utils.utilityfunctions import (
|
| 27 |
+
expectation_value_of_pauli_observable,
|
| 28 |
+
is_statevector_backend,
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def add_pauli_operators_to_circuit(
|
| 33 |
+
circuit: QuantumCircuit, pauli: Pauli, location=None
|
| 34 |
+
):
|
| 35 |
+
if location is None:
|
| 36 |
+
location = len(circuit.data)
|
| 37 |
+
original_circuit_length = len(circuit.data)
|
| 38 |
+
# Add rotation gates
|
| 39 |
+
pauli_circuit = QuantumCircuit(circuit.num_qubits)
|
| 40 |
+
for i, pauli_axis in enumerate(reversed(pauli.to_label())):
|
| 41 |
+
if pauli_axis == "I":
|
| 42 |
+
continue
|
| 43 |
+
elif pauli_axis == "X":
|
| 44 |
+
UGate(np.pi, -0.5 * np.pi, 0.5 * np.pi)
|
| 45 |
+
elif pauli_axis == "Y":
|
| 46 |
+
UGate(np.pi, 0, 0)
|
| 47 |
+
elif pauli_axis == "Z":
|
| 48 |
+
PhaseGate(np.pi)
|
| 49 |
+
else:
|
| 50 |
+
raise ValueError(f"Unexpected pauli axis {pauli_axis}")
|
| 51 |
+
|
| 52 |
+
# Add post rotation gates (copied from pauli_measurement in
|
| 53 |
+
# qiskit.aqua.operators.common)
|
| 54 |
+
for qubit_idx in range(circuit.num_qubits):
|
| 55 |
+
if pauli.x[qubit_idx]:
|
| 56 |
+
if pauli.z[qubit_idx]:
|
| 57 |
+
# Measure Y
|
| 58 |
+
pauli_circuit.p(-np.pi / 2, qubit_idx) # sdg
|
| 59 |
+
pauli_circuit.u(np.pi / 2, 0.0, np.pi, qubit_idx) # h
|
| 60 |
+
else:
|
| 61 |
+
# Measure X
|
| 62 |
+
pauli_circuit.u(np.pi / 2, 0.0, np.pi, qubit_idx) # h
|
| 63 |
+
add_to_circuit(
|
| 64 |
+
circuit, pauli_circuit, location=location, transpile_before_adding=False
|
| 65 |
+
)
|
| 66 |
+
pauli_circuit_len = len(circuit.data) - original_circuit_length
|
| 67 |
+
pauli_operators_gate_range = (location, location + pauli_circuit_len)
|
| 68 |
+
return pauli_operators_gate_range
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def expectation_value_of_pauli_operator(
|
| 72 |
+
circuit: QuantumCircuit,
|
| 73 |
+
operator: dict,
|
| 74 |
+
backend,
|
| 75 |
+
backend_options=None,
|
| 76 |
+
execute_kwargs=None,
|
| 77 |
+
):
|
| 78 |
+
expectation_value = 0
|
| 79 |
+
cl_ops_data = remove_classical_operations(circuit)
|
| 80 |
+
creg = ClassicalRegister(circuit.num_qubits)
|
| 81 |
+
circuit.add_register(creg)
|
| 82 |
+
for pauli_lbl in operator.keys():
|
| 83 |
+
if pauli_lbl == "I" * len(pauli_lbl):
|
| 84 |
+
expectation_value += operator[pauli_lbl] * 1
|
| 85 |
+
continue
|
| 86 |
+
pauli_obj = Pauli(pauli_lbl)
|
| 87 |
+
pauli_circuit_gate_range = add_pauli_operators_to_circuit(circuit, pauli_obj)
|
| 88 |
+
if not is_statevector_backend(backend):
|
| 89 |
+
[
|
| 90 |
+
circuit.measure(circuit.qregs[0][x], creg[x])
|
| 91 |
+
for x in range(circuit.num_qubits)
|
| 92 |
+
]
|
| 93 |
+
counts = run_circuit_without_transpilation(
|
| 94 |
+
circuit, backend, backend_options, execute_kwargs
|
| 95 |
+
)
|
| 96 |
+
remove_classical_operations(circuit)
|
| 97 |
+
eval_po = expectation_value_of_pauli_observable(counts, pauli_obj)
|
| 98 |
+
expectation_value += operator[pauli_lbl] * eval_po
|
| 99 |
+
|
| 100 |
+
remove_inner_circuit(circuit, pauli_circuit_gate_range)
|
| 101 |
+
circuit.cregs.remove(creg)
|
| 102 |
+
add_classical_operations(circuit, cl_ops_data)
|
| 103 |
+
return expectation_value
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def convert_qubit_op_to_pauli_dict(qubit_op: QubitOperator):
|
| 107 |
+
paulis = []
|
| 108 |
+
base_pauli = ["I"]
|
| 109 |
+
for action_pairs, coeff in qubit_op.terms.items():
|
| 110 |
+
if not np.isreal(coeff):
|
| 111 |
+
raise ValueError("Complex coefficients unsupported")
|
| 112 |
+
else:
|
| 113 |
+
coeff = np.real(coeff)
|
| 114 |
+
this_pauli = list(base_pauli)
|
| 115 |
+
for qubit_index, pauli_op in action_pairs:
|
| 116 |
+
if qubit_index >= len(base_pauli):
|
| 117 |
+
# Add extra ops to all pauli strings
|
| 118 |
+
diff = (qubit_index + 1) - len(base_pauli)
|
| 119 |
+
base_pauli += ["I"] * diff
|
| 120 |
+
this_pauli += ["I"] * diff
|
| 121 |
+
for key in [x[0] for x in paulis]:
|
| 122 |
+
key += ["I"] * diff
|
| 123 |
+
this_pauli[qubit_index] = pauli_op
|
| 124 |
+
paulis.append((this_pauli, coeff))
|
| 125 |
+
|
| 126 |
+
pauli_dict = {"".join(pauli_list[::-1]): coeff for (pauli_list, coeff) in paulis}
|
| 127 |
+
return pauli_dict
|
utils/adapt-aqc/adaptaqc/utils/circuit_operations/circuit_operations_running.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# (C) Copyright IBM 2025.
|
| 2 |
+
#
|
| 3 |
+
# This code is licensed under the Apache License, Version 2.0. You may
|
| 4 |
+
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
| 5 |
+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
| 6 |
+
#
|
| 7 |
+
# Any modifications or derivative works of this code must retain this
|
| 8 |
+
# copyright notice, and modified files need to carry a notice indicating
|
| 9 |
+
# that they have been altered from the originals.
|
| 10 |
+
|
| 11 |
+
import logging
|
| 12 |
+
|
| 13 |
+
import numpy as np
|
| 14 |
+
from qiskit import QuantumCircuit, transpile
|
| 15 |
+
from qiskit.circuit.library import CXGate
|
| 16 |
+
from qiskit_aer.backends.aerbackend import AerBackend
|
| 17 |
+
from qiskit_aer.noise import thermal_relaxation_error, NoiseModel
|
| 18 |
+
from scipy.optimize import curve_fit
|
| 19 |
+
|
| 20 |
+
from adaptaqc.backends.aer_sv_backend import AerSVBackend
|
| 21 |
+
from adaptaqc.backends.python_default_backends import QASM_SIM
|
| 22 |
+
from adaptaqc.backends.qiskit_sampling_backend import QiskitSamplingBackend
|
| 23 |
+
from adaptaqc.utils.utilityfunctions import (
|
| 24 |
+
counts_data_from_statevector,
|
| 25 |
+
is_statevector_backend,
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
logger = logging.getLogger(__name__)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def run_circuit_with_transpilation(
|
| 32 |
+
circuit: QuantumCircuit,
|
| 33 |
+
backend=QASM_SIM,
|
| 34 |
+
backend_options=None,
|
| 35 |
+
execute_kwargs=None,
|
| 36 |
+
return_statevector=False,
|
| 37 |
+
):
|
| 38 |
+
transpiled_circuit = transpile(circuit, backend.simulator)
|
| 39 |
+
return run_circuit_without_transpilation(
|
| 40 |
+
transpiled_circuit, backend, backend_options, execute_kwargs, return_statevector
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def run_circuit_without_transpilation(
|
| 45 |
+
circuit: QuantumCircuit,
|
| 46 |
+
backend: QiskitSamplingBackend | AerSVBackend = QASM_SIM,
|
| 47 |
+
backend_options=None,
|
| 48 |
+
execute_kwargs=None,
|
| 49 |
+
return_statevector=False,
|
| 50 |
+
):
|
| 51 |
+
if execute_kwargs is None:
|
| 52 |
+
execute_kwargs = {}
|
| 53 |
+
|
| 54 |
+
# Backend options only supported for simulators
|
| 55 |
+
if backend_options is None or not isinstance(backend, AerBackend):
|
| 56 |
+
backend_options = {}
|
| 57 |
+
# executing the circuits on the backend and returning the job
|
| 58 |
+
job = backend.simulator.run(circuit, **backend_options, **execute_kwargs)
|
| 59 |
+
|
| 60 |
+
result = job.result()
|
| 61 |
+
if is_statevector_backend(backend):
|
| 62 |
+
if return_statevector:
|
| 63 |
+
output = result.get_statevector()
|
| 64 |
+
else:
|
| 65 |
+
output = counts_data_from_statevector(result.get_statevector())
|
| 66 |
+
else:
|
| 67 |
+
output = result.get_counts()
|
| 68 |
+
|
| 69 |
+
return output
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def create_noisemodel(t1, t2, log_fidelities=True):
|
| 73 |
+
# Instruction times (in nanoseconds)
|
| 74 |
+
time_u1 = 0 # virtual gate
|
| 75 |
+
time_u2 = 50 # (single X90 pulse)
|
| 76 |
+
time_u3 = 100 # (two X90 pulses)
|
| 77 |
+
time_cx = 300
|
| 78 |
+
time_reset = 1000 # 1 microsecond
|
| 79 |
+
time_measure = 1000 # 1 microsecond
|
| 80 |
+
|
| 81 |
+
t1 = t1 * 1e6
|
| 82 |
+
t2 = t2 * 1e6
|
| 83 |
+
|
| 84 |
+
# QuantumError objects
|
| 85 |
+
error_reset = thermal_relaxation_error(t1, t2, time_reset)
|
| 86 |
+
error_measure = thermal_relaxation_error(t1, t2, time_measure)
|
| 87 |
+
error_u1 = thermal_relaxation_error(t1, t2, time_u1)
|
| 88 |
+
error_u2 = thermal_relaxation_error(t1, t2, time_u2)
|
| 89 |
+
error_u3 = thermal_relaxation_error(t1, t2, time_u3)
|
| 90 |
+
error_cx = thermal_relaxation_error(t1, t2, time_cx).expand(
|
| 91 |
+
thermal_relaxation_error(t1, t2, time_cx)
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
# Add errors to noise model
|
| 95 |
+
noise_thermal = NoiseModel()
|
| 96 |
+
noise_thermal.add_all_qubit_quantum_error(error_reset, "reset")
|
| 97 |
+
noise_thermal.add_all_qubit_quantum_error(error_measure, "measure")
|
| 98 |
+
noise_thermal.add_all_qubit_quantum_error(error_u1, "u1")
|
| 99 |
+
noise_thermal.add_all_qubit_quantum_error(error_u2, "u2")
|
| 100 |
+
noise_thermal.add_all_qubit_quantum_error(error_u3, "u3")
|
| 101 |
+
noise_thermal.add_all_qubit_quantum_error(error_cx, "cx")
|
| 102 |
+
|
| 103 |
+
if log_fidelities:
|
| 104 |
+
logger.info("Noise model fidelities:")
|
| 105 |
+
for qubit_error in noise_thermal.to_dict()["errors"]:
|
| 106 |
+
logging.info(
|
| 107 |
+
f"{qubit_error['operations']}: " f"{max(qubit_error['probabilities'])}"
|
| 108 |
+
)
|
| 109 |
+
return noise_thermal
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def zero_noise_extrapolate(
|
| 113 |
+
circuit: QuantumCircuit, measurement_function, num_points=10
|
| 114 |
+
):
|
| 115 |
+
calculated_values = []
|
| 116 |
+
probabilities = np.linspace(0, 1, num_points)
|
| 117 |
+
for prob in probabilities:
|
| 118 |
+
circuit_data_copy = circuit.data.copy()
|
| 119 |
+
for i, (gate, qargs, cargs) in list(enumerate(circuit.data))[::-1]:
|
| 120 |
+
if isinstance(gate, CXGate):
|
| 121 |
+
if np.random.random() < prob:
|
| 122 |
+
circuit.data.insert(i, (gate, qargs, cargs))
|
| 123 |
+
circuit.data.insert(i, (gate, qargs, cargs))
|
| 124 |
+
|
| 125 |
+
calculated_values.append(measurement_function())
|
| 126 |
+
circuit.data = circuit_data_copy
|
| 127 |
+
|
| 128 |
+
def exp_decay(x, intercept, amp, decay_rate):
|
| 129 |
+
return intercept + amp * np.exp(-1 * x / decay_rate)
|
| 130 |
+
|
| 131 |
+
try:
|
| 132 |
+
popt, pcov = curve_fit(
|
| 133 |
+
exp_decay, probabilities, calculated_values, [0, calculated_values[0], 1]
|
| 134 |
+
)
|
| 135 |
+
zne_val = exp_decay(-0.5, *popt)
|
| 136 |
+
return zne_val
|
| 137 |
+
except RuntimeError as e:
|
| 138 |
+
logger.warning(f"Failed to zero-noise-extrapolate. Error was {e}")
|
| 139 |
+
return measurement_function()
|
utils/adapt-aqc/adaptaqc/utils/circuit_operations/circuit_operations_variational.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# (C) Copyright IBM 2025.
|
| 2 |
+
#
|
| 3 |
+
# This code is licensed under the Apache License, Version 2.0. You may
|
| 4 |
+
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
| 5 |
+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
| 6 |
+
#
|
| 7 |
+
# Any modifications or derivative works of this code must retain this
|
| 8 |
+
# copyright notice, and modified files need to carry a notice indicating
|
| 9 |
+
# that they have been altered from the originals.
|
| 10 |
+
|
| 11 |
+
import numpy as np
|
| 12 |
+
from qiskit import QuantumCircuit
|
| 13 |
+
from qiskit.circuit import CircuitInstruction
|
| 14 |
+
from qiskit.circuit.library import U3Gate, U1Gate
|
| 15 |
+
|
| 16 |
+
from adaptaqc.utils.circuit_operations.circuit_operations_basic import (
|
| 17 |
+
is_supported_1q_gate,
|
| 18 |
+
)
|
| 19 |
+
from adaptaqc.utils.utilityfunctions import normalized_angles
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def find_angles_in_circuit(circuit, gate_range=None):
|
| 23 |
+
"""
|
| 24 |
+
Find the angles of rotation gates in the circuit. Ordering is relative
|
| 25 |
+
to position of gate in circuit.data.
|
| 26 |
+
Ignores gates that have the label FIXED_GATE_LABEL
|
| 27 |
+
:param circuit: QuantumCircuit
|
| 28 |
+
:param gate_range: The search space for the angles (full circuit if None)
|
| 29 |
+
:return: Angles in circuit (list)
|
| 30 |
+
"""
|
| 31 |
+
angles = []
|
| 32 |
+
if gate_range is None:
|
| 33 |
+
gate_range = (0, len(circuit.data))
|
| 34 |
+
angle_index = 0
|
| 35 |
+
for gate_index in range(*gate_range):
|
| 36 |
+
gate = circuit.data[gate_index].operation
|
| 37 |
+
if is_supported_1q_gate(gate):
|
| 38 |
+
# Normalize angle to between -pi and pi
|
| 39 |
+
angles += [normalized_angles(gate.params[0])]
|
| 40 |
+
angle_index += 1
|
| 41 |
+
return angles
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def update_angles_in_circuit(circuit: QuantumCircuit, angles, gate_range=None):
|
| 45 |
+
"""
|
| 46 |
+
Changes the angle of all rotation gates in the circuit except those with
|
| 47 |
+
label = FIXED_GATE_LABEL
|
| 48 |
+
:param circuit: Circuit to modify
|
| 49 |
+
:param angles: New angles (list/np.ndarray)
|
| 50 |
+
:param gate_range: The range of gates in which the 1q gates are located
|
| 51 |
+
for the angles (full circuit if None)
|
| 52 |
+
"""
|
| 53 |
+
if gate_range is None:
|
| 54 |
+
gate_range = (0, len(circuit.data))
|
| 55 |
+
angle_index = 0
|
| 56 |
+
for gate_index in range(*gate_range):
|
| 57 |
+
circ_instr = circuit.data[gate_index]
|
| 58 |
+
gate = circ_instr.operation
|
| 59 |
+
if is_supported_1q_gate(gate):
|
| 60 |
+
gate.params[0] = angles[angle_index]
|
| 61 |
+
angle_index += 1
|
| 62 |
+
circuit.data[gate_index] = circ_instr
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def create_variational_circuit(circuit: QuantumCircuit):
|
| 66 |
+
new_circ = QuantumCircuit(*circuit.qregs, *circuit.cregs)
|
| 67 |
+
|
| 68 |
+
for circ_instr in circuit.data:
|
| 69 |
+
gate = circ_instr.operation
|
| 70 |
+
if isinstance(gate, U1Gate):
|
| 71 |
+
gate.label = "rz"
|
| 72 |
+
elif isinstance(gate, U3Gate):
|
| 73 |
+
if gate.params[1] == gate.params[2] and gate.params[2] == 0:
|
| 74 |
+
gate.label = "ry"
|
| 75 |
+
elif np.isclose(-0.5 * np.pi, gate.params[1]) and np.isclose(
|
| 76 |
+
0.5 * np.pi, gate.params[2]
|
| 77 |
+
):
|
| 78 |
+
gate.label = "rx"
|
| 79 |
+
new_circ.data.append(
|
| 80 |
+
CircuitInstruction(
|
| 81 |
+
operation=gate, qubits=circ_instr.qubits, clbits=circ_instr.clbits
|
| 82 |
+
)
|
| 83 |
+
)
|
| 84 |
+
return new_circ
|
utils/adapt-aqc/adaptaqc/utils/constants.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# (C) Copyright IBM 2025.
|
| 2 |
+
#
|
| 3 |
+
# This code is licensed under the Apache License, Version 2.0. You may
|
| 4 |
+
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
| 5 |
+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
| 6 |
+
#
|
| 7 |
+
# Any modifications or derivative works of this code must retain this
|
| 8 |
+
# copyright notice, and modified files need to carry a notice indicating
|
| 9 |
+
# that they have been altered from the originals.
|
| 10 |
+
|
| 11 |
+
"""Contains constants"""
|
| 12 |
+
from typing import List, Tuple
|
| 13 |
+
|
| 14 |
+
import numpy as np
|
| 15 |
+
|
| 16 |
+
# Type of MPS data as it outputted by Qiskit.
|
| 17 |
+
QiskitMPS = Tuple[List[Tuple[np.ndarray, np.ndarray]], List[np.ndarray]]
|
| 18 |
+
|
| 19 |
+
ALG_ROTOSOLVE = "rotosolve"
|
| 20 |
+
ALG_ROTOSELECT = "rotoselect"
|
| 21 |
+
ALG_NLOPT = "nlopt"
|
| 22 |
+
ALG_SCIPY = "scipy"
|
| 23 |
+
ALG_PYBOBYQA = "pybobyqa"
|
| 24 |
+
|
| 25 |
+
FIXED_GATE_LABEL = "fixed_gate"
|
| 26 |
+
|
| 27 |
+
CMAP_FULL = "CMAP_FULL"
|
| 28 |
+
CMAP_LINEAR = "CMAP_LINEAR"
|
| 29 |
+
CMAP_LADDER = "CMAP_LADDER"
|
| 30 |
+
|
| 31 |
+
DEFAULT_SUFFICIENT_COST = 1e-2
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def generate_coupling_map(num_qubits, map_kind, both_dir=False, loop=False):
|
| 35 |
+
if map_kind == CMAP_FULL:
|
| 36 |
+
return coupling_map_fully_entangled(num_qubits, both_dir)
|
| 37 |
+
elif map_kind == CMAP_LINEAR:
|
| 38 |
+
return coupling_map_linear(num_qubits, both_dir, loop)
|
| 39 |
+
elif map_kind == CMAP_LADDER:
|
| 40 |
+
return coupling_map_ladder(num_qubits, both_dir, loop)
|
| 41 |
+
else:
|
| 42 |
+
raise ValueError(f"Invalid coupling map type {map_kind}")
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def coupling_map_fully_entangled(num_qubits, both_dir=False):
|
| 46 |
+
"""
|
| 47 |
+
Coupling map with all qubits connected to each other
|
| 48 |
+
:param num_qubits: Number of qubits
|
| 49 |
+
:param both_dir: If true, map will include gates with control and target
|
| 50 |
+
swapped
|
| 51 |
+
:return: [(control(int),target(int))]
|
| 52 |
+
"""
|
| 53 |
+
c_map = []
|
| 54 |
+
for i in range(1, num_qubits):
|
| 55 |
+
for j in range(num_qubits - i):
|
| 56 |
+
c_map.append((j, j + i))
|
| 57 |
+
if both_dir:
|
| 58 |
+
c_map_rev = [(target, source) for (source, target) in c_map]
|
| 59 |
+
c_map += c_map_rev
|
| 60 |
+
return c_map
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def coupling_map_linear(num_qubits, both_dir=False, loop=False):
|
| 64 |
+
"""
|
| 65 |
+
Coupling map with qubits connected to adjacent qubits
|
| 66 |
+
:param num_qubits: Number of qubits
|
| 67 |
+
:param both_dir: If true, map will include gates with control and target
|
| 68 |
+
swapped
|
| 69 |
+
:param loop: If true, the first qubit will be connected to the last
|
| 70 |
+
qubit as well
|
| 71 |
+
:return: [(control(int),target(int))]
|
| 72 |
+
"""
|
| 73 |
+
c_map = []
|
| 74 |
+
for j in range(num_qubits - 1):
|
| 75 |
+
c_map.append((j, j + 1))
|
| 76 |
+
if loop:
|
| 77 |
+
c_map.append((num_qubits - 1, 0))
|
| 78 |
+
if both_dir:
|
| 79 |
+
c_map_rev = [(target, source) for (source, target) in c_map]
|
| 80 |
+
c_map += c_map_rev
|
| 81 |
+
return c_map
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def coupling_map_ladder(num_qubits, both_dir=False, loop=False):
|
| 85 |
+
"""
|
| 86 |
+
Low depth coupling map with qubits connected to adjacent qubits
|
| 87 |
+
:param num_qubits: Number of qubits
|
| 88 |
+
:param both_dir: If true, map will include gates with control and target
|
| 89 |
+
swapped
|
| 90 |
+
:param loop: If true, the first qubit will be connected to the last
|
| 91 |
+
qubit as well
|
| 92 |
+
:return: [(control(int),target(int))]
|
| 93 |
+
"""
|
| 94 |
+
c_map = []
|
| 95 |
+
j = 0
|
| 96 |
+
while j + 1 <= num_qubits - 1:
|
| 97 |
+
c_map.append((j, j + 1))
|
| 98 |
+
j += 2
|
| 99 |
+
j = 1
|
| 100 |
+
if loop and num_qubits % 2 == 1:
|
| 101 |
+
c_map.append((num_qubits - 1, 0))
|
| 102 |
+
while j + 1 <= num_qubits - 1:
|
| 103 |
+
c_map.append((j, j + 1))
|
| 104 |
+
j += 2
|
| 105 |
+
if loop and num_qubits % 2 == 0:
|
| 106 |
+
c_map.append((num_qubits - 1, 0))
|
| 107 |
+
if both_dir:
|
| 108 |
+
c_map_rev = [(target, source) for (source, target) in c_map]
|
| 109 |
+
c_map += c_map_rev
|
| 110 |
+
return c_map
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def convert_cmap_to_qiskit_format(c_map):
|
| 114 |
+
"""
|
| 115 |
+
Convert a list of tuples to a list of lists that qiskit expects for transpiling with a c_map.
|
| 116 |
+
:param c_map: List of tuples [(int, int)]
|
| 117 |
+
:return: List of lists [[int, int]]
|
| 118 |
+
"""
|
| 119 |
+
return [list(pair) for pair in c_map]
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def get_initial_layout(circuit):
|
| 123 |
+
"""
|
| 124 |
+
Extracts initial layout of a circuit.
|
| 125 |
+
|
| 126 |
+
:param circuit: The original circuit to determine the layout for.
|
| 127 |
+
:return: Dictionary for initial_layout in the form {logical_qubit: physical_qubit}
|
| 128 |
+
"""
|
| 129 |
+
# map logical qubits to their indices in the circuit
|
| 130 |
+
initial_layout = {qubit: idx for idx, qubit in enumerate(circuit.qubits)}
|
| 131 |
+
return initial_layout
|
utils/adapt-aqc/adaptaqc/utils/cost_minimiser.py
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# (C) Copyright IBM 2025.
|
| 2 |
+
#
|
| 3 |
+
# This code is licensed under the Apache License, Version 2.0. You may
|
| 4 |
+
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
| 5 |
+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
| 6 |
+
#
|
| 7 |
+
# Any modifications or derivative works of this code must retain this
|
| 8 |
+
# copyright notice, and modified files need to carry a notice indicating
|
| 9 |
+
# that they have been altered from the originals.
|
| 10 |
+
|
| 11 |
+
"""Contains CostMinimiser"""
|
| 12 |
+
import logging
|
| 13 |
+
import random
|
| 14 |
+
from typing import Tuple
|
| 15 |
+
|
| 16 |
+
import numpy as np
|
| 17 |
+
from scipy.optimize import minimize
|
| 18 |
+
|
| 19 |
+
import adaptaqc.utils.circuit_operations as co
|
| 20 |
+
import adaptaqc.utils.constants as vconstants
|
| 21 |
+
from adaptaqc.utils.circuit_operations import SUPPORTED_1Q_GATES
|
| 22 |
+
from adaptaqc.utils.utilityfunctions import (
|
| 23 |
+
derivative_of_sinusoidal,
|
| 24 |
+
has_stopped_improving,
|
| 25 |
+
minimum_of_sinusoidal,
|
| 26 |
+
find_rotation_indices,
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
logger = logging.getLogger(__name__)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class CostMinimiser:
|
| 33 |
+
"""
|
| 34 |
+
Minimizer that minimizes a cost function
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
def __init__(
|
| 38 |
+
self,
|
| 39 |
+
cost_finder,
|
| 40 |
+
variational_circuit_range,
|
| 41 |
+
full_circuit,
|
| 42 |
+
rotosolve_fraction=1.0,
|
| 43 |
+
):
|
| 44 |
+
"""
|
| 45 |
+
:param cost_finder: Callable that returns cost(float)
|
| 46 |
+
"""
|
| 47 |
+
self.cost_finder = cost_finder
|
| 48 |
+
self.variational_circuit_range = variational_circuit_range
|
| 49 |
+
self.full_circuit = full_circuit
|
| 50 |
+
self.rotosolve_fraction = rotosolve_fraction
|
| 51 |
+
|
| 52 |
+
def minimize_cost(
|
| 53 |
+
self,
|
| 54 |
+
algorithm_kind=vconstants.ALG_ROTOSOLVE,
|
| 55 |
+
algorithm_identifier=None,
|
| 56 |
+
max_cycles=1000,
|
| 57 |
+
stop_val=-np.inf,
|
| 58 |
+
tol=1e-10,
|
| 59 |
+
indexes_to_modify=None,
|
| 60 |
+
alg_kwargs=None,
|
| 61 |
+
):
|
| 62 |
+
"""
|
| 63 |
+
Minimize the cost by varying rotation gate angles (and axes in case
|
| 64 |
+
of ALG_ROTOSELECT). Gates with label
|
| 65 |
+
FIXED_GATE_LABEL will not be varied.
|
| 66 |
+
:param algorithm_kind:
|
| 67 |
+
:param algorithm_identifier:
|
| 68 |
+
:param max_cycles: For ALG_ROTOSOLVE,ALG_ROTOSELECT, this is the max
|
| 69 |
+
number of cycles
|
| 70 |
+
:param stop_val: Minimization will stop when this value is reached
|
| 71 |
+
:param tol: Tolerance (float). Difference algorithms have different
|
| 72 |
+
implementations of this value
|
| 73 |
+
:param indexes_to_modify: If not None, only gates with the given
|
| 74 |
+
indexes (index of gate in variational_circuit.data) will be varied
|
| 75 |
+
(only valid for rotosolve/rotoselect)
|
| 76 |
+
:param alg_kwargs: Keyword arguments supplied to particular optimiser
|
| 77 |
+
:return:
|
| 78 |
+
"""
|
| 79 |
+
if alg_kwargs is None:
|
| 80 |
+
alg_kwargs = {}
|
| 81 |
+
if (
|
| 82 |
+
algorithm_kind == vconstants.ALG_ROTOSOLVE
|
| 83 |
+
or algorithm_kind == vconstants.ALG_ROTOSELECT
|
| 84 |
+
):
|
| 85 |
+
if algorithm_kind == vconstants.ALG_ROTOSOLVE:
|
| 86 |
+
alg_name = "ROTOSOLVE"
|
| 87 |
+
else:
|
| 88 |
+
alg_name = "ROTOSELECT"
|
| 89 |
+
|
| 90 |
+
cost_history = []
|
| 91 |
+
cost = self.cost_finder()
|
| 92 |
+
cycles = 0
|
| 93 |
+
logger.info(f"Starting {alg_name}")
|
| 94 |
+
while cost > stop_val and cycles < max_cycles:
|
| 95 |
+
cost = self._reduce_cost(
|
| 96 |
+
algorithm_kind == vconstants.ALG_ROTOSELECT, indexes_to_modify
|
| 97 |
+
)
|
| 98 |
+
cycles += 1
|
| 99 |
+
logger.info(f"{alg_name} cycle: {cycles}")
|
| 100 |
+
cost_history.append(cost)
|
| 101 |
+
if len(cost_history) > 3 and has_stopped_improving(
|
| 102 |
+
cost_history[-3:], tol
|
| 103 |
+
):
|
| 104 |
+
break
|
| 105 |
+
logger.info(f"{alg_name} finished with cost {cost}")
|
| 106 |
+
return cost
|
| 107 |
+
|
| 108 |
+
elif algorithm_kind == vconstants.ALG_NLOPT:
|
| 109 |
+
try:
|
| 110 |
+
import nlopt
|
| 111 |
+
except ModuleNotFoundError as e:
|
| 112 |
+
logger.error(
|
| 113 |
+
"NLOPT not installed. Use 'conda install -c conda-forge "
|
| 114 |
+
"nlopt' to install nlopt using conda"
|
| 115 |
+
)
|
| 116 |
+
raise e
|
| 117 |
+
initial_angles = co.find_angles_in_circuit(
|
| 118 |
+
self.full_circuit, self.variational_circuit_range()
|
| 119 |
+
)
|
| 120 |
+
if len(initial_angles) == 0:
|
| 121 |
+
return self.cost_finder()
|
| 122 |
+
# Setup optimizer
|
| 123 |
+
opt = nlopt.opt(algorithm_identifier, len(initial_angles))
|
| 124 |
+
opt.set_upper_bounds([np.pi] * len(initial_angles))
|
| 125 |
+
opt.set_lower_bounds([-np.pi] * len(initial_angles))
|
| 126 |
+
opt.set_stopval(stop_val)
|
| 127 |
+
opt.set_ftol_rel(tol)
|
| 128 |
+
opt.set_xtol_abs(1e-10)
|
| 129 |
+
opt.set_min_objective(self._find_cost_with_angles)
|
| 130 |
+
|
| 131 |
+
# Start optimization
|
| 132 |
+
try:
|
| 133 |
+
final_angles = opt.optimize(initial_angles)
|
| 134 |
+
except RuntimeError as e:
|
| 135 |
+
logger.error(f"Nlopt optimisation failed")
|
| 136 |
+
raise e
|
| 137 |
+
|
| 138 |
+
co.update_angles_in_circuit(
|
| 139 |
+
self.full_circuit, final_angles, self.variational_circuit_range()
|
| 140 |
+
)
|
| 141 |
+
return opt.last_optimum_value()
|
| 142 |
+
|
| 143 |
+
elif algorithm_kind == vconstants.ALG_SCIPY:
|
| 144 |
+
initial_angles = co.find_angles_in_circuit(
|
| 145 |
+
self.full_circuit, self.variational_circuit_range()
|
| 146 |
+
)
|
| 147 |
+
optimization_result = minimize(
|
| 148 |
+
fun=self._find_cost_with_angles,
|
| 149 |
+
method=algorithm_identifier,
|
| 150 |
+
x0=initial_angles,
|
| 151 |
+
tol=tol,
|
| 152 |
+
**alg_kwargs,
|
| 153 |
+
)
|
| 154 |
+
co.update_angles_in_circuit(
|
| 155 |
+
self.full_circuit,
|
| 156 |
+
optimization_result["x"],
|
| 157 |
+
self.variational_circuit_range(),
|
| 158 |
+
)
|
| 159 |
+
return optimization_result["fun"]
|
| 160 |
+
elif algorithm_kind == vconstants.ALG_PYBOBYQA:
|
| 161 |
+
try:
|
| 162 |
+
import pybobyqa
|
| 163 |
+
except ModuleNotFoundError as e:
|
| 164 |
+
logger.error(
|
| 165 |
+
"PyBOBYQA not installed. Use 'pip install Py-BOBYQA' "
|
| 166 |
+
"to install using pip"
|
| 167 |
+
)
|
| 168 |
+
raise e
|
| 169 |
+
|
| 170 |
+
initial_angles = co.find_angles_in_circuit(
|
| 171 |
+
self.full_circuit, self.variational_circuit_range()
|
| 172 |
+
)
|
| 173 |
+
bounds = ([-np.pi] * len(initial_angles), [np.pi] * len(initial_angles))
|
| 174 |
+
try:
|
| 175 |
+
result = pybobyqa.solve(
|
| 176 |
+
self._find_cost_with_angles,
|
| 177 |
+
initial_angles,
|
| 178 |
+
bounds=bounds,
|
| 179 |
+
objfun_has_noise=True,
|
| 180 |
+
print_progress=False,
|
| 181 |
+
do_logging=False,
|
| 182 |
+
**alg_kwargs,
|
| 183 |
+
)
|
| 184 |
+
co.update_angles_in_circuit(
|
| 185 |
+
self.full_circuit, result.x, self.variational_circuit_range()
|
| 186 |
+
)
|
| 187 |
+
return result.f
|
| 188 |
+
except Exception as e:
|
| 189 |
+
logger.error(f"BOBYQA failed with exception: {e}")
|
| 190 |
+
co.update_angles_in_circuit(
|
| 191 |
+
self.full_circuit, initial_angles, self.variational_circuit_range()
|
| 192 |
+
)
|
| 193 |
+
return self.cost_finder()
|
| 194 |
+
else:
|
| 195 |
+
raise ValueError(f"Invalid algorithm kind {algorithm_kind}")
|
| 196 |
+
|
| 197 |
+
def try_escaping_periodic_local_minimum(
|
| 198 |
+
self, gap_between_minima, first_minima_loc, penalty_amp=0.1
|
| 199 |
+
):
|
| 200 |
+
initial_cost = self.cost_finder()
|
| 201 |
+
initial_angles = co.find_angles_in_circuit(
|
| 202 |
+
self.full_circuit, self.variational_circuit_range()
|
| 203 |
+
)
|
| 204 |
+
num_attempts = 5
|
| 205 |
+
stochastic_param = 1
|
| 206 |
+
|
| 207 |
+
def find_cost_with_penalty_for_angles(angles, grad=None):
|
| 208 |
+
cost = self._find_cost_with_angles(angles, grad)
|
| 209 |
+
# Create a sinusoidally varying potential that has maxima at the
|
| 210 |
+
# local minima locations
|
| 211 |
+
penalty = penalty_amp * np.cos(
|
| 212 |
+
np.pi
|
| 213 |
+
+ (
|
| 214 |
+
(cost - first_minima_loc)
|
| 215 |
+
* 2
|
| 216 |
+
* np.pi
|
| 217 |
+
* (1 / gap_between_minima)
|
| 218 |
+
* stochastic_param
|
| 219 |
+
)
|
| 220 |
+
)
|
| 221 |
+
return cost + penalty
|
| 222 |
+
|
| 223 |
+
actual_cost = initial_cost
|
| 224 |
+
for i in range(num_attempts):
|
| 225 |
+
res = minimize(
|
| 226 |
+
find_cost_with_penalty_for_angles, initial_angles, method="Nelder-Mead"
|
| 227 |
+
)
|
| 228 |
+
final_angles = res.x
|
| 229 |
+
|
| 230 |
+
co.update_angles_in_circuit(
|
| 231 |
+
self.full_circuit, final_angles, self.variational_circuit_range()
|
| 232 |
+
)
|
| 233 |
+
cost_with_penalty = res.fun
|
| 234 |
+
|
| 235 |
+
co.update_angles_in_circuit(
|
| 236 |
+
self.full_circuit, final_angles, self.variational_circuit_range()
|
| 237 |
+
)
|
| 238 |
+
actual_cost = self.cost_finder()
|
| 239 |
+
logging.debug(
|
| 240 |
+
f"{i}th Attempt to escape minima: initial cost = "
|
| 241 |
+
f"{initial_cost}, final cost with penalty "
|
| 242 |
+
f"= {cost_with_penalty}, "
|
| 243 |
+
f"actual final cost = {actual_cost}"
|
| 244 |
+
)
|
| 245 |
+
stochastic_param = np.random.random() * 10
|
| 246 |
+
if actual_cost < initial_cost:
|
| 247 |
+
break
|
| 248 |
+
return actual_cost
|
| 249 |
+
|
| 250 |
+
def _find_cost_with_angles(self, angles, grad=None):
|
| 251 |
+
"""
|
| 252 |
+
Find the cost with self.full_circuit with the given angles.
|
| 253 |
+
This method changes the angles of self.full_circuit
|
| 254 |
+
:param angles: New angles
|
| 255 |
+
:param grad: Gradient of circuit (used by gradient-based optimizers)
|
| 256 |
+
which is modified in place
|
| 257 |
+
:return: Cost (float)
|
| 258 |
+
"""
|
| 259 |
+
co.update_angles_in_circuit(
|
| 260 |
+
self.full_circuit, angles, self.variational_circuit_range()
|
| 261 |
+
)
|
| 262 |
+
if grad is not None and grad.size > 0:
|
| 263 |
+
self._update_gradient_of_circuit(grad)
|
| 264 |
+
cost = self.cost_finder()
|
| 265 |
+
return cost
|
| 266 |
+
|
| 267 |
+
def _reduce_cost(
|
| 268 |
+
self,
|
| 269 |
+
change_1q_gate_kind=False,
|
| 270 |
+
indexes_to_modify: Tuple[int, int] = None,
|
| 271 |
+
):
|
| 272 |
+
"""
|
| 273 |
+
For each gate in the full circuit, find the optimal angle (and gate
|
| 274 |
+
kind) while keeping all other gates fixed.
|
| 275 |
+
Sequentially cycles over gates w.r.t their index in circuit.data
|
| 276 |
+
:param change_1q_gate_kind: If true, the optimal gate kind (
|
| 277 |
+
rx/ry/rz) will be chosen for each gate
|
| 278 |
+
:param indexes_to_modify: If not None, all gates except those at
|
| 279 |
+
specified indexes will be fixed.
|
| 280 |
+
Indexes are relative to full_circuit.data
|
| 281 |
+
:return: New cost
|
| 282 |
+
"""
|
| 283 |
+
cost = 1
|
| 284 |
+
variational_circuit_range = self.variational_circuit_range()
|
| 285 |
+
if indexes_to_modify is None:
|
| 286 |
+
indexes_to_modify = variational_circuit_range
|
| 287 |
+
else:
|
| 288 |
+
indexes_to_modify = (
|
| 289 |
+
max(indexes_to_modify[0], variational_circuit_range[0]),
|
| 290 |
+
min(indexes_to_modify[1], variational_circuit_range[1]),
|
| 291 |
+
)
|
| 292 |
+
|
| 293 |
+
if self.rotosolve_fraction < 1.0 and not change_1q_gate_kind:
|
| 294 |
+
indexes_to_modify_list = list(range(*indexes_to_modify))
|
| 295 |
+
indexes_to_modify_list = find_rotation_indices(
|
| 296 |
+
self.full_circuit, indexes_to_modify_list
|
| 297 |
+
)
|
| 298 |
+
num_to_sample = int(
|
| 299 |
+
np.ceil(self.rotosolve_fraction * len(indexes_to_modify_list))
|
| 300 |
+
)
|
| 301 |
+
sample = random.sample(indexes_to_modify_list, num_to_sample)
|
| 302 |
+
sample.sort()
|
| 303 |
+
else:
|
| 304 |
+
sample = list(range(*indexes_to_modify))
|
| 305 |
+
|
| 306 |
+
for index in sample:
|
| 307 |
+
old_gate = self.full_circuit.data[index].operation
|
| 308 |
+
|
| 309 |
+
if change_1q_gate_kind and co.is_supported_1q_gate(old_gate):
|
| 310 |
+
cost = self.replace_with_best_1q_gate(index)
|
| 311 |
+
elif co.is_supported_1q_gate(old_gate):
|
| 312 |
+
angle, cost = self.find_best_angle(index, old_gate.label)
|
| 313 |
+
co.replace_1q_gate(self.full_circuit, index, old_gate.label, angle)
|
| 314 |
+
else:
|
| 315 |
+
continue
|
| 316 |
+
return cost
|
| 317 |
+
|
| 318 |
+
def replace_with_best_1q_gate(self, gate_index):
|
| 319 |
+
"""
|
| 320 |
+
Find the gate which results in the lowest cost and replace the gate
|
| 321 |
+
at gate_index with the best gate
|
| 322 |
+
:param gate_index: The index of the gate that is to be replaced
|
| 323 |
+
:return: New cost
|
| 324 |
+
"""
|
| 325 |
+
# Find cost at 0 angle separately because it is the same regardless
|
| 326 |
+
# of gate kind
|
| 327 |
+
co.replace_1q_gate(self.full_circuit, gate_index, "rx", 0)
|
| 328 |
+
cost_identity = self.cost_finder()
|
| 329 |
+
best_gate_name, best_gate_angle, best_gate_cost = None, None, 1
|
| 330 |
+
# TODO Could this loop be parallelised?
|
| 331 |
+
for gate_name in SUPPORTED_1Q_GATES:
|
| 332 |
+
min_angle, cost = self.find_best_angle(gate_index, gate_name, cost_identity)
|
| 333 |
+
if cost < best_gate_cost:
|
| 334 |
+
best_gate_name, best_gate_angle, best_gate_cost = (
|
| 335 |
+
gate_name,
|
| 336 |
+
min_angle,
|
| 337 |
+
cost,
|
| 338 |
+
)
|
| 339 |
+
co.replace_1q_gate(
|
| 340 |
+
self.full_circuit, gate_index, best_gate_name, best_gate_angle
|
| 341 |
+
)
|
| 342 |
+
return best_gate_cost
|
| 343 |
+
|
| 344 |
+
def find_best_angle(self, gate_index, gate_name, cost_for_identity=None):
|
| 345 |
+
"""
|
| 346 |
+
Find the angle of the specified gate which results in the lowest cost
|
| 347 |
+
:param gate_index: The index of the gate that is to be checked
|
| 348 |
+
:param gate_name: Name of the gate kind that is to be used
|
| 349 |
+
:param cost_for_identity: The cost when the angle is 0
|
| 350 |
+
:return: best_gate_angle, best_cost
|
| 351 |
+
"""
|
| 352 |
+
# Remember original gate
|
| 353 |
+
circ_instr = self.full_circuit.data[gate_index]
|
| 354 |
+
|
| 355 |
+
costs = []
|
| 356 |
+
angles_to_run = [0, np.pi / 2, -np.pi / 2]
|
| 357 |
+
if cost_for_identity is not None:
|
| 358 |
+
costs.append(cost_for_identity)
|
| 359 |
+
angles_to_run.remove(0)
|
| 360 |
+
|
| 361 |
+
for theta in angles_to_run:
|
| 362 |
+
co.replace_1q_gate(self.full_circuit, gate_index, gate_name, theta)
|
| 363 |
+
costs.append(self.cost_finder())
|
| 364 |
+
theta_min, cost_min = minimum_of_sinusoidal(costs[0], costs[1], costs[2])
|
| 365 |
+
|
| 366 |
+
# Replace with original gate
|
| 367 |
+
self.full_circuit.data[gate_index] = circ_instr
|
| 368 |
+
return theta_min, cost_min
|
| 369 |
+
|
| 370 |
+
def _update_gradient_of_circuit(self, grad, method="parameter_shift"):
|
| 371 |
+
"""
|
| 372 |
+
Evaluates the gradient of the circuit (list of partial derivatives
|
| 373 |
+
of cost w.r.t each rotation angle)
|
| 374 |
+
:param grad: Old gradient (modified in place)
|
| 375 |
+
"""
|
| 376 |
+
angles = co.find_angles_in_circuit(self.full_circuit)
|
| 377 |
+
angle_index = 0
|
| 378 |
+
for gate_index in range(*self.variational_circuit_range()):
|
| 379 |
+
gate = self.full_circuit.data[gate_index].operation
|
| 380 |
+
if co.is_supported_1q_gate(gate):
|
| 381 |
+
# Calculate partial derivative
|
| 382 |
+
if method == "parameter_shift":
|
| 383 |
+
r = 0.5
|
| 384 |
+
shift = np.pi * (1 / (4 * r))
|
| 385 |
+
current_angle = angles[angle_index]
|
| 386 |
+
co.replace_1q_gate(
|
| 387 |
+
self.full_circuit, gate_index, gate.label, current_angle + shift
|
| 388 |
+
)
|
| 389 |
+
value_plus = self.cost_finder()
|
| 390 |
+
co.replace_1q_gate(
|
| 391 |
+
self.full_circuit, gate_index, gate.label, current_angle - shift
|
| 392 |
+
)
|
| 393 |
+
value_minus = self.cost_finder()
|
| 394 |
+
|
| 395 |
+
grad[angle_index] = r * (value_plus - value_minus)
|
| 396 |
+
|
| 397 |
+
else:
|
| 398 |
+
co.replace_1q_gate(self.full_circuit, gate_index, gate.label, 0)
|
| 399 |
+
value_0 = self.cost_finder()
|
| 400 |
+
co.replace_1q_gate(
|
| 401 |
+
self.full_circuit, gate_index, gate.label, np.pi / 2
|
| 402 |
+
)
|
| 403 |
+
value_pi_by_2 = self.cost_finder()
|
| 404 |
+
co.replace_1q_gate(
|
| 405 |
+
self.full_circuit, gate_index, gate.label, -np.pi / 2
|
| 406 |
+
)
|
| 407 |
+
value_minus_pi_by_2 = self.cost_finder()
|
| 408 |
+
|
| 409 |
+
grad[angle_index] = derivative_of_sinusoidal(
|
| 410 |
+
angles[angle_index], value_0, value_pi_by_2, value_minus_pi_by_2
|
| 411 |
+
)
|
| 412 |
+
|
| 413 |
+
# Return circuit back to original
|
| 414 |
+
co.replace_1q_gate(
|
| 415 |
+
self.full_circuit, gate_index, gate.label, angles[angle_index]
|
| 416 |
+
)
|
| 417 |
+
|
| 418 |
+
angle_index += 1
|
utils/adapt-aqc/adaptaqc/utils/entanglement_measures.py
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# (C) Copyright IBM 2025.
|
| 2 |
+
#
|
| 3 |
+
# This code is licensed under the Apache License, Version 2.0. You may
|
| 4 |
+
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
| 5 |
+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
| 6 |
+
#
|
| 7 |
+
# Any modifications or derivative works of this code must retain this
|
| 8 |
+
# copyright notice, and modified files need to carry a notice indicating
|
| 9 |
+
# that they have been altered from the originals.
|
| 10 |
+
|
| 11 |
+
"""Contains functions to measure quantum correlations"""
|
| 12 |
+
import copy
|
| 13 |
+
import itertools
|
| 14 |
+
import logging
|
| 15 |
+
|
| 16 |
+
import aqc_research.mps_operations as mpsops
|
| 17 |
+
import numpy as np
|
| 18 |
+
from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister
|
| 19 |
+
from qiskit import quantum_info as qi
|
| 20 |
+
from qiskit_aer.backends.aerbackend import AerBackend
|
| 21 |
+
from qiskit_experiments.library import StateTomography
|
| 22 |
+
from scipy import linalg
|
| 23 |
+
from scipy.linalg import eig
|
| 24 |
+
|
| 25 |
+
import adaptaqc.utils.circuit_operations as co
|
| 26 |
+
from adaptaqc.backends.aer_mps_backend import AerMPSBackend
|
| 27 |
+
from adaptaqc.backends.aqc_backend import AQCBackend
|
| 28 |
+
from adaptaqc.utils.utilityfunctions import is_statevector_backend
|
| 29 |
+
|
| 30 |
+
logger = logging.getLogger(__name__)
|
| 31 |
+
|
| 32 |
+
EM_OBSERVABLE_CONCURRENCE_LOWER_BOUND = "EM_OBSERVABLE_CONCURRENCE_LOWER_BOUND"
|
| 33 |
+
EM_TOMOGRAPHY_EOF = "EM_TOMOGRAPHY_EOF"
|
| 34 |
+
EM_TOMOGRAPHY_CONCURRENCE = "EM_TOMOGRAPHY_CONCURRENCE"
|
| 35 |
+
EM_TOMOGRAPHY_NEGATIVITY = "EM_TOMOGRAPHY_NEGATIVITY"
|
| 36 |
+
EM_TOMOGRAPHY_LOG_NEGATIVITY = "EM_TOMOGRAPHY_LOG_NEGATIVITY"
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def calculate_entanglement_measure(
|
| 40 |
+
method,
|
| 41 |
+
circuit,
|
| 42 |
+
qubit_1,
|
| 43 |
+
qubit_2,
|
| 44 |
+
backend: AQCBackend,
|
| 45 |
+
backend_options=None,
|
| 46 |
+
execute_kwargs=None,
|
| 47 |
+
mps=None,
|
| 48 |
+
):
|
| 49 |
+
"""
|
| 50 |
+
Measure quantum correlations between two qubits in a state resulting
|
| 51 |
+
from running a given
|
| 52 |
+
QuantumCircuit
|
| 53 |
+
:param method: Which entanglement measure/method to use
|
| 54 |
+
:param circuit: QuantumCircuit
|
| 55 |
+
:param qubit_1: Index of first qubit
|
| 56 |
+
:param qubit_2: Index of second qubit
|
| 57 |
+
:param backend: Backend on which circuits are to be run. Not relevant
|
| 58 |
+
for tomography based
|
| 59 |
+
entanglement measures.
|
| 60 |
+
Note that observable based entanglement measures can't run on
|
| 61 |
+
statevector_simulators
|
| 62 |
+
:param backend_options:
|
| 63 |
+
:param execute_kwargs:
|
| 64 |
+
:return: Value of quantum correlation
|
| 65 |
+
"""
|
| 66 |
+
if method == EM_OBSERVABLE_CONCURRENCE_LOWER_BOUND:
|
| 67 |
+
return measure_concurrence_lower_bound(
|
| 68 |
+
circuit, qubit_1, qubit_2, backend, backend_options, execute_kwargs
|
| 69 |
+
)
|
| 70 |
+
else:
|
| 71 |
+
if is_statevector_backend(backend):
|
| 72 |
+
statevector = co.run_circuit_without_transpilation(
|
| 73 |
+
circuit, backend, return_statevector=True
|
| 74 |
+
)
|
| 75 |
+
rho = partial_trace(statevector, qubit_1, qubit_2)
|
| 76 |
+
elif isinstance(backend, AerMPSBackend):
|
| 77 |
+
rho = mpsops.partial_trace(
|
| 78 |
+
mps, [qubit_1, qubit_2], already_preprocessed=True
|
| 79 |
+
)
|
| 80 |
+
else:
|
| 81 |
+
rho = perform_quantum_tomography(
|
| 82 |
+
circuit,
|
| 83 |
+
qubit_1,
|
| 84 |
+
qubit_2,
|
| 85 |
+
backend.simulator,
|
| 86 |
+
backend_options,
|
| 87 |
+
execute_kwargs,
|
| 88 |
+
)
|
| 89 |
+
if method == EM_TOMOGRAPHY_EOF:
|
| 90 |
+
return eof(rho)
|
| 91 |
+
elif method == EM_TOMOGRAPHY_CONCURRENCE:
|
| 92 |
+
return concurrence(rho)
|
| 93 |
+
elif method == EM_TOMOGRAPHY_NEGATIVITY:
|
| 94 |
+
return negativity(rho)
|
| 95 |
+
elif method == EM_TOMOGRAPHY_LOG_NEGATIVITY:
|
| 96 |
+
return log_negativity(rho)
|
| 97 |
+
else:
|
| 98 |
+
raise ValueError("Invalid entanglement measure method")
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def perform_quantum_tomography(
|
| 102 |
+
circuit: QuantumCircuit,
|
| 103 |
+
qubit_1,
|
| 104 |
+
qubit_2,
|
| 105 |
+
backend,
|
| 106 |
+
backend_options=None,
|
| 107 |
+
execute_kwargs=None,
|
| 108 |
+
):
|
| 109 |
+
"""
|
| 110 |
+
Performs quantum state tomography on the reduced state of qubit_1 and
|
| 111 |
+
qubit_2
|
| 112 |
+
:param circuit:
|
| 113 |
+
:param qubit_1:
|
| 114 |
+
:param qubit_2:
|
| 115 |
+
:param backend:
|
| 116 |
+
:param backend_options:
|
| 117 |
+
:param execute_kwargs:
|
| 118 |
+
:return:
|
| 119 |
+
"""
|
| 120 |
+
execute_kwargs = {} if execute_kwargs is None else execute_kwargs
|
| 121 |
+
old_cregs = circuit.cregs.copy()
|
| 122 |
+
circuit.cregs = []
|
| 123 |
+
tomography_exp = StateTomography(
|
| 124 |
+
circuit, measurement_indices=sorted([qubit_1, qubit_2])
|
| 125 |
+
)
|
| 126 |
+
circuit.cregs = old_cregs
|
| 127 |
+
|
| 128 |
+
# Backend options only supported for simulators
|
| 129 |
+
if backend_options is None or not isinstance(backend, AerBackend):
|
| 130 |
+
backend_options = {}
|
| 131 |
+
|
| 132 |
+
tomography_data = tomography_exp.run(backend, **execute_kwargs)
|
| 133 |
+
rho = tomography_data.analysis_results("state").value._data
|
| 134 |
+
assert isinstance(rho, np.ndarray)
|
| 135 |
+
return rho
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def measure_concurrence_lower_bound(
|
| 139 |
+
circuit: QuantumCircuit,
|
| 140 |
+
qubit_1,
|
| 141 |
+
qubit_2,
|
| 142 |
+
backend,
|
| 143 |
+
backend_options=None,
|
| 144 |
+
execute_kwargs=None,
|
| 145 |
+
):
|
| 146 |
+
"""
|
| 147 |
+
Measures the lower limit of the concurrence of the mixed, bipartite
|
| 148 |
+
state resulting from a
|
| 149 |
+
partial trace over all
|
| 150 |
+
qubits except those in qubit_pair
|
| 151 |
+
Lower bound based on 10.1103/PhysRevLett.98.140505 and upper bound based on
|
| 152 |
+
10.1103/PhysRevA.78.042308
|
| 153 |
+
Concurrence C bounds: K_1,K_2>= C^2 >= V_1,V_2 :
|
| 154 |
+
V_1 = 4((P-)-(P+))x(P-) = 4(2(P-)-I)x(P-) = 8(P-)x(P-) - 4(I)x(P-),
|
| 155 |
+
V_2 = 4(P-)x((P-)-(P+)) = 4(P-)x(2(P-)-I) = 8(P-)x(P-) - 4(P-)x(I),
|
| 156 |
+
K_1 = 4(P-)x(I),
|
| 157 |
+
K_2 = 4(I)x(P-),
|
| 158 |
+
where (P+) and (P-) are projectors on the symmetric and antisymmetric
|
| 159 |
+
subspace
|
| 160 |
+
of the two copies of either subsystem. I is the identity operator
|
| 161 |
+
:param circuit: QuantumCircuit
|
| 162 |
+
:param qubit_1: Index of the first qubit forming the bipartite state
|
| 163 |
+
:param qubit_2: Index of the second qubit forming the bipartite state
|
| 164 |
+
:param backend: Backend on which circuit is to be run (can't be
|
| 165 |
+
statevector_simulator)
|
| 166 |
+
:param backend_options:
|
| 167 |
+
:param execute_kwargs:
|
| 168 |
+
:returns Minimum value of concurrence
|
| 169 |
+
"""
|
| 170 |
+
# Remove measurements and other classical gates
|
| 171 |
+
classical_gates = co.remove_classical_operations(circuit)
|
| 172 |
+
num_qubits = circuit.num_qubits
|
| 173 |
+
|
| 174 |
+
qc = QuantumCircuit(2 * num_qubits, 4)
|
| 175 |
+
co.add_to_circuit(qc, circuit.copy(), qubit_subset=list(range(0, num_qubits)))
|
| 176 |
+
co.add_to_circuit(
|
| 177 |
+
qc, circuit.copy(), qubit_subset=list(range(num_qubits, 2 * num_qubits))
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
transpile_kwargs = {"backend": backend.simulator}
|
| 181 |
+
|
| 182 |
+
p_minus_p_minus_circuit = qc.copy()
|
| 183 |
+
co.add_to_circuit(
|
| 184 |
+
p_minus_p_minus_circuit,
|
| 185 |
+
antisymmetric_subspace_projector_measurement_circuit(),
|
| 186 |
+
qubit_subset=[qubit_1, num_qubits + qubit_1],
|
| 187 |
+
clbit_subset=[0, 1],
|
| 188 |
+
transpile_before_adding=True,
|
| 189 |
+
transpile_kwargs=transpile_kwargs,
|
| 190 |
+
)
|
| 191 |
+
co.add_to_circuit(
|
| 192 |
+
p_minus_p_minus_circuit,
|
| 193 |
+
antisymmetric_subspace_projector_measurement_circuit(),
|
| 194 |
+
qubit_subset=[qubit_2, num_qubits + qubit_2],
|
| 195 |
+
clbit_subset=[2, 3],
|
| 196 |
+
transpile_before_adding=True,
|
| 197 |
+
transpile_kwargs=transpile_kwargs,
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
p_minus_i_circuit = qc.copy()
|
| 201 |
+
co.add_to_circuit(
|
| 202 |
+
p_minus_i_circuit,
|
| 203 |
+
antisymmetric_subspace_projector_measurement_circuit(),
|
| 204 |
+
qubit_subset=[qubit_1, num_qubits + qubit_1],
|
| 205 |
+
clbit_subset=[0, 1],
|
| 206 |
+
transpile_before_adding=True,
|
| 207 |
+
transpile_kwargs=transpile_kwargs,
|
| 208 |
+
)
|
| 209 |
+
|
| 210 |
+
i_p_minus_circuit = qc.copy()
|
| 211 |
+
co.add_to_circuit(
|
| 212 |
+
i_p_minus_circuit,
|
| 213 |
+
antisymmetric_subspace_projector_measurement_circuit(),
|
| 214 |
+
qubit_subset=[qubit_2, num_qubits + qubit_2],
|
| 215 |
+
clbit_subset=[2, 3],
|
| 216 |
+
transpile_before_adding=True,
|
| 217 |
+
transpile_kwargs=transpile_kwargs,
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
p_minus_p_minus_counts = co.run_circuit_without_transpilation(
|
| 221 |
+
p_minus_p_minus_circuit, backend, backend_options, execute_kwargs
|
| 222 |
+
)
|
| 223 |
+
p_minus_i_counts = co.run_circuit_without_transpilation(
|
| 224 |
+
p_minus_i_circuit, backend, backend_options, execute_kwargs
|
| 225 |
+
)
|
| 226 |
+
i_p_minus_counts = co.run_circuit_without_transpilation(
|
| 227 |
+
i_p_minus_circuit, backend, backend_options, execute_kwargs
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
if "1111" not in p_minus_p_minus_counts:
|
| 231 |
+
p_minus_p_minus_eval = 0
|
| 232 |
+
else:
|
| 233 |
+
p_minus_p_minus_eval = p_minus_p_minus_counts["1111"] / sum(
|
| 234 |
+
p_minus_p_minus_counts.values()
|
| 235 |
+
)
|
| 236 |
+
|
| 237 |
+
if "1100" not in i_p_minus_counts:
|
| 238 |
+
i_p_minus_eval = 0
|
| 239 |
+
else:
|
| 240 |
+
i_p_minus_eval = i_p_minus_counts["1100"] / sum(i_p_minus_counts.values())
|
| 241 |
+
|
| 242 |
+
if "0011" not in p_minus_i_counts:
|
| 243 |
+
p_minus_i_eval = 0
|
| 244 |
+
else:
|
| 245 |
+
p_minus_i_eval = p_minus_i_counts["0011"] / sum(p_minus_i_counts.values())
|
| 246 |
+
|
| 247 |
+
v1 = 8 * p_minus_p_minus_eval - 4 * i_p_minus_eval
|
| 248 |
+
v2 = 8 * p_minus_p_minus_eval - 4 * p_minus_i_eval
|
| 249 |
+
lower_bound = max(v1, v2)
|
| 250 |
+
# k1 = 4 * p_minus_i_eval
|
| 251 |
+
# k2 = 4 * i_p_minus_eval
|
| 252 |
+
# upper_bound = min(k1, k2)
|
| 253 |
+
|
| 254 |
+
# Add back the classical gates
|
| 255 |
+
co.add_classical_operations(circuit, classical_gates)
|
| 256 |
+
return lower_bound
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
# Tomography based entanglement measures
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
def eof(rho):
|
| 263 |
+
"""
|
| 264 |
+
Mixed state entanglement of formation as defined in PhysRevLett.80.2245
|
| 265 |
+
:param rho: 2-qubit density matrix (pure or mixed)
|
| 266 |
+
:return:
|
| 267 |
+
"""
|
| 268 |
+
|
| 269 |
+
def h(x):
|
| 270 |
+
return (-x * np.log2(x)) - ((1 - x) * np.log2(1 - x))
|
| 271 |
+
|
| 272 |
+
c = concurrence(rho)
|
| 273 |
+
if c == 0:
|
| 274 |
+
return 0
|
| 275 |
+
return h(0.5 * (1 + np.sqrt(1 - c**2)))
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
def concurrence(rho):
|
| 279 |
+
"""
|
| 280 |
+
Mixed state concurrence as defined in PhysRevLett.80.2245
|
| 281 |
+
:param rho: 2-qubit density matrix (pure or mixed)
|
| 282 |
+
:return:
|
| 283 |
+
"""
|
| 284 |
+
sigma_y = np.array([[0, -1j], [1j, 0]])
|
| 285 |
+
sigma_y_sigma_y = np.kron(sigma_y, sigma_y)
|
| 286 |
+
rho_tilda = sigma_y_sigma_y @ rho.conjugate() @ sigma_y_sigma_y
|
| 287 |
+
eigenvalues = eig(rho @ rho_tilda, left=False, right=False)
|
| 288 |
+
# Make sure eigenvalues are real
|
| 289 |
+
if np.allclose(np.imag(eigenvalues), 0):
|
| 290 |
+
eigenvalues = np.real(eigenvalues)
|
| 291 |
+
else:
|
| 292 |
+
logger.warning(f"When calculating concurrence,eigenvalues were not real")
|
| 293 |
+
return 0
|
| 294 |
+
lambdas = np.sqrt(eigenvalues.clip(min=0))
|
| 295 |
+
lambdas = sorted(lambdas, reverse=True)
|
| 296 |
+
return np.max([0, lambdas[0] - lambdas[1] - lambdas[2] - lambdas[3]])
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
def negativity(rho):
|
| 300 |
+
transposed = partial_transpose(rho)
|
| 301 |
+
t_norm = trace_norm(transposed)
|
| 302 |
+
return (t_norm - 1) / 2
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
def log_negativity(rho):
|
| 306 |
+
transposed = partial_transpose(rho)
|
| 307 |
+
t_norm = trace_norm(transposed)
|
| 308 |
+
return np.log2(t_norm)
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
# Helper functions
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
def antisymmetric_subspace_projector_measurement_circuit():
|
| 315 |
+
qr = QuantumRegister(2, "projection_qr")
|
| 316 |
+
cr = ClassicalRegister(2, "projection_cr")
|
| 317 |
+
qc = QuantumCircuit(qr, cr)
|
| 318 |
+
qc.cx(0, 1)
|
| 319 |
+
qc.h(0)
|
| 320 |
+
qc.measure(0, 0)
|
| 321 |
+
qc.measure(1, 1)
|
| 322 |
+
return qc.copy()
|
| 323 |
+
|
| 324 |
+
|
| 325 |
+
def partial_trace(statevector, a, b):
|
| 326 |
+
"""
|
| 327 |
+
Partial trace over all subsystems except qubit a and qubit b
|
| 328 |
+
:param statevector: Statevector
|
| 329 |
+
:param a: qubit a
|
| 330 |
+
:param b: qubit b
|
| 331 |
+
:return: Density matrix
|
| 332 |
+
"""
|
| 333 |
+
num_qubits = int(np.log2(len(statevector)))
|
| 334 |
+
if num_qubits == 2:
|
| 335 |
+
return np.outer(statevector, statevector.conj())
|
| 336 |
+
qubits_to_trace_over = list(range(num_qubits))
|
| 337 |
+
qubits_to_trace_over.remove(a)
|
| 338 |
+
qubits_to_trace_over.remove(b)
|
| 339 |
+
|
| 340 |
+
return qi.partial_trace(statevector, qubits_to_trace_over).data
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
def partial_transpose(density_matrix, wrt=1):
|
| 344 |
+
"""
|
| 345 |
+
Partial transpose of density matrix
|
| 346 |
+
:param density_matrix: Bipartite system density matrix
|
| 347 |
+
:param wrt: Which subsystem transpose is supposed to be carried over
|
| 348 |
+
:return: density matrix
|
| 349 |
+
"""
|
| 350 |
+
tp = copy.deepcopy(density_matrix)
|
| 351 |
+
for ja, ka, jb, kb in itertools.product(range(2), range(2), range(2), range(2)):
|
| 352 |
+
if wrt == 1:
|
| 353 |
+
tp[ka * 2 + jb][ja * 2 + kb] = density_matrix[ja * 2 + jb][ka * 2 + kb]
|
| 354 |
+
elif wrt == 2:
|
| 355 |
+
tp[ja * 2 + kb][ka * 2 + jb] = density_matrix[ja * 2 + jb][ka * 2 + kb]
|
| 356 |
+
return tp
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
def trace_norm(density_matrix):
|
| 360 |
+
"""
|
| 361 |
+
Evaluate trace norm of density matrix
|
| 362 |
+
:return: float
|
| 363 |
+
"""
|
| 364 |
+
return np.real(
|
| 365 |
+
np.trace(
|
| 366 |
+
linalg.sqrtm(
|
| 367 |
+
np.matmul(density_matrix, np.conjugate(density_matrix).transpose())
|
| 368 |
+
)
|
| 369 |
+
)
|
| 370 |
+
)
|
utils/adapt-aqc/adaptaqc/utils/fixed_ansatz_circuits.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# (C) Copyright IBM 2025.
|
| 2 |
+
#
|
| 3 |
+
# This code is licensed under the Apache License, Version 2.0. You may
|
| 4 |
+
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
| 5 |
+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
| 6 |
+
#
|
| 7 |
+
# Any modifications or derivative works of this code must retain this
|
| 8 |
+
# copyright notice, and modified files need to carry a notice indicating
|
| 9 |
+
# that they have been altered from the originals.
|
| 10 |
+
|
| 11 |
+
"""Contains variational circuit ansatz such as hardware efficient ansatz"""
|
| 12 |
+
from qiskit import QuantumCircuit, QuantumRegister
|
| 13 |
+
|
| 14 |
+
import adaptaqc.utils.circuit_operations as co
|
| 15 |
+
import adaptaqc.utils.constants as vconstants
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def hardware_efficient_circuit(
|
| 19 |
+
num_qubits,
|
| 20 |
+
ansatz_kind,
|
| 21 |
+
ansatz_depth,
|
| 22 |
+
entangling_gate="cx",
|
| 23 |
+
coupling_map=None,
|
| 24 |
+
gates_to_fix=None,
|
| 25 |
+
gates_to_remove=None,
|
| 26 |
+
):
|
| 27 |
+
"""
|
| 28 |
+
Create a hardware efficient ansatz circuit. Each depth has a layers of
|
| 29 |
+
rotation gates followed by entangling gates.
|
| 30 |
+
The indexes are relative to the order in which the rotation gates are
|
| 31 |
+
added.
|
| 32 |
+
In example circuit, index of gate is shown after the gate name.
|
| 33 |
+
Example circuit (num_qubits:3, ansatz_kind: 'rxry', ansatz_depth:2,
|
| 34 |
+
linear entangling):
|
| 35 |
+
┌───────┐┌───────┐ ┌───────┐┌───────┐
|
| 36 |
+
┤ Rx(0) ├┤ Ry(1) ├──■──┤ Rx(6) ├┤ Ry(7) ├───────────■───────────
|
| 37 |
+
├───────┤├───────┤┌─┴─┐└───────┘├───────┤┌───────┐┌─┴─┐
|
| 38 |
+
┤ Rx(2) ├┤ Ry(3) ├┤ X ├────■────┤ Rx(8) ├┤ Ry(9) ├┤ X ├────■────
|
| 39 |
+
├───────┤├───────┤└───┘ ┌─┴─┐ ├───────┤├───────┤└───┘ ┌─┴─┐
|
| 40 |
+
┤ Rx(4) ├┤ Ry(5) ├───────┤ X ├──┤ Rx(10)├┤ Ry(11)├───────┤ X ├──
|
| 41 |
+
└───────┘└───────┘ └───┘ └───────┘└───────┘ └───┘
|
| 42 |
+
:param num_qubits: The number of qubits in circuit
|
| 43 |
+
:param ansatz_kind: String name of rotation gates (e.g. 'ry', 'rxry',
|
| 44 |
+
'rzryrz')
|
| 45 |
+
:param ansatz_depth: Number of layers of (rotation gates+entangling gates)
|
| 46 |
+
:param entangling_gate: Entangling gate to use ('cx' or 'cz')
|
| 47 |
+
:param coupling_map: Map of entangling gates of the form [(control,
|
| 48 |
+
target)]. Gates are added sequentially.
|
| 49 |
+
If None, linear coupling layout is used
|
| 50 |
+
:param gates_to_fix: The indexes and angles of the gates which are to be
|
| 51 |
+
fixed (FIXED_GATE_LABEL is added to gate)
|
| 52 |
+
Must be of form {index:angle}
|
| 53 |
+
:param gates_to_remove: Indexes of gates which are to be removed
|
| 54 |
+
:return: QuantumCircuit
|
| 55 |
+
"""
|
| 56 |
+
qr = QuantumRegister(num_qubits)
|
| 57 |
+
qc = QuantumCircuit(qr)
|
| 58 |
+
|
| 59 |
+
if coupling_map is None:
|
| 60 |
+
coupling_map = vconstants.coupling_map_linear(num_qubits)
|
| 61 |
+
if gates_to_remove is None:
|
| 62 |
+
gates_to_remove = []
|
| 63 |
+
if gates_to_fix is None:
|
| 64 |
+
gates_to_fix = {}
|
| 65 |
+
|
| 66 |
+
index = 0
|
| 67 |
+
for _ in range(ansatz_depth):
|
| 68 |
+
# Add rotation gates
|
| 69 |
+
for qubit in range(num_qubits):
|
| 70 |
+
for gate_name in [
|
| 71 |
+
ansatz_kind[i : i + 2] for i in range(0, len(ansatz_kind), 2)
|
| 72 |
+
]:
|
| 73 |
+
gate = co.create_1q_gate(gate_name, 0)
|
| 74 |
+
if index in gates_to_fix:
|
| 75 |
+
gate.label = vconstants.FIXED_GATE_LABEL
|
| 76 |
+
gate.params[0] = gates_to_fix[index]
|
| 77 |
+
if index not in gates_to_remove:
|
| 78 |
+
qc.append(gate, [qr[qubit]])
|
| 79 |
+
index += 1
|
| 80 |
+
|
| 81 |
+
for control, target in coupling_map:
|
| 82 |
+
qc.append(co.create_2q_gate(entangling_gate), [qr[control], qr[target]])
|
| 83 |
+
|
| 84 |
+
return qc
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def number_preserving_ansatz(num_qubits, ansatz_depth):
|
| 88 |
+
coupling_map = vconstants.coupling_map_ladder(num_qubits)
|
| 89 |
+
|
| 90 |
+
qc = QuantumCircuit(num_qubits)
|
| 91 |
+
index = 0
|
| 92 |
+
for layer in range(ansatz_depth):
|
| 93 |
+
for control, target in coupling_map:
|
| 94 |
+
rz_gate = co.create_independent_parameterised_gate(
|
| 95 |
+
"rz", f"theta_" f"{index}"
|
| 96 |
+
)
|
| 97 |
+
minus_rz_gate = co.create_dependent_parameterised_gate(
|
| 98 |
+
"rz", f"-theta_" f"{index}"
|
| 99 |
+
)
|
| 100 |
+
ry_gate = co.create_independent_parameterised_gate("ry", f"phi_{index}")
|
| 101 |
+
minus_ry_gate = co.create_dependent_parameterised_gate(
|
| 102 |
+
"ry", f"-phi_" f"{index}"
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
qc.cx(control, target)
|
| 106 |
+
co.add_gate(qc, minus_rz_gate.copy(), qubit_indexes=[control])
|
| 107 |
+
co.add_gate(qc, minus_ry_gate.copy(), qubit_indexes=[control])
|
| 108 |
+
qc.cx(target, control)
|
| 109 |
+
co.add_gate(qc, ry_gate.copy(), qubit_indexes=[control])
|
| 110 |
+
co.add_gate(qc, rz_gate.copy(), qubit_indexes=[control])
|
| 111 |
+
qc.cx(control, target)
|
| 112 |
+
index += 1
|
| 113 |
+
return qc
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def custom_ansatz(num_qubits, two_qubit_circuit, ansatz_depth, coupling_map=None):
|
| 117 |
+
if coupling_map is None:
|
| 118 |
+
coupling_map = vconstants.coupling_map_ladder(num_qubits)
|
| 119 |
+
|
| 120 |
+
qc = QuantumCircuit(num_qubits)
|
| 121 |
+
for layer in range(ansatz_depth):
|
| 122 |
+
for control, target in coupling_map:
|
| 123 |
+
co.add_to_circuit(
|
| 124 |
+
qc, two_qubit_circuit.copy(), qubit_subset=[control, target]
|
| 125 |
+
)
|
| 126 |
+
return qc
|
utils/adapt-aqc/adaptaqc/utils/gate_tomography.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# (C) Copyright IBM 2025.
|
| 2 |
+
#
|
| 3 |
+
# This code is licensed under the Apache License, Version 2.0. You may
|
| 4 |
+
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
| 5 |
+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
| 6 |
+
#
|
| 7 |
+
# Any modifications or derivative works of this code must retain this
|
| 8 |
+
# copyright notice, and modified files need to carry a notice indicating
|
| 9 |
+
# that they have been altered from the originals.
|
| 10 |
+
|
| 11 |
+
"""Contains methods for performing n-gate tomography"""
|
| 12 |
+
import numpy as np
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def angle_sets_to_evaluate(num_params):
|
| 16 |
+
"""Return the angles at which the expectation values
|
| 17 |
+
are to be measured for num_param gate tomography
|
| 18 |
+
|
| 19 |
+
Args:
|
| 20 |
+
num_params (int): Number of rotation gates the
|
| 21 |
+
tomography is with respect to
|
| 22 |
+
|
| 23 |
+
Returns:
|
| 24 |
+
np.ndarray([3**num_params,num_params]): Ordered
|
| 25 |
+
array of parameters values
|
| 26 |
+
"""
|
| 27 |
+
angles = np.zeros([3**num_params, num_params])
|
| 28 |
+
for i in range(3**num_params):
|
| 29 |
+
base_3_str = np.base_repr(i, 3).zfill(num_params)
|
| 30 |
+
for j, ind in zip(range(num_params), base_3_str):
|
| 31 |
+
if ind == "0":
|
| 32 |
+
angles[i, j] = -np.pi / 2
|
| 33 |
+
elif ind == "1":
|
| 34 |
+
angles[i, j] = 0
|
| 35 |
+
elif ind == "2":
|
| 36 |
+
angles[i, j] = np.pi / 2
|
| 37 |
+
return angles
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def measurements_to_zero_delta_pi_bases(measurements):
|
| 41 |
+
"""Tomography requires expactation values when each
|
| 42 |
+
angle is either 0, np.pi/2,-np.pi/2, and np.pi.
|
| 43 |
+
This method calculates the value for np.pi from
|
| 44 |
+
the other 3 values and arranges the data in
|
| 45 |
+
appropriate form
|
| 46 |
+
|
| 47 |
+
Args:
|
| 48 |
+
measurements (np.ndarray): The expectation values
|
| 49 |
+
with angles obtained from angle_sets_to_evaluate
|
| 50 |
+
|
| 51 |
+
Returns:
|
| 52 |
+
np.ndarray: New expectation value measurements
|
| 53 |
+
"""
|
| 54 |
+
num_params = int(np.log(len(measurements)) / np.log(3))
|
| 55 |
+
new_measurements = np.array(measurements)
|
| 56 |
+
for j in range(num_params):
|
| 57 |
+
for i in range(3 ** (num_params - 1)):
|
| 58 |
+
if num_params == 1:
|
| 59 |
+
base_3_str = ""
|
| 60 |
+
else:
|
| 61 |
+
base_3_str = np.base_repr(i, 3).zfill(num_params - 1)
|
| 62 |
+
l_str = base_3_str[: num_params - (j + 1)]
|
| 63 |
+
r_str = base_3_str[num_params - (j + 1) :]
|
| 64 |
+
ind_0 = int(l_str + "0" + r_str, 3)
|
| 65 |
+
ind_1 = int(l_str + "1" + r_str, 3)
|
| 66 |
+
ind_2 = int(l_str + "2" + r_str, 3)
|
| 67 |
+
|
| 68 |
+
val_minus_pi_by_2 = new_measurements[ind_0]
|
| 69 |
+
val_0 = new_measurements[ind_1]
|
| 70 |
+
val_pi_by_2 = new_measurements[ind_2]
|
| 71 |
+
|
| 72 |
+
new_measurements[ind_0] = val_0
|
| 73 |
+
new_measurements[ind_1] = val_pi_by_2 - val_minus_pi_by_2
|
| 74 |
+
new_measurements[ind_2] = (val_pi_by_2 + val_minus_pi_by_2) - val_0
|
| 75 |
+
|
| 76 |
+
return new_measurements
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def reconstructed_cost(angles, measurements):
|
| 80 |
+
"""Calculate the cost from the tomography-reconstructed cost function
|
| 81 |
+
|
| 82 |
+
Args:
|
| 83 |
+
angles (np.ndarray): Angles to evaluate expectation value at
|
| 84 |
+
measurements (np.ndarray): Expectation values of tomography measurements
|
| 85 |
+
|
| 86 |
+
Returns:
|
| 87 |
+
float: Expectation value
|
| 88 |
+
"""
|
| 89 |
+
total = 0
|
| 90 |
+
num_params = len(angles)
|
| 91 |
+
for i in range(3**num_params):
|
| 92 |
+
product = 1
|
| 93 |
+
product *= measurements[i]
|
| 94 |
+
base_3_str = np.base_repr(i, 3).zfill(num_params)
|
| 95 |
+
for j in range(num_params):
|
| 96 |
+
angle = angles[j] / 2
|
| 97 |
+
if base_3_str[j] == "0":
|
| 98 |
+
product *= np.cos(angle) * np.cos(angle)
|
| 99 |
+
elif base_3_str[j] == "1":
|
| 100 |
+
product *= np.cos(angle) * np.sin(angle)
|
| 101 |
+
elif base_3_str[j] == "2":
|
| 102 |
+
product *= np.sin(angle) * np.sin(angle)
|
| 103 |
+
total += product
|
| 104 |
+
return total
|
utils/adapt-aqc/adaptaqc/utils/gradients.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# (C) Copyright IBM 2025.
|
| 2 |
+
#
|
| 3 |
+
# This code is licensed under the Apache License, Version 2.0. You may
|
| 4 |
+
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
| 5 |
+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
| 6 |
+
#
|
| 7 |
+
# Any modifications or derivative works of this code must retain this
|
| 8 |
+
# copyright notice, and modified files need to carry a notice indicating
|
| 9 |
+
# that they have been altered from the originals.
|
| 10 |
+
|
| 11 |
+
from typing import List, Tuple
|
| 12 |
+
|
| 13 |
+
import numpy as np
|
| 14 |
+
from aqc_research.mps_operations import mps_from_circuit, mps_dot
|
| 15 |
+
from qiskit import QuantumCircuit
|
| 16 |
+
|
| 17 |
+
from adaptaqc.backends.aer_mps_backend import AerMPSBackend
|
| 18 |
+
from adaptaqc.backends.python_default_backends import MPS_SIM
|
| 19 |
+
from adaptaqc.utils.circuit_operations import remove_unnecessary_2q_gates_from_circuit
|
| 20 |
+
from adaptaqc.utils.utilityfunctions import get_distinct_items_and_degeneracies
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def general_grad_of_pairs(
|
| 24 |
+
circuit: QuantumCircuit,
|
| 25 |
+
inverse_zero_ansatz: QuantumCircuit,
|
| 26 |
+
generators: List[QuantumCircuit],
|
| 27 |
+
degeneracies: List[int],
|
| 28 |
+
coupling_map: List[Tuple],
|
| 29 |
+
starting_circuit=None,
|
| 30 |
+
backend: AerMPSBackend = MPS_SIM,
|
| 31 |
+
):
|
| 32 |
+
"""
|
| 33 |
+
For an ansatz of the form U(θ) = U_N(θ_N) * ... * U_1(θ_1), parameterised by θ = (θ_1, ..., θ_N),
|
| 34 |
+
and with U_k(θ_k) = exp(-i * (θ_k / 2) * A_k), this function:
|
| 35 |
+
1. Calculates the cost-gradient with respect to each θ_k at θ=0. The gradient is given by:
|
| 36 |
+
dC/d(θ_k)|θ=0 = -imag(<s|G_k|ψ><ψ|U†(0)|s>) = g_k
|
| 37 |
+
where:
|
| 38 |
+
• |s> is the state obtained by acting with the starting_circuit on |0>
|
| 39 |
+
• U†(0) is the inverse of the ansatz evaluated at θ=0
|
| 40 |
+
• G_k = U_N(0) * ... * U_(k+1)(0) * A_k * U_(k-1)(0) * ... * U_1(0) I.e. the ansatz
|
| 41 |
+
evaluated at θ=0 BUT with U_k replaced by its generator A_k
|
| 42 |
+
2. Calculates the Euclidean norm of the gradients: g = sqrt(g_1 ** 2 + ... + g_N ** 2)
|
| 43 |
+
3. Returns a list of the gradient g for each pair in the coupling map
|
| 44 |
+
|
| 45 |
+
Args:
|
| 46 |
+
circuit (QuantumCircuit): a circuit representing |ψ>
|
| 47 |
+
inverse_zero_ansatz (QuantumCircuit): a circuit representing U†(0)
|
| 48 |
+
generators (List[QuantumCircuit]): a list of quantum circuits representing (G_k)†
|
| 49 |
+
degeneracies (List[int]): a list of the degeneracies of the generators
|
| 50 |
+
coupling_map (List[Tuple]): the list of all pairs of qubits for which to calculate the gradient
|
| 51 |
+
starting_circuit (QuantumCircuit): a circuit representing |s>
|
| 52 |
+
backend (AerSimulator): Aer MPS simulator used to generate relevant states
|
| 53 |
+
Returns:
|
| 54 |
+
gradients (List): List of gradients g for each pair
|
| 55 |
+
"""
|
| 56 |
+
gradients = []
|
| 57 |
+
ansatz_resolves_to_id = inverse_zero_ansatz == QuantumCircuit(2)
|
| 58 |
+
|
| 59 |
+
# Get MPS of |ψ>
|
| 60 |
+
circ_mps = mps_from_circuit(
|
| 61 |
+
circuit.copy(), return_preprocessed=True, sim=backend.simulator
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
# Get the starting circuit
|
| 65 |
+
if starting_circuit is not None:
|
| 66 |
+
starting_circuit = starting_circuit
|
| 67 |
+
else:
|
| 68 |
+
starting_circuit = QuantumCircuit(circuit.num_qubits)
|
| 69 |
+
|
| 70 |
+
# Only calculate <ψ|U†(0)|s> = <ψ|s> once if ansatz resolves to identity
|
| 71 |
+
if ansatz_resolves_to_id:
|
| 72 |
+
# Find |s>
|
| 73 |
+
starting_circuit_mps = mps_from_circuit(
|
| 74 |
+
starting_circuit.copy(), return_preprocessed=True, sim=backend.simulator
|
| 75 |
+
)
|
| 76 |
+
# Find <ψ|s>
|
| 77 |
+
zero_ansatz_overlap = mps_dot(
|
| 78 |
+
circ_mps, starting_circuit_mps, already_preprocessed=True
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
for control, target in coupling_map:
|
| 82 |
+
# Calculate <ψ|U†(0)|s> for each pair if ansatz does not resolve to identity
|
| 83 |
+
if not ansatz_resolves_to_id:
|
| 84 |
+
# Find U†(0)|s>
|
| 85 |
+
ansatz_on_starting_circuit = starting_circuit.compose(
|
| 86 |
+
inverse_zero_ansatz, [control, target]
|
| 87 |
+
)
|
| 88 |
+
ansatz_on_starting_circuit_mps = mps_from_circuit(
|
| 89 |
+
ansatz_on_starting_circuit,
|
| 90 |
+
return_preprocessed=True,
|
| 91 |
+
sim=backend.simulator,
|
| 92 |
+
)
|
| 93 |
+
# Find <ψ|U†(0)|s>
|
| 94 |
+
zero_ansatz_overlap = mps_dot(
|
| 95 |
+
circ_mps, ansatz_on_starting_circuit_mps, already_preprocessed=True
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
gradient = 0
|
| 99 |
+
for i, generator in enumerate(generators):
|
| 100 |
+
# Find (G_k)†|s>
|
| 101 |
+
generator_on_starting_circuit = starting_circuit.compose(
|
| 102 |
+
generator, [control, target]
|
| 103 |
+
)
|
| 104 |
+
generator_on_starting_circuit_mps = mps_from_circuit(
|
| 105 |
+
generator_on_starting_circuit,
|
| 106 |
+
return_preprocessed=True,
|
| 107 |
+
sim=backend.simulator,
|
| 108 |
+
)
|
| 109 |
+
# Find <s|G_k|ψ>, computed as the dot product of (G_k)†|s> and |ψ>
|
| 110 |
+
generator_overlap = mps_dot(
|
| 111 |
+
generator_on_starting_circuit_mps, circ_mps, already_preprocessed=True
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
generator_gradient = -1 * np.imag(generator_overlap * zero_ansatz_overlap)
|
| 115 |
+
|
| 116 |
+
# Add contribution to gradient from generator, accounting for degeneracy
|
| 117 |
+
gradient += (generator_gradient**2) * degeneracies[i]
|
| 118 |
+
|
| 119 |
+
# Calculate the Euclidean norm of the gradients for each generator
|
| 120 |
+
grad_norm = np.sqrt(gradient)
|
| 121 |
+
|
| 122 |
+
gradients.append(grad_norm)
|
| 123 |
+
|
| 124 |
+
return gradients
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def get_generators_and_degeneracies(
|
| 128 |
+
ansatz: QuantumCircuit, rotoselect: bool = False, inverse: bool = False
|
| 129 |
+
):
|
| 130 |
+
"""
|
| 131 |
+
For an ansatz of the form U(θ) = U_N(θ_N) * ... * U_1(θ_1), parameterised by θ = (θ_1, ..., θ_N),
|
| 132 |
+
and with U_k(θ_k) = exp(-i * (θ_k / 2) * A_k), this function finds the generators of the ansatz:
|
| 133 |
+
|
| 134 |
+
G_k = U_N(0) * ... * U_(k+1)(0) * A_k * U_(k-1)(0) * ... * U_1(0) I.e. the ansatz evaluated at
|
| 135 |
+
θ=0 BUT with U_k replaced by its generator A_k.
|
| 136 |
+
|
| 137 |
+
If rotoselect=True, for every rotation gate in the ansatz, return all three generators as if the
|
| 138 |
+
rotation gate was Rx, Ry, or Rz.
|
| 139 |
+
|
| 140 |
+
Args:
|
| 141 |
+
ansatz (QuantumCircuit): a circuit representing the ansatz U
|
| 142 |
+
rotoselect (bool): set to True to return the x, y, z generators for each rotation gate, set
|
| 143 |
+
to False to only return the specific generator for the gate.
|
| 144 |
+
inverse (bool): set to True to return the inverse of the generators
|
| 145 |
+
Returns:
|
| 146 |
+
generator_circuits (List[QuantumCircuit]): List of generators G_k (or their inverses), one
|
| 147 |
+
for each parameterised gate if rotoselect=False, three if rotoselect=True.
|
| 148 |
+
degeneracies (List[int]): List of degeneracies of generators.
|
| 149 |
+
"""
|
| 150 |
+
parameterised_gates = ["rx", "ry", "rz"]
|
| 151 |
+
generator_circuits = []
|
| 152 |
+
for i, circ_instr in enumerate(ansatz):
|
| 153 |
+
if circ_instr.operation.name in parameterised_gates:
|
| 154 |
+
if rotoselect:
|
| 155 |
+
# Get all Rx, Ry, Rz generators
|
| 156 |
+
for op in parameterised_gates:
|
| 157 |
+
generator = get_generator(ansatz, i, op)
|
| 158 |
+
generator_circuits.append(
|
| 159 |
+
generator.inverse() if inverse else generator
|
| 160 |
+
)
|
| 161 |
+
else:
|
| 162 |
+
# Get the generator for the specific gate
|
| 163 |
+
generator = get_generator(ansatz, i, circ_instr.operation.name)
|
| 164 |
+
generator_circuits.append(generator.inverse() if inverse else generator)
|
| 165 |
+
|
| 166 |
+
distinct_generators, degeneracies = get_distinct_items_and_degeneracies(
|
| 167 |
+
generator_circuits
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
return (distinct_generators, degeneracies)
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def get_generator(ansatz: QuantumCircuit, index: int, op: str):
|
| 174 |
+
"""
|
| 175 |
+
Given an ansatz consisting of only rx, ry, rz and cx gates, this function replaces the gate at
|
| 176 |
+
index=index with the generator of op, removes all other rotation gates, and removes consecutive
|
| 177 |
+
cx gates that would resolve to the identity.
|
| 178 |
+
|
| 179 |
+
Example:
|
| 180 |
+
index = 4, op = 'ry', ansatz:
|
| 181 |
+
┌───────┐ ┌───────┐ ┌───────┐
|
| 182 |
+
q_0: ┤ Rx(0) ├──■──┤ Rx(0) ├──■──┤ Rx(0) ├
|
| 183 |
+
├───────┤┌─┴─┐├───────┤┌─┴─┐├───────┤
|
| 184 |
+
q_1: ┤ Rx(0) ├┤ X ├┤ Rx(0) ├┤ X ├┤ Rx(0) ├
|
| 185 |
+
└───────┘└───┘└───────┘└───┘└───────┘
|
| 186 |
+
|
| 187 |
+
will return:
|
| 188 |
+
q_0: ──■─────────■──
|
| 189 |
+
┌─┴─┐┌───┐┌─┴─┐
|
| 190 |
+
q_1: ┤ X ├┤ Y ├┤ X ├
|
| 191 |
+
└───┘└───┘└───┘
|
| 192 |
+
|
| 193 |
+
Args:
|
| 194 |
+
ansatz (QuantumCircuit): a circuit representing the ansatz
|
| 195 |
+
index: the index of the operator to be replaced
|
| 196 |
+
op: the operator, one of rx, ry or rz, the generator of which will replace the gate at
|
| 197 |
+
index=index
|
| 198 |
+
Returns:
|
| 199 |
+
generator (QuantumCircuit): The generator
|
| 200 |
+
"""
|
| 201 |
+
supported_ops = ["rx", "ry", "rz"]
|
| 202 |
+
if op not in supported_ops:
|
| 203 |
+
raise ValueError("op must be one of rx, ry or rz")
|
| 204 |
+
|
| 205 |
+
generator = QuantumCircuit(2)
|
| 206 |
+
for i, circ_instr in enumerate(ansatz):
|
| 207 |
+
operation = circ_instr.operation
|
| 208 |
+
qubits = circ_instr.qubits
|
| 209 |
+
if operation.name not in ["rx", "ry", "rz", "cx"]:
|
| 210 |
+
raise ValueError("Circuit must only contain rx, ry, rz and cx gates")
|
| 211 |
+
if i == index:
|
| 212 |
+
if op == "rx":
|
| 213 |
+
generator.x(qubits[0])
|
| 214 |
+
if op == "ry":
|
| 215 |
+
generator.y(qubits[0])
|
| 216 |
+
if op == "rz":
|
| 217 |
+
generator.z(qubits[0])
|
| 218 |
+
if operation.name == "cx":
|
| 219 |
+
generator.cx(qubits[0], qubits[1])
|
| 220 |
+
|
| 221 |
+
# remove consecutive cx gates which resolve to the identity
|
| 222 |
+
remove_unnecessary_2q_gates_from_circuit(generator)
|
| 223 |
+
|
| 224 |
+
return generator
|
utils/adapt-aqc/adaptaqc/utils/hamiltonians.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# (C) Copyright IBM 2025.
|
| 2 |
+
#
|
| 3 |
+
# This code is licensed under the Apache License, Version 2.0. You may
|
| 4 |
+
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
| 5 |
+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
| 6 |
+
#
|
| 7 |
+
# Any modifications or derivative works of this code must retain this
|
| 8 |
+
# copyright notice, and modified files need to carry a notice indicating
|
| 9 |
+
# that they have been altered from the originals.
|
| 10 |
+
|
| 11 |
+
import numpy as np
|
| 12 |
+
from openfermion import (
|
| 13 |
+
FermionOperator,
|
| 14 |
+
QubitOperator,
|
| 15 |
+
get_ground_state,
|
| 16 |
+
get_sparse_operator,
|
| 17 |
+
jordan_wigner,
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def heisenberg_hamiltonian(
|
| 22 |
+
n=4, jx=1.0, jy=0.0, jz=0.0, hx=0.0, hy=0.0, hz=0.0, periodic_bc=False
|
| 23 |
+
):
|
| 24 |
+
"""
|
| 25 |
+
H = -sum_over_nn(j_x*X_i*X_(i+1) + j_y*Y_i*Y_(i+1) + j_z*Z_i*Z_(i+1))
|
| 26 |
+
-sum(h_x*X_i + h_y*Y_i + h_z*Z_i)
|
| 27 |
+
"""
|
| 28 |
+
ham = QubitOperator()
|
| 29 |
+
max_index = n if periodic_bc else n - 1
|
| 30 |
+
for i in range(max_index):
|
| 31 |
+
next_neighbour_index = 0 if i == n - 1 and periodic_bc else i + 1
|
| 32 |
+
ham += QubitOperator(f"X{i} X{next_neighbour_index}", -jx)
|
| 33 |
+
ham += QubitOperator(f"Y{i} Y{next_neighbour_index}", -jy)
|
| 34 |
+
ham += QubitOperator(f"Z{i} Z{next_neighbour_index}", -jz)
|
| 35 |
+
for i in range(n):
|
| 36 |
+
ham += QubitOperator(f"X{i}", -hx)
|
| 37 |
+
ham += QubitOperator(f"Y{i}", -hy)
|
| 38 |
+
ham += QubitOperator(f"Z{i}", -hz)
|
| 39 |
+
return ham
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def anderson_model_fermionic_hamiltonian(
|
| 43 |
+
v_i=np.array([0, 1]), epsilon_i=np.array([2, 2]), u=4, mu=0
|
| 44 |
+
):
|
| 45 |
+
if len(v_i) != len(epsilon_i):
|
| 46 |
+
raise ValueError(
|
| 47 |
+
f"Number of elements in v_i ({len(v_i)}) must equal number of "
|
| 48 |
+
f"elements in epsilon_i({len(epsilon_i)})"
|
| 49 |
+
)
|
| 50 |
+
num_bath_sites = len(v_i) - 1
|
| 51 |
+
ham = FermionOperator()
|
| 52 |
+
|
| 53 |
+
# Coulomb repulsion
|
| 54 |
+
ham += FermionOperator(f"0^ 0 {num_bath_sites + 1}^ {num_bath_sites + 1}", float(u))
|
| 55 |
+
|
| 56 |
+
# Bath site energies
|
| 57 |
+
for site_index in range(0, 1 + num_bath_sites):
|
| 58 |
+
for spin in range(2):
|
| 59 |
+
i = site_index + (spin * (1 + num_bath_sites))
|
| 60 |
+
ham += FermionOperator(f"{i}^ {i}", float(epsilon_i[site_index] - mu))
|
| 61 |
+
# Hybridization energies
|
| 62 |
+
for site_index in range(1, 1 + num_bath_sites):
|
| 63 |
+
for spin in range(2):
|
| 64 |
+
i = site_index + (spin * (1 + num_bath_sites))
|
| 65 |
+
impurity_index = spin * (1 + num_bath_sites)
|
| 66 |
+
ham += FermionOperator(f"{impurity_index}^ {i}", float(v_i[site_index]))
|
| 67 |
+
ham += FermionOperator(f"{i}^ {impurity_index}", float(v_i[site_index]))
|
| 68 |
+
|
| 69 |
+
return ham
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def anderson_model_qubit_hamiltonian(
|
| 73 |
+
v_i=np.array([0, 1]), epsilon_i=np.array([2, 2]), u=4, mu=0
|
| 74 |
+
):
|
| 75 |
+
f_ham = anderson_model_fermionic_hamiltonian(v_i, epsilon_i, u, mu)
|
| 76 |
+
qubit_ham = jordan_wigner(f_ham)
|
| 77 |
+
return qubit_ham
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def calculate_ground_state(hamiltonian):
|
| 81 |
+
gs_energy, gs_wf = get_ground_state(get_sparse_operator(hamiltonian))
|
| 82 |
+
# eigvals, eigvecs = eigh(get_sparse_operator(hamiltonian).toarray())
|
| 83 |
+
# gs_energy = eigvals[0]
|
| 84 |
+
# gs_wf = eigvecs[:,0]
|
| 85 |
+
return gs_energy, gs_wf
|
utils/adapt-aqc/adaptaqc/utils/utilityfunctions.py
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# (C) Copyright IBM 2025.
|
| 2 |
+
#
|
| 3 |
+
# This code is licensed under the Apache License, Version 2.0. You may
|
| 4 |
+
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
| 5 |
+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
| 6 |
+
#
|
| 7 |
+
# Any modifications or derivative works of this code must retain this
|
| 8 |
+
# copyright notice, and modified files need to carry a notice indicating
|
| 9 |
+
# that they have been altered from the originals.
|
| 10 |
+
|
| 11 |
+
"""Contains functions"""
|
| 12 |
+
|
| 13 |
+
import copy
|
| 14 |
+
import functools
|
| 15 |
+
from collections.abc import Iterable
|
| 16 |
+
from typing import Union, Dict, List, Tuple
|
| 17 |
+
|
| 18 |
+
import aqc_research.mps_operations as mpsop
|
| 19 |
+
import numpy as np
|
| 20 |
+
from qiskit import QuantumCircuit
|
| 21 |
+
from qiskit import transpile
|
| 22 |
+
from qiskit.result import Counts
|
| 23 |
+
from qiskit_aer.backends.compatibility import Statevector
|
| 24 |
+
from tenpy import SpinHalfSite, SpinSite
|
| 25 |
+
from tenpy.networks.mps import MPS
|
| 26 |
+
|
| 27 |
+
from adaptaqc.backends.aer_sv_backend import AerSVBackend
|
| 28 |
+
from adaptaqc.utils.circuit_operations import SUPPORTED_1Q_GATES
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
# ------------------Trigonometric functions------------------ #
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def minimum_of_sinusoidal(value_0, value_pi_by_2, value_minus_pi_by_2):
|
| 35 |
+
"""
|
| 36 |
+
Find the minimum of a sinusoidal function with period 2*pi and of the
|
| 37 |
+
form f(x) = a*sin(x+b)+c
|
| 38 |
+
:param value_0: f(0)
|
| 39 |
+
:param value_pi_by_2: f(pi/2)
|
| 40 |
+
:param value_minus_pi_by_2: f(-pi/2)
|
| 41 |
+
:return: (x_min, f(x_min))
|
| 42 |
+
"""
|
| 43 |
+
theta_min = -(np.pi / 2) - np.arctan2(
|
| 44 |
+
2 * value_0 - value_pi_by_2 - value_minus_pi_by_2,
|
| 45 |
+
value_pi_by_2 - value_minus_pi_by_2,
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
theta_min = normalized_angles(theta_min)
|
| 49 |
+
|
| 50 |
+
intercept_c = 0.5 * (value_pi_by_2 + value_minus_pi_by_2)
|
| 51 |
+
value_pi = (value_pi_by_2 + value_minus_pi_by_2) - value_0
|
| 52 |
+
amplitude_a = 0.5 * (
|
| 53 |
+
((value_0 - value_pi) ** 2 + (value_pi_by_2 - value_minus_pi_by_2) ** 2) ** 0.5
|
| 54 |
+
)
|
| 55 |
+
value_theta_min = intercept_c - amplitude_a
|
| 56 |
+
|
| 57 |
+
return theta_min, value_theta_min
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def amplitude_of_sinusoidal(value_0, value_pi_by_2, value_minus_pi_by_2):
|
| 61 |
+
"""
|
| 62 |
+
Find the amplitude of a sinusoidal function with period 2*pi and of the
|
| 63 |
+
form f(x) = a*sin(x+b)+c
|
| 64 |
+
:param value_0: f(0)
|
| 65 |
+
:param value_pi_by_2: f(pi/2)
|
| 66 |
+
:param value_minus_pi_by_2: f(-pi/2)
|
| 67 |
+
:return: Amplitude
|
| 68 |
+
"""
|
| 69 |
+
|
| 70 |
+
value_pi = (value_pi_by_2 + value_minus_pi_by_2) - value_0
|
| 71 |
+
amplitude_a = 0.5 * (
|
| 72 |
+
((value_0 - value_pi) ** 2 + (value_pi_by_2 - value_minus_pi_by_2) ** 2) ** 0.5
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
return amplitude_a
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def derivative_of_sinusoidal(theta, value_0, value_pi_by_2, value_minus_pi_by_2):
|
| 79 |
+
"""
|
| 80 |
+
Find the derivative of a sinusoidal function with period 2*pi and of the
|
| 81 |
+
form f(x) = a*sin(x+b)+c at x=theta
|
| 82 |
+
:param theta: Angle at which derivative is to be evaluated
|
| 83 |
+
:param value_0: f(0)
|
| 84 |
+
:param value_pi_by_2: f(pi/2)
|
| 85 |
+
:param value_minus_pi_by_2: f(-pi/2)
|
| 86 |
+
:return: df(x)/dx at x=theta
|
| 87 |
+
"""
|
| 88 |
+
value_pi = (value_pi_by_2 + value_minus_pi_by_2) - value_0
|
| 89 |
+
amplitude_a = 0.5 * (
|
| 90 |
+
((value_0 - value_pi) ** 2 + (value_pi_by_2 - value_minus_pi_by_2) ** 2) ** 0.5
|
| 91 |
+
)
|
| 92 |
+
phase_b = np.arctan2(value_0 - value_pi, value_pi_by_2 - value_minus_pi_by_2)
|
| 93 |
+
|
| 94 |
+
derivative = amplitude_a * np.cos(theta + phase_b)
|
| 95 |
+
return derivative
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def normalized_angles(angles):
|
| 99 |
+
"""
|
| 100 |
+
Normalize angle(s) to between -pi, pi by adding/subtracting multiples of
|
| 101 |
+
2pi
|
| 102 |
+
:param angles: float or Iterable(float)
|
| 103 |
+
:return: float or Iterable(float)
|
| 104 |
+
"""
|
| 105 |
+
single = not isinstance(angles, Iterable)
|
| 106 |
+
if single:
|
| 107 |
+
angles = [angles]
|
| 108 |
+
new_angles = []
|
| 109 |
+
for angle in angles:
|
| 110 |
+
while (angle > np.pi) or (angle < -np.pi):
|
| 111 |
+
if angle > np.pi:
|
| 112 |
+
angle -= 2 * np.pi
|
| 113 |
+
elif angle < np.pi:
|
| 114 |
+
angle += 2 * np.pi
|
| 115 |
+
new_angles += [angle]
|
| 116 |
+
return new_angles[0] if single else new_angles
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
# ------------------Misc. functions------------------ #
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def is_statevector_backend(backend):
|
| 123 |
+
"""
|
| 124 |
+
Check if backend is a statevector simulator backed
|
| 125 |
+
:param backend: Simulator backend
|
| 126 |
+
:return: Boolean
|
| 127 |
+
"""
|
| 128 |
+
if isinstance(backend, AerSVBackend):
|
| 129 |
+
return True
|
| 130 |
+
return False
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def counts_data_from_statevector(
|
| 134 |
+
statevector,
|
| 135 |
+
num_shots=2**40,
|
| 136 |
+
):
|
| 137 |
+
"""
|
| 138 |
+
Get counts data from statevector by multiplying amplitude squares with num_shots.
|
| 139 |
+
Note: Doesn't guarantee total number of shots in returned counts data will be num_shots.
|
| 140 |
+
Warning: Doesn't work well if num_shots << number of non-zero elements in statevector
|
| 141 |
+
:param statevector: Statevector (list/array)
|
| 142 |
+
:return: Counts data (e.g. {'00':13, '10':7}) with bitstrings ordered
|
| 143 |
+
with decreasing qubit number
|
| 144 |
+
"""
|
| 145 |
+
num_qubits = int(np.log2(len(statevector)))
|
| 146 |
+
counts = {}
|
| 147 |
+
probs = np.absolute(statevector) ** 2
|
| 148 |
+
bit_str_array = [bin(i)[2:].zfill(num_qubits) for i in range(2**num_qubits)]
|
| 149 |
+
counts = dict(zip(bit_str_array, np.asarray(probs * num_shots, int)))
|
| 150 |
+
# counts = dict(zip(*np.unique(np.random.choice(bit_str_array, num_shots,p=probs),return_counts=True)))
|
| 151 |
+
return counts
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def statevector_from_counts_data(counts):
|
| 155 |
+
"""
|
| 156 |
+
Get statevector from counts (works only for real, positive states)
|
| 157 |
+
:param: Counts data (e.g. {'00':13, '10':7})
|
| 158 |
+
:return statevector: Statevector (list/array)
|
| 159 |
+
"""
|
| 160 |
+
num_qubits = len(list(counts.keys())[0])
|
| 161 |
+
sv = np.zeros(2**num_qubits)
|
| 162 |
+
for i in range(2**num_qubits):
|
| 163 |
+
bitstr = bin(i)[2:].zfill(num_qubits)
|
| 164 |
+
if bitstr in counts:
|
| 165 |
+
sv[i] = counts[bitstr] ** 0.5
|
| 166 |
+
sv /= np.linalg.norm(sv)
|
| 167 |
+
return sv
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def expectation_value_of_qubits(data: Union[Counts, Dict, Statevector]):
|
| 171 |
+
"""
|
| 172 |
+
Expectation value of qubits (in computational basis)
|
| 173 |
+
:param counts: Counts data (e.g. {'00':13, '10':7})
|
| 174 |
+
:return: [expectation_value(float)]
|
| 175 |
+
"""
|
| 176 |
+
data = Statevector(data) if isinstance(data, np.ndarray) else data
|
| 177 |
+
|
| 178 |
+
num_qubits = (
|
| 179 |
+
data.num_qubits if isinstance(data, Statevector) else len(list(data)[0])
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
expectation_values = []
|
| 183 |
+
for i in range(num_qubits):
|
| 184 |
+
expectation_values.append(_expectation_value_of_qubit(i, data, num_qubits))
|
| 185 |
+
return expectation_values
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def expectation_value_of_qubits_mps(circuit: QuantumCircuit, sim=None):
|
| 189 |
+
"""
|
| 190 |
+
Expectation value of qubits (in computational basis) using mps
|
| 191 |
+
:param circuit: Circuit corresponding to state
|
| 192 |
+
:param sim: MPS AerSimulator instance. If none, will use default in AQC Research.
|
| 193 |
+
:return: [expectation_value(float)]
|
| 194 |
+
"""
|
| 195 |
+
# Get mps from circuit
|
| 196 |
+
circ = circuit.copy()
|
| 197 |
+
mps = mpsop.mps_from_circuit(circ, return_preprocessed=True, sim=sim)
|
| 198 |
+
|
| 199 |
+
num_qubits = circuit.num_qubits
|
| 200 |
+
|
| 201 |
+
expectation_values = [
|
| 202 |
+
(mpsop.mps_expectation(mps, "Z", i, already_preprocessed=True))
|
| 203 |
+
for i in range(num_qubits)
|
| 204 |
+
]
|
| 205 |
+
return expectation_values
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def _expectation_value_of_qubit(
|
| 209 |
+
qubit_index, data: Union[Counts, Statevector], num_qubits
|
| 210 |
+
):
|
| 211 |
+
"""
|
| 212 |
+
Expectation value of qubit (in computational basis) at given index
|
| 213 |
+
:param qubit_index: Index of qubit (int)
|
| 214 |
+
:param data: Counts data (e.g. {'00':13, '10':7}) or Statevector
|
| 215 |
+
:return: [expectation_value(float)]
|
| 216 |
+
"""
|
| 217 |
+
if qubit_index >= num_qubits:
|
| 218 |
+
raise ValueError("qubit_index outside of register range")
|
| 219 |
+
|
| 220 |
+
reverse_index = num_qubits - (qubit_index + 1)
|
| 221 |
+
|
| 222 |
+
if type(data) is Statevector:
|
| 223 |
+
[p0, p1] = data.probabilities([qubit_index])
|
| 224 |
+
exp_val = p0 - p1
|
| 225 |
+
return exp_val
|
| 226 |
+
|
| 227 |
+
else:
|
| 228 |
+
exp_val = 0
|
| 229 |
+
total_counts = 0
|
| 230 |
+
for bitstring in list(data):
|
| 231 |
+
exp_val += (1 if bitstring[reverse_index] == "0" else -1) * data[bitstring]
|
| 232 |
+
total_counts += data[bitstring]
|
| 233 |
+
return exp_val / total_counts
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
def expectation_value_of_pauli_observable(counts, pauli):
|
| 237 |
+
"""
|
| 238 |
+
Copied from measure_pauli_z in qiskit.aqua.operators.common
|
| 239 |
+
|
| 240 |
+
Args:
|
| 241 |
+
counts (dict): a dictionary of the form counts = {'00000': 10} ({
|
| 242 |
+
str: int})
|
| 243 |
+
pauli (Pauli): a Pauli object
|
| 244 |
+
Returns:
|
| 245 |
+
float: Expected value of paulis given data
|
| 246 |
+
"""
|
| 247 |
+
observable = 0.0
|
| 248 |
+
num_shots = sum(counts.values())
|
| 249 |
+
p_z_or_x = np.logical_or(pauli.z, pauli.x)
|
| 250 |
+
for key, value in counts.items():
|
| 251 |
+
bitstr = np.asarray(list(key))[::-1].astype(bool)
|
| 252 |
+
sign = (
|
| 253 |
+
-1.0
|
| 254 |
+
if functools.reduce(np.logical_xor, np.logical_and(bitstr, p_z_or_x))
|
| 255 |
+
else 1.0
|
| 256 |
+
)
|
| 257 |
+
observable += sign * value
|
| 258 |
+
observable /= num_shots
|
| 259 |
+
return observable
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
def remove_permutations_from_coupling_map(coupling_map):
|
| 263 |
+
seen = set()
|
| 264 |
+
unique_list = []
|
| 265 |
+
for pair in coupling_map:
|
| 266 |
+
if tuple(sorted(pair)) not in seen:
|
| 267 |
+
seen.add(tuple(sorted(pair)))
|
| 268 |
+
unique_list.append(pair)
|
| 269 |
+
return unique_list
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
def has_stopped_improving(cost_history, rel_tol=1e-2):
|
| 273 |
+
try:
|
| 274 |
+
poly_fit_res = np.polyfit(list(range(len(cost_history))), cost_history, 1)
|
| 275 |
+
grad = poly_fit_res[0] / np.absolute(np.mean(cost_history))
|
| 276 |
+
return grad > -1 * rel_tol
|
| 277 |
+
except np.linalg.LinAlgError:
|
| 278 |
+
return False
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
def multi_qubit_gate_depth(qc: QuantumCircuit) -> int:
|
| 282 |
+
"""
|
| 283 |
+
Return the multi-qubit gate depth.
|
| 284 |
+
|
| 285 |
+
When the circuit has been transpiled for IBM Quantum hardware
|
| 286 |
+
this will be equivalent to the CNOT depth.
|
| 287 |
+
"""
|
| 288 |
+
return qc.depth(filter_function=lambda instr: len(instr.qubits) > 1)
|
| 289 |
+
|
| 290 |
+
|
| 291 |
+
def tenpy_to_qiskit_mps(tenpy_mps):
|
| 292 |
+
num_sites = tenpy_mps.L
|
| 293 |
+
tenpy_mps.canonical_form()
|
| 294 |
+
|
| 295 |
+
# Check convention of basis states
|
| 296 |
+
flip = check_flipped_basis_states(tenpy_mps)
|
| 297 |
+
|
| 298 |
+
gam = [0] * num_sites
|
| 299 |
+
lam = [0] * (num_sites - 1)
|
| 300 |
+
permutation = None
|
| 301 |
+
for n in range(num_sites):
|
| 302 |
+
# Get the tenpy "B" tensor for site n, with indices in Qiskit MPS order (p, L, R)
|
| 303 |
+
g_n = tenpy_mps.get_B(n, form="G").itranspose(["p", "vL", "vR"]).to_ndarray()
|
| 304 |
+
if permutation is not None:
|
| 305 |
+
g_n[:] = g_n[
|
| 306 |
+
:, permutation, :
|
| 307 |
+
] # permute left index in the same way the left singlular values were permuted
|
| 308 |
+
if n < num_sites - 1:
|
| 309 |
+
l_n = tenpy_mps.get_SR(n) # Get singular values to the right of tensor n
|
| 310 |
+
permutation = np.argsort(l_n)[::-1]
|
| 311 |
+
l_n = np.sort(l_n)[::-1] # Sort singular values in descending order
|
| 312 |
+
lam[n] = l_n
|
| 313 |
+
if permutation is not None:
|
| 314 |
+
g_n[:] = g_n[
|
| 315 |
+
:, :, permutation
|
| 316 |
+
] # permute right index in the same way the right singular values were permuted
|
| 317 |
+
|
| 318 |
+
# Split physical dimension into two parts of a tuple
|
| 319 |
+
if flip[n]:
|
| 320 |
+
gam[n] = (g_n[1], g_n[0])
|
| 321 |
+
else:
|
| 322 |
+
gam[n] = (g_n[0], g_n[1])
|
| 323 |
+
|
| 324 |
+
qiskit_mps = (gam, lam)
|
| 325 |
+
|
| 326 |
+
return copy.deepcopy(qiskit_mps)
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
def tenpy_chi_1_mps_to_circuit(mps: MPS) -> QuantumCircuit:
|
| 330 |
+
if not np.allclose(mps.chi, 1):
|
| 331 |
+
raise Exception("MPS must have bond dimension 1 for all bonds.")
|
| 332 |
+
|
| 333 |
+
flip = check_flipped_basis_states(mps)
|
| 334 |
+
|
| 335 |
+
qc = QuantumCircuit(mps.L)
|
| 336 |
+
for i in range(mps.L):
|
| 337 |
+
# 2 x 1 x 1 array representing the state of site i
|
| 338 |
+
array = mps.get_B(i, form="B").itranspose(["p", "vL", "vR"]).to_ndarray()
|
| 339 |
+
# Extract the length-2 vector, with the correct basis-ordering
|
| 340 |
+
if flip[i]:
|
| 341 |
+
vec = array[::-1, 0, 0]
|
| 342 |
+
else:
|
| 343 |
+
vec = array[:, 0, 0]
|
| 344 |
+
|
| 345 |
+
# Make unitary with column 0 corresponding to the state of site i
|
| 346 |
+
U = np.zeros((2, 2), dtype=array.dtype)
|
| 347 |
+
U[:, 0] = vec
|
| 348 |
+
U[0, 1] = np.conj(U[1, 0])
|
| 349 |
+
U[1, 1] = -np.conj(U[0, 0])
|
| 350 |
+
qc.unitary(U, i)
|
| 351 |
+
|
| 352 |
+
qc = transpile(qc, basis_gates=["rx", "ry", "rz"])
|
| 353 |
+
return qc
|
| 354 |
+
|
| 355 |
+
|
| 356 |
+
def qiskit_to_tenpy_mps(qiskit_mps, return_form: str = "SpinSite") -> MPS:
|
| 357 |
+
"""
|
| 358 |
+
Converts a Qiskit MPS to a Tenpy MPS.
|
| 359 |
+
|
| 360 |
+
Args:
|
| 361 |
+
qiskit_mps: The Qiskit MPS.
|
| 362 |
+
return_form: The type of site to use for the Tenpy MPS.
|
| 363 |
+
Returns:
|
| 364 |
+
tenpy_mps: The Tenpy MPS
|
| 365 |
+
"""
|
| 366 |
+
# If not preprocessed, preprocess MPS
|
| 367 |
+
if isinstance(qiskit_mps[0], List):
|
| 368 |
+
qiskit_mps = mpsop._preprocess_mps(qiskit_mps)
|
| 369 |
+
|
| 370 |
+
N = len(qiskit_mps)
|
| 371 |
+
|
| 372 |
+
if return_form == "SpinSite":
|
| 373 |
+
sites = [SpinSite(conserve=None)] * N
|
| 374 |
+
# Flip basis state ordering for SpinSite
|
| 375 |
+
qiskit_mps = [tensor[::-1, :, :] for tensor in qiskit_mps]
|
| 376 |
+
elif return_form == "SpinHalfSite":
|
| 377 |
+
sites = [SpinHalfSite(conserve=None)] * N
|
| 378 |
+
else:
|
| 379 |
+
raise ValueError(
|
| 380 |
+
f"Invalid return_form: {return_form}. Must be SpinSite or SpinHalfSite"
|
| 381 |
+
)
|
| 382 |
+
|
| 383 |
+
tenpy_mps = MPS.from_Bflat(sites, qiskit_mps, SVs=None)
|
| 384 |
+
|
| 385 |
+
return tenpy_mps
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
def find_rotation_indices(qc: QuantumCircuit, indices: List[int]) -> List[int]:
|
| 389 |
+
"""
|
| 390 |
+
Given a QuantumCircuit and a list of indices, returns a list containing the subset of indices
|
| 391 |
+
corresponding to rotation gates in the circuit
|
| 392 |
+
"""
|
| 393 |
+
rotation_indices = []
|
| 394 |
+
for index in indices:
|
| 395 |
+
if qc.data[index].operation.name in SUPPORTED_1Q_GATES:
|
| 396 |
+
rotation_indices.append(index)
|
| 397 |
+
|
| 398 |
+
return rotation_indices
|
| 399 |
+
|
| 400 |
+
|
| 401 |
+
def get_distinct_items_and_degeneracies(items: List) -> Tuple[List, List[int]]:
|
| 402 |
+
"""
|
| 403 |
+
Given a list of items, return a list containing the distinct items, along with their
|
| 404 |
+
degeneracies (number of repetitions).
|
| 405 |
+
|
| 406 |
+
Args:
|
| 407 |
+
items: List of items.
|
| 408 |
+
Returns:
|
| 409 |
+
distinct_items: List of distinct items.
|
| 410 |
+
degeneracies: List of degeneracies.
|
| 411 |
+
"""
|
| 412 |
+
distinct_items = []
|
| 413 |
+
degeneracies = []
|
| 414 |
+
for i in range(len(items)):
|
| 415 |
+
item = items[i]
|
| 416 |
+
distinct = True
|
| 417 |
+
for j in range(len(distinct_items)):
|
| 418 |
+
if item == distinct_items[j]:
|
| 419 |
+
degeneracies[j] += 1
|
| 420 |
+
distinct = False
|
| 421 |
+
break
|
| 422 |
+
if distinct:
|
| 423 |
+
distinct_items.append(item)
|
| 424 |
+
degeneracies.append(1)
|
| 425 |
+
|
| 426 |
+
return (distinct_items, degeneracies)
|
| 427 |
+
|
| 428 |
+
|
| 429 |
+
def check_flipped_basis_states(mps: MPS) -> List[bool]:
|
| 430 |
+
"""
|
| 431 |
+
Given a Tenpy MPS, generate a list where the ith element is False(True) if the ith site of the
|
| 432 |
+
MPS is(isn't) ordering the basis states with the same convention as Qiskit.
|
| 433 |
+
|
| 434 |
+
Args:
|
| 435 |
+
mps: The Tenpy MPS.
|
| 436 |
+
Returns:
|
| 437 |
+
flipped_basis_states: The list of basis conventions.
|
| 438 |
+
"""
|
| 439 |
+
|
| 440 |
+
flipped_basis_states = [None] * mps.L
|
| 441 |
+
|
| 442 |
+
for i in range(mps.L):
|
| 443 |
+
sz_matrix = mps.sites[i].get_op("Sz").to_ndarray()
|
| 444 |
+
if np.array_equal(sz_matrix, [[0.5, 0], [0, -0.5]]):
|
| 445 |
+
flipped_basis_states[i] = False
|
| 446 |
+
elif np.array_equal(sz_matrix, [[-0.5, 0], [0, 0.5]]):
|
| 447 |
+
flipped_basis_states[i] = True
|
| 448 |
+
else:
|
| 449 |
+
raise ValueError(f"Invalid Tenpy convention for site {i}")
|
| 450 |
+
|
| 451 |
+
return flipped_basis_states
|
| 452 |
+
|
| 453 |
+
|
| 454 |
+
def tenpy_mps_to_statevector(mps: MPS) -> np.ndarray:
|
| 455 |
+
"""
|
| 456 |
+
Convert a Tenpy MPS to a little-endian statevector
|
| 457 |
+
|
| 458 |
+
Args:
|
| 459 |
+
mps: The MPS.
|
| 460 |
+
Returns:
|
| 461 |
+
sv: The statevector.
|
| 462 |
+
"""
|
| 463 |
+
|
| 464 |
+
# Get the 2 x 2 x ... tensor representing the state
|
| 465 |
+
sv = mps.get_theta(0, mps.L).to_ndarray().reshape([2] * mps.L)
|
| 466 |
+
|
| 467 |
+
# Flip the basis ordering for any sites using the opposite convention to Qiskit
|
| 468 |
+
flip = check_flipped_basis_states(mps)
|
| 469 |
+
for i in range(mps.L):
|
| 470 |
+
if flip[i]:
|
| 471 |
+
sv = np.flip(sv, axis=i)
|
| 472 |
+
else:
|
| 473 |
+
continue
|
| 474 |
+
|
| 475 |
+
# Convert from big-endian to little-endian ordering
|
| 476 |
+
sv = np.transpose(sv, axes=range(mps.L)[::-1])
|
| 477 |
+
|
| 478 |
+
# Convert to 2^N dimensional vector
|
| 479 |
+
sv = sv.flatten()
|
| 480 |
+
|
| 481 |
+
return sv
|
utils/adapt-aqc/docs/future_heuristic_ideas.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
This document is to detail some ideas for potential future features.
|
| 2 |
+
|
| 3 |
+
# Local minima
|
| 4 |
+
|
| 5 |
+
## random_cost_noise
|
| 6 |
+
|
| 7 |
+
**What is it:** \
|
| 8 |
+
Every time the cost function is evaluated, some noise value $\epsilon$ is added. The distribution
|
| 9 |
+
that $\epsilon$ is drawn from could be uniform, Gaussian or another and perhaps user defined. Likely
|
| 10 |
+
the magnitude of $\epsilon$ would need to be adjusted depending on the number of qubits.
|
| 11 |
+
|
| 12 |
+
**What problem it aims to solve:** \
|
| 13 |
+
Adding noise is thought to help avoid getting stuck in local minima. This is a technique applied
|
| 14 |
+
in DMRG (see https://itensor.org/support/2529/details-of-how-dmrg-works), and also explored e.g., in
|
| 15 |
+
deep learning (https://arxiv.org/abs/1511.06807).
|
| 16 |
+
|
| 17 |
+
## add_local_minima_to_cost
|
| 18 |
+
|
| 19 |
+
**What is it:** \
|
| 20 |
+
If we have identified that ADAPT-AQC is stuck in a local minima, we could save the MPS $|LM\rangle$
|
| 21 |
+
at
|
| 22 |
+
this point. Then, we could explicitly add to the cost function a new term that would be the overlap
|
| 23 |
+
of the trial solution to this MPS. So the cost would then be
|
| 24 |
+
|
| 25 |
+
$$C = 1 - |\langle 0 | V^\dagger U|0\rangle|^2 + |\langle LM| V^\dagger U|0\rangle|^2$$$
|
| 26 |
+
|
| 27 |
+
Since the cost is being minimised, this encourages ADAPT-AQC to minimise the overlap between the
|
| 28 |
+
current
|
| 29 |
+
solution $V^\dagger U|0\rangle$ and the state that was in a local minima $|LM\rangle$.
|
| 30 |
+
|
| 31 |
+
**What problem it aims to solve:** \
|
| 32 |
+
This is another technique borrowed from DMRG, which we learnt about in a seminar (reference needed
|
| 33 |
+
for what this is called in DMRG literature). The idea is once we have identified a local minima, we
|
| 34 |
+
can repel the optimisation away from this area of the cost landscape.
|
| 35 |
+
|
| 36 |
+
# Performance
|
| 37 |
+
|
| 38 |
+
## optimiser == "gradient_descent"
|
| 39 |
+
|
| 40 |
+
**What is it:** \
|
| 41 |
+
Gradient descent is the most popular optimisation algorithm in classical and quantum ML alike. At
|
| 42 |
+
the moment, ADAPT-AQC does not use gradient descent and instead uses non-gradient based sequential
|
| 43 |
+
optimisation in the form of the Rotosolve/Rotoselect algorithms.
|
| 44 |
+
|
| 45 |
+
**What problem it aims to solve:** \
|
| 46 |
+
Gradient descent was originally not used due to fears over encountering barren plateaus. However,
|
| 47 |
+
when running ADAPT-AQC fully classically, barren plateaus should not be an issue as we can access
|
| 48 |
+
observables with exponential precision. The benefit of gradient descent would be a potentially
|
| 49 |
+
large performance improvement, as the gradients of each parameter can be calculated independently
|
| 50 |
+
of one another. Note, however, that this improvement would be mostly dependent on calculating
|
| 51 |
+
gradients using backpropagation (for simulated circuits), as opposed to parameter-shift.
|
| 52 |
+
|
| 53 |
+
## parallel_rotosolve
|
| 54 |
+
|
| 55 |
+
**What is it:** \
|
| 56 |
+
Given $P$ parameterised gates, Rotosolve cycles through them one-by-one finding the optimal angle
|
| 57 |
+
in the case that all others are fixed. Since the optimal gate angles are dependent on one another,
|
| 58 |
+
this cycle is repeated until the cost function converges (i.e., does not change by a defined amount
|
| 59 |
+
between two cycles). However, if we assume independence of the parameters, we could optimise each
|
| 60 |
+
gate in parallel. This could be done with no downside if the gates truly are independent (e.g., not
|
| 61 |
+
in a light-cone of each other) or as an approximation with the hope that the optimal angles in
|
| 62 |
+
parallel are not too far from those in sequence.
|
| 63 |
+
|
| 64 |
+
**What problem it aims to solve:** \
|
| 65 |
+
With enough computational threads, parallelising Rotosolve could lead to a performance
|
| 66 |
+
improvement.
|
utils/adapt-aqc/docs/running_options_explained.md
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
This document is to give a further in-detail explanation about the different ADAPT-AQC options made
|
| 2 |
+
available through `AdaptConfig` and `AdaptCompiler`.
|
| 3 |
+
|
| 4 |
+
# AdaptConfig()
|
| 5 |
+
|
| 6 |
+
## method="general_gradient"
|
| 7 |
+
|
| 8 |
+
This is the core heuristics of ADAPT-AQC, whereby the next two-qubit unitary is placed on the
|
| 9 |
+
pair of qubits which would give the largest cost gradient $\Vert\vec{\nabla} C\Vert$.
|
| 10 |
+
For more details please see Appendix A of https://arxiv.org/abs/2503.09683.
|
| 11 |
+
|
| 12 |
+
## method="ISL"
|
| 13 |
+
|
| 14 |
+
**What is it:** \
|
| 15 |
+
When using this method, the ansatz is adaptively constructed
|
| 16 |
+
by prioritising pairs of qubits which have larger pairwise entanglement. This is the original
|
| 17 |
+
heuristic from https://github.com/abhishekagarwal2301/isl which has been kept as the default here
|
| 18 |
+
as it is the only one which supports all backends.
|
| 19 |
+
|
| 20 |
+
When
|
| 21 |
+
`AdaptConfig.reuse_exponent = 0`, which is the default setting, the pair with the largest
|
| 22 |
+
entanglement is always picked, except for picking the same pair twice. For any other
|
| 23 |
+
value of `reuse_exponent`, the entanglement of the pair is weighted against how
|
| 24 |
+
recently a layer has been applied to it.
|
| 25 |
+
|
| 26 |
+
If the pairwise entanglement between all qubits in the coupling map is zero, this method falls
|
| 27 |
+
back to the `expectation` method defined below.
|
| 28 |
+
|
| 29 |
+
**What problem it aims to solve:** \
|
| 30 |
+
The goal here is to use the entanglement structure of the compilation target to inform the adaptive
|
| 31 |
+
ansatz. This is motivated by the fact that the compiling succeeds by finding a set of gates that
|
| 32 |
+
"undoes" the target circuit back to the $|00..0\rangle$ state. Since the $|00..0\rangle$ state has
|
| 33 |
+
no pairwise entanglement, it makes sense that we want to iteratively reduce this.
|
| 34 |
+
|
| 35 |
+
## method="expectation"
|
| 36 |
+
|
| 37 |
+
**What is it:** \
|
| 38 |
+
This is another mode of operation for compiling. When using this method, the ansatz is adaptively
|
| 39 |
+
constructed by prioritising pairs which have smallest summed $\hat{\sigma}_z$ expectation values (
|
| 40 |
+
i.e., the closest to the minimum value of -2)
|
| 41 |
+
|
| 42 |
+
For the default value of `AdaptConfig.expectation_reuse_exponent = 1` the pair with the smallest
|
| 43 |
+
expectation value is also weighted against how recently a layer has been applied to it.
|
| 44 |
+
|
| 45 |
+
**What problem it aims to solve:** \
|
| 46 |
+
In this case, we aim to use the qubit magnetisation of the target to inform the adaptive ansatz.
|
| 47 |
+
This has a similar motivation to above, in that each qubit in the $|00..0\rangle$ state has
|
| 48 |
+
an expectation value of $\langle0|\hat{\sigma}_z|0\rangle = 1$
|
| 49 |
+
|
| 50 |
+
## bad_qubit_pair_memory
|
| 51 |
+
|
| 52 |
+
**What is it:** \
|
| 53 |
+
For the ADAPT-AQC method, if acting on a qubit pair leads to entanglement increasing, it is labelled
|
| 54 |
+
a
|
| 55 |
+
"bad pair". After this, for a number of layers corresponding to the bad_qubit_pair_memory,
|
| 56 |
+
this pair will not be selected.
|
| 57 |
+
|
| 58 |
+
**What problem it aims to solve:** \
|
| 59 |
+
Although pairwise entanglement is used to select a two-qubit pair to act on, the variational
|
| 60 |
+
ansatz is optimised with respect to the overlap with the $|00..0\rangle$ state. The algorithm thus
|
| 61 |
+
does
|
| 62 |
+
not directly minimise entanglement. This means that in certain situations, the optimal angles of an
|
| 63 |
+
ansatz layer could actually increase the entanglement. This would lead to a high priority for acting
|
| 64 |
+
on that pair of qubits in the near-future, despite the fact that it was recently optimised. This
|
| 65 |
+
can lead ADAPT-AQC to get stuck, particularly if there are several unconnected bad qubit pairs. The
|
| 66 |
+
use
|
| 67 |
+
of `bad_qubit_pair_memory` is to make sure that by the time the pair is acted on again,
|
| 68 |
+
the state of connected qubits has sufficiently changed so that optimising the bad pairs will lead
|
| 69 |
+
to new optimal angles.
|
| 70 |
+
|
| 71 |
+
## reuse_exponent
|
| 72 |
+
|
| 73 |
+
**What is it:** \
|
| 74 |
+
For the ADAPT-AQC, expectation or general_gradient methods, this controls how much priority should
|
| 75 |
+
be given to picking qubits not recently
|
| 76 |
+
acted on. Specifically, given a qubit pair has been last acted on $l$ layers ago, it is given a
|
| 77 |
+
reuse priority $P_r$ of
|
| 78 |
+
|
| 79 |
+
$$P_r = 1-2^{\frac{-l}{k}},$$
|
| 80 |
+
|
| 81 |
+
where $k$ is the value of `reuse_exponent`. This is then multiplied with the
|
| 82 |
+
entanglement measure or gradient (for ADAPT-AQC and general_gradient respectively) to produce the
|
| 83 |
+
combined priority $P_c$ = $E*P_r$. For expectation mode, the combined priority is calculated
|
| 84 |
+
differently. Given a pair of qubits, the combined priority is calculated as
|
| 85 |
+
|
| 86 |
+
$$P_c = (2 - \langle Z_1 \rangle + \langle Z_2 \rangle) *P_r$$,
|
| 87 |
+
|
| 88 |
+
where $\langle Z_1 \rangle$ ($\langle Z_2 \rangle$) is the $\\hat{\sigma}_z$ expectation value of
|
| 89 |
+
qubit
|
| 90 |
+
1 (2).
|
| 91 |
+
|
| 92 |
+
The qubit pair with the highest combined priority is then picked for the next layer.
|
| 93 |
+
|
| 94 |
+
This means that for larger $k$, more weighting is given to how recently the pair was used.
|
| 95 |
+
Conversely, if $k=0$ then no
|
| 96 |
+
weighting is given.
|
| 97 |
+
|
| 98 |
+
**What problem it aims to solve:** \
|
| 99 |
+
The goal of approximate quantum compiling (AQC) is to produce a circuit that approximately prepares
|
| 100 |
+
a target state **with less depth** than the original circuit. The aim of this heuristic is to make
|
| 101 |
+
ADAPT-AQC depth-aware, so that e.g., the same pairs of qubits are not repeatedly picked if they are
|
| 102 |
+
only
|
| 103 |
+
marginally higher entanglement than other pairs that haven't been used. Ultimately, compiling
|
| 104 |
+
with a larger exponent produces shallower solutions, at the cost of longer compiling times.
|
| 105 |
+
|
| 106 |
+
## reuse_priority_mode
|
| 107 |
+
|
| 108 |
+
**What is it:** \
|
| 109 |
+
The reuse priority system is used to de-prioritise qubits that were recently acted on. When
|
| 110 |
+
`reuse_priority_mode="pair"`, the priority of a pair of qubits (a, b) is calculated as
|
| 111 |
+
|
| 112 |
+
$$P_r = 1-2^{\frac{-l}{k}},$$
|
| 113 |
+
|
| 114 |
+
where $l$ is the number of layers since that pair had a layer applied to it.
|
| 115 |
+
|
| 116 |
+
When `reuse_priority_mode="qubit"`, the priority is instead calculated as
|
| 117 |
+
|
| 118 |
+
$$P_r = \mathrm{min}\[1-2^{\frac{-(l_a + 1)}{k}}, 1-2^{\frac{-(l_b + 1)}{k}}\],$$
|
| 119 |
+
|
| 120 |
+
where $l_a$ or $l_b$ is the number of layers since qubit a or b has been acted on respectively. Note
|
| 121 |
+
that in both cases, the priority of the most recently used qubit pair is set to -1 so that it is
|
| 122 |
+
never chosen. Additionally, the priority of a pair that has never been used is manually set to 1, so
|
| 123 |
+
that it receives maximum priority.
|
| 124 |
+
|
| 125 |
+
**What problem it aims to solve:** \
|
| 126 |
+
This heuristic is meant to reflect that, given a pair of qubits (a, b) was recently acted on, the
|
| 127 |
+
depth of the compiled solution will increase if _either_ a or b are acted on in a new layer.
|
| 128 |
+
Previously, the "pair" option was the only type of reuse priority in ADAPT-AQC, leading often to
|
| 129 |
+
solutions where successive layers might act on pairs (a, a+1), (a+1, a+2), (a+2, a+3)... These
|
| 130 |
+
branch-like structures significantly increase the depth of the solution.
|
| 131 |
+
|
| 132 |
+
## rotosolve_frequency
|
| 133 |
+
|
| 134 |
+
**What is it:** \
|
| 135 |
+
The main optimisation algorithms used by ADAPT-AQC are the Rotoselect and Rotosolve algorithms, more
|
| 136 |
+
details of which can be found at https://quantum-journal.org/papers/q-2021-01-28-391/. Put simply,
|
| 137 |
+
the Roto algorithms use sequential optimisation. Given a set of $L$ parameterised gates, the
|
| 138 |
+
procedure
|
| 139 |
+
works by fixing $L-1$ of the gates and varying the remaining one to minimise the cost function.
|
| 140 |
+
This is then repeated for the remaining $L-1$ gates, unfixing one at a time and fixing the others.
|
| 141 |
+
As the changing of later gates in the layer will affect the loss landscape of the first gates, we
|
| 142 |
+
repeatedly cycle over all rotation gates until a termination criteria is reached.
|
| 143 |
+
|
| 144 |
+
`rotosolve_frequency` defines how often the ansatz is optimised using specifically the Rotosolve
|
| 145 |
+
algorithm, which only changes the angles of parameterised gates. Specifically, Rotosolve is called
|
| 146 |
+
after every `rotosolve_frequency` number of layers have been added. In the context of ADAPT-AQC, it
|
| 147 |
+
is
|
| 148 |
+
notable that _only rotosolve_ has the ability to modify previous layers. Specifically, the last
|
| 149 |
+
`AdaptConfig.max_layers_to_modify` layers will be optimised using Rotosolve. This makes it an
|
| 150 |
+
expensive step but often necessary to reach convergence.
|
| 151 |
+
|
| 152 |
+
NOTE Setting the value `rotosolve_frequency=0` will disable rotosolve. This can lead to a large
|
| 153 |
+
performance improvement when using the matrix product state (MPS) backends, since the guarantee
|
| 154 |
+
that previous layers won't be modified allows us to cache the state of the system during evolution.
|
| 155 |
+
|
| 156 |
+
**What problem it aims to solve:** \
|
| 157 |
+
The use of Rotosolve reflects the idea that after adding layers, the optimal parameters of
|
| 158 |
+
previous layers may have changed. Thus it may be more efficient (in terms of final circuit depth)
|
| 159 |
+
to attempt to re-optimise previous layers than to only add new layers. As such, when not using
|
| 160 |
+
Rotosolve, generally the solution will be deeper.
|
| 161 |
+
|
| 162 |
+
# AdaptCompiler()
|
| 163 |
+
|
| 164 |
+
## coupling_map
|
| 165 |
+
|
| 166 |
+
**What is it:** \
|
| 167 |
+
A user specified list of tuples, each of which represents a connection between qubits. ADAPT-AQC
|
| 168 |
+
will be
|
| 169 |
+
restricted to only adding CNOT gates between pairs which are in this coupling map.
|
| 170 |
+
|
| 171 |
+
**What problem it aims to solve:** \
|
| 172 |
+
Often we want to run the ADAPT-AQC solution on a specific connectivity hardware (e.g., heavy hex).
|
| 173 |
+
Without
|
| 174 |
+
this option, it would be possible to convert any solution to a connectivity via SWAP gates, however
|
| 175 |
+
this can be extremely expensive in terms of number of gates. This option exists to allow ADAPT-AQC
|
| 176 |
+
to
|
| 177 |
+
restrict the solution space during compiling.
|
| 178 |
+
|
| 179 |
+
## custom_layer_2q_gate
|
| 180 |
+
|
| 181 |
+
**What is it:** \
|
| 182 |
+
A Qiskit `QuantumCircuit` to be used as the ansatz layers.
|
| 183 |
+
|
| 184 |
+
**What problem it aims to solve:** \
|
| 185 |
+
ADAPT-AQC uses by default a thinly-dressed CNOT gate (i.e., CNOT surrounded by 4 single qubit
|
| 186 |
+
rotations).
|
| 187 |
+
This ansatz is not universal and has not been shown to be objectively better than others, but is a
|
| 188 |
+
heuristic that originally worked. As such it may be valuable to change what ansatz layer is used.
|
| 189 |
+
|
| 190 |
+
## starting_circuit
|
| 191 |
+
|
| 192 |
+
**What is it:** \
|
| 193 |
+
This is a `QuantumCircuit` that will be used as a set of initial fixed gates for the compiled
|
| 194 |
+
solution $\hat{V}$. Because during ADAPT-AQC we are variationally optimising $\hat{V}^\dagger$, the
|
| 195 |
+
inverse of `starting_circuit` will be placed at the end of the ansatz.
|
| 196 |
+
|
| 197 |
+
**What problem it aims to solve:** \
|
| 198 |
+
`starting_circuit` is a useful heuristic when we have some knowledge of the structure of the
|
| 199 |
+
solution. A good example of what this aims to solve is when a compilation target includes
|
| 200 |
+
a distinct state preparation step. For example, consider compiling the evolution of a spin-chain
|
| 201 |
+
starting in the Neel state. The compiling problem is much more efficient if ADAPT-AQC does not need
|
| 202 |
+
to
|
| 203 |
+
learn to start by applying an X gate to every other qubit.
|
| 204 |
+
|
| 205 |
+
## local_cost_function
|
| 206 |
+
|
| 207 |
+
**What is it:**\
|
| 208 |
+
Normally, ADAPT-AQC uses a a cost $C_\mathrm{LET} = 1- |\langle 0 | V^\dagger U|0\rangle|^2$. The
|
| 209 |
+
fidelity
|
| 210 |
+
term,
|
| 211 |
+
as defined in section IIIB of https://arxiv.org/abs/1908.04416, is generated using the Loschmidt
|
| 212 |
+
Echo Test (LET), which is the formal name for acting $U|0\rangle$ followed by $V^\dagger$ to get
|
| 213 |
+
the fidelity. We note that the cost is global with respect to the Hilbert space, since the overlap
|
| 214 |
+
with the $|00...0\rangle$ state spans the full state vector.
|
| 215 |
+
|
| 216 |
+
By contrast, when setting `local_cost_function=True`, ADAPT-AQC will use a cost derived from the
|
| 217 |
+
Local
|
| 218 |
+
Loschmidt Echo Test (LLET). Specifically, the cost is defined as
|
| 219 |
+
|
| 220 |
+
$$C_\mathrm{LLET} = 1 - \frac{1}{n} \sum_{j=1}^{n} \langle 0|\rho^j|0\rangle,$$
|
| 221 |
+
|
| 222 |
+
where the second term can be
|
| 223 |
+
recognised as the sum of the probabilities that each qubit is in the $|0\rangle$ state. Since the
|
| 224 |
+
cost function does not span the entire Hilbert space, it is described as local.
|
| 225 |
+
|
| 226 |
+
**What problem it aims to solve:** \
|
| 227 |
+
The distinction between global and local cost functions is very important in the context of
|
| 228 |
+
trainability and barren plateaus (see https://www.nature.com/articles/s41467-021-21728-w), where
|
| 229 |
+
a global cost function is difficult to train for large numbers of qubits. By contrast the local
|
| 230 |
+
cost function is trainable. However, we note that $C_\mathrm{LLET} <= C_\mathrm{LET}$, meaning
|
| 231 |
+
that ADAPT-AQC may not have achieved the desired global fidelity just because the local cost
|
| 232 |
+
converges.
|
| 233 |
+
|
| 234 |
+
## initial_single_qubit_layer
|
| 235 |
+
|
| 236 |
+
**What is it:** \
|
| 237 |
+
When `initial_single_qubit_layer=True`, the first layer of the ADAPT-AQC ansatz will be
|
| 238 |
+
a trainable single-qubit rotation on each qubit. Since this layer will be optimised by Rotoselect,
|
| 239 |
+
this means that both the angles and the bases of rotations can be modified. Note that since this
|
| 240 |
+
is the first layer of the ADAPT-AQC ansatz $V^\dagger$, it will end up being the final layer of the
|
| 241 |
+
returned solution $V$. So we can think of this feature as adding a trainable basis change before
|
| 242 |
+
measuring the cost function.
|
| 243 |
+
|
| 244 |
+
**What problem it aims to solve:** \
|
| 245 |
+
ADAPT-AQC only applies layers in two-qubit blocks, which means that in certain situations ADAPT-AQC
|
| 246 |
+
won't be
|
| 247 |
+
able to find the optimal depth solution. A good example of this is when only a subset of the
|
| 248 |
+
qubits are entangled. To demonstrate why, for the extreme case of compiling the $n$ qubit
|
| 249 |
+
$|++..+\rangle$ state, ADAPT-AQC without this feature will need to apply $n$ CNOT gates. By
|
| 250 |
+
contrast,
|
| 251 |
+
with `initial_single_qubit_layer=True`, a solution can be found in depth 1 with no CNOT gates.
|
| 252 |
+
It is possible the same issue can arise for any target state, if during compiling ADAPT-AQC is left
|
| 253 |
+
with an intermediate low-entangled state.
|
| 254 |
+
|
| 255 |
+
## AdaptCompiler.compile_in_parts()
|
| 256 |
+
|
| 257 |
+
**What is it:** \
|
| 258 |
+
Compiling in parts, (also called ladder-ADAPT-AQC), is the idea of splitting up a
|
| 259 |
+
circuit into chunks that we compile sequentially. For example, given a depth 50 circuit $U_
|
| 260 |
+
{50}|0\rangle$, we can compile the first 10 depth of gates $U_{0-10}|0\rangle$ to produce
|
| 261 |
+
$V_{0-10}^\dagger|0\rangle \approx U_{0-10}|0\rangle$. This can then be used to construct a new
|
| 262 |
+
target $U_{11-20}V_{0-10}^\dagger|0\rangle$.
|
| 263 |
+
|
| 264 |
+
A particularly good example of this is for time evolution circuits. Here, we start by compiling only
|
| 265 |
+
the first Trotter step. Once we have a solution $V^\dagger_1$, we append a Trotter step to it and
|
| 266 |
+
use it as the target state for compiling 2 Trotter steps worth of evolution. We can "ladder" this
|
| 267 |
+
all the way to the desired number of Trotter steps. This is shown in Fig.7
|
| 268 |
+
of https://arxiv.org/abs/2002.04612.
|
| 269 |
+
|
| 270 |
+
When applied to time dynamics, compiling in parts is also referred to as restarted quantum dynamics
|
| 271 |
+
(https://arxiv.org/abs/1910.06284), iterative variational Trotter
|
| 272 |
+
compression (https://arxiv.org/abs/2404.10044) and compressed quantum
|
| 273 |
+
circuits (https://arxiv.org/abs/2008.10322). There are inevitably more references using the same
|
| 274 |
+
idea.
|
| 275 |
+
|
| 276 |
+
**What problem it aims to solve:** \
|
| 277 |
+
There are two key benefits to compiling in parts. Firstly, compiling smaller chunks makes each
|
| 278 |
+
individual optimisation problem easier and less likely to suffer from trainability issues. If
|
| 279 |
+
we consider the extreme case of compiling one gate at a time, it is clear that compiling only
|
| 280 |
+
needs to learn the application of one more gate on the starting state.
|
| 281 |
+
|
| 282 |
+
Secondly, if running approximate quantum compiling on real quantum hardware, compiling in parts
|
| 283 |
+
allows one to limit the depth of any circuit executed. For example, if the target circuit has a
|
| 284 |
+
depth of 20, but a device is limited to depth 10 before noise ruins the computation, one can compile
|
| 285 |
+
in blocks of 5 depth at a time. If successful, $V^\dagger$ will never be deeper than $U$, meaning
|
| 286 |
+
that the compiling circuit $V^\dagger U |0\rangle$ will never be more than 10 depth.
|
| 287 |
+
|
| 288 |
+
There are two key drawbacks to compiling in parts. Firstly, we need to solve several sequential
|
| 289 |
+
compilation problems, which can take longer than compiling the entire circuit at once (if possible).
|
| 290 |
+
Secondly, the approximation error of each individual solution will multiply every time it is used
|
| 291 |
+
as the input for the next compiling. Thus the approximation error of the final solution grows
|
| 292 |
+
exponentially. For example, if we compile 18 sub-circuits one at a time, with a sufficient
|
| 293 |
+
overlap for each one of $0.99$, the overlap of the final solution would be $0.99^{18} = 0.83$. Thus,
|
| 294 |
+
one
|
| 295 |
+
would need to instead use a much higher sufficient overlap of 0.9995 for each sub-ciruit to get
|
| 296 |
+
the desired final overlap of $0.99$.
|
utils/adapt-aqc/examples/advanced_mps_example.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Example script for running ADAPT-AQC recompilation using more advanced options
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import logging
|
| 6 |
+
import matplotlib.pyplot as plt
|
| 7 |
+
|
| 8 |
+
from tenpy import SpinChain, MPS
|
| 9 |
+
from tenpy.algorithms import dmrg
|
| 10 |
+
|
| 11 |
+
from adaptaqc.backends.aer_mps_backend import AerMPSBackend, mps_sim_with_args
|
| 12 |
+
from adaptaqc.compilers import AdaptCompiler, AdaptConfig
|
| 13 |
+
from adaptaqc.utils.ansatzes import identity_resolvable
|
| 14 |
+
from adaptaqc.utils.utilityfunctions import tenpy_to_qiskit_mps
|
| 15 |
+
|
| 16 |
+
logging.basicConfig()
|
| 17 |
+
logger = logging.getLogger("adaptaqc")
|
| 18 |
+
logger.setLevel(logging.INFO)
|
| 19 |
+
|
| 20 |
+
# Generate a ground state of the XXZ model using TenPy
|
| 21 |
+
l = 20
|
| 22 |
+
model_params = dict(
|
| 23 |
+
S=0.5, L=l, Jx=1.0, Jy=1.0, Jz=5.0, hz=1.0, bc_MPS="finite", conserve="None"
|
| 24 |
+
)
|
| 25 |
+
model = SpinChain(model_params)
|
| 26 |
+
|
| 27 |
+
psi = MPS.from_product_state(
|
| 28 |
+
model.lat.mps_sites(), ["up", "down"] * (l // 2), bc=model_params["bc_MPS"]
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
# Run the DMRG algorithm to obtain the ground state
|
| 32 |
+
dmrg_params = {"trunc_params": {"trunc_cut": 1e-4}}
|
| 33 |
+
dmrg_engine = dmrg.TwoSiteDMRGEngine(psi, model, dmrg_params)
|
| 34 |
+
E, psi = dmrg_engine.run()
|
| 35 |
+
logger.info(f"Ground state created with maximum bond dimension {max(psi.chi)}")
|
| 36 |
+
|
| 37 |
+
# Convert it to a format compatible with the Qiskit Aer MPS simulator
|
| 38 |
+
qiskit_mps = tenpy_to_qiskit_mps(psi)
|
| 39 |
+
|
| 40 |
+
# Set compiler to use the general gradient method as laid out in https://arxiv.org/abs/2503.09683
|
| 41 |
+
config = AdaptConfig(
|
| 42 |
+
method="general_gradient", cost_improvement_num_layers=1e3, rotosolve_frequency=10
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
# Create an instance of Qiskit's MPS simulator with a specified truncation threshold
|
| 46 |
+
qiskit_mps_sim = mps_sim_with_args(mps_truncation_threshold=1e-8)
|
| 47 |
+
|
| 48 |
+
# Create an AQCBackend object
|
| 49 |
+
backend = AerMPSBackend(simulator=qiskit_mps_sim)
|
| 50 |
+
|
| 51 |
+
# Create a compiler with the target to be an MPS rather than a circuit
|
| 52 |
+
adapt_compiler = AdaptCompiler(
|
| 53 |
+
target=qiskit_mps,
|
| 54 |
+
backend=backend,
|
| 55 |
+
adapt_config=config,
|
| 56 |
+
starting_circuit="tenpy_product_state", # Start compiling from best χ=1 compression of target
|
| 57 |
+
custom_layer_2q_gate=identity_resolvable(), # Use ansatz from https://arxiv.org/abs/2503.09683
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
result = adapt_compiler.compile()
|
| 61 |
+
approx_circuit = result.circuit
|
| 62 |
+
print(f"Overlap between circuits is {result.overlap}")
|
| 63 |
+
|
| 64 |
+
# Draw the circuit that prepares the target random MPS
|
| 65 |
+
approx_circuit.draw(output="mpl", fold=-1)
|
| 66 |
+
plt.show()
|
utils/adapt-aqc/examples/advanced_sv_example.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Example script for running ADAPT-AQC recompilation using more advanced options
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import logging
|
| 6 |
+
|
| 7 |
+
from qiskit import QuantumCircuit, transpile
|
| 8 |
+
from qiskit.circuit.random import random_circuit
|
| 9 |
+
|
| 10 |
+
from adaptaqc.compilers import AdaptCompiler, AdaptConfig
|
| 11 |
+
|
| 12 |
+
logging.basicConfig()
|
| 13 |
+
logger = logging.getLogger("adaptaqc")
|
| 14 |
+
logger.setLevel(logging.INFO)
|
| 15 |
+
|
| 16 |
+
n = 4
|
| 17 |
+
state_prep_circuit = QuantumCircuit(n)
|
| 18 |
+
state_prep_circuit.h(range(n))
|
| 19 |
+
|
| 20 |
+
# Create a random circuit starting with a layer of hadamard gates
|
| 21 |
+
qc = state_prep_circuit.compose(random_circuit(n, 16, 2, seed=0))
|
| 22 |
+
|
| 23 |
+
config = AdaptConfig(
|
| 24 |
+
# We expect the solution to take longer to converge, so decrease the threshold for exiting
|
| 25 |
+
# early.
|
| 26 |
+
cost_improvement_tol=1e-5,
|
| 27 |
+
# Run Rotosolve only every 10th layer to reduce computational cost.
|
| 28 |
+
rotosolve_frequency=10,
|
| 29 |
+
# Choose Rotosolve to modify only the last 10 layers.
|
| 30 |
+
max_layers_to_modify=10,
|
| 31 |
+
# Setting this value > 0 prioritises not using the same qubit pairs too often.
|
| 32 |
+
reuse_exponent=1,
|
| 33 |
+
# Increase the amount the cost needs to decrease by to terminate Rotosolve. This stops spending
|
| 34 |
+
# too much time fine-tuning the angles.
|
| 35 |
+
rotosolve_tol=1e-2,
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
# Since we know the solution starts with Hadamards, we can pass this information into ADAPT-AQC
|
| 39 |
+
starting_circuit = state_prep_circuit
|
| 40 |
+
|
| 41 |
+
adapt_compiler = AdaptCompiler(
|
| 42 |
+
target=qc,
|
| 43 |
+
adapt_config=config,
|
| 44 |
+
starting_circuit=starting_circuit,
|
| 45 |
+
initial_single_qubit_layer=True,
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
result = adapt_compiler.compile()
|
| 49 |
+
approx_circuit = result.circuit
|
| 50 |
+
print(f"Overlap between circuits is {result.overlap}")
|
| 51 |
+
|
| 52 |
+
# Transpile the original circuits to the common basis set with maximum Qiskit optimization
|
| 53 |
+
qc_in_basis_gates = transpile(
|
| 54 |
+
qc, basis_gates=["ry", "rz", "rx", "u3", "cx"], optimization_level=3
|
| 55 |
+
)
|
| 56 |
+
print("Original circuit gates:", qc_in_basis_gates.count_ops())
|
| 57 |
+
print("Original circuit depth:", qc_in_basis_gates.depth())
|
| 58 |
+
|
| 59 |
+
# Compare with compiled circuit
|
| 60 |
+
print("Compiled circuit gates:", approx_circuit.count_ops())
|
| 61 |
+
print("Compiled circuit depth:", approx_circuit.depth())
|
utils/adapt-aqc/examples/readme_example.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
|
| 3 |
+
from qiskit import QuantumCircuit, transpile
|
| 4 |
+
from qiskit.circuit.random import random_circuit
|
| 5 |
+
|
| 6 |
+
from adaptaqc.compilers import AdaptCompiler, AdaptConfig
|
| 7 |
+
from adaptaqc.utils.entanglement_measures import EM_TOMOGRAPHY_CONCURRENCE
|
| 8 |
+
|
| 9 |
+
logging.basicConfig()
|
| 10 |
+
logger = logging.getLogger("adaptaqc")
|
| 11 |
+
logger.setLevel(logging.INFO)
|
| 12 |
+
|
| 13 |
+
# Setup the circuit
|
| 14 |
+
qc = QuantumCircuit(3)
|
| 15 |
+
qc.rx(1.23, 0)
|
| 16 |
+
qc.cx(0, 1)
|
| 17 |
+
qc.ry(2.5, 1)
|
| 18 |
+
qc.rx(-1.6, 2)
|
| 19 |
+
qc.ccx(2, 1, 0)
|
| 20 |
+
|
| 21 |
+
# Compile
|
| 22 |
+
compiler = AdaptCompiler(qc)
|
| 23 |
+
result = compiler.compile()
|
| 24 |
+
compiled_circuit = result.circuit
|
| 25 |
+
|
| 26 |
+
# See the compiled output
|
| 27 |
+
print(f'{"-" * 10} ORIGINAL CIRCUIT {"-" * 10}')
|
| 28 |
+
print(qc)
|
| 29 |
+
print(f'{"-" * 10} RECOMPILED CIRCUIT {"-" * 10}')
|
| 30 |
+
print(compiled_circuit)
|
| 31 |
+
|
| 32 |
+
qc = random_circuit(5, 5, seed=1)
|
| 33 |
+
|
| 34 |
+
for i, (instr, _, _) in enumerate(qc.data):
|
| 35 |
+
if instr.name == "id":
|
| 36 |
+
qc.data.__delitem__(i)
|
| 37 |
+
|
| 38 |
+
# Compile
|
| 39 |
+
config = AdaptConfig(sufficient_cost=1e-2)
|
| 40 |
+
compiler = AdaptCompiler(
|
| 41 |
+
qc, entanglement_measure=EM_TOMOGRAPHY_CONCURRENCE, adapt_config=config
|
| 42 |
+
)
|
| 43 |
+
result = compiler.compile()
|
| 44 |
+
print(result)
|
| 45 |
+
compiled_circuit = result.circuit
|
| 46 |
+
|
| 47 |
+
# See the original circuit
|
| 48 |
+
print(f'{"-" * 10} ORIGINAL CIRCUIT {"-" * 10}')
|
| 49 |
+
print(qc)
|
| 50 |
+
|
| 51 |
+
# See the compiled solution
|
| 52 |
+
print(f'{"-" * 10} RECOMPILED CIRCUIT {"-" * 10}')
|
| 53 |
+
print(compiled_circuit)
|
| 54 |
+
|
| 55 |
+
# Transpile the original circuits to the common basis set
|
| 56 |
+
qc_in_basis_gates = transpile(
|
| 57 |
+
qc, basis_gates=["u1", "u2", "u3", "cx"], optimization_level=3
|
| 58 |
+
)
|
| 59 |
+
print(qc_in_basis_gates.count_ops())
|
| 60 |
+
print(qc_in_basis_gates.depth())
|
| 61 |
+
|
| 62 |
+
# Compare with compiled circuit
|
| 63 |
+
print(compiled_circuit.count_ops())
|
| 64 |
+
print(compiled_circuit.depth())
|
utils/adapt-aqc/examples/simple_mps_example.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Example script for running ADAPT-AQC recompilation using a Matrix Product State (MPS) backend.
|
| 3 |
+
MPS is an alternative quantum state representation to state-vector, and is better suited to handle
|
| 4 |
+
large, low-entanglement states.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import logging
|
| 8 |
+
|
| 9 |
+
from qiskit import QuantumCircuit
|
| 10 |
+
|
| 11 |
+
from adaptaqc.backends.aer_mps_backend import AerMPSBackend
|
| 12 |
+
from adaptaqc.compilers import AdaptCompiler
|
| 13 |
+
|
| 14 |
+
logging.basicConfig()
|
| 15 |
+
logger = logging.getLogger("adaptaqc")
|
| 16 |
+
logger.setLevel(logging.INFO)
|
| 17 |
+
|
| 18 |
+
# --------------------------------------------------------------------------------
|
| 19 |
+
# Very simple MPS example
|
| 20 |
+
# Create a large circuit where only some qubits are entangled
|
| 21 |
+
n = 50
|
| 22 |
+
qc = QuantumCircuit(n)
|
| 23 |
+
qc.h(0)
|
| 24 |
+
qc.cx(0, 1)
|
| 25 |
+
qc.h(2)
|
| 26 |
+
qc.cx(2, 3)
|
| 27 |
+
qc.h(range(4, n))
|
| 28 |
+
|
| 29 |
+
# Create compiler with the default MPS simulator, which has very minimal truncation.
|
| 30 |
+
adapt_compiler = AdaptCompiler(qc, backend=AerMPSBackend())
|
| 31 |
+
|
| 32 |
+
result = adapt_compiler.compile()
|
| 33 |
+
print(f"Overlap between circuits is {result.overlap}")
|
utils/adapt-aqc/examples/simple_sv_example.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
|
| 3 |
+
import adaptaqc.utils.circuit_operations as co
|
| 4 |
+
from adaptaqc.compilers import AdaptCompiler
|
| 5 |
+
|
| 6 |
+
logging.basicConfig()
|
| 7 |
+
logger = logging.getLogger("adaptaqc")
|
| 8 |
+
logger.setLevel(logging.INFO)
|
| 9 |
+
|
| 10 |
+
# Create circuit creating a random initial state
|
| 11 |
+
qc = co.create_random_initial_state_circuit(4)
|
| 12 |
+
|
| 13 |
+
adapt_compiler = AdaptCompiler(qc)
|
| 14 |
+
|
| 15 |
+
result = adapt_compiler.compile()
|
| 16 |
+
approx_circuit = result.circuit
|
| 17 |
+
print(f"Overlap between circuits is {result.overlap}")
|
| 18 |
+
print(f'{"-" * 32}')
|
| 19 |
+
print(f'{"-" * 10}OLD CIRCUIT{"-" * 10}')
|
| 20 |
+
print(f'{"-" * 32}')
|
| 21 |
+
print(qc)
|
| 22 |
+
print(f'{"-" * 32}')
|
| 23 |
+
print(f'{"-" * 10}ADAPT-AQC CIRCUIT{"-" * 10}')
|
| 24 |
+
print(f'{"-" * 32}')
|
| 25 |
+
print(approx_circuit)
|
utils/adapt-aqc/requirements.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
.
|
utils/adapt-aqc/setup.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from setuptools import find_packages, setup
|
| 2 |
+
|
| 3 |
+
setup(
|
| 4 |
+
name="adaptaqc",
|
| 5 |
+
version="1.0.0",
|
| 6 |
+
author="Ben Jaderberg, George Pennington, Abhishek Agarwal, Kate Marshall, Lewis Anderson",
|
| 7 |
+
author_email="benjamin.jaderberg@ibm.com, george.penngton@stfc.com, kate.marshall@ibm.com, lewis.anderson@ibm.com",
|
| 8 |
+
packages=find_packages(),
|
| 9 |
+
scripts=[],
|
| 10 |
+
url="https://github.com/todo/",
|
| 11 |
+
license="LICENSE",
|
| 12 |
+
description="A package for implementing the Adaptive \
|
| 13 |
+
Approximate Quantum Compiling (ADAPT-AQC) algorithm",
|
| 14 |
+
long_description=open("README.md").read(),
|
| 15 |
+
long_description_content_type="text/markdown",
|
| 16 |
+
install_requires=[
|
| 17 |
+
"qiskit>=1.3.1",
|
| 18 |
+
"qiskit_aer>=0.16.0",
|
| 19 |
+
"qiskit_experiments>=0.6.1",
|
| 20 |
+
"numpy",
|
| 21 |
+
"scipy",
|
| 22 |
+
"scipy",
|
| 23 |
+
"openfermion~=1.6",
|
| 24 |
+
"sympy",
|
| 25 |
+
"aqc_research @ git+ssh://git@github.com/bjader/aqc-research.git",
|
| 26 |
+
"physics-tenpy~=1.0.2",
|
| 27 |
+
],
|
| 28 |
+
)
|
utils/adapt-aqc/test/__init__.py
ADDED
|
File without changes
|
utils/adapt-aqc/test/recompilers/__init__.py
ADDED
|
File without changes
|
utils/adapt-aqc/test/recompilers/test_adapt_compiler.py
ADDED
|
@@ -0,0 +1,1543 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import copy
|
| 2 |
+
import logging
|
| 3 |
+
import os
|
| 4 |
+
import pickle
|
| 5 |
+
import random
|
| 6 |
+
import shutil
|
| 7 |
+
import tempfile
|
| 8 |
+
from unittest import TestCase
|
| 9 |
+
from unittest.mock import patch
|
| 10 |
+
|
| 11 |
+
import numpy as np
|
| 12 |
+
from aqc_research.model_sp_lhs.trotter.trotter import trotter_circuit
|
| 13 |
+
from aqc_research.mps_operations import mps_from_circuit
|
| 14 |
+
from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister
|
| 15 |
+
from qiskit.compiler import transpile
|
| 16 |
+
from qiskit.quantum_info import Statevector
|
| 17 |
+
from tenpy.algorithms import tebd
|
| 18 |
+
from tenpy.models import XXZChain
|
| 19 |
+
from tenpy.networks.mps import MPS
|
| 20 |
+
|
| 21 |
+
import adaptaqc.utils.ansatzes as ans
|
| 22 |
+
import adaptaqc.utils.circuit_operations as co
|
| 23 |
+
from adaptaqc.backends.python_default_backends import SV_SIM, MPS_SIM, QASM_SIM
|
| 24 |
+
from adaptaqc.compilers import AdaptConfig, AdaptCompiler
|
| 25 |
+
from adaptaqc.utils.constants import DEFAULT_SUFFICIENT_COST
|
| 26 |
+
from adaptaqc.utils.entanglement_measures import EM_TOMOGRAPHY_NEGATIVITY
|
| 27 |
+
from adaptaqc.utils.utilityfunctions import multi_qubit_gate_depth, tenpy_to_qiskit_mps
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def create_initial_ansatz():
|
| 31 |
+
initial_ansatz = QuantumCircuit(4)
|
| 32 |
+
initial_ansatz.ry(0, [0, 1, 2, 3])
|
| 33 |
+
initial_ansatz.cx(0, 1)
|
| 34 |
+
initial_ansatz.cx(1, 2)
|
| 35 |
+
initial_ansatz.cx(2, 3)
|
| 36 |
+
initial_ansatz.rx(0, [0, 1, 2, 3])
|
| 37 |
+
|
| 38 |
+
return initial_ansatz
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class TestAdapt(TestCase):
|
| 42 |
+
def test_adapt_procedure_sv(self):
|
| 43 |
+
qc = co.create_random_initial_state_circuit(3, seed=1)
|
| 44 |
+
qc = co.unroll_to_basis_gates(qc)
|
| 45 |
+
|
| 46 |
+
adapt_compiler = AdaptCompiler(
|
| 47 |
+
qc, backend=SV_SIM, adapt_config=AdaptConfig(sufficient_cost=1e-2)
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
result = adapt_compiler.compile()
|
| 51 |
+
approx_circuit = result.circuit
|
| 52 |
+
|
| 53 |
+
overlap = co.calculate_overlap_between_circuits(approx_circuit, qc)
|
| 54 |
+
assert overlap > 1 - DEFAULT_SUFFICIENT_COST
|
| 55 |
+
|
| 56 |
+
def test_adapt_procedure_qasm(self):
|
| 57 |
+
qc = co.create_random_initial_state_circuit(3, seed=1)
|
| 58 |
+
qc = co.unroll_to_basis_gates(qc)
|
| 59 |
+
|
| 60 |
+
shots = 1e4
|
| 61 |
+
adapt_compiler_qasm = AdaptCompiler(
|
| 62 |
+
qc, backend=QASM_SIM, execute_kwargs={"shots": shots}
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
result_qasm = adapt_compiler_qasm.compile()
|
| 66 |
+
approx_circuit_qasm = result_qasm.circuit
|
| 67 |
+
overlap = co.calculate_overlap_between_circuits(approx_circuit_qasm, qc)
|
| 68 |
+
assert overlap > 1 - DEFAULT_SUFFICIENT_COST - 5 / np.sqrt(shots)
|
| 69 |
+
|
| 70 |
+
def test_adapt_procedure_mps(self):
|
| 71 |
+
qc = co.create_random_initial_state_circuit(3, seed=1)
|
| 72 |
+
qc = co.unroll_to_basis_gates(qc)
|
| 73 |
+
|
| 74 |
+
shots = 1e4
|
| 75 |
+
adapt_compiler_mps = AdaptCompiler(
|
| 76 |
+
qc, backend=MPS_SIM, execute_kwargs={"shots": shots}
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
result_mps = adapt_compiler_mps.compile()
|
| 80 |
+
approx_circuit_mps = result_mps.circuit
|
| 81 |
+
|
| 82 |
+
overlap = co.calculate_overlap_between_circuits(approx_circuit_mps, qc)
|
| 83 |
+
assert overlap > 1 - DEFAULT_SUFFICIENT_COST - 5 / np.sqrt(shots)
|
| 84 |
+
|
| 85 |
+
def test_adapt_procedure_when_input_mps_directly(self):
|
| 86 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 87 |
+
qc = co.unroll_to_basis_gates(qc)
|
| 88 |
+
mps = mps_from_circuit(qc.copy(), sim=MPS_SIM.simulator)
|
| 89 |
+
|
| 90 |
+
# Input MPS for recompilation rather than QuantumCircuit
|
| 91 |
+
compiler = AdaptCompiler(mps, backend=MPS_SIM)
|
| 92 |
+
|
| 93 |
+
result = compiler.compile()
|
| 94 |
+
approx_circuit = result.circuit
|
| 95 |
+
|
| 96 |
+
overlap = co.calculate_overlap_between_circuits(approx_circuit, qc)
|
| 97 |
+
assert overlap > 1 - DEFAULT_SUFFICIENT_COST
|
| 98 |
+
|
| 99 |
+
def test_GHZ(self):
|
| 100 |
+
qc = QuantumCircuit(5)
|
| 101 |
+
|
| 102 |
+
qc.h(0)
|
| 103 |
+
for i in range(4):
|
| 104 |
+
qc.cx(i, i + 1)
|
| 105 |
+
|
| 106 |
+
qc = co.unroll_to_basis_gates(qc)
|
| 107 |
+
|
| 108 |
+
adapt_compiler = AdaptCompiler(
|
| 109 |
+
qc, backend=SV_SIM, adapt_config=AdaptConfig(sufficient_cost=1e-2)
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
result = adapt_compiler.compile()
|
| 113 |
+
approx_circuit = result.circuit
|
| 114 |
+
|
| 115 |
+
overlap = co.calculate_overlap_between_circuits(approx_circuit, qc)
|
| 116 |
+
assert overlap > 1 - DEFAULT_SUFFICIENT_COST
|
| 117 |
+
|
| 118 |
+
def test_exact_overlap_close_to_approx_overlap(self):
|
| 119 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 120 |
+
qc = co.unroll_to_basis_gates(qc)
|
| 121 |
+
|
| 122 |
+
adapt_compiler = AdaptCompiler(qc)
|
| 123 |
+
|
| 124 |
+
result = adapt_compiler.compile()
|
| 125 |
+
approx_circuit = result.circuit
|
| 126 |
+
approx_overlap = result.overlap
|
| 127 |
+
exact_overlap = result.exact_overlap
|
| 128 |
+
self.assertAlmostEqual(approx_overlap, exact_overlap, delta=1e-2)
|
| 129 |
+
|
| 130 |
+
def test_exact_overlap_calculated_correctly(self):
|
| 131 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 132 |
+
qc = co.unroll_to_basis_gates(qc)
|
| 133 |
+
|
| 134 |
+
adapt_compiler = AdaptCompiler(qc)
|
| 135 |
+
|
| 136 |
+
result = adapt_compiler.compile()
|
| 137 |
+
approx_circuit = result.circuit
|
| 138 |
+
exact_overlap1 = result.exact_overlap
|
| 139 |
+
exact_overlap2 = co.calculate_overlap_between_circuits(approx_circuit, qc)
|
| 140 |
+
self.assertAlmostEqual(exact_overlap1, exact_overlap2, delta=1e-2)
|
| 141 |
+
|
| 142 |
+
def test_local_cost_sv(self):
|
| 143 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 144 |
+
qc = co.unroll_to_basis_gates(qc)
|
| 145 |
+
adapt_config = AdaptConfig(cost_improvement_num_layers=10)
|
| 146 |
+
|
| 147 |
+
adapt_compiler = AdaptCompiler(
|
| 148 |
+
qc, optimise_local_cost=True, backend=SV_SIM, adapt_config=adapt_config
|
| 149 |
+
)
|
| 150 |
+
result = adapt_compiler.compile()
|
| 151 |
+
cost = adapt_compiler.evaluate_cost()
|
| 152 |
+
assert cost < DEFAULT_SUFFICIENT_COST
|
| 153 |
+
|
| 154 |
+
def test_custom_layer_gate(self):
|
| 155 |
+
from qiskit import QuantumCircuit
|
| 156 |
+
|
| 157 |
+
from adaptaqc.utils.fixed_ansatz_circuits import number_preserving_ansatz
|
| 158 |
+
|
| 159 |
+
# Initialize to a supervision of states with bit sum 2
|
| 160 |
+
statevector = [
|
| 161 |
+
0,
|
| 162 |
+
0,
|
| 163 |
+
0,
|
| 164 |
+
-((1 / 3) ** 0.5),
|
| 165 |
+
0,
|
| 166 |
+
1j * (1 / 3) ** 0.5,
|
| 167 |
+
-1 * (1 / 3) ** 0.5,
|
| 168 |
+
0,
|
| 169 |
+
]
|
| 170 |
+
qc = co.initial_state_to_circuit(statevector)
|
| 171 |
+
|
| 172 |
+
initial_circuit = QuantumCircuit(3)
|
| 173 |
+
initial_circuit.x(0)
|
| 174 |
+
initial_circuit.x(1)
|
| 175 |
+
|
| 176 |
+
adapt_compiler = AdaptCompiler(
|
| 177 |
+
qc,
|
| 178 |
+
custom_layer_2q_gate=number_preserving_ansatz(2, 1),
|
| 179 |
+
starting_circuit=initial_circuit,
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
result = adapt_compiler.compile()
|
| 183 |
+
approx_circuit = result.circuit
|
| 184 |
+
|
| 185 |
+
overlap = co.calculate_overlap_between_circuits(approx_circuit, qc)
|
| 186 |
+
assert overlap > 1 - DEFAULT_SUFFICIENT_COST
|
| 187 |
+
|
| 188 |
+
def test_with_initial_ansatz(self):
|
| 189 |
+
from adaptaqc.utils.fixed_ansatz_circuits import hardware_efficient_circuit
|
| 190 |
+
|
| 191 |
+
qc = hardware_efficient_circuit(3, "rxrz", 3)
|
| 192 |
+
|
| 193 |
+
qc_mod = qc.copy()
|
| 194 |
+
qc_mod.cx(0, 1)
|
| 195 |
+
qc_mod.h(1)
|
| 196 |
+
qc_mod.cx(1, 2)
|
| 197 |
+
|
| 198 |
+
adapt_compiler = AdaptCompiler(qc_mod)
|
| 199 |
+
|
| 200 |
+
result = adapt_compiler.compile(initial_ansatz=qc)
|
| 201 |
+
approx_circuit = result.circuit
|
| 202 |
+
|
| 203 |
+
overlap = co.calculate_overlap_between_circuits(approx_circuit, qc_mod)
|
| 204 |
+
assert overlap > 1 - DEFAULT_SUFFICIENT_COST
|
| 205 |
+
|
| 206 |
+
def test_expectation_method(self):
|
| 207 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 208 |
+
qc = co.unroll_to_basis_gates(qc)
|
| 209 |
+
config = AdaptConfig(method="expectation")
|
| 210 |
+
|
| 211 |
+
adapt_compiler = AdaptCompiler(qc, adapt_config=config)
|
| 212 |
+
result = adapt_compiler.compile()
|
| 213 |
+
approx_circuit = result.circuit
|
| 214 |
+
overlap = co.calculate_overlap_between_circuits(approx_circuit, qc)
|
| 215 |
+
assert overlap > 1 - DEFAULT_SUFFICIENT_COST
|
| 216 |
+
|
| 217 |
+
def test_basic_methods(self):
|
| 218 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 219 |
+
qc = co.unroll_to_basis_gates(qc)
|
| 220 |
+
config = AdaptConfig(method="basic")
|
| 221 |
+
|
| 222 |
+
adapt_compiler = AdaptCompiler(qc, adapt_config=config)
|
| 223 |
+
result = adapt_compiler.compile()
|
| 224 |
+
approx_circuit = result.circuit
|
| 225 |
+
overlap = co.calculate_overlap_between_circuits(approx_circuit, qc)
|
| 226 |
+
assert overlap > 1 - DEFAULT_SUFFICIENT_COST
|
| 227 |
+
|
| 228 |
+
def test_random_methods(self):
|
| 229 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 230 |
+
qc = co.unroll_to_basis_gates(qc)
|
| 231 |
+
config = AdaptConfig(method="random")
|
| 232 |
+
|
| 233 |
+
adapt_compiler = AdaptCompiler(qc, adapt_config=config)
|
| 234 |
+
result = adapt_compiler.compile()
|
| 235 |
+
approx_circuit = result.circuit
|
| 236 |
+
overlap = co.calculate_overlap_between_circuits(approx_circuit, qc)
|
| 237 |
+
assert overlap > 1 - DEFAULT_SUFFICIENT_COST
|
| 238 |
+
|
| 239 |
+
def test_given_circuit_with_non_basis_gates_when_recompiling_then_no_error(self):
|
| 240 |
+
qc1 = QuantumCircuit(2)
|
| 241 |
+
qc1.h([0, 1])
|
| 242 |
+
qc2 = QuantumCircuit(2)
|
| 243 |
+
qc2.x(1)
|
| 244 |
+
qc2.append(qc1.to_instruction(), qc2.qregs[0])
|
| 245 |
+
compiler = AdaptCompiler(qc2)
|
| 246 |
+
compiler.compile()
|
| 247 |
+
|
| 248 |
+
def test_given_starting_circuit_when_compile_with_debug_logging_then_happy(self):
|
| 249 |
+
logging.basicConfig()
|
| 250 |
+
logging.getLogger("adaptaqc").setLevel(logging.DEBUG)
|
| 251 |
+
|
| 252 |
+
n = 3
|
| 253 |
+
|
| 254 |
+
starting_ansatz_circuit = QuantumCircuit(n)
|
| 255 |
+
starting_ansatz_circuit.x(range(0, n, 2))
|
| 256 |
+
|
| 257 |
+
qc = co.create_random_initial_state_circuit(n)
|
| 258 |
+
|
| 259 |
+
compiler = AdaptCompiler(qc, starting_circuit=starting_ansatz_circuit)
|
| 260 |
+
|
| 261 |
+
compiler.compile()
|
| 262 |
+
logging.getLogger("adaptaqc").setLevel(logging.WARNING)
|
| 263 |
+
|
| 264 |
+
def test_given_starting_circuit_when_compile_then_solution_starts_with_it(self):
|
| 265 |
+
n = 2
|
| 266 |
+
starting_ansatz_circuit = QuantumCircuit(n)
|
| 267 |
+
starting_ansatz_circuit.x(0)
|
| 268 |
+
|
| 269 |
+
qc = co.create_random_initial_state_circuit(n)
|
| 270 |
+
|
| 271 |
+
for boolean in [False, True]:
|
| 272 |
+
compiler = AdaptCompiler(
|
| 273 |
+
qc,
|
| 274 |
+
starting_circuit=starting_ansatz_circuit,
|
| 275 |
+
initial_single_qubit_layer=boolean,
|
| 276 |
+
)
|
| 277 |
+
|
| 278 |
+
result = compiler.compile()
|
| 279 |
+
compiled_qc: QuantumCircuit = result.circuit
|
| 280 |
+
del compiled_qc.data[1:]
|
| 281 |
+
overlap = (
|
| 282 |
+
np.abs(
|
| 283 |
+
np.dot(
|
| 284 |
+
Statevector(compiled_qc).conjugate(),
|
| 285 |
+
Statevector(starting_ansatz_circuit),
|
| 286 |
+
)
|
| 287 |
+
)
|
| 288 |
+
** 2
|
| 289 |
+
)
|
| 290 |
+
self.assertAlmostEqual(overlap, 1)
|
| 291 |
+
|
| 292 |
+
def test_given_two_registers_when_recompiling_then_no_error(self):
|
| 293 |
+
qr1 = QuantumRegister(2)
|
| 294 |
+
qr2 = QuantumRegister(2)
|
| 295 |
+
qc = QuantumCircuit(qr1, qr2)
|
| 296 |
+
compiler = AdaptCompiler(qc)
|
| 297 |
+
result = compiler.compile()
|
| 298 |
+
|
| 299 |
+
def test_given_two_registers_when_recompiling_then_register_names_preserved(self):
|
| 300 |
+
qr1 = QuantumRegister(2, "reg1")
|
| 301 |
+
qr2 = QuantumRegister(2, "reg2")
|
| 302 |
+
qc = QuantumCircuit(qr1, qr2)
|
| 303 |
+
qc.h(1)
|
| 304 |
+
qc.cx(1, 2)
|
| 305 |
+
qc.x(3)
|
| 306 |
+
compiler = AdaptCompiler(qc)
|
| 307 |
+
result = compiler.compile()
|
| 308 |
+
final_circuit = result.circuit
|
| 309 |
+
assert final_circuit.qregs == qc.qregs
|
| 310 |
+
|
| 311 |
+
def test_given_circuit_with_cregs_when_recompiling_then_no_error(self):
|
| 312 |
+
qreg = QuantumRegister(2)
|
| 313 |
+
creg = ClassicalRegister(2)
|
| 314 |
+
qc = QuantumCircuit(qreg, creg)
|
| 315 |
+
|
| 316 |
+
compiler = AdaptCompiler(qc)
|
| 317 |
+
compiler.compile()
|
| 318 |
+
|
| 319 |
+
def test_given_circuit_with_cregs_when_recompiling_then_register_names_preserved(
|
| 320 |
+
self,
|
| 321 |
+
):
|
| 322 |
+
qreg = QuantumRegister(2)
|
| 323 |
+
creg = ClassicalRegister(2)
|
| 324 |
+
qc = QuantumCircuit(qreg, creg)
|
| 325 |
+
|
| 326 |
+
compiler = AdaptCompiler(qc)
|
| 327 |
+
result = compiler.compile()
|
| 328 |
+
final_circuit = result.circuit
|
| 329 |
+
assert final_circuit.cregs == qc.cregs
|
| 330 |
+
|
| 331 |
+
# TODO this test fails when if setting initial_single_qubit_layer=True
|
| 332 |
+
# TODO Not priority fix as unusual case of |00..0> target state and circ with measurements.
|
| 333 |
+
def test_given_circuit_with_measurements_when_recompiling_then_no_error(self):
|
| 334 |
+
qreg = QuantumRegister(2)
|
| 335 |
+
creg = ClassicalRegister(2)
|
| 336 |
+
qc = QuantumCircuit(qreg, creg)
|
| 337 |
+
qc.cx(0, 1)
|
| 338 |
+
qc.measure(0, 0)
|
| 339 |
+
compiler = AdaptCompiler(qc, initial_single_qubit_layer=False)
|
| 340 |
+
compiler.compile()
|
| 341 |
+
|
| 342 |
+
def test_circuit_output_regularly_saved(self):
|
| 343 |
+
qc = co.create_random_initial_state_circuit(3, seed=1)
|
| 344 |
+
qc = co.unroll_to_basis_gates(qc)
|
| 345 |
+
|
| 346 |
+
shots = 1e4
|
| 347 |
+
adapt_compiler = AdaptCompiler(
|
| 348 |
+
qc,
|
| 349 |
+
backend=MPS_SIM,
|
| 350 |
+
execute_kwargs={"shots": shots},
|
| 351 |
+
save_circuit_history=True,
|
| 352 |
+
)
|
| 353 |
+
|
| 354 |
+
result = adapt_compiler.compile()
|
| 355 |
+
self.assertTrue(
|
| 356 |
+
len(result.circuit_history) == len(result.global_cost_history) - 1
|
| 357 |
+
)
|
| 358 |
+
self.assertTrue(
|
| 359 |
+
len(result.circuit_history[-1]) > len(result.circuit_history[-2])
|
| 360 |
+
)
|
| 361 |
+
|
| 362 |
+
def test_circuit_output_not_saved_when_not_flagged(self):
|
| 363 |
+
qc = co.create_random_initial_state_circuit(3, seed=1)
|
| 364 |
+
qc = co.unroll_to_basis_gates(qc)
|
| 365 |
+
|
| 366 |
+
shots = 1e4
|
| 367 |
+
adapt_compiler = AdaptCompiler(
|
| 368 |
+
qc, backend=MPS_SIM, execute_kwargs={"shots": shots}
|
| 369 |
+
)
|
| 370 |
+
|
| 371 |
+
result = adapt_compiler.compile()
|
| 372 |
+
self.assertFalse(len(result.circuit_history))
|
| 373 |
+
|
| 374 |
+
# TODO See above
|
| 375 |
+
def test_given_circuit_with_one_measurement_when_recompiling_then_preserve_measurement(
|
| 376 |
+
self,
|
| 377 |
+
):
|
| 378 |
+
qreg = QuantumRegister(2)
|
| 379 |
+
creg = ClassicalRegister(2)
|
| 380 |
+
qc = QuantumCircuit(qreg, creg)
|
| 381 |
+
qc.cx(0, 1)
|
| 382 |
+
qc.measure(0, 0)
|
| 383 |
+
compiler = AdaptCompiler(qc, initial_single_qubit_layer=False)
|
| 384 |
+
result = compiler.compile()
|
| 385 |
+
assert result.circuit.data[-1] == qc.data[-1]
|
| 386 |
+
|
| 387 |
+
# TODO See above
|
| 388 |
+
def test_given_circuit_with_multi_measurement_when_recompiling_then_preserve_measurement(
|
| 389 |
+
self,
|
| 390 |
+
):
|
| 391 |
+
num_measurements = 3
|
| 392 |
+
qreg = QuantumRegister(num_measurements + 2)
|
| 393 |
+
creg = ClassicalRegister(num_measurements + 2)
|
| 394 |
+
qc = QuantumCircuit(qreg, creg)
|
| 395 |
+
qc.cx(0, 1)
|
| 396 |
+
for i in range(num_measurements):
|
| 397 |
+
qc.measure(i, i)
|
| 398 |
+
compiler = AdaptCompiler(qc, initial_single_qubit_layer=False)
|
| 399 |
+
result = compiler.compile()
|
| 400 |
+
assert result.circuit.data[-num_measurements:] == qc.data[-num_measurements:]
|
| 401 |
+
|
| 402 |
+
def test_given_compiler_when_float_cost_improvement_num_layers_then_no_error(
|
| 403 |
+
self,
|
| 404 |
+
):
|
| 405 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 406 |
+
config = AdaptConfig(cost_improvement_num_layers=4.0, cost_improvement_tol=1)
|
| 407 |
+
compiler = AdaptCompiler(qc, adapt_config=config)
|
| 408 |
+
compiler.compile()
|
| 409 |
+
|
| 410 |
+
def test_given_initial_single_qubit_layer_when_compiling_then_then_good_solution(
|
| 411 |
+
self,
|
| 412 |
+
):
|
| 413 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 414 |
+
compiler = AdaptCompiler(qc, initial_single_qubit_layer=True)
|
| 415 |
+
result = compiler.compile()
|
| 416 |
+
approx_circuit = result.circuit
|
| 417 |
+
overlap = co.calculate_overlap_between_circuits(approx_circuit, qc)
|
| 418 |
+
self.assertTrue(overlap > 1 - DEFAULT_SUFFICIENT_COST)
|
| 419 |
+
|
| 420 |
+
def test_given_isql_when_compiling_zero_state_then_zero_depth_solution(self):
|
| 421 |
+
qc = QuantumCircuit(3)
|
| 422 |
+
compiler = AdaptCompiler(qc, initial_single_qubit_layer=True)
|
| 423 |
+
result = compiler.compile()
|
| 424 |
+
approx_circuit = result.circuit
|
| 425 |
+
self.assertEqual(approx_circuit.depth(), 0, "Depth of solution should be zero")
|
| 426 |
+
|
| 427 |
+
def test_given_isql_when_compiling_then_ansatz_starts_with_n_single_qubit_gates(
|
| 428 |
+
self,
|
| 429 |
+
):
|
| 430 |
+
n = 3
|
| 431 |
+
qc = co.create_random_initial_state_circuit(n)
|
| 432 |
+
config = AdaptConfig(max_layers=2)
|
| 433 |
+
compiler = AdaptCompiler(
|
| 434 |
+
qc, adapt_config=config, initial_single_qubit_layer=True
|
| 435 |
+
)
|
| 436 |
+
result = compiler.compile()
|
| 437 |
+
|
| 438 |
+
ansatz_start, ansatz_end = compiler.variational_circuit_range()
|
| 439 |
+
ansatz = compiler.full_circuit[ansatz_start:ansatz_end]
|
| 440 |
+
for instr in ansatz[:n]:
|
| 441 |
+
self.assertIn(instr[0].name, ["rx", "ry", "rz"])
|
| 442 |
+
|
| 443 |
+
def test_given_isql_when_compiling_then_results_object_elements_correct_length(
|
| 444 |
+
self,
|
| 445 |
+
):
|
| 446 |
+
qc = QuantumCircuit(3)
|
| 447 |
+
compiler = AdaptCompiler(qc, initial_single_qubit_layer=True)
|
| 448 |
+
result = compiler.compile()
|
| 449 |
+
self.assertTrue(
|
| 450 |
+
len(result.global_cost_history) - 1
|
| 451 |
+
== len(result.entanglement_measures_history)
|
| 452 |
+
== len(result.e_val_history)
|
| 453 |
+
== len(result.qubit_pair_history)
|
| 454 |
+
== len(result.method_history)
|
| 455 |
+
)
|
| 456 |
+
|
| 457 |
+
def test_given_adapt_mode_when_compile_circuit_with_very_small_entanglement_then_expectation_method_used(
|
| 458 |
+
self,
|
| 459 |
+
):
|
| 460 |
+
qc = QuantumCircuit(2)
|
| 461 |
+
qc.h(0)
|
| 462 |
+
qc.crx(1e-15, 0, 1)
|
| 463 |
+
|
| 464 |
+
compiler = AdaptCompiler(qc, entanglement_measure=EM_TOMOGRAPHY_NEGATIVITY)
|
| 465 |
+
result = compiler.compile()
|
| 466 |
+
self.assertTrue("expectation" in result.method_history)
|
| 467 |
+
|
| 468 |
+
@patch.object(SV_SIM, "measure_qubit_expectation_values")
|
| 469 |
+
def test_given_entanglement_when_find_highest_entanglement_pair_then_evals_not_evaluated(
|
| 470 |
+
self, mock_get_evals
|
| 471 |
+
):
|
| 472 |
+
compiler = AdaptCompiler(QuantumCircuit(2))
|
| 473 |
+
compiler._find_best_entanglement_qubit_pair([0.5])
|
| 474 |
+
mock_get_evals.assert_not_called()
|
| 475 |
+
|
| 476 |
+
@patch.object(SV_SIM, "measure_qubit_expectation_values")
|
| 477 |
+
def test_given_entanglement_when_find_appropriate_pair_then_evals_not_evaluated(
|
| 478 |
+
self, mock_get_evals
|
| 479 |
+
):
|
| 480 |
+
qc = QuantumCircuit(2)
|
| 481 |
+
qc.h(0)
|
| 482 |
+
qc.cx(0, 1)
|
| 483 |
+
compiler = AdaptCompiler(qc)
|
| 484 |
+
compiler._find_appropriate_qubit_pair()
|
| 485 |
+
mock_get_evals.assert_not_called()
|
| 486 |
+
|
| 487 |
+
def test_given_compiling_with_isql_when_add_layer_then_correct_indices_modified(
|
| 488 |
+
self,
|
| 489 |
+
):
|
| 490 |
+
qc = co.create_random_initial_state_circuit(3, seed=0)
|
| 491 |
+
num_gates_u = len(qc.data)
|
| 492 |
+
config = AdaptConfig(rotosolve_frequency=1e5)
|
| 493 |
+
compiler = AdaptCompiler(
|
| 494 |
+
qc,
|
| 495 |
+
initial_single_qubit_layer=True,
|
| 496 |
+
adapt_config=config,
|
| 497 |
+
)
|
| 498 |
+
compiler._add_layer(0)
|
| 499 |
+
full_circuit = compiler.full_circuit.copy()
|
| 500 |
+
layer_1_data = full_circuit[num_gates_u:]
|
| 501 |
+
for gate in layer_1_data:
|
| 502 |
+
self.assertTrue(
|
| 503 |
+
gate[0].params[0] != 0, "Added layer should have modified angles"
|
| 504 |
+
)
|
| 505 |
+
|
| 506 |
+
compiler._add_layer(1)
|
| 507 |
+
full_circuit = compiler.full_circuit.copy()
|
| 508 |
+
layer_1_and_2_data = full_circuit[num_gates_u:]
|
| 509 |
+
self.assertEqual(
|
| 510 |
+
layer_1_data,
|
| 511 |
+
layer_1_and_2_data[: len(layer_1_data)],
|
| 512 |
+
"Adding next layer and using Rotoselect should not modify previous layer",
|
| 513 |
+
)
|
| 514 |
+
|
| 515 |
+
layer_2_data = layer_1_and_2_data[len(layer_1_data) :]
|
| 516 |
+
for gate in layer_2_data:
|
| 517 |
+
if gate[0].name != "cx":
|
| 518 |
+
self.assertTrue(
|
| 519 |
+
gate[0].params[0] != 0, "Added layer should have modified angles"
|
| 520 |
+
)
|
| 521 |
+
|
| 522 |
+
def test_given_random_circuit_and_starting_circuit_True_when_count_gates_in_solution_then_correct(
|
| 523 |
+
self,
|
| 524 |
+
):
|
| 525 |
+
qc = co.create_random_circuit(3)
|
| 526 |
+
starting_circuit = co.create_random_initial_state_circuit(3)
|
| 527 |
+
compiler = AdaptCompiler(qc, starting_circuit=starting_circuit)
|
| 528 |
+
result = compiler.compile()
|
| 529 |
+
|
| 530 |
+
num_1q_gates = 0
|
| 531 |
+
num_2q_gates = 0
|
| 532 |
+
for instr in result.circuit.data:
|
| 533 |
+
if instr.operation.name == "cx":
|
| 534 |
+
num_2q_gates += 1
|
| 535 |
+
elif instr.operation.name == "rx" or "ry" or "rz":
|
| 536 |
+
num_1q_gates += 1
|
| 537 |
+
|
| 538 |
+
self.assertEqual(
|
| 539 |
+
(num_1q_gates, num_2q_gates), (result.num_1q_gates, result.num_2q_gates)
|
| 540 |
+
)
|
| 541 |
+
|
| 542 |
+
def test_given_wrong_reuse_prio_mode_when_compile_then_error(self):
|
| 543 |
+
qc = co.create_random_initial_state_circuit(4)
|
| 544 |
+
config = AdaptConfig(reuse_priority_mode="foo")
|
| 545 |
+
compiler = AdaptCompiler(qc, adapt_config=config)
|
| 546 |
+
with self.assertRaises(ValueError):
|
| 547 |
+
compiler.compile()
|
| 548 |
+
|
| 549 |
+
def test_when_add_layer_then_previous_pair_reuse_priority_minus_1(self):
|
| 550 |
+
qc = co.create_random_initial_state_circuit(4)
|
| 551 |
+
config = AdaptConfig(rotosolve_frequency=1e5)
|
| 552 |
+
compiler = AdaptCompiler(
|
| 553 |
+
qc,
|
| 554 |
+
adapt_config=config,
|
| 555 |
+
)
|
| 556 |
+
compiler._add_layer(0)
|
| 557 |
+
|
| 558 |
+
pair_acted_on = compiler.qubit_pair_history[0]
|
| 559 |
+
priority = compiler._get_qubit_reuse_priority(pair_acted_on, k=0)
|
| 560 |
+
|
| 561 |
+
self.assertEqual(priority, -1)
|
| 562 |
+
|
| 563 |
+
def test_given_exponent_equal_to_zero_when_find_reuse_priorities_then_correct(self):
|
| 564 |
+
qc = co.create_random_initial_state_circuit(4)
|
| 565 |
+
config = AdaptConfig(rotosolve_frequency=1e5)
|
| 566 |
+
compiler = AdaptCompiler(
|
| 567 |
+
qc,
|
| 568 |
+
adapt_config=config,
|
| 569 |
+
)
|
| 570 |
+
compiler._add_layer(0)
|
| 571 |
+
|
| 572 |
+
pair_acted_on = compiler.qubit_pair_history[0]
|
| 573 |
+
priorities = compiler._get_all_qubit_pair_reuse_priorities(k=0)
|
| 574 |
+
|
| 575 |
+
for pair in compiler.coupling_map:
|
| 576 |
+
if pair != pair_acted_on:
|
| 577 |
+
self.assertEqual(priorities[compiler.coupling_map.index(pair)], 1)
|
| 578 |
+
|
| 579 |
+
def test_given_exponent_equal_to_one_when_find_qubit_reuse_priorities_then_correct(
|
| 580 |
+
self,
|
| 581 |
+
):
|
| 582 |
+
qc = co.create_random_initial_state_circuit(4)
|
| 583 |
+
config = AdaptConfig(
|
| 584 |
+
rotosolve_frequency=1e5, reuse_exponent=1, reuse_priority_mode="qubit"
|
| 585 |
+
)
|
| 586 |
+
compiler = AdaptCompiler(
|
| 587 |
+
qc,
|
| 588 |
+
adapt_config=config,
|
| 589 |
+
)
|
| 590 |
+
compiler._add_layer(0)
|
| 591 |
+
|
| 592 |
+
pair_acted_on = compiler.qubit_pair_history[0]
|
| 593 |
+
priorities = compiler._get_all_qubit_pair_reuse_priorities(k=1)
|
| 594 |
+
|
| 595 |
+
for pair in compiler.coupling_map:
|
| 596 |
+
if pair != pair_acted_on:
|
| 597 |
+
if pair[0] in pair_acted_on or pair[1] in pair_acted_on:
|
| 598 |
+
self.assertEqual(priorities[compiler.coupling_map.index(pair)], 0.5)
|
| 599 |
+
else:
|
| 600 |
+
self.assertEqual(priorities[compiler.coupling_map.index(pair)], 1)
|
| 601 |
+
|
| 602 |
+
def test_given_random_exponents_when_add_layer_then_same_qubit_pair_never_acted_on_twice_in_a_row(
|
| 603 |
+
self,
|
| 604 |
+
):
|
| 605 |
+
qc = co.create_random_initial_state_circuit(4)
|
| 606 |
+
config = AdaptConfig(
|
| 607 |
+
rotosolve_frequency=1e5,
|
| 608 |
+
reuse_exponent=np.random.rand() * 2,
|
| 609 |
+
)
|
| 610 |
+
compiler = AdaptCompiler(
|
| 611 |
+
qc,
|
| 612 |
+
adapt_config=config,
|
| 613 |
+
)
|
| 614 |
+
compiler._add_layer(0)
|
| 615 |
+
for i in range(10):
|
| 616 |
+
compiler._add_layer(i + 1)
|
| 617 |
+
self.assertTrue(
|
| 618 |
+
compiler.qubit_pair_history[-1] != compiler.qubit_pair_history[-2],
|
| 619 |
+
"Same pair should not be acted on twice",
|
| 620 |
+
)
|
| 621 |
+
|
| 622 |
+
def test_given_circuit_when_manually_find_correct_pair_to_act_on_then_pair_acted_on_by_add_layer(
|
| 623 |
+
self,
|
| 624 |
+
):
|
| 625 |
+
qc = co.create_random_initial_state_circuit(4)
|
| 626 |
+
config = AdaptConfig(rotosolve_frequency=1e5, reuse_exponent=1)
|
| 627 |
+
compiler = AdaptCompiler(
|
| 628 |
+
qc,
|
| 629 |
+
adapt_config=config,
|
| 630 |
+
)
|
| 631 |
+
compiler._add_layer(0)
|
| 632 |
+
|
| 633 |
+
# Manually find pair which should be acted on when add_layer() is called
|
| 634 |
+
reuse_priorities = compiler._get_all_qubit_pair_reuse_priorities(k=1)
|
| 635 |
+
entanglements = compiler._get_all_qubit_pair_entanglement_measures()
|
| 636 |
+
priorities = [
|
| 637 |
+
reuse_priorities[i] * entanglements[i] for i in range(len(reuse_priorities))
|
| 638 |
+
]
|
| 639 |
+
correct_pair = compiler.coupling_map[priorities.index(max(priorities))]
|
| 640 |
+
|
| 641 |
+
compiler._add_layer(1)
|
| 642 |
+
|
| 643 |
+
self.assertTrue(compiler.qubit_pair_history[-1] == correct_pair)
|
| 644 |
+
|
| 645 |
+
def test_given_random_rotosolve_frequency_and_max_layers_to_modify_values_when_compile_mps_then_works(
|
| 646 |
+
self,
|
| 647 |
+
):
|
| 648 |
+
n = 3
|
| 649 |
+
starting_circuit = QuantumCircuit(n)
|
| 650 |
+
starting_circuit.x(range(0, n, 2))
|
| 651 |
+
for isql in [True, False]:
|
| 652 |
+
for sc in [starting_circuit, None, "tenpy_product_state"]:
|
| 653 |
+
qc = co.create_random_initial_state_circuit(n)
|
| 654 |
+
rotosolve_frequency = np.random.randint(1, 101)
|
| 655 |
+
max_layers_to_modify = np.random.randint(1, 101)
|
| 656 |
+
config = AdaptConfig(
|
| 657 |
+
rotosolve_frequency=rotosolve_frequency,
|
| 658 |
+
max_layers_to_modify=max_layers_to_modify,
|
| 659 |
+
cost_improvement_num_layers=100,
|
| 660 |
+
)
|
| 661 |
+
compiler = AdaptCompiler(
|
| 662 |
+
qc,
|
| 663 |
+
backend=MPS_SIM,
|
| 664 |
+
adapt_config=config,
|
| 665 |
+
starting_circuit=sc,
|
| 666 |
+
initial_single_qubit_layer=isql,
|
| 667 |
+
)
|
| 668 |
+
result = compiler.compile()
|
| 669 |
+
overlap = co.calculate_overlap_between_circuits(qc, result.circuit)
|
| 670 |
+
|
| 671 |
+
self.assertGreater(overlap, 1 - DEFAULT_SUFFICIENT_COST)
|
| 672 |
+
|
| 673 |
+
def test_given_mps_backend_when_add_layer_then_num_gates_not_in_mps_is_as_expected(
|
| 674 |
+
self,
|
| 675 |
+
):
|
| 676 |
+
# Test both cases of using/not using an initial ansatz
|
| 677 |
+
initial_ansatz_circ = create_initial_ansatz()
|
| 678 |
+
|
| 679 |
+
qc = co.create_random_initial_state_circuit(4)
|
| 680 |
+
config = AdaptConfig(rotosolve_frequency=4, max_layers_to_modify=3)
|
| 681 |
+
# Rotosolve happens on layers 4, 8, 12...
|
| 682 |
+
# Add layer 0: absorb layer -> 0 gates left in ansatz
|
| 683 |
+
# Add layer 1: absorb layer -> 0 gates
|
| 684 |
+
# Add layer 2: don't absorb layer -> 5 gates
|
| 685 |
+
# Add layer 3: don't absorb layer -> 10 gates
|
| 686 |
+
# Add layer 4: absorb layers 2, 3, 4 -> 0 gates
|
| 687 |
+
# Etc.
|
| 688 |
+
expected_num_gates = [0, 0, 5, 10, 0, 0, 5, 10, 0, 0, 5, 10, 0]
|
| 689 |
+
for initial_ansatz in [initial_ansatz_circ, None]:
|
| 690 |
+
compiler = AdaptCompiler(qc, backend=MPS_SIM, adapt_config=config)
|
| 691 |
+
actual_num_gates = []
|
| 692 |
+
if initial_ansatz is not None:
|
| 693 |
+
# initial_ansatz should be absorbed into MPS and added to ref_circuit_as_gates
|
| 694 |
+
compiler._add_initial_ansatz(
|
| 695 |
+
initial_ansatz, optimise_initial_ansatz=True
|
| 696 |
+
)
|
| 697 |
+
self.assertEqual(len(compiler.full_circuit), 1)
|
| 698 |
+
self.assertEqual(len(compiler.ref_circuit_as_gates), 12)
|
| 699 |
+
for i in range(13):
|
| 700 |
+
compiler._add_layer(i)
|
| 701 |
+
actual_num_gates.append(len(compiler.full_circuit.data) - 1)
|
| 702 |
+
|
| 703 |
+
np.testing.assert_equal(actual_num_gates, expected_num_gates)
|
| 704 |
+
|
| 705 |
+
def test_given_max_layers_larger_than_freq_when_add_layer_then_num_gates_not_in_mps_as_expected(
|
| 706 |
+
self,
|
| 707 |
+
):
|
| 708 |
+
qc = co.create_random_initial_state_circuit(4)
|
| 709 |
+
config = AdaptConfig(rotosolve_frequency=4, max_layers_to_modify=5)
|
| 710 |
+
compiler = AdaptCompiler(qc, backend=MPS_SIM, adapt_config=config)
|
| 711 |
+
# layer counter 0 1 2 3 4 5 6 7 8 9 10 11 12
|
| 712 |
+
expected_num_gates = [5, 10, 15, 20, 5, 10, 15, 20, 5, 10, 15, 20, 5]
|
| 713 |
+
actual_num_gates = []
|
| 714 |
+
for i in range(13):
|
| 715 |
+
compiler._add_layer(i)
|
| 716 |
+
actual_num_gates.append(len(compiler.full_circuit.data) - 1)
|
| 717 |
+
|
| 718 |
+
np.testing.assert_equal(actual_num_gates, expected_num_gates)
|
| 719 |
+
|
| 720 |
+
def test_given_optimise_local_cost_when_compile_then_global_cost_converged(self):
|
| 721 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 722 |
+
compiler = AdaptCompiler(qc, optimise_local_cost=True)
|
| 723 |
+
result = compiler.compile()
|
| 724 |
+
circuit = result.circuit
|
| 725 |
+
overlap = co.calculate_overlap_between_circuits(qc, circuit)
|
| 726 |
+
self.assertGreater(overlap, 1 - DEFAULT_SUFFICIENT_COST)
|
| 727 |
+
|
| 728 |
+
def test_given_optimise_local_cost_when_compile_then_global_and_local_cost_histories_correct(
|
| 729 |
+
self,
|
| 730 |
+
):
|
| 731 |
+
# Tests that:
|
| 732 |
+
# a) global_cost_history has one extra element (final cost)
|
| 733 |
+
# b) every global cost is greater than its corresponding local cost
|
| 734 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 735 |
+
compiler = AdaptCompiler(qc, optimise_local_cost=True)
|
| 736 |
+
result = compiler.compile()
|
| 737 |
+
self.assertEqual(
|
| 738 |
+
len(result.global_cost_history), len(result.local_cost_history) + 1
|
| 739 |
+
)
|
| 740 |
+
for global_cost, local_cost in zip(
|
| 741 |
+
result.global_cost_history[:-1], result.local_cost_history
|
| 742 |
+
):
|
| 743 |
+
self.assertGreaterEqual(np.round(global_cost, 15), np.round(local_cost, 15))
|
| 744 |
+
|
| 745 |
+
def test_given_initial_ansatz_and_starting_circuit_and_isql_and_layer_caching_then_solution_has_correct_gates(
|
| 746 |
+
self,
|
| 747 |
+
):
|
| 748 |
+
qc = co.create_random_initial_state_circuit(4)
|
| 749 |
+
|
| 750 |
+
starting_circuit = QuantumCircuit(4)
|
| 751 |
+
starting_circuit.x([0, 1, 2, 3])
|
| 752 |
+
|
| 753 |
+
initial_ansatz = create_initial_ansatz()
|
| 754 |
+
|
| 755 |
+
config = AdaptConfig(rotosolve_frequency=4, max_layers_to_modify=2)
|
| 756 |
+
compiler = AdaptCompiler(
|
| 757 |
+
qc,
|
| 758 |
+
backend=MPS_SIM,
|
| 759 |
+
adapt_config=config,
|
| 760 |
+
starting_circuit=starting_circuit,
|
| 761 |
+
initial_single_qubit_layer=True,
|
| 762 |
+
)
|
| 763 |
+
|
| 764 |
+
compiler._add_initial_ansatz(
|
| 765 |
+
initial_ansatz=initial_ansatz, optimise_initial_ansatz=True
|
| 766 |
+
)
|
| 767 |
+
[compiler._add_layer(i) for i in range(5)]
|
| 768 |
+
|
| 769 |
+
# Delete set_matrix_product_state instruction
|
| 770 |
+
del compiler.ref_circuit_as_gates.data[0]
|
| 771 |
+
full_circuit = compiler.ref_circuit_as_gates.copy()
|
| 772 |
+
|
| 773 |
+
# First 11 gates should be the same type as in initial_ansatz inverse
|
| 774 |
+
initial_ansatz_part = [gate for gate in full_circuit[:11]]
|
| 775 |
+
self.assertEqual(
|
| 776 |
+
[gate.operation.name for gate in initial_ansatz_part],
|
| 777 |
+
["rx", "rx", "rx", "rx", "cx", "cx", "cx", "ry", "ry", "ry", "ry"],
|
| 778 |
+
)
|
| 779 |
+
|
| 780 |
+
# Next 4 gates should be initial single qubit layer
|
| 781 |
+
isql_part = [gate for gate in full_circuit[11:15]]
|
| 782 |
+
self.assertTrue(
|
| 783 |
+
all(gate.operation.name in ["rx", "ry", "rz"] for gate in isql_part)
|
| 784 |
+
)
|
| 785 |
+
|
| 786 |
+
# Everything in between should be thinly-dressed CNOTs
|
| 787 |
+
middle_part = [gate for gate in full_circuit[15:-4]]
|
| 788 |
+
for i, gate in enumerate(middle_part):
|
| 789 |
+
if i % 5 == 2:
|
| 790 |
+
self.assertEqual(gate.operation.name, "cx")
|
| 791 |
+
else:
|
| 792 |
+
self.assertTrue(gate.operation.name in ["rx", "ry", "rz"])
|
| 793 |
+
self.assertEqual(len(middle_part) % 5, 0)
|
| 794 |
+
|
| 795 |
+
# Final 4 gates should be starting_circuit inverse
|
| 796 |
+
starting_circuit_part = [gate for gate in full_circuit[-4:]]
|
| 797 |
+
self.assertTrue(
|
| 798 |
+
all(gate.operation.name == "rx" for gate in starting_circuit_part)
|
| 799 |
+
)
|
| 800 |
+
self.assertTrue(
|
| 801 |
+
all(gate.operation.params[0] == np.pi for gate in starting_circuit_part)
|
| 802 |
+
)
|
| 803 |
+
|
| 804 |
+
# Make sure the circuit has been partitioned correctly
|
| 805 |
+
reconstruct_circuit = (
|
| 806 |
+
initial_ansatz_part + isql_part + middle_part + starting_circuit_part
|
| 807 |
+
)
|
| 808 |
+
self.assertEqual(compiler.ref_circuit_as_gates.data, reconstruct_circuit)
|
| 809 |
+
|
| 810 |
+
def test_given_optimise_initial_ansatz_false_then_initial_ansatz_gates_unchanged(
|
| 811 |
+
self,
|
| 812 |
+
):
|
| 813 |
+
qc = co.create_random_initial_state_circuit(2)
|
| 814 |
+
|
| 815 |
+
initial_ansatz = QuantumCircuit(2)
|
| 816 |
+
initial_ansatz.cz(0, 1)
|
| 817 |
+
initial_ansatz.ry(2.67, 0)
|
| 818 |
+
initial_ansatz.rx(0.53, 1)
|
| 819 |
+
|
| 820 |
+
compiler = AdaptCompiler(qc)
|
| 821 |
+
result = compiler.compile(
|
| 822 |
+
initial_ansatz=initial_ansatz, optimise_initial_ansatz=False
|
| 823 |
+
)
|
| 824 |
+
|
| 825 |
+
self.assertEqual(result.circuit.data[-3:], initial_ansatz.data)
|
| 826 |
+
|
| 827 |
+
def test_given_initial_ansatz_when_add_layer_then_initial_ansatz_unchanged(self):
|
| 828 |
+
qc = co.create_random_initial_state_circuit(4)
|
| 829 |
+
target_gates = len(qc)
|
| 830 |
+
initial_ansatz = create_initial_ansatz()
|
| 831 |
+
|
| 832 |
+
config = AdaptConfig(rotosolve_frequency=1, max_layers_to_modify=10)
|
| 833 |
+
compiler = AdaptCompiler(qc, adapt_config=config)
|
| 834 |
+
|
| 835 |
+
compiler._add_initial_ansatz(initial_ansatz, optimise_initial_ansatz=True)
|
| 836 |
+
ia_gates_before = [
|
| 837 |
+
gate for gate in compiler.full_circuit[target_gates : (target_gates + 11)]
|
| 838 |
+
]
|
| 839 |
+
|
| 840 |
+
# Rotosolve will occur during layer 1, not layer 0
|
| 841 |
+
compiler._add_layer(0)
|
| 842 |
+
compiler._add_layer(1)
|
| 843 |
+
ia_gates_after = [
|
| 844 |
+
gate for gate in compiler.full_circuit[target_gates : (target_gates + 11)]
|
| 845 |
+
]
|
| 846 |
+
|
| 847 |
+
self.assertEqual(ia_gates_before, ia_gates_after)
|
| 848 |
+
|
| 849 |
+
def test_cnot_depth_in_adapt_result_correct(self):
|
| 850 |
+
qc = co.create_random_initial_state_circuit(4, seed=1)
|
| 851 |
+
compiler = AdaptCompiler(qc)
|
| 852 |
+
result = compiler.compile()
|
| 853 |
+
circuit = result.circuit
|
| 854 |
+
self.assertEqual(multi_qubit_gate_depth(circuit), result.cnot_depth_history[-1])
|
| 855 |
+
|
| 856 |
+
def test_recompiling_from_tenpy_mps_works(self):
|
| 857 |
+
n = 3
|
| 858 |
+
num_trotter_steps = 5
|
| 859 |
+
dt = 0.4
|
| 860 |
+
# Target from tenpy
|
| 861 |
+
# NOTE: our Hamiltonian with delta and field is equivalent to tenpy's XXZChain model with
|
| 862 |
+
# Jxx = -1, Jz = -delta, hz = -field.
|
| 863 |
+
model = XXZChain(
|
| 864 |
+
{
|
| 865 |
+
"L": n,
|
| 866 |
+
"Jxx": -1.0,
|
| 867 |
+
"Jz": -1.0,
|
| 868 |
+
"hz": 0.0,
|
| 869 |
+
"bc_MPS": "finite",
|
| 870 |
+
}
|
| 871 |
+
)
|
| 872 |
+
neel_state = ["down", "up", "down"]
|
| 873 |
+
psi = MPS.from_product_state(
|
| 874 |
+
model.lat.mps_sites(), neel_state, bc=model.lat.bc_MPS
|
| 875 |
+
)
|
| 876 |
+
|
| 877 |
+
tebd_params = {
|
| 878 |
+
"N_steps": num_trotter_steps,
|
| 879 |
+
"dt": dt,
|
| 880 |
+
"order": 2,
|
| 881 |
+
"trunc_params": {"chi_max": 100, "svd_min": 1.0e-12},
|
| 882 |
+
}
|
| 883 |
+
|
| 884 |
+
eng = tebd.TEBDEngine(psi, model, tebd_params)
|
| 885 |
+
eng.run()
|
| 886 |
+
target_mps = tenpy_to_qiskit_mps(psi)
|
| 887 |
+
|
| 888 |
+
# Compile
|
| 889 |
+
starting_circuit = QuantumCircuit(n)
|
| 890 |
+
starting_circuit.x(range(0, n, 2))
|
| 891 |
+
|
| 892 |
+
compiler = AdaptCompiler(
|
| 893 |
+
target_mps, backend=MPS_SIM, starting_circuit=starting_circuit
|
| 894 |
+
)
|
| 895 |
+
result = compiler.compile()
|
| 896 |
+
|
| 897 |
+
# Target circuit created independently for comparison
|
| 898 |
+
qc = QuantumCircuit(n)
|
| 899 |
+
qc.x(range(0, n, 2))
|
| 900 |
+
|
| 901 |
+
trotter_circuit(
|
| 902 |
+
qc,
|
| 903 |
+
dt=dt,
|
| 904 |
+
delta=1.0,
|
| 905 |
+
field=0.0,
|
| 906 |
+
num_trotter_steps=num_trotter_steps,
|
| 907 |
+
second_order=True,
|
| 908 |
+
)
|
| 909 |
+
|
| 910 |
+
overlap = co.calculate_overlap_between_circuits(result.circuit, qc)
|
| 911 |
+
|
| 912 |
+
self.assertGreater(overlap, 1 - DEFAULT_SUFFICIENT_COST)
|
| 913 |
+
|
| 914 |
+
def test_given_general_gradient_method_when_compile_then_works(self):
|
| 915 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 916 |
+
|
| 917 |
+
config = AdaptConfig(method="general_gradient")
|
| 918 |
+
compiler = AdaptCompiler(
|
| 919 |
+
qc,
|
| 920 |
+
backend=MPS_SIM,
|
| 921 |
+
adapt_config=config,
|
| 922 |
+
custom_layer_2q_gate=ans.identity_resolvable(),
|
| 923 |
+
)
|
| 924 |
+
result = compiler.compile()
|
| 925 |
+
|
| 926 |
+
overlap = co.calculate_overlap_between_circuits(qc, result.circuit)
|
| 927 |
+
self.assertGreater(overlap, 1 - DEFAULT_SUFFICIENT_COST)
|
| 928 |
+
|
| 929 |
+
def test_given_general_gradient_when_compile_with_reuse_exponent_then_works(self):
|
| 930 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 931 |
+
|
| 932 |
+
config = AdaptConfig(method="general_gradient", reuse_exponent=1)
|
| 933 |
+
compiler = AdaptCompiler(
|
| 934 |
+
qc,
|
| 935 |
+
backend=MPS_SIM,
|
| 936 |
+
adapt_config=config,
|
| 937 |
+
custom_layer_2q_gate=ans.identity_resolvable(),
|
| 938 |
+
)
|
| 939 |
+
result = compiler.compile()
|
| 940 |
+
|
| 941 |
+
overlap = co.calculate_overlap_between_circuits(qc, result.circuit)
|
| 942 |
+
self.assertGreater(overlap, 1 - DEFAULT_SUFFICIENT_COST)
|
| 943 |
+
|
| 944 |
+
def test_given_soften_global_cost_when_compile_then_works(self):
|
| 945 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 946 |
+
|
| 947 |
+
compiler = AdaptCompiler(qc, backend=MPS_SIM, soften_global_cost=True)
|
| 948 |
+
result = compiler.compile()
|
| 949 |
+
self.assertLessEqual(compiler.evaluate_cost(), DEFAULT_SUFFICIENT_COST)
|
| 950 |
+
|
| 951 |
+
@patch(
|
| 952 |
+
"adaptaqc.backends.aer_mps_backend.AerMPSBackend.evaluate_hamming_weight_one_overlaps"
|
| 953 |
+
)
|
| 954 |
+
def test_given_soften_global_cost_true_or_false_when_evaluate_cost_then_appropriate_logic_executed(
|
| 955 |
+
self, mock
|
| 956 |
+
):
|
| 957 |
+
# This test checks that when evaluate_cost is called, the Hamming-weight-one overlaps are
|
| 958 |
+
# calculated if soften_global_cost=True and not calculated if soften_global_cost=False.
|
| 959 |
+
compiler = AdaptCompiler(
|
| 960 |
+
QuantumCircuit(3),
|
| 961 |
+
backend=MPS_SIM,
|
| 962 |
+
soften_global_cost=False,
|
| 963 |
+
)
|
| 964 |
+
compiler.global_cost_history = []
|
| 965 |
+
compiler.evaluate_cost()
|
| 966 |
+
mock.assert_not_called()
|
| 967 |
+
|
| 968 |
+
compiler = AdaptCompiler(
|
| 969 |
+
QuantumCircuit(3),
|
| 970 |
+
backend=MPS_SIM,
|
| 971 |
+
soften_global_cost=True,
|
| 972 |
+
)
|
| 973 |
+
compiler.global_cost_history = []
|
| 974 |
+
compiler.evaluate_cost()
|
| 975 |
+
mock.assert_called()
|
| 976 |
+
|
| 977 |
+
def test_given_soften_global_cost_and_aer_sv_backend_then_error(self):
|
| 978 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 979 |
+
compiler = AdaptCompiler(
|
| 980 |
+
qc,
|
| 981 |
+
backend=SV_SIM,
|
| 982 |
+
soften_global_cost=True,
|
| 983 |
+
)
|
| 984 |
+
with self.assertRaises(NotImplementedError):
|
| 985 |
+
compiler.compile()
|
| 986 |
+
|
| 987 |
+
def test_given_soften_global_cost_and_qiskit_sampling_backend_then_error(self):
|
| 988 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 989 |
+
compiler = AdaptCompiler(
|
| 990 |
+
qc,
|
| 991 |
+
backend=QASM_SIM,
|
| 992 |
+
soften_global_cost=True,
|
| 993 |
+
)
|
| 994 |
+
with self.assertRaises(NotImplementedError):
|
| 995 |
+
compiler.compile()
|
| 996 |
+
|
| 997 |
+
def test_given_tenpy_starting_circuit_when_compile_then_works(self):
|
| 998 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 999 |
+
compiler = AdaptCompiler(qc, starting_circuit="tenpy_product_state")
|
| 1000 |
+
result = compiler.compile()
|
| 1001 |
+
|
| 1002 |
+
overlap = co.calculate_overlap_between_circuits(result.circuit, qc)
|
| 1003 |
+
self.assertGreater(overlap, 1 - DEFAULT_SUFFICIENT_COST)
|
| 1004 |
+
|
| 1005 |
+
def test_given_tenpy_starting_circuit_then_solution_starts_with_rzryrz_on_each_qubit(
|
| 1006 |
+
self,
|
| 1007 |
+
):
|
| 1008 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 1009 |
+
compiler = AdaptCompiler(qc, starting_circuit="tenpy_product_state")
|
| 1010 |
+
result = compiler.compile()
|
| 1011 |
+
|
| 1012 |
+
qubit_0_gates = []
|
| 1013 |
+
qubit_1_gates = []
|
| 1014 |
+
qubit_2_gates = []
|
| 1015 |
+
|
| 1016 |
+
for instruction in result.circuit.data:
|
| 1017 |
+
if min([len(qubit_0_gates), len(qubit_1_gates), len(qubit_2_gates)]) >= 3:
|
| 1018 |
+
break
|
| 1019 |
+
elif (instruction.qubits[0] == result.circuit.qubits[0]) and (
|
| 1020 |
+
len(qubit_0_gates) < 3
|
| 1021 |
+
):
|
| 1022 |
+
qubit_0_gates.append(instruction.operation.name)
|
| 1023 |
+
elif (instruction.qubits[0] == result.circuit.qubits[1]) and (
|
| 1024 |
+
len(qubit_1_gates) < 3
|
| 1025 |
+
):
|
| 1026 |
+
qubit_1_gates.append(instruction.operation.name)
|
| 1027 |
+
elif (instruction.qubits[0] == result.circuit.qubits[2]) and (
|
| 1028 |
+
len(qubit_2_gates) < 3
|
| 1029 |
+
):
|
| 1030 |
+
qubit_2_gates.append(instruction.operation.name)
|
| 1031 |
+
|
| 1032 |
+
self.assertEqual(qubit_0_gates, ["rz", "ry", "rz"])
|
| 1033 |
+
self.assertEqual(qubit_1_gates, ["rz", "ry", "rz"])
|
| 1034 |
+
self.assertEqual(qubit_2_gates, ["rz", "ry", "rz"])
|
| 1035 |
+
|
| 1036 |
+
def test_given_tenpy_starting_circuit_then_better_starting_cost(self):
|
| 1037 |
+
qc = co.create_random_initial_state_circuit(5)
|
| 1038 |
+
compiler_1 = AdaptCompiler(qc)
|
| 1039 |
+
compiler_2 = AdaptCompiler(qc, starting_circuit="tenpy_product_state")
|
| 1040 |
+
|
| 1041 |
+
cost_1 = compiler_1.evaluate_cost()
|
| 1042 |
+
cost_2 = compiler_2.evaluate_cost()
|
| 1043 |
+
|
| 1044 |
+
self.assertGreater(cost_1, cost_2)
|
| 1045 |
+
|
| 1046 |
+
def test_given_advanced_transpilation_option_passed_then_compiled_circuits_equivalent(
|
| 1047 |
+
self,
|
| 1048 |
+
):
|
| 1049 |
+
qc = co.create_random_initial_state_circuit(4)
|
| 1050 |
+
compiler = AdaptCompiler(
|
| 1051 |
+
qc,
|
| 1052 |
+
use_advanced_transpilation=True,
|
| 1053 |
+
)
|
| 1054 |
+
|
| 1055 |
+
result = compiler.compile()
|
| 1056 |
+
circuit = result.circuit
|
| 1057 |
+
|
| 1058 |
+
overlap = co.calculate_overlap_between_circuits(qc, circuit)
|
| 1059 |
+
self.assertGreater(overlap, 1 - DEFAULT_SUFFICIENT_COST)
|
| 1060 |
+
|
| 1061 |
+
def test_given_advanced_transpilation_option_passed_then_reference_circuit_updated_correctly(
|
| 1062 |
+
self,
|
| 1063 |
+
):
|
| 1064 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 1065 |
+
compiler = AdaptCompiler(
|
| 1066 |
+
qc,
|
| 1067 |
+
backend=MPS_SIM,
|
| 1068 |
+
use_advanced_transpilation=True,
|
| 1069 |
+
)
|
| 1070 |
+
for i in range(3):
|
| 1071 |
+
compiler._add_layer(i)
|
| 1072 |
+
full_circuit = compiler.full_circuit.copy()
|
| 1073 |
+
self.assertEqual(compiler.ref_circuit_as_gates.data, full_circuit.data)
|
| 1074 |
+
|
| 1075 |
+
|
| 1076 |
+
class TestAdaptCheckpointing(TestCase):
|
| 1077 |
+
def test_given_checkpoint_every_1_when_compile_then_n_layer_number_of_checkpoints(
|
| 1078 |
+
self,
|
| 1079 |
+
):
|
| 1080 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 1081 |
+
compiler = AdaptCompiler(qc)
|
| 1082 |
+
with tempfile.TemporaryDirectory() as d:
|
| 1083 |
+
result = compiler.compile(checkpoint_every=1, checkpoint_dir=d)
|
| 1084 |
+
self.assertEqual(len(os.listdir(d)), len(result.qubit_pair_history))
|
| 1085 |
+
|
| 1086 |
+
def test_given_delete_prev_chkpt_when_compile_then_1_checkpoint(self):
|
| 1087 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 1088 |
+
compiler = AdaptCompiler(qc)
|
| 1089 |
+
with tempfile.TemporaryDirectory() as d:
|
| 1090 |
+
compiler.compile(
|
| 1091 |
+
checkpoint_every=1, checkpoint_dir=d, delete_prev_chkpt=True
|
| 1092 |
+
)
|
| 1093 |
+
self.assertEqual(len(os.listdir(d)), 1)
|
| 1094 |
+
|
| 1095 |
+
def test_given_delete_prev_chkpt_when_save_then_load_then_load_checkpoint_deleted(
|
| 1096 |
+
self,
|
| 1097 |
+
):
|
| 1098 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 1099 |
+
compiler = AdaptCompiler(qc, adapt_config=AdaptConfig(max_layers=2))
|
| 1100 |
+
with tempfile.TemporaryDirectory() as d:
|
| 1101 |
+
compiler.compile(
|
| 1102 |
+
checkpoint_every=1, checkpoint_dir=d, delete_prev_chkpt=True
|
| 1103 |
+
)
|
| 1104 |
+
with open(os.path.join(d, "1.pkl"), "rb") as myfile:
|
| 1105 |
+
loaded_compiler = pickle.load(myfile)
|
| 1106 |
+
loaded_compiler.compile(
|
| 1107 |
+
checkpoint_every=1, checkpoint_dir=d, delete_prev_chkpt=True
|
| 1108 |
+
)
|
| 1109 |
+
self.assertEqual(len(os.listdir(d)), 1)
|
| 1110 |
+
|
| 1111 |
+
def test_given_checkpoint_every_large_when_compile_then_2_checkpoints(self):
|
| 1112 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 1113 |
+
compiler = AdaptCompiler(qc)
|
| 1114 |
+
with tempfile.TemporaryDirectory() as d:
|
| 1115 |
+
compiler.compile(checkpoint_every=100, checkpoint_dir=d)
|
| 1116 |
+
self.assertEqual(len(os.listdir(d)), 2)
|
| 1117 |
+
|
| 1118 |
+
def test_given_checkpoint_every_0_when_compile_then_no_dir_created(self):
|
| 1119 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 1120 |
+
compiler = AdaptCompiler(qc)
|
| 1121 |
+
with tempfile.TemporaryDirectory() as d:
|
| 1122 |
+
shutil.rmtree(d)
|
| 1123 |
+
compiler.compile(checkpoint_every=0, checkpoint_dir=d)
|
| 1124 |
+
self.assertFalse(os.path.isdir(d))
|
| 1125 |
+
|
| 1126 |
+
def test_given_checkpointing_when_compile_then_dir_created(self):
|
| 1127 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 1128 |
+
compiler = AdaptCompiler(qc)
|
| 1129 |
+
with tempfile.TemporaryDirectory() as d:
|
| 1130 |
+
shutil.rmtree(d)
|
| 1131 |
+
compiler.compile(checkpoint_every=100, checkpoint_dir=d)
|
| 1132 |
+
self.assertTrue(os.path.isdir(d))
|
| 1133 |
+
|
| 1134 |
+
def test_given_save_and_resume_from_different_points_then_non_time_results_equal(
|
| 1135 |
+
self,
|
| 1136 |
+
):
|
| 1137 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 1138 |
+
compiler = AdaptCompiler(qc)
|
| 1139 |
+
with tempfile.TemporaryDirectory() as d:
|
| 1140 |
+
result = compiler.compile(checkpoint_every=1, checkpoint_dir=d)
|
| 1141 |
+
for c in ["0", "1"]:
|
| 1142 |
+
with open(os.path.join(d, c + ".pkl"), "rb") as myfile:
|
| 1143 |
+
loaded_compiler = pickle.load(myfile)
|
| 1144 |
+
result_1 = loaded_compiler.compile()
|
| 1145 |
+
for key in result.__dict__.keys():
|
| 1146 |
+
if key != "time_taken":
|
| 1147 |
+
self.assertEqual(result.__dict__[key], result_1.__dict__[key])
|
| 1148 |
+
|
| 1149 |
+
def test_given_save_and_resume_from_any_point_then_time_taken_within_100ms(self):
|
| 1150 |
+
qc = co.create_random_initial_state_circuit(3, seed=3)
|
| 1151 |
+
compiler = AdaptCompiler(qc)
|
| 1152 |
+
with tempfile.TemporaryDirectory() as d:
|
| 1153 |
+
result = compiler.compile(checkpoint_every=1, checkpoint_dir=d)
|
| 1154 |
+
int_cn = [int(cn[:-4]) for cn in os.listdir(d)]
|
| 1155 |
+
for c in [str(i) for i in range(max(int_cn) + 1)]:
|
| 1156 |
+
with open(os.path.join(d, c + ".pkl"), "rb") as myfile:
|
| 1157 |
+
loaded_compiler = pickle.load(myfile)
|
| 1158 |
+
result_1 = loaded_compiler.compile()
|
| 1159 |
+
self.assertAlmostEqual(
|
| 1160 |
+
result.time_taken, result_1.time_taken, delta=0.1
|
| 1161 |
+
)
|
| 1162 |
+
self.assertLess(result_1.time_taken, 100)
|
| 1163 |
+
|
| 1164 |
+
def test_given_save_and_resume_and_save_and_resume_then_overwrites(self):
|
| 1165 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 1166 |
+
compiler = AdaptCompiler(qc)
|
| 1167 |
+
with tempfile.TemporaryDirectory() as d:
|
| 1168 |
+
compiler.compile(checkpoint_every=1, checkpoint_dir=d)
|
| 1169 |
+
with open(os.path.join(d, "0.pkl"), "rb") as myfile:
|
| 1170 |
+
loaded_compiler = pickle.load(myfile)
|
| 1171 |
+
loaded_compiler.compile(checkpoint_every=1, checkpoint_dir=d)
|
| 1172 |
+
with open(os.path.join(d, "1.pkl"), "rb") as myfile:
|
| 1173 |
+
loaded_compiler = pickle.load(myfile)
|
| 1174 |
+
result = loaded_compiler.compile()
|
| 1175 |
+
self.assertEqual(len(os.listdir(d)), len(result.qubit_pair_history))
|
| 1176 |
+
|
| 1177 |
+
def test_given_resume_and_freeze_layers_when_compile_then_works(self):
|
| 1178 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 1179 |
+
for backend in [SV_SIM, MPS_SIM]:
|
| 1180 |
+
compiler = AdaptCompiler(qc, backend=backend)
|
| 1181 |
+
with tempfile.TemporaryDirectory() as d:
|
| 1182 |
+
compiler.compile(checkpoint_every=1, checkpoint_dir=d)
|
| 1183 |
+
with open(os.path.join(d, "2.pkl"), "rb") as myfile:
|
| 1184 |
+
loaded_compiler = pickle.load(myfile)
|
| 1185 |
+
result = loaded_compiler.compile(freeze_prev_layers=True)
|
| 1186 |
+
overlap = co.calculate_overlap_between_circuits(result.circuit, qc)
|
| 1187 |
+
self.assertGreater(overlap, 1 - DEFAULT_SUFFICIENT_COST)
|
| 1188 |
+
|
| 1189 |
+
def test_given_resume_and_freeze_layers_multiple_times_when_compile_then_works(
|
| 1190 |
+
self,
|
| 1191 |
+
):
|
| 1192 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 1193 |
+
sc = QuantumCircuit(3)
|
| 1194 |
+
sc.h([0, 2])
|
| 1195 |
+
for backend in [SV_SIM, MPS_SIM]:
|
| 1196 |
+
compiler = AdaptCompiler(qc, backend=backend, starting_circuit=sc)
|
| 1197 |
+
with tempfile.TemporaryDirectory() as d:
|
| 1198 |
+
compiler.compile(checkpoint_every=1, checkpoint_dir=d)
|
| 1199 |
+
with open(os.path.join(d, "0.pkl"), "rb") as myfile:
|
| 1200 |
+
# Load compiler after layer 0, freeze layer 0, compile
|
| 1201 |
+
loaded_compiler_0 = pickle.load(myfile)
|
| 1202 |
+
|
| 1203 |
+
with tempfile.TemporaryDirectory() as d:
|
| 1204 |
+
loaded_compiler_0.compile(
|
| 1205 |
+
checkpoint_every=1, checkpoint_dir=d, freeze_prev_layers=True
|
| 1206 |
+
)
|
| 1207 |
+
with open(os.path.join(d, "1.pkl"), "rb") as myfile:
|
| 1208 |
+
# Load compiler after layer 1, freeze layers 0, 1, compile
|
| 1209 |
+
loaded_compiler_1 = pickle.load(myfile)
|
| 1210 |
+
|
| 1211 |
+
with tempfile.TemporaryDirectory() as d:
|
| 1212 |
+
loaded_compiler_1.compile(
|
| 1213 |
+
checkpoint_every=1, checkpoint_dir=d, freeze_prev_layers=True
|
| 1214 |
+
)
|
| 1215 |
+
with open(os.path.join(d, "2.pkl"), "rb") as myfile:
|
| 1216 |
+
# Load compiler after layer 2, freeze layers 0, 1, 2, compile
|
| 1217 |
+
loaded_compiler_2 = pickle.load(myfile)
|
| 1218 |
+
|
| 1219 |
+
result = loaded_compiler_2.compile(freeze_prev_layers=True)
|
| 1220 |
+
overlap = co.calculate_overlap_between_circuits(result.circuit, qc)
|
| 1221 |
+
self.assertGreater(overlap, 1 - DEFAULT_SUFFICIENT_COST)
|
| 1222 |
+
|
| 1223 |
+
def test_given_freeze_prev_layers_then_parameters_unchanged_sv(self):
|
| 1224 |
+
# This tests that given freeze_prev_layers=False(True), then the layers added before the
|
| 1225 |
+
# checkpoint are(are not) changed during the resumed recompilation.
|
| 1226 |
+
qc = co.create_random_initial_state_circuit(3, seed=0)
|
| 1227 |
+
target_length = len(qc)
|
| 1228 |
+
# We will load the compiler after two layers have been added, so if we freeze those layers,
|
| 1229 |
+
# these gates should be in the range:
|
| 1230 |
+
frozen_gate_range = (target_length, target_length + 10)
|
| 1231 |
+
compiler = AdaptCompiler(qc, backend=SV_SIM)
|
| 1232 |
+
with tempfile.TemporaryDirectory() as d:
|
| 1233 |
+
compiler.compile(checkpoint_every=1, checkpoint_dir=d)
|
| 1234 |
+
with open(os.path.join(d, "1.pkl"), "rb") as myfile:
|
| 1235 |
+
compiler_freeze = pickle.load(myfile)
|
| 1236 |
+
compiler_no_freeze = copy.deepcopy(compiler_freeze)
|
| 1237 |
+
|
| 1238 |
+
layers_added_before_checkpoint = co.extract_inner_circuit(
|
| 1239 |
+
compiler_freeze.full_circuit, frozen_gate_range
|
| 1240 |
+
)
|
| 1241 |
+
compiler_freeze.compile(freeze_prev_layers=True)
|
| 1242 |
+
compiler_no_freeze.compile(freeze_prev_layers=False)
|
| 1243 |
+
|
| 1244 |
+
layers_after_recompiling_with_freezing = co.extract_inner_circuit(
|
| 1245 |
+
compiler_freeze.full_circuit, frozen_gate_range
|
| 1246 |
+
)
|
| 1247 |
+
layers_after_recompiling_without_freezing = co.extract_inner_circuit(
|
| 1248 |
+
compiler_no_freeze.full_circuit, frozen_gate_range
|
| 1249 |
+
)
|
| 1250 |
+
|
| 1251 |
+
self.assertEqual(
|
| 1252 |
+
layers_added_before_checkpoint, layers_after_recompiling_with_freezing
|
| 1253 |
+
)
|
| 1254 |
+
self.assertNotEqual(
|
| 1255 |
+
layers_added_before_checkpoint,
|
| 1256 |
+
layers_after_recompiling_without_freezing,
|
| 1257 |
+
)
|
| 1258 |
+
|
| 1259 |
+
def test_given_freeze_prev_layers_then_parameters_unchanged_mps(self):
|
| 1260 |
+
# This test is the same above, but for the aer mps backend.
|
| 1261 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 1262 |
+
# For mps backend, the target is a set_matrix_product_state in compiler.ref_circuit_as_gates
|
| 1263 |
+
frozen_gate_range = (1, 11)
|
| 1264 |
+
compiler = AdaptCompiler(qc, backend=MPS_SIM)
|
| 1265 |
+
with tempfile.TemporaryDirectory() as d:
|
| 1266 |
+
compiler.compile(checkpoint_every=1, checkpoint_dir=d)
|
| 1267 |
+
with open(os.path.join(d, "1.pkl"), "rb") as myfile:
|
| 1268 |
+
compiler_freeze = pickle.load(myfile)
|
| 1269 |
+
compiler_no_freeze = copy.deepcopy(compiler_freeze)
|
| 1270 |
+
|
| 1271 |
+
layers_added_before_checkpoint = co.extract_inner_circuit(
|
| 1272 |
+
compiler_freeze.ref_circuit_as_gates, frozen_gate_range
|
| 1273 |
+
)
|
| 1274 |
+
compiler_freeze.compile(freeze_prev_layers=True)
|
| 1275 |
+
compiler_no_freeze.compile(freeze_prev_layers=False)
|
| 1276 |
+
|
| 1277 |
+
layers_after_recompiling_with_freezing = co.extract_inner_circuit(
|
| 1278 |
+
compiler_freeze.ref_circuit_as_gates, frozen_gate_range
|
| 1279 |
+
)
|
| 1280 |
+
layers_after_recompiling_without_freezing = co.extract_inner_circuit(
|
| 1281 |
+
compiler_no_freeze.ref_circuit_as_gates, frozen_gate_range
|
| 1282 |
+
)
|
| 1283 |
+
|
| 1284 |
+
self.assertEqual(
|
| 1285 |
+
layers_added_before_checkpoint, layers_after_recompiling_with_freezing
|
| 1286 |
+
)
|
| 1287 |
+
self.assertNotEqual(
|
| 1288 |
+
layers_added_before_checkpoint,
|
| 1289 |
+
layers_after_recompiling_without_freezing,
|
| 1290 |
+
)
|
| 1291 |
+
|
| 1292 |
+
def test_given_freeze_prev_layers_then_lhs_gate_count_different_from_orig_during_recompiling(
|
| 1293 |
+
self,
|
| 1294 |
+
):
|
| 1295 |
+
# When doing rotosolve, AdaptCompiler._calculate_multi_layer_optimisation_indices is called
|
| 1296 |
+
# with "ansatz_start_index" as argument. This is equal to variational_circuit_range()[0],
|
| 1297 |
+
# which is equal to lhs_gate_count. We can check that this is different from
|
| 1298 |
+
# original_lhs_gate_count
|
| 1299 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 1300 |
+
compiler = AdaptCompiler(qc)
|
| 1301 |
+
with tempfile.TemporaryDirectory() as d:
|
| 1302 |
+
compiler.compile(checkpoint_every=1, checkpoint_dir=d)
|
| 1303 |
+
with open(os.path.join(d, "1.pkl"), "rb") as myfile:
|
| 1304 |
+
loaded_compiler = pickle.load(myfile)
|
| 1305 |
+
|
| 1306 |
+
# Since we loaded the compiler after two layers had been added, we expect the
|
| 1307 |
+
# lhs_gate_count used during recompilation to be: old_lhs + 10
|
| 1308 |
+
expected_input = loaded_compiler.original_lhs_gate_count + 10
|
| 1309 |
+
|
| 1310 |
+
with patch.object(
|
| 1311 |
+
loaded_compiler, "_calculate_multi_layer_optimisation_indices"
|
| 1312 |
+
) as mock:
|
| 1313 |
+
# Dummy return value
|
| 1314 |
+
mock.return_value = loaded_compiler.variational_circuit_range()
|
| 1315 |
+
# Compile and assert that all calls to the function use the right input
|
| 1316 |
+
loaded_compiler.compile(freeze_prev_layers=True)
|
| 1317 |
+
for call in mock.call_args_list:
|
| 1318 |
+
self.assertEqual(call.args[0], expected_input)
|
| 1319 |
+
|
| 1320 |
+
def test_given_save_and_resume_then_rotosolve_fraction_is_not_overwritten(self):
|
| 1321 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 1322 |
+
compiler = AdaptCompiler(qc, rotosolve_fraction=0.5)
|
| 1323 |
+
|
| 1324 |
+
rotosolve_fractions = []
|
| 1325 |
+
# Before recompilation
|
| 1326 |
+
rotosolve_fractions.append(compiler.minimizer.rotosolve_fraction)
|
| 1327 |
+
with tempfile.TemporaryDirectory() as d:
|
| 1328 |
+
compiler.compile(checkpoint_every=1, checkpoint_dir=d)
|
| 1329 |
+
# After recompilation
|
| 1330 |
+
rotosolve_fractions.append(compiler.minimizer.rotosolve_fraction)
|
| 1331 |
+
with open(os.path.join(d, "1.pkl"), "rb") as myfile:
|
| 1332 |
+
loaded_compiler = pickle.load(myfile)
|
| 1333 |
+
|
| 1334 |
+
# After loading, before resuming recompilation
|
| 1335 |
+
rotosolve_fractions.append(loaded_compiler.minimizer.rotosolve_fraction)
|
| 1336 |
+
loaded_compiler.compile(checkpoint_every=1, checkpoint_dir=d)
|
| 1337 |
+
# After loading, after resuming recompilation
|
| 1338 |
+
rotosolve_fractions.append(loaded_compiler.minimizer.rotosolve_fraction)
|
| 1339 |
+
|
| 1340 |
+
self.assertEqual(rotosolve_fractions, [0.5, 0.5, 0.5, 0.5])
|
| 1341 |
+
|
| 1342 |
+
|
| 1343 |
+
class TestAdaptRandomRotosolve(TestCase):
|
| 1344 |
+
def test_given_different_rotosolve_fractions_when_compile_then_works(self):
|
| 1345 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 1346 |
+
|
| 1347 |
+
for rotosolve_fraction in [0.2, 0.5, 0.8]:
|
| 1348 |
+
compiler = AdaptCompiler(
|
| 1349 |
+
qc, backend=MPS_SIM, rotosolve_fraction=rotosolve_fraction
|
| 1350 |
+
)
|
| 1351 |
+
result = compiler.compile()
|
| 1352 |
+
|
| 1353 |
+
overlap = co.calculate_overlap_between_circuits(qc, result.circuit)
|
| 1354 |
+
|
| 1355 |
+
self.assertGreater(overlap, 1 - DEFAULT_SUFFICIENT_COST)
|
| 1356 |
+
|
| 1357 |
+
def test_given_rotosolve_fraction_then_results_reproducible(self):
|
| 1358 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 1359 |
+
|
| 1360 |
+
compiler_1 = AdaptCompiler(qc, backend=MPS_SIM, rotosolve_fraction=0.5)
|
| 1361 |
+
compiler_2 = AdaptCompiler(qc, backend=MPS_SIM, rotosolve_fraction=0.5)
|
| 1362 |
+
|
| 1363 |
+
random.seed(1)
|
| 1364 |
+
result_1 = compiler_1.compile()
|
| 1365 |
+
|
| 1366 |
+
random.seed(1)
|
| 1367 |
+
result_2 = compiler_2.compile()
|
| 1368 |
+
|
| 1369 |
+
self.assertEqual(result_1.global_cost_history, result_2.global_cost_history)
|
| 1370 |
+
self.assertEqual(result_1.circuit, result_2.circuit)
|
| 1371 |
+
|
| 1372 |
+
def test_given_invalid_or_valid_rotosolve_fraction_then_error_or_no_error(self):
|
| 1373 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 1374 |
+
|
| 1375 |
+
# Should error
|
| 1376 |
+
with self.assertRaises(ValueError):
|
| 1377 |
+
compiler = AdaptCompiler(qc, backend=MPS_SIM, rotosolve_fraction=0)
|
| 1378 |
+
|
| 1379 |
+
with self.assertRaises(ValueError):
|
| 1380 |
+
compiler = AdaptCompiler(
|
| 1381 |
+
qc, backend=MPS_SIM, rotosolve_fraction=1.000000001
|
| 1382 |
+
)
|
| 1383 |
+
|
| 1384 |
+
# Shouldn't error
|
| 1385 |
+
compiler = AdaptCompiler(qc, backend=MPS_SIM, rotosolve_fraction=1)
|
| 1386 |
+
compiler = AdaptCompiler(qc, backend=MPS_SIM, rotosolve_fraction=0.000000001)
|
| 1387 |
+
|
| 1388 |
+
|
| 1389 |
+
try:
|
| 1390 |
+
from itensornetworks_qiskit.utils import qiskit_circ_to_it_circ
|
| 1391 |
+
from juliacall import Main as jl, JuliaError
|
| 1392 |
+
from adaptaqc.backends.julia_default_backends import ITENSOR_SIM
|
| 1393 |
+
|
| 1394 |
+
jl.seval("using ITensorNetworksQiskit")
|
| 1395 |
+
jl.seval("using ITensors: siteinds")
|
| 1396 |
+
module_failed = False
|
| 1397 |
+
except Exception:
|
| 1398 |
+
module_failed = True
|
| 1399 |
+
|
| 1400 |
+
|
| 1401 |
+
class TestITensor(TestCase):
|
| 1402 |
+
def setUp(self):
|
| 1403 |
+
if module_failed:
|
| 1404 |
+
self.skipTest("Skipping as ITensor backend not set up")
|
| 1405 |
+
|
| 1406 |
+
def test_given_itensor_backend_when_compile_with_basic_then_works(self):
|
| 1407 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 1408 |
+
qc = transpile(qc, basis_gates=["cx", "rx", "ry", "rz"])
|
| 1409 |
+
config = AdaptConfig(method="basic")
|
| 1410 |
+
compiler = AdaptCompiler(qc, backend=ITENSOR_SIM, adapt_config=config)
|
| 1411 |
+
result = compiler.compile()
|
| 1412 |
+
overlap = co.calculate_overlap_between_circuits(qc, result.circuit)
|
| 1413 |
+
self.assertGreater(overlap, 1 - DEFAULT_SUFFICIENT_COST)
|
| 1414 |
+
|
| 1415 |
+
def test_given_itensor_backend_when_compile_with_adapt_then_error(self):
|
| 1416 |
+
with self.assertRaises(NotImplementedError):
|
| 1417 |
+
AdaptCompiler(QuantumCircuit(1), backend=ITENSOR_SIM).compile()
|
| 1418 |
+
|
| 1419 |
+
def test_given_itensor_backend_when_compile_with_expectation_then_error(self):
|
| 1420 |
+
with self.assertRaises(NotImplementedError):
|
| 1421 |
+
config = AdaptConfig(method="expectation")
|
| 1422 |
+
AdaptCompiler(
|
| 1423 |
+
QuantumCircuit(1), backend=ITENSOR_SIM, adapt_config=config
|
| 1424 |
+
).compile()
|
| 1425 |
+
|
| 1426 |
+
def test_given_itensor_backend_then_target_cached(self):
|
| 1427 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 1428 |
+
qc = transpile(qc, basis_gates=["cx", "rx", "ry", "rz"])
|
| 1429 |
+
compiler = AdaptCompiler(qc, backend=ITENSOR_SIM)
|
| 1430 |
+
|
| 1431 |
+
s = compiler.itensor_sites
|
| 1432 |
+
cached_target = compiler.itensor_target
|
| 1433 |
+
gates = qiskit_circ_to_it_circ(qc)
|
| 1434 |
+
actual_target = jl.mps_from_circuit_itensors(3, gates, 10, s)
|
| 1435 |
+
|
| 1436 |
+
overlap = jl.overlap_itensors(cached_target, actual_target)
|
| 1437 |
+
self.assertAlmostEqual(overlap, 1)
|
| 1438 |
+
|
| 1439 |
+
def test_given_itensor_backend_then_cache_not_modified(self):
|
| 1440 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 1441 |
+
qc = transpile(qc, basis_gates=["cx", "rx", "ry", "rz"])
|
| 1442 |
+
config = AdaptConfig(method="basic")
|
| 1443 |
+
compiler = AdaptCompiler(qc, backend=ITENSOR_SIM, adapt_config=config)
|
| 1444 |
+
cached_target = compiler.itensor_target
|
| 1445 |
+
compiler._add_layer(0)
|
| 1446 |
+
compiler._add_layer(1)
|
| 1447 |
+
compiler._add_layer(2)
|
| 1448 |
+
|
| 1449 |
+
cached_target_after_layers_added = compiler.itensor_target
|
| 1450 |
+
|
| 1451 |
+
overlap = jl.overlap_itensors(cached_target, cached_target_after_layers_added)
|
| 1452 |
+
self.assertAlmostEqual(overlap, 1)
|
| 1453 |
+
|
| 1454 |
+
def test_given_soften_global_cost_and_itensor_backend_then_error(self):
|
| 1455 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 1456 |
+
compiler = AdaptCompiler(
|
| 1457 |
+
qc,
|
| 1458 |
+
backend=ITENSOR_SIM,
|
| 1459 |
+
soften_global_cost=True,
|
| 1460 |
+
)
|
| 1461 |
+
with self.assertRaises(NotImplementedError):
|
| 1462 |
+
compiler.compile()
|
| 1463 |
+
|
| 1464 |
+
|
| 1465 |
+
class TestBrickwall(TestCase):
|
| 1466 |
+
def test_given_brickwall_pair_selection_method_when_compile_then_works(self):
|
| 1467 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 1468 |
+
config = AdaptConfig(method="brickwall")
|
| 1469 |
+
compiler = AdaptCompiler(qc, adapt_config=config)
|
| 1470 |
+
|
| 1471 |
+
result = compiler.compile()
|
| 1472 |
+
|
| 1473 |
+
overlap = co.calculate_overlap_between_circuits(qc, result.circuit)
|
| 1474 |
+
|
| 1475 |
+
self.assertGreater(overlap, 1 - DEFAULT_SUFFICIENT_COST)
|
| 1476 |
+
|
| 1477 |
+
def test_given_brickwall_mode_and_all_options_when_compile_then_works(self):
|
| 1478 |
+
qc = co.create_random_initial_state_circuit(3)
|
| 1479 |
+
starting_circuit = QuantumCircuit(3)
|
| 1480 |
+
starting_circuit.x([0, 2])
|
| 1481 |
+
initial_ansatz = QuantumCircuit(3)
|
| 1482 |
+
initial_ansatz.ry(0.5, 0)
|
| 1483 |
+
|
| 1484 |
+
config = AdaptConfig(
|
| 1485 |
+
cost_improvement_num_layers=50,
|
| 1486 |
+
max_layers_to_modify=5,
|
| 1487 |
+
method="brickwall",
|
| 1488 |
+
rotosolve_frequency=3,
|
| 1489 |
+
)
|
| 1490 |
+
compiler = AdaptCompiler(
|
| 1491 |
+
qc,
|
| 1492 |
+
backend=MPS_SIM,
|
| 1493 |
+
adapt_config=config,
|
| 1494 |
+
custom_layer_2q_gate=ans.identity_resolvable(),
|
| 1495 |
+
starting_circuit=starting_circuit,
|
| 1496 |
+
rotosolve_fraction=0.8,
|
| 1497 |
+
soften_global_cost=True,
|
| 1498 |
+
initial_single_qubit_layer=True,
|
| 1499 |
+
)
|
| 1500 |
+
|
| 1501 |
+
result = compiler.compile(
|
| 1502 |
+
initial_ansatz=initial_ansatz, optimise_initial_ansatz=True
|
| 1503 |
+
)
|
| 1504 |
+
|
| 1505 |
+
overlap = co.calculate_overlap_between_circuits(qc, result.circuit)
|
| 1506 |
+
|
| 1507 |
+
self.assertGreater(overlap, 1 - DEFAULT_SUFFICIENT_COST)
|
| 1508 |
+
|
| 1509 |
+
def test_given_brickwall_mode_then_qubit_pair_history_correct(self):
|
| 1510 |
+
# Odd number of qubits
|
| 1511 |
+
qc = QuantumCircuit(5)
|
| 1512 |
+
expected_order = [(0, 1), (2, 3), (1, 2), (3, 4)]
|
| 1513 |
+
config = AdaptConfig(max_layers=10, method="brickwall")
|
| 1514 |
+
compiler = AdaptCompiler(qc, adapt_config=config)
|
| 1515 |
+
[compiler._add_layer(i) for i in range(5 * len(expected_order))]
|
| 1516 |
+
for i, pair in enumerate(compiler.qubit_pair_history):
|
| 1517 |
+
expected_pair = expected_order[i % len(expected_order)]
|
| 1518 |
+
self.assertEqual(pair, expected_pair)
|
| 1519 |
+
|
| 1520 |
+
# Even number of qubits
|
| 1521 |
+
qc = QuantumCircuit(4)
|
| 1522 |
+
expected_order = [(0, 1), (2, 3), (1, 2)]
|
| 1523 |
+
config = AdaptConfig(max_layers=10, method="brickwall")
|
| 1524 |
+
compiler = AdaptCompiler(qc, adapt_config=config)
|
| 1525 |
+
[compiler._add_layer(i) for i in range(5 * len(expected_order))]
|
| 1526 |
+
for i, pair in enumerate(compiler.qubit_pair_history):
|
| 1527 |
+
expected_pair = expected_order[i % len(expected_order)]
|
| 1528 |
+
self.assertEqual(pair, expected_pair)
|
| 1529 |
+
|
| 1530 |
+
def test_given_two_qubits_and_brickwall_mode_then_works(self):
|
| 1531 |
+
qc = co.create_random_initial_state_circuit(2)
|
| 1532 |
+
config = AdaptConfig(method="brickwall")
|
| 1533 |
+
compiler = AdaptCompiler(qc, adapt_config=config)
|
| 1534 |
+
result = compiler.compile()
|
| 1535 |
+
for pair in result.qubit_pair_history:
|
| 1536 |
+
self.assertEqual(pair, (0, 1))
|
| 1537 |
+
|
| 1538 |
+
def test_given_less_than_two_qubits_and_brickwall_mode_then_error(self):
|
| 1539 |
+
qc = QuantumCircuit(1)
|
| 1540 |
+
config = AdaptConfig(method="brickwall")
|
| 1541 |
+
compiler = AdaptCompiler(qc, adapt_config=config)
|
| 1542 |
+
with self.assertRaises(ValueError):
|
| 1543 |
+
result = compiler.compile()
|
utils/adapt-aqc/test/recompilers/test_approximate_compiler.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from unittest import TestCase
|
| 2 |
+
from unittest.mock import patch
|
| 3 |
+
|
| 4 |
+
import numpy as np
|
| 5 |
+
from qiskit import QuantumCircuit
|
| 6 |
+
|
| 7 |
+
import adaptaqc.utils.circuit_operations as co
|
| 8 |
+
from adaptaqc.backends.python_default_backends import QASM_SIM, SV_SIM, MPS_SIM
|
| 9 |
+
from adaptaqc.compilers.adapt.adapt_compiler import AdaptCompiler
|
| 10 |
+
from adaptaqc.compilers.approximate_compiler import ApproximateCompiler
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@patch.multiple(ApproximateCompiler, __abstractmethods__=set())
|
| 14 |
+
class TestApproximateCompiler(TestCase):
|
| 15 |
+
def setUp(self) -> None:
|
| 16 |
+
self.qc = QuantumCircuit(1)
|
| 17 |
+
|
| 18 |
+
def test_when_init_with_mps_backend_then_mps_backend_flag_true(self):
|
| 19 |
+
self.assertTrue(ApproximateCompiler(self.qc, MPS_SIM).is_aer_mps_backend)
|
| 20 |
+
|
| 21 |
+
def test_when_init_with_sv_backend_then_sv_backend_flag_true(self):
|
| 22 |
+
self.assertTrue(ApproximateCompiler(self.qc, SV_SIM).is_statevector_backend)
|
| 23 |
+
|
| 24 |
+
def test_given_global_cost_and_SV_backend_when_evaluate_cost_then_correct_function_called(
|
| 25 |
+
self,
|
| 26 |
+
):
|
| 27 |
+
compiler = ApproximateCompiler(QuantumCircuit(1), SV_SIM)
|
| 28 |
+
with patch.object(compiler.backend, "evaluate_global_cost") as mock:
|
| 29 |
+
compiler.evaluate_cost()
|
| 30 |
+
mock.assert_called_once()
|
| 31 |
+
|
| 32 |
+
def test_given_global_cost_and_MPS_backend_when_evaluate_cost_then_correct_function_called(
|
| 33 |
+
self,
|
| 34 |
+
):
|
| 35 |
+
compiler = ApproximateCompiler(QuantumCircuit(1), MPS_SIM)
|
| 36 |
+
with patch.object(compiler.backend, "evaluate_global_cost") as mock:
|
| 37 |
+
compiler.evaluate_cost()
|
| 38 |
+
mock.assert_called_once()
|
| 39 |
+
|
| 40 |
+
def test_given_global_cost_and_QASM_backend_when_evaluate_cost_then_correct_function_called(
|
| 41 |
+
self,
|
| 42 |
+
):
|
| 43 |
+
compiler = ApproximateCompiler(QuantumCircuit(1), QASM_SIM)
|
| 44 |
+
with patch.object(compiler.backend, "evaluate_global_cost") as mock:
|
| 45 |
+
compiler.evaluate_cost()
|
| 46 |
+
mock.assert_called_once()
|
| 47 |
+
|
| 48 |
+
def test_given_local_cost_and_SV_backend_when_evaluate_cost_then_correct_function_called(
|
| 49 |
+
self,
|
| 50 |
+
):
|
| 51 |
+
compiler = ApproximateCompiler(
|
| 52 |
+
QuantumCircuit(1), SV_SIM, optimise_local_cost=True
|
| 53 |
+
)
|
| 54 |
+
with patch.object(compiler.backend, "evaluate_local_cost") as mock:
|
| 55 |
+
compiler.evaluate_cost()
|
| 56 |
+
mock.assert_called_once()
|
| 57 |
+
|
| 58 |
+
def test_given_local_cost_and_MPS_backend_when_evaluate_cost_then_correct_function_called(
|
| 59 |
+
self,
|
| 60 |
+
):
|
| 61 |
+
compiler = ApproximateCompiler(
|
| 62 |
+
QuantumCircuit(1), MPS_SIM, optimise_local_cost=True
|
| 63 |
+
)
|
| 64 |
+
with patch.object(compiler.backend, "evaluate_local_cost") as mock:
|
| 65 |
+
compiler.evaluate_cost()
|
| 66 |
+
mock.assert_called_once()
|
| 67 |
+
|
| 68 |
+
def test_given_local_cost_and_QASM_backend_when_evaluate_cost_then_correct_function_called(
|
| 69 |
+
self,
|
| 70 |
+
):
|
| 71 |
+
compiler = ApproximateCompiler(
|
| 72 |
+
QuantumCircuit(1), QASM_SIM, optimise_local_cost=True
|
| 73 |
+
)
|
| 74 |
+
with patch.object(compiler.backend, "evaluate_local_cost") as mock:
|
| 75 |
+
compiler.evaluate_cost()
|
| 76 |
+
mock.assert_called_once()
|
| 77 |
+
|
| 78 |
+
def test_given_random_circuit_when_evaluate_local_cost_all_three_methods_return_same_cost(
|
| 79 |
+
self,
|
| 80 |
+
):
|
| 81 |
+
qc = co.create_random_initial_state_circuit(4)
|
| 82 |
+
|
| 83 |
+
compiler_sv = AdaptCompiler(qc, backend=SV_SIM, optimise_local_cost=True)
|
| 84 |
+
compiler_mps = AdaptCompiler(qc, backend=MPS_SIM, optimise_local_cost=True)
|
| 85 |
+
compiler_qasm = AdaptCompiler(qc, backend=QASM_SIM, optimise_local_cost=True)
|
| 86 |
+
|
| 87 |
+
cost_sv = compiler_sv.evaluate_cost()
|
| 88 |
+
cost_mps = compiler_mps.evaluate_cost()
|
| 89 |
+
cost_qasm = compiler_qasm.evaluate_cost()
|
| 90 |
+
|
| 91 |
+
# Looser pass threshold for qasm because it includes some form of noise
|
| 92 |
+
np.testing.assert_almost_equal(cost_sv, cost_mps, decimal=5)
|
| 93 |
+
np.testing.assert_almost_equal(cost_sv, cost_qasm, decimal=2)
|
| 94 |
+
np.testing.assert_almost_equal(cost_mps, cost_qasm, decimal=2)
|
| 95 |
+
|
| 96 |
+
def test_given_random_circuit_when_evaluate_global_cost_all_three_methods_return_same_cost(
|
| 97 |
+
self,
|
| 98 |
+
):
|
| 99 |
+
qc = co.create_random_initial_state_circuit(4)
|
| 100 |
+
|
| 101 |
+
compiler_sv = AdaptCompiler(qc, backend=SV_SIM)
|
| 102 |
+
compiler_mps = AdaptCompiler(qc, backend=MPS_SIM)
|
| 103 |
+
compiler_qasm = AdaptCompiler(qc, backend=QASM_SIM)
|
| 104 |
+
|
| 105 |
+
cost_sv = compiler_sv.evaluate_cost()
|
| 106 |
+
cost_mps = compiler_mps.evaluate_cost()
|
| 107 |
+
cost_qasm = compiler_qasm.evaluate_cost()
|
| 108 |
+
|
| 109 |
+
# Looser pass threshold for qasm because it includes some form of noise
|
| 110 |
+
np.testing.assert_almost_equal(cost_sv, cost_mps, decimal=5)
|
| 111 |
+
np.testing.assert_almost_equal(cost_sv, cost_qasm, decimal=2)
|
| 112 |
+
np.testing.assert_almost_equal(cost_mps, cost_qasm, decimal=2)
|
| 113 |
+
|
| 114 |
+
def test_given_simple_states_when_evaluate_global_and_local_costs_then_correct_value(
|
| 115 |
+
self,
|
| 116 |
+
):
|
| 117 |
+
# Analytically calculable costs:
|
| 118 |
+
# |0000> global=0, local=0
|
| 119 |
+
# |1010> global=1, local=1/2
|
| 120 |
+
# 4-qubit GHZ global=1/2, local=1/2
|
| 121 |
+
# |++++> global=15/16, local=1/2
|
| 122 |
+
# Using equations 9 and 11 from arXiv:1908.04416
|
| 123 |
+
|
| 124 |
+
analytic_costs = [0, 0, 1, 1 / 2, 1 / 2, 1 / 2, 15 / 16, 1 / 2]
|
| 125 |
+
adapt_costs = []
|
| 126 |
+
|
| 127 |
+
zero = QuantumCircuit(4)
|
| 128 |
+
|
| 129 |
+
neel = QuantumCircuit(4)
|
| 130 |
+
neel.x([0, 2])
|
| 131 |
+
|
| 132 |
+
ghz = QuantumCircuit(4)
|
| 133 |
+
ghz.h(0)
|
| 134 |
+
for i in range(3):
|
| 135 |
+
ghz.cx(0, i + 1)
|
| 136 |
+
|
| 137 |
+
hadamard = QuantumCircuit(4)
|
| 138 |
+
hadamard.h([0, 1, 2, 3])
|
| 139 |
+
|
| 140 |
+
circuits = [zero, neel, ghz, hadamard]
|
| 141 |
+
|
| 142 |
+
for circuit in circuits:
|
| 143 |
+
for optimise_local_cost in [False, True]:
|
| 144 |
+
compiler = AdaptCompiler(
|
| 145 |
+
circuit, backend=SV_SIM, optimise_local_cost=optimise_local_cost
|
| 146 |
+
)
|
| 147 |
+
cost = compiler.evaluate_cost()
|
| 148 |
+
adapt_costs.append(cost)
|
| 149 |
+
|
| 150 |
+
np.testing.assert_allclose(adapt_costs, analytic_costs)
|
| 151 |
+
|
| 152 |
+
def test_given_random_circuit_when_calculate_cost_local_less_or_equal_to_global(
|
| 153 |
+
self,
|
| 154 |
+
):
|
| 155 |
+
qc = co.create_random_initial_state_circuit(4)
|
| 156 |
+
|
| 157 |
+
compiler_local = AdaptCompiler(qc, backend=SV_SIM, optimise_local_cost=True)
|
| 158 |
+
compiler_global = AdaptCompiler(qc, backend=SV_SIM)
|
| 159 |
+
|
| 160 |
+
cost_local = compiler_local.evaluate_cost()
|
| 161 |
+
cost_global = compiler_global.evaluate_cost()
|
| 162 |
+
|
| 163 |
+
self.assertLessEqual(cost_local, cost_global)
|
| 164 |
+
|
| 165 |
+
@patch("qiskit.QuantumCircuit.set_matrix_product_state")
|
| 166 |
+
def test_set_matrix_product_state_used_when_mps_backend(
|
| 167 |
+
self, mock_set_matrix_product_state
|
| 168 |
+
):
|
| 169 |
+
ApproximateCompiler(QuantumCircuit(1), MPS_SIM)
|
| 170 |
+
mock_set_matrix_product_state.assert_called_once()
|
utils/adapt-aqc/test/utils/__init__.py
ADDED
|
File without changes
|