harishaseebat92 commited on
Commit
6d8ed8c
·
1 Parent(s): c8de2be

Fix: Add adapt-aqc as regular files, not submodule

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. utils/adapt-aqc +0 -1
  2. utils/adapt-aqc/.gitignore +364 -0
  3. utils/adapt-aqc/LICENSE +203 -0
  4. utils/adapt-aqc/README.md +111 -0
  5. utils/adapt-aqc/adaptaqc/__init__.py +10 -0
  6. utils/adapt-aqc/adaptaqc/backends/__init__.py +10 -0
  7. utils/adapt-aqc/adaptaqc/backends/aer_mps_backend.py +93 -0
  8. utils/adapt-aqc/adaptaqc/backends/aer_sv_backend.py +59 -0
  9. utils/adapt-aqc/adaptaqc/backends/aqc_backend.py +29 -0
  10. utils/adapt-aqc/adaptaqc/backends/itensor_backend.py +62 -0
  11. utils/adapt-aqc/adaptaqc/backends/julia_default_backends.py +13 -0
  12. utils/adapt-aqc/adaptaqc/backends/python_default_backends.py +19 -0
  13. utils/adapt-aqc/adaptaqc/backends/qiskit_sampling_backend.py +108 -0
  14. utils/adapt-aqc/adaptaqc/compilers/__init__.py +13 -0
  15. utils/adapt-aqc/adaptaqc/compilers/adapt/adapt_compiler.py +1163 -0
  16. utils/adapt-aqc/adaptaqc/compilers/adapt/adapt_config.py +97 -0
  17. utils/adapt-aqc/adaptaqc/compilers/adapt/adapt_result.py +70 -0
  18. utils/adapt-aqc/adaptaqc/compilers/approximate_compiler.py +527 -0
  19. utils/adapt-aqc/adaptaqc/utils/__init__.py +11 -0
  20. utils/adapt-aqc/adaptaqc/utils/ansatzes.py +100 -0
  21. utils/adapt-aqc/adaptaqc/utils/circuit_operations/__init__.py +17 -0
  22. utils/adapt-aqc/adaptaqc/utils/circuit_operations/circuit_operations_basic.py +262 -0
  23. utils/adapt-aqc/adaptaqc/utils/circuit_operations/circuit_operations_circuit_division.py +144 -0
  24. utils/adapt-aqc/adaptaqc/utils/circuit_operations/circuit_operations_full_circuit.py +465 -0
  25. utils/adapt-aqc/adaptaqc/utils/circuit_operations/circuit_operations_optimisation.py +231 -0
  26. utils/adapt-aqc/adaptaqc/utils/circuit_operations/circuit_operations_pauli_ops.py +127 -0
  27. utils/adapt-aqc/adaptaqc/utils/circuit_operations/circuit_operations_running.py +139 -0
  28. utils/adapt-aqc/adaptaqc/utils/circuit_operations/circuit_operations_variational.py +84 -0
  29. utils/adapt-aqc/adaptaqc/utils/constants.py +131 -0
  30. utils/adapt-aqc/adaptaqc/utils/cost_minimiser.py +418 -0
  31. utils/adapt-aqc/adaptaqc/utils/entanglement_measures.py +370 -0
  32. utils/adapt-aqc/adaptaqc/utils/fixed_ansatz_circuits.py +126 -0
  33. utils/adapt-aqc/adaptaqc/utils/gate_tomography.py +104 -0
  34. utils/adapt-aqc/adaptaqc/utils/gradients.py +224 -0
  35. utils/adapt-aqc/adaptaqc/utils/hamiltonians.py +85 -0
  36. utils/adapt-aqc/adaptaqc/utils/utilityfunctions.py +481 -0
  37. utils/adapt-aqc/docs/future_heuristic_ideas.md +66 -0
  38. utils/adapt-aqc/docs/running_options_explained.md +296 -0
  39. utils/adapt-aqc/examples/advanced_mps_example.py +66 -0
  40. utils/adapt-aqc/examples/advanced_sv_example.py +61 -0
  41. utils/adapt-aqc/examples/readme_example.py +64 -0
  42. utils/adapt-aqc/examples/simple_mps_example.py +33 -0
  43. utils/adapt-aqc/examples/simple_sv_example.py +25 -0
  44. utils/adapt-aqc/requirements.txt +1 -0
  45. utils/adapt-aqc/setup.py +28 -0
  46. utils/adapt-aqc/test/__init__.py +0 -0
  47. utils/adapt-aqc/test/recompilers/__init__.py +0 -0
  48. utils/adapt-aqc/test/recompilers/test_adapt_compiler.py +1543 -0
  49. utils/adapt-aqc/test/recompilers/test_approximate_compiler.py +170 -0
  50. 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