├── docs ├── .nojekyll ├── images │ ├── logo.png │ ├── qiskit_cold_atoms_uml.png │ ├── qiskit_cold_atom_logo_with_text.png │ └── qiskit_cold_atom_logo_with_text.svg ├── tutorials │ ├── aux_files │ │ ├── collective_spin_array.png │ │ └── fermionic_tweezer_array.png │ └── index.rst ├── apidocs │ ├── qiskit_cold_atom.spins.rst │ ├── qiskit_cold_atom.fermions.rst │ ├── qiskit_cold_atom.providers.rst │ ├── qiskit_cold_atom.applications.rst │ └── qiskit_cold_atom.rst ├── _templates │ ├── autosummary │ │ ├── base.rst │ │ ├── module.rst │ │ ├── class.rst │ │ └── class_no_inherited_members.rst │ └── theme_variables.jinja ├── guides │ ├── access.rst │ ├── install.rst │ └── usage.rst ├── index.rst ├── Makefile └── conf.py ├── qiskit_cold_atom ├── VERSION.txt ├── transpiler │ ├── __init__.py │ └── single_spin_gates.py ├── exceptions.py ├── applications │ ├── __init__.py │ ├── fermionic_evolution_problem.py │ ├── time_evolution_solver.py │ └── fermi_hubbard.py ├── providers │ ├── __init__.py │ ├── collective_spin_backend.py │ ├── cold_atom_job.py │ ├── fermionic_tweezer_backend.py │ └── remote_backend.py ├── spins │ ├── base_spin_backend.py │ ├── __init__.py │ ├── spin_circuit_solver.py │ └── spin_simulator_backend.py ├── __init__.py └── fermions │ ├── __init__.py │ ├── fermionic_basis.py │ ├── fermionic_state.py │ ├── base_fermion_backend.py │ └── fermion_simulator_backend.py ├── .stestr.conf ├── MANIFEST.in ├── requirements.txt ├── pyproject.toml ├── releasenotes ├── notes │ ├── drop-python3.7-bc4d3cd8ab136197.yaml │ ├── 0.2.1 │ │ ├── PR-66-d414127b770af185.yaml │ │ ├── api-changes-2ac317c7be96adea.yaml │ │ └── fix-qargs-parsing-spins-2b00917c29f1b7fb.yaml │ ├── dense-sign-663a4bb9989ba526.yaml │ ├── replace-numpy-alltrue-a6fbe0478678c9b7.yaml │ ├── ffsim-backend-d4e557a368af5808.yaml │ └── upgrade-of-qiskit-nature-f808e73a446c1c93.yaml └── config.yaml ├── .github ├── ISSUE_TEMPLATE │ ├── documentation.md │ ├── FEATURE_REQUEST.md │ └── BUG_REPORT.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── deploy-docs.yml │ └── main.yml ├── requirements-dev.txt ├── CODE_OF_CONDUCT.md ├── test ├── __init__.py ├── transpiler │ ├── __init__.py │ └── test_transpiler.py ├── test_fermion_state.py ├── test_fermionic_basis.py ├── test_cold_atom_provider.py ├── test_spin_simulator_backend.py ├── test_circuit_tools.py └── test_fermionic_gates.py ├── tox.ini ├── setup.py ├── .gitignore ├── CONTRIBUTING.md ├── tools └── verify_headers.py └── README.md /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /qiskit_cold_atom/VERSION.txt: -------------------------------------------------------------------------------- 1 | 0.2.1 -------------------------------------------------------------------------------- /.stestr.conf: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | test_path=./test 3 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt 2 | include requirements.txt 3 | include qiskit_cold_atom/VERSION.txt -------------------------------------------------------------------------------- /docs/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiskit-community/qiskit-cold-atom/HEAD/docs/images/logo.png -------------------------------------------------------------------------------- /docs/images/qiskit_cold_atoms_uml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiskit-community/qiskit-cold-atom/HEAD/docs/images/qiskit_cold_atoms_uml.png -------------------------------------------------------------------------------- /docs/images/qiskit_cold_atom_logo_with_text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiskit-community/qiskit-cold-atom/HEAD/docs/images/qiskit_cold_atom_logo_with_text.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy>=1.17 2 | scipy>=1.4 3 | requests>=2.25.1 4 | qiskit-terra>= 0.45.1 5 | qiskit-nature>=0.7.1 6 | qiskit-aer>=0.8.0 7 | matplotlib>=3.3 8 | -------------------------------------------------------------------------------- /docs/tutorials/aux_files/collective_spin_array.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiskit-community/qiskit-cold-atom/HEAD/docs/tutorials/aux_files/collective_spin_array.png -------------------------------------------------------------------------------- /docs/tutorials/aux_files/fermionic_tweezer_array.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiskit-community/qiskit-cold-atom/HEAD/docs/tutorials/aux_files/fermionic_tweezer_array.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.black] 6 | line-length = 100 7 | target-version = ['py36', 'py37', 'py38', 'py39'] 8 | -------------------------------------------------------------------------------- /docs/apidocs/qiskit_cold_atom.spins.rst: -------------------------------------------------------------------------------- 1 | Spins 2 | ****** 3 | 4 | .. _qiskit_cold_atom-spins: 5 | 6 | .. automodule:: qiskit_cold_atom.spins 7 | :no-members: 8 | :no-inherited-members: 9 | :no-special-members: -------------------------------------------------------------------------------- /releasenotes/notes/drop-python3.7-bc4d3cd8ab136197.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | upgrade: 3 | - | 4 | Support for running with Python 3.6 and 3.7 has been removed. The new requirements for Python versions are from version 3.8 to 3.11. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation 3 | about: 'Create a report to help us improve the documentation ' 4 | title: '' 5 | labels: documentation 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /docs/apidocs/qiskit_cold_atom.fermions.rst: -------------------------------------------------------------------------------- 1 | Fermions 2 | ********* 3 | 4 | .. _qiskit_cold_atom-fermions: 5 | 6 | .. automodule:: qiskit_cold_atom.fermions 7 | :no-members: 8 | :no-inherited-members: 9 | :no-special-members: -------------------------------------------------------------------------------- /releasenotes/notes/0.2.1/PR-66-d414127b770af185.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | features: 3 | - | 4 | Added gates that are necessary for circuits based on Rydberg atoms together 5 | with a tutorial that highlights how they might be used. 6 | -------------------------------------------------------------------------------- /docs/apidocs/qiskit_cold_atom.providers.rst: -------------------------------------------------------------------------------- 1 | Providers 2 | ********* 3 | 4 | .. _qiskit_cold_atom-providers: 5 | 6 | .. automodule:: qiskit_cold_atom.providers 7 | :no-members: 8 | :no-inherited-members: 9 | :no-special-members: -------------------------------------------------------------------------------- /docs/apidocs/qiskit_cold_atom.applications.rst: -------------------------------------------------------------------------------- 1 | Applications 2 | ************ 3 | 4 | .. _qiskit_cold_atom-applications: 5 | 6 | .. automodule:: qiskit_cold_atom.applications 7 | :no-members: 8 | :no-inherited-members: 9 | :no-special-members: -------------------------------------------------------------------------------- /docs/_templates/autosummary/base.rst: -------------------------------------------------------------------------------- 1 | {% if referencefile %} 2 | .. include:: {{ referencefile }} 3 | {% endif %} 4 | 5 | {{ objname }} 6 | {{ underline }} 7 | 8 | .. currentmodule:: {{ module }} 9 | 10 | .. auto{{ objtype }}:: {{ objname }} 11 | -------------------------------------------------------------------------------- /docs/_templates/theme_variables.jinja: -------------------------------------------------------------------------------- 1 | {%- set external_urls = { 2 | 'github': 'https://github.com/Qiskit-Extensions/qiskit-cold-atom', 3 | 'tutorials': 'https://github.com/Qiskit-Extensions/qiskit-cold-atom/tree/master/docs/tutorials', 4 | } 5 | -%} 6 | -------------------------------------------------------------------------------- /releasenotes/notes/dense-sign-663a4bb9989ba526.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | fixes: 3 | - | 4 | Fixes a bug in which the fermionic circuit simulator handled sign incorrectly. 5 | See https://github.com/qiskit-community/qiskit-cold-atom/issues/92 for details. 6 | -------------------------------------------------------------------------------- /docs/guides/access.rst: -------------------------------------------------------------------------------- 1 | Access 2 | ====== 3 | 4 | If you would like to use cold-atomic hardware and simulators beyond the default ones in the 5 | :class:`ColdAtomProvider` please contact www.synqs.org/ to request more information about 6 | gaining access. 7 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | black~=22.0 2 | stestr 3 | Sphinx>=6.0.0 4 | pylint==2.7.1 5 | reno>=3.2.0 6 | qiskit-sphinx-theme~=1.16.0 7 | sphinx-autodoc-typehints 8 | sphinx-panels 9 | sphinx-tabs 10 | jupyter 11 | jupyter-sphinx 12 | nbsphinx 13 | pylatexenc 14 | -------------------------------------------------------------------------------- /releasenotes/notes/replace-numpy-alltrue-a6fbe0478678c9b7.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | fixes: 3 | - | 4 | Replaced usage of `numpy.alltrue` with `numpy.all` throughout the codebase, 5 | to be specific in: `test/test_fermion_circuit_solver.py`, `test/test_spin_circuit_solver.py` 6 | -------------------------------------------------------------------------------- /docs/apidocs/qiskit_cold_atom.rst: -------------------------------------------------------------------------------- 1 | ============================== 2 | Qiskit Cold Atom API Reference 3 | ============================== 4 | 5 | .. _qiskit_cold_atom: 6 | 7 | .. automodule:: qiskit_cold_atom 8 | :no-members: 9 | :no-inherited-members: 10 | :no-special-members: -------------------------------------------------------------------------------- /docs/tutorials/index.rst: -------------------------------------------------------------------------------- 1 | ########################## 2 | Qiskit Cold Atom Tutorials 3 | ########################## 4 | 5 | 6 | .. nbgallery:: 7 | :glob: 8 | 9 | * 10 | 11 | 12 | .. Hiding - Indices and tables 13 | :ref:`genindex` 14 | :ref:`modindex` 15 | :ref:`search` 16 | -------------------------------------------------------------------------------- /releasenotes/notes/0.2.1/api-changes-2ac317c7be96adea.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | upgrade: 3 | - | 4 | The API for a Cold Atom Job now takes a token instead of a password. Furthermore, 5 | get requests are no-longer done with an entry under json in the request params 6 | but are instead done using a job_id entry. Post requests are now done with 7 | the structure json={"job": ..., ...} instead of data={"json": ..., ...}. 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Code of Conduct 4 | All members of this project agree to adhere to the Qiskit Code of Conduct listed at [https://github.com/Qiskit/qiskit/blob/master/CODE_OF_CONDUCT.md](https://github.com/Qiskit/qiskit/blob/master/CODE_OF_CONDUCT.md) 5 | 6 | ---- 7 | 8 | License: [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), 9 | Copyright Contributors to Qiskit. -------------------------------------------------------------------------------- /releasenotes/notes/0.2.1/fix-qargs-parsing-spins-2b00917c29f1b7fb.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | fixes: 3 | - | 4 | Fix incorrect parsing of spin operators in SpinCircuitSolver._embed_operator: 5 | The previous implementation of this method was not able to handle gates where the generator 6 | included terms that had a different number of non-identity operators. 7 | The new implementation of this method is able to handle the more general case as well. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F680 Feature request" 3 | about: "Suggest an idea for this project \U0001F4A1!" 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | 13 | ### What is the expected behavior? 14 | -------------------------------------------------------------------------------- /releasenotes/notes/ffsim-backend-d4e557a368af5808.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | features: 3 | - | 4 | Adds a simulator backend based on `ffsim`_. 5 | For supported circuits, the simulator is orders of magnitude faster than the existing 6 | :class:`~.FermionSimulator` backend. This backend is not supported on Windows. 7 | For more details, see https://github.com/qiskit-community/qiskit-cold-atom/pull/90. 8 | .. _ffsim: https://github.com/qiskit-community/ffsim -------------------------------------------------------------------------------- /releasenotes/notes/upgrade-of-qiskit-nature-f808e73a446c1c93.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | upgrade: 3 | - | 4 | Upgrade the syntax to be compatible with the version 0.7 of the `qiskit-nature`. 5 | This affects the definitions of FermionicOp as the generator of a FermionicGate and SpinOp for a SpinGate. 6 | FermionicOp now are now built from sparse labels with creators ("+") and annihilators ("-") and SpinOp allows for sparse labels with "X", "Y" and "Z". 7 | Please see the documentation of qiskit-nature for more details. 8 | 9 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2021. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """Testing module.""" 14 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 11 | 12 | ### Summary 13 | 14 | 15 | 16 | ### Details and comments 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/transpiler/__init__.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2021. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """Testing module for transpiling.""" 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug report" 3 | about: "Create a report to help us improve \U0001F914." 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | 13 | ### Informations 14 | 15 | - **Qiskit Cold atoms version**: 16 | - **Python version**: 17 | - **Operating system**: 18 | 19 | ### What is the current behavior? 20 | 21 | 22 | 23 | ### Steps to reproduce the problem 24 | 25 | 26 | 27 | ### What is the expected behavior? 28 | 29 | 30 | 31 | ### Suggested solutions 32 | -------------------------------------------------------------------------------- /qiskit_cold_atom/transpiler/__init__.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2021. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """Transpiler passes for spin and fermionic backends.""" 14 | 15 | from .single_spin_gates import Optimize1SpinGates 16 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ############################### 2 | Qiskit Cold Atoms documentation 3 | ############################### 4 | 5 | .. toctree:: 6 | :hidden: 7 | 8 | Home 9 | 10 | .. toctree:: 11 | :maxdepth: 1 12 | 13 | Installation 14 | Access 15 | Submitting circuits 16 | 17 | Qiskit Cold Atoms API and Tutorials 18 | =================================== 19 | 20 | .. toctree:: 21 | :maxdepth: 2 22 | 23 | API References 24 | Tutorials 25 | 26 | .. Hiding - Indices and tables 27 | :ref:`genindex` 28 | :ref:`modindex` 29 | :ref:`search` 30 | 31 | .. image:: images/qiskit_cold_atom_logo_with_text.svg 32 | :alt: Missing Logo 33 | -------------------------------------------------------------------------------- /qiskit_cold_atom/exceptions.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2021. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """Cold Atom Exception.""" 14 | 15 | from qiskit.exceptions import QiskitError 16 | 17 | 18 | class QiskitColdAtomError(QiskitError): 19 | """Class for errors returned by Qiskit's Cold Atom module.""" 20 | 21 | pass 22 | -------------------------------------------------------------------------------- /docs/guides/install.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | You can install qiskit cold atoms using pip: 5 | 6 | .. code-block:: python 7 | 8 | pip install qiskit-cold-atoms 9 | 10 | Provider Setup 11 | -------------- 12 | 13 | You can instantiate the provider by running 14 | 15 | .. code-block:: python 16 | 17 | provider = ColdAtomProvider.load_account() 18 | 19 | which will load any cold-atom saved credentials that you have. 20 | If no credentials have been stored this will provide the 21 | :class:`FermionicTweezerSimulator` and :class:`CollectiveSpinSimulator` 22 | simulators. 23 | If you have a username, URL, and token for a cold-atom setup you can save 24 | your credentials by running 25 | 26 | .. code-block:: python 27 | 28 | ColdAtomProvider.save_account(urls=list_of_urls, username="JohnDoe",token="123456") 29 | -------------------------------------------------------------------------------- /docs/_templates/autosummary/module.rst: -------------------------------------------------------------------------------- 1 | {% if referencefile %} 2 | .. include:: {{ referencefile }} 3 | {% endif %} 4 | 5 | {{ objname }} 6 | {{ underline }} 7 | 8 | .. automodule:: {{ fullname }} 9 | 10 | {% block functions %} 11 | {% if functions %} 12 | .. rubric:: Functions 13 | 14 | .. autosummary:: 15 | {% for item in functions %} 16 | {{ item }} 17 | {%- endfor %} 18 | {% endif %} 19 | {% endblock %} 20 | 21 | {% block classes %} 22 | {% if classes %} 23 | .. rubric:: Classes 24 | 25 | .. autosummary:: 26 | {% for item in classes %} 27 | {{ item }} 28 | {%- endfor %} 29 | {% endif %} 30 | {% endblock %} 31 | 32 | {% block exceptions %} 33 | {% if exceptions %} 34 | .. rubric:: Exceptions 35 | 36 | .. autosummary:: 37 | {% for item in exceptions %} 38 | {{ item }} 39 | {%- endfor %} 40 | {% endif %} 41 | {% endblock %} 42 | -------------------------------------------------------------------------------- /docs/_templates/autosummary/class.rst: -------------------------------------------------------------------------------- 1 | {% if referencefile %} 2 | .. include:: {{ referencefile }} 3 | {% endif %} 4 | 5 | {{ objname }} 6 | {{ underline }} 7 | 8 | .. currentmodule:: {{ module }} 9 | 10 | .. autoclass:: {{ objname }} 11 | :no-members: 12 | :no-inherited-members: 13 | :no-special-members: 14 | :show-inheritance: 15 | 16 | {% block attributes_summary %} 17 | {% if attributes %} 18 | .. rubric:: Attributes 19 | {% for item in all_attributes %} 20 | {%- if not item.startswith('_') %} 21 | .. autoattribute:: {{ name }}.{{ item }} 22 | {%- endif -%} 23 | {%- endfor %} 24 | {% endif %} 25 | {% endblock %} 26 | 27 | {% block methods_summary %} 28 | {% if methods %} 29 | .. rubric:: Methods 30 | {% for item in all_methods %} 31 | {%- if not item.startswith('_') or item in ['__call__', '__mul__', '__getitem__', '__len__'] %} 32 | .. automethod:: {{ name }}.{{ item }} 33 | {%- endif -%} 34 | {%- endfor %} 35 | 36 | {% endif %} 37 | {% endblock %} -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2018, 2021. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | # You can set these variables from the command line. 14 | SPHINXOPTS = 15 | SPHINXBUILD = sphinx-build 16 | SOURCEDIR = . 17 | BUILDDIR = _build 18 | 19 | # Put it first so that "make" without argument is like "make help". 20 | help: 21 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 22 | 23 | .PHONY: help Makefile 24 | 25 | # Catch-all target: route all unknown targets to Sphinx using the new 26 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 27 | %: Makefile 28 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 29 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 3.3.0 3 | envlist = py310,py39,py38,py37,lint 4 | skipsdist = True 5 | 6 | [testenv] 7 | usedevelop = True 8 | install_command = pip install -U {opts} {packages} 9 | setenv = 10 | VIRTUAL_ENV={envdir} 11 | QISKIT_SUPPRESS_PACKAGING_WARNINGS=Y 12 | deps = 13 | -r{toxinidir}/requirements.txt 14 | -r{toxinidir}/requirements-dev.txt 15 | commands = stestr run {posargs} 16 | 17 | [testenv:lint] 18 | envdir = .tox/lint 19 | commands = 20 | black --check {posargs} qiskit_cold_atom test 21 | pylint -rn -j 0 --rcfile={toxinidir}/.pylintrc qiskit_cold_atom/ test/ 22 | python tools/verify_headers.py 23 | 24 | [testenv:black] 25 | commands = black {posargs} qiskit_cold_atom test 26 | 27 | [testenv:docs] 28 | deps = 29 | -r{toxinidir}/requirements.txt 30 | -r{toxinidir}/requirements-dev.txt 31 | .[ffsim] 32 | commands = 33 | sphinx-build -j auto -b html -W {posargs} docs/ docs/_build/html 34 | 35 | [testenv:docs-clean] 36 | skip_install = true 37 | deps = 38 | allowlist_externals = rm 39 | commands = rm -rf {toxinidir}/docs/stubs/ {toxinidir}/docs/_build 40 | 41 | [testenv:ffsim] 42 | deps = .[ffsim] 43 | commands = python -m unittest test/test_ffsim_backend.py -------------------------------------------------------------------------------- /docs/_templates/autosummary/class_no_inherited_members.rst: -------------------------------------------------------------------------------- 1 | {# This is identical to class.rst, except for the filtering of the inherited_members. -#} 2 | 3 | {% if referencefile %} 4 | .. include:: {{ referencefile }} 5 | {% endif %} 6 | 7 | {{ objname }} 8 | {{ underline }} 9 | 10 | .. currentmodule:: {{ module }} 11 | 12 | .. autoclass:: {{ objname }} 13 | :no-members: 14 | :no-inherited-members: 15 | :no-special-members: 16 | :show-inheritance: 17 | 18 | {% block attributes_summary %} 19 | {% if attributes %} 20 | .. rubric:: Attributes 21 | {% for item in all_attributes %} 22 | {%- if not item.startswith('_') %} 23 | .. autoattribute:: {{ name }}.{{ item }} 24 | {%- endif -%} 25 | {%- endfor %} 26 | {% endif %} 27 | {% endblock %} 28 | 29 | {% block methods_summary %} 30 | {% if methods %} 31 | .. rubric:: Methods 32 | {% for item in all_methods %} 33 | {%- if item not in inherited_members %} 34 | {%- if not item.startswith('_') or item in ['__call__', '__mul__', '__getitem__', '__len__'] %} 35 | .. automethod:: {{ name }}.{{ item }} 36 | {%- endif -%} 37 | {%- endif -%} 38 | {%- endfor %} 39 | 40 | {% endif %} 41 | {% endblock %} -------------------------------------------------------------------------------- /qiskit_cold_atom/applications/__init__.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2021. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """ 14 | Module to study applications using cold atomic setups. Users of the qiskit_cold_atoms.applications 15 | module can create Fermionic time-evolution problems to simulate their dynamics on simulator 16 | backends or cold-atom based hardware that natively supports the Hamiltonian of the fermionic 17 | problem. 18 | 19 | For lattice-based problems, users must create subclasses of the :class:`FermionicLattice` class. 20 | An example of which is the one-dimensional :class:`FermiHubbard1D` which describes a 21 | one-dimensional Fermi-Hubbard lattice with spin-up and spin-down particles. 22 | 23 | .. autosummary:: 24 | :toctree: ../stubs/ 25 | 26 | FermionicLattice 27 | FermiHubbard1D 28 | FermionicEvolutionProblem 29 | TimeEvolutionSolver 30 | 31 | """ 32 | 33 | from .fermi_hubbard import FermionicLattice, FermiHubbard1D 34 | from .fermionic_evolution_problem import FermionicEvolutionProblem 35 | from .time_evolution_solver import TimeEvolutionSolver 36 | -------------------------------------------------------------------------------- /qiskit_cold_atom/providers/__init__.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2021. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """ 14 | Cold Atom Backend Provider 15 | =========================== 16 | 17 | This module contains a provider to interface with cold atomic device backends. 18 | To access these devices, the user has to specify the url of the backend, a username and an access token. 19 | With these credentials, the provider can be accessed like this: 20 | 21 | .. code-block:: python 22 | 23 | from qiskit_cold_atom.providers import ColdAtomProvider 24 | 25 | # save account with personal credentials 26 | ColdAtomProvider.save_account(url="my_url", username="my_name", token="my_url") 27 | 28 | provider = ColdAtomProvider.load_account() 29 | backends = provider.backends() 30 | 31 | .. autosummary:: 32 | :toctree: ../stubs/ 33 | 34 | ColdAtomProvider 35 | 36 | 37 | Cold Atom Jobs 38 | ============== 39 | 40 | When executing jobs, cold atom backends will return instances of :class:`ColdAtomJob`. 41 | 42 | .. autosummary:: 43 | :toctree: ../stubs/ 44 | 45 | ColdAtomJob 46 | 47 | """ 48 | 49 | from .cold_atom_provider import ColdAtomProvider 50 | from .cold_atom_job import ColdAtomJob 51 | -------------------------------------------------------------------------------- /docs/guides/usage.rst: -------------------------------------------------------------------------------- 1 | =================== 2 | Submitting circuits 3 | =================== 4 | 5 | Quantum circuits for cold atomic backends should be built from the library of gates included in 6 | the fermions and spin packages. These circuits can be sent to a backend. These gates are 7 | automatically added to Qiskit's QuantumCircuit class when importing the cold atom provider. 8 | The code below shows an example of a simulation of a rotation of a system with a size 20 spin. 9 | 10 | .. code-block:: python 11 | 12 | import numpy as np 13 | 14 | from qiskit.circuit import QuantumCircuit 15 | from qiskit.visualization import plot_histogram 16 | from qiskit_cold_atom.providers import ColdAtomProvider 17 | 18 | provider = ColdAtomProvider() 19 | backend = provider.get_backend("collective_spin_simulator") 20 | 21 | circuit = QuantumCircuit(1, 1) 22 | circuit.lx(np.pi/2, 0) 23 | circuit.measure(0, 0) 24 | 25 | job_rabi = backend.run(circuit, shots=1024, spin=20, seed=5462) 26 | plot_histogram(job_rabi.result().get_counts(0)) 27 | 28 | 29 | Basis gates and transpilation 30 | ============================= 31 | 32 | The cold atom backends supported in this package are typically not universal quantum computers 33 | but quantum simulators with gates that implement the hardware-native Hamiltonian. 34 | Therefor, the circuits to submit to the hardware must be built with the fermion and spin gates 35 | provided by the package. 36 | 37 | .. code-block:: python 38 | 39 | from qiskit import QuantumCircuit 40 | from qiskit_cold_atom.spins.spins_gate_library import LXGate, LZGate, LZ2Gate 41 | 42 | circ = QuantumCircuit(1, 1) 43 | circ.lx(-np.pi/2, 0) 44 | circ.lz2(0.3, 0) 45 | circ.lz(-np.pi/2, 0) 46 | circ.lx(-0.15, 0) 47 | circ.measure(0, 0) 48 | 49 | Additional details are in the tutorials. 50 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2018, 2021. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | import os 14 | import sys 15 | import setuptools 16 | import inspect 17 | 18 | long_description = """Qiskit cold atom is an open-source library to describe cold atomic quantum 19 | experiments in a gate- and circuit-based framework.""" 20 | 21 | with open("requirements.txt") as f: 22 | REQUIREMENTS = f.read().splitlines() 23 | 24 | VERSION_PATH = os.path.join(os.path.dirname(__file__), "qiskit_cold_atom", "VERSION.txt") 25 | with open(VERSION_PATH, "r") as version_file: 26 | VERSION = version_file.read().strip() 27 | 28 | setuptools.setup( 29 | name="qiskit-cold-atom", 30 | version=VERSION, 31 | description="Integration of cold atomic experiments into the Qiskit SDK.", 32 | long_description=long_description, 33 | long_description_content_type="text/markdown", 34 | url="https://github.com/qiskit-community/qiskit-cold-atom", 35 | author="Qiskit cold atom development team", 36 | license="Apache 2.0", 37 | classifiers=[ 38 | "Environment :: Console", 39 | "License :: OSI Approved :: Apache Software License", 40 | "Intended Audience :: Developers", 41 | "Intended Audience :: Science/Research", 42 | "Operating System :: Microsoft :: Windows", 43 | "Operating System :: MacOS", 44 | "Operating System :: POSIX :: Linux", 45 | "Programming Language :: Python :: 3 :: Only", 46 | "Programming Language :: Python :: 3.8", 47 | "Programming Language :: Python :: 3.9", 48 | "Programming Language :: Python :: 3.10", 49 | "Programming Language :: Python :: 3.11", 50 | "Topic :: Scientific/Engineering", 51 | ], 52 | keywords="qiskit sdk quantum cold atoms", 53 | packages=setuptools.find_packages(include=["qiskit_cold_atom", "qiskit_cold_atom.*"]), 54 | install_requires=REQUIREMENTS, 55 | extras_require={"ffsim": ["ffsim==0.0.21"]}, 56 | include_package_data=True, 57 | python_requires=">=3.8", 58 | zip_safe=False, 59 | ) 60 | -------------------------------------------------------------------------------- /.github/workflows/deploy-docs.yml: -------------------------------------------------------------------------------- 1 | # This code is part of a Qiskit project. 2 | # 3 | # (C) Copyright IBM 2022, 2023. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | name: Deploy Docs 14 | 15 | on: 16 | workflow_dispatch: 17 | 18 | concurrency: 19 | group: ${{ github.repository }}-${{ github.ref }}-${{ github.head_ref }}-${{ github.workflow }} 20 | cancel-in-progress: true 21 | 22 | jobs: 23 | docs_build: 24 | if: ${{ contains('["eggerdj", "ElePT"]', github.actor) }} 25 | runs-on: ubuntu-latest 26 | strategy: 27 | matrix: 28 | python-version: [3.8] 29 | steps: 30 | - uses: actions/checkout@v3 31 | with: 32 | fetch-depth: 0 33 | - uses: actions/setup-python@v4 34 | with: 35 | python-version: ${{ matrix.python-version }} 36 | cache: 'pip' 37 | cache-dependency-path: | 38 | requirements.txt 39 | requirements-dev.txt 40 | - name: Install Deps 41 | run: | 42 | python -m pip install -U tox setuptools virtualenv wheel 43 | sudo apt-get install -y pandoc graphviz 44 | - name: Install and Run Docs 45 | run: tox -e docs 46 | - name: Run upload documentation artifacts 47 | uses: actions/upload-artifact@v3 48 | with: 49 | name: page 50 | path: docs/_build/html 51 | deploy: 52 | runs-on: ubuntu-latest 53 | needs: docs_build 54 | # Grant GITHUB_TOKEN the permissions required to make a Pages deployment 55 | permissions: 56 | pages: write # to deploy to Pages 57 | id-token: write # to verify the deployment originates from an appropriate source 58 | environment: 59 | name: github-pages 60 | url: ${{steps.deployment.outputs.page_url}} 61 | steps: 62 | - uses: actions/download-artifact@v3 63 | with: 64 | name: page 65 | path: . 66 | - uses: actions/configure-pages@v1 67 | - uses: actions/upload-pages-artifact@v1 68 | with: 69 | path: . 70 | - name: Deploy to GitHub Pages 71 | id: deployment 72 | uses: actions/deploy-pages@v1 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # editor files 7 | .vscode/ 8 | .idea/ 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | pip-wheel-metadata/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | docs/stubs 78 | 79 | # PyBuilder 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 100 | __pypackages__/ 101 | 102 | # Celery stuff 103 | celerybeat-schedule 104 | celerybeat.pid 105 | 106 | # SageMath parsed files 107 | *.sage.py 108 | 109 | # Environments 110 | .env 111 | .venv 112 | env/ 113 | venv/ 114 | ENV/ 115 | env.bak/ 116 | venv.bak/ 117 | 118 | # Spyder project settings 119 | .spyderproject 120 | .spyproject 121 | 122 | # Rope project settings 123 | .ropeproject 124 | 125 | # mkdocs documentation 126 | /site 127 | 128 | # mypy 129 | .mypy_cache/ 130 | .dmypy.json 131 | dmypy.json 132 | 133 | # Pyre type checker 134 | .pyre/ 135 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | **We appreciate all kinds of help, so thank you!** 4 | 5 | First please read the overall project contributing guidelines. These are 6 | included in the Qiskit documentation here: 7 | 8 | https://github.com/Qiskit/qiskit/blob/main/CONTRIBUTING.md 9 | 10 | ## Contributing to Qiskit Cold Atom 11 | 12 | If you've noticed a bug or have a feature request, we encourage to open an issue in 13 | [this repo's issues tracker](https://github.com/qiskit-partners/qiskit-cold-atom/issues), 14 | whether you plan to address it yourself or not. 15 | 16 | If you intend to contribute code, please still start the contribution process by opening a new issue or 17 | making a comment on an existing issue briefly explaining what you intend to address and how. 18 | This helps us understand your intent/approach and provide support and commentary before you take the 19 | time to actually write code, and ensures multiple people aren't accidentally working on the same thing. 20 | 21 | ### Project Code Style. 22 | 23 | Code in Qiskit Cold Atom should conform to PEP8 and style/lint checks are run to validate 24 | this. Line length must be limited to no more than 100 characters. Docstrings 25 | should be written using the Google docstring format. 26 | 27 | ### Making a pull request 28 | 29 | When you're ready to make a pull request, please make sure the following is true: 30 | 31 | 1. The code matches the project's code style 32 | 2. The documentation, _including any docstrings for changed methods_, has been updated 33 | 3. If appropriate for your change, that new tests have been added to address any new functionality, or that existing tests have been updated as appropriate 34 | 4. All of the tests (new and old) still pass! 35 | 5. You have added notes in the pull request that explains what has changed and links to the relevant issues in the issues tracker 36 | 37 | 38 | ### Building the documentation 39 | 40 | The documentation for the Python SDK is auto-generated from Python 41 | docstrings using [Sphinx](http://www.sphinx-doc.org. Please follow [Google's Python Style 42 | Guide](https://google.github.io/styleguide/pyguide.html?showone=Comments#Comments) 43 | for docstrings. A good example of the style can also be found with 44 | [Sphinx's napolean converter 45 | documentation](http://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html). 46 | To build the documentation locally navigate to `.\docs\` and run 47 | 48 | ```bash 49 | sphinx-build -M html . _build 50 | ``` 51 | which creates the documentation at `.\docs\_build\html\index.html` . 52 | 53 | ### Running the tests 54 | 55 | This package uses the [pytest](https://docs.pytest.org/en/stable/) test runner. 56 | 57 | To use pytest directly, just run: 58 | 59 | ```bash 60 | pytest [pytest-args] 61 | ``` 62 | -------------------------------------------------------------------------------- /test/transpiler/test_transpiler.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2021. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """Testing module for transpiling.""" 14 | 15 | import numpy as np 16 | 17 | from qiskit import QuantumCircuit 18 | from qiskit.transpiler import PassManager 19 | from qiskit.test import QiskitTestCase 20 | 21 | from qiskit_cold_atom.transpiler import Optimize1SpinGates 22 | 23 | 24 | # pylint: disable=no-member 25 | class TestSpinTranspilation(QiskitTestCase): 26 | """Test class for spin-based transpilation.""" 27 | 28 | def test_optimize_1s_gates(self): 29 | """Test the single-spin gate transpilation.""" 30 | 31 | circ = QuantumCircuit(1) 32 | circ.rlx(np.pi / 2, 0) 33 | circ.rlx(np.pi / 2, 0) 34 | circ.rlx(np.pi / 2, 0) 35 | circ.rly(np.pi / 2, 0) 36 | circ.rly(np.pi / 2, 0) 37 | circ.rlz2(np.pi / 4, 0) 38 | circ.rlz2(np.pi / 4, 0) 39 | circ.rlz2(np.pi / 4, 0) 40 | circ.rlx(np.pi / 2, 0) 41 | 42 | pass_manager = PassManager(Optimize1SpinGates()) 43 | 44 | circ_new = pass_manager.run(circ) 45 | 46 | self.assertEqual(circ_new.count_ops()["rlx"], 2) 47 | self.assertEqual(circ_new.count_ops()["rly"], 1) 48 | self.assertEqual(circ_new.count_ops()["rlz2"], 1) 49 | 50 | self.assertTrue(np.allclose(circ_new.data[0][0].params[0], 3 * np.pi / 2)) 51 | self.assertTrue(np.allclose(circ_new.data[1][0].params[0], np.pi)) 52 | self.assertTrue(np.allclose(circ_new.data[2][0].params[0], 3 * np.pi / 4)) 53 | self.assertTrue(np.allclose(circ_new.data[3][0].params[0], np.pi / 2)) 54 | 55 | def test_optimize_1s_gates_multi_spin(self): 56 | """Test the single-spin gate transpilation.""" 57 | 58 | circ = QuantumCircuit(2) 59 | circ.rlx(np.pi / 3, 0) 60 | circ.rlx(np.pi / 3, 0) 61 | circ.rlx(np.pi / 3, 0) 62 | circ.rly(np.pi / 4, 1) 63 | circ.rly(np.pi / 4, 1) 64 | 65 | pass_manager = PassManager(Optimize1SpinGates()) 66 | 67 | circ_new = pass_manager.run(circ) 68 | 69 | self.assertEqual(circ_new.count_ops()["rlx"], 1) 70 | self.assertEqual(circ_new.count_ops()["rly"], 1) 71 | 72 | self.assertTrue(np.allclose(circ_new.data[0][0].params[0], np.pi)) 73 | self.assertTrue(np.allclose(circ_new.data[1][0].params[0], np.pi / 2)) 74 | -------------------------------------------------------------------------------- /qiskit_cold_atom/spins/base_spin_backend.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2021. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """Module for cold-atom spin backends.""" 14 | 15 | from abc import ABC 16 | 17 | from qiskit.providers import BackendV1 as Backend 18 | from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister 19 | from qiskit_cold_atom import QiskitColdAtomError 20 | 21 | 22 | class BaseSpinBackend(Backend, ABC): 23 | """Abstract base class for atomic mixture backends.""" 24 | 25 | def get_empty_circuit(self) -> QuantumCircuit: 26 | """ 27 | Convenience function to set up an empty circuit with the right QuantumRegisters. 28 | For each atomic species specified in the config file, a quantum register is added to the circuit. 29 | 30 | Returns: 31 | qc: An empty quantum circuit ready to use in spin-based cold-atom setups. 32 | 33 | Raises: 34 | QiskitColdAtomError: 35 | - If backend has no config file. 36 | - If number of wires of the backend config is not a multiple of the atomic species. 37 | """ 38 | config = self.configuration().to_dict() 39 | 40 | try: 41 | num_wires = config["n_qubits"] 42 | num_species = len(config["atomic_species"]) 43 | 44 | except NameError as name_error: 45 | raise QiskitColdAtomError( 46 | "backend needs to be initialized with config file first" 47 | ) from name_error 48 | 49 | if not (isinstance(num_wires, int) and num_wires % num_species == 0): 50 | raise QiskitColdAtomError( 51 | "num_wires {num_wires} must be multiple of num_species {num_species}" 52 | ) 53 | 54 | qregs = [ 55 | QuantumRegister(num_wires / num_species, species) 56 | for species in config["atomic_species"] 57 | ] 58 | 59 | class_reg = ClassicalRegister(num_wires, "c{}".format(num_wires)) 60 | empty_circuit = QuantumCircuit(*qregs, class_reg) 61 | 62 | return empty_circuit 63 | 64 | def draw(self, qc: QuantumCircuit, **draw_options): 65 | """Modified circuit drawer to better display atomic mixture quantum circuits. 66 | 67 | For now this method is just an alias to `QuantumCircuit.draw()` but in the future this method 68 | may be modified and tailored to spin quantum circuits. 69 | 70 | Args: 71 | qc: The quantum circuit to draw. 72 | draw_options: Key word arguments for the drawing of circuits. 73 | """ 74 | qc.draw(**draw_options) 75 | -------------------------------------------------------------------------------- /test/test_fermion_state.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2021. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """ Fermionic state tests.""" 14 | 15 | from qiskit import QuantumCircuit 16 | from qiskit.test import QiskitTestCase 17 | 18 | from qiskit_cold_atom.exceptions import QiskitColdAtomError 19 | from qiskit_cold_atom.fermions.fermion_circuit_solver import FermionicState 20 | 21 | 22 | class TestFermionState(QiskitTestCase): 23 | """Class to test the fermion state class.""" 24 | 25 | def test_initialize(self): 26 | """Test the initialization of fermionic states.""" 27 | 28 | state = FermionicState([0, 1, 1, 0]) 29 | 30 | self.assertEqual(state.sites, 4) 31 | self.assertEqual(state.num_species, 1) 32 | 33 | with self.assertRaises(QiskitColdAtomError): 34 | FermionicState([0, 2]) 35 | 36 | with self.assertRaises(QiskitColdAtomError): 37 | FermionicState([[0, 1], [1, 0, 1]]) 38 | 39 | state = FermionicState([[0, 1, 0], [1, 0, 1]]) 40 | 41 | self.assertEqual(state.occupations_flat, [0, 1, 0, 1, 0, 1]) 42 | self.assertEqual(state.num_species, 2) 43 | 44 | def test_string(self): 45 | """Test the string representation.""" 46 | 47 | state = FermionicState([0, 1]) 48 | 49 | self.assertEqual(str(state), "|0, 1>") 50 | 51 | state = FermionicState([[0, 1], [1, 0]]) 52 | self.assertEqual(str(state), "|0, 1>|1, 0>") 53 | 54 | def test_occupations(self): 55 | """Test that to get the fermionic occupations.""" 56 | state = FermionicState([0, 1]) 57 | 58 | self.assertEqual(state.occupations, [[0, 1]]) 59 | 60 | def test_from_flat_list(self): 61 | """Test the creation of fermionic states from flat lists.""" 62 | 63 | state = FermionicState.from_total_occupations([0, 1, 1, 0], 2) 64 | self.assertEqual(state.occupations, [[0, 1], [1, 0]]) 65 | 66 | with self.assertRaises(QiskitColdAtomError): 67 | FermionicState.from_total_occupations([0, 1, 1, 0], 3) 68 | 69 | state = FermionicState.from_total_occupations([0, 1, 1, 0], 1) 70 | self.assertEqual(state.occupations, [[0, 1, 1, 0]]) 71 | 72 | def test_from_initial_state(self): 73 | """Test that we can load an initial state from a circuit.""" 74 | 75 | circ = QuantumCircuit(4) 76 | circ.fload(0) 77 | circ.fload(2) 78 | 79 | state = FermionicState.initial_state(circ, 2) 80 | self.assertEqual(state.occupations, [[1, 0], [1, 0]]) 81 | self.assertEqual(state.occupations_flat, [1, 0, 1, 0]) 82 | 83 | state = FermionicState.initial_state(circ, 1) 84 | self.assertEqual(state.occupations, [[1, 0, 1, 0]]) 85 | self.assertEqual(state.occupations_flat, [1, 0, 1, 0]) 86 | -------------------------------------------------------------------------------- /qiskit_cold_atom/transpiler/single_spin_gates.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2021. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """A transpiler pass to simplify single-qubit gates.""" 14 | 15 | from qiskit.transpiler.basepasses import TransformationPass 16 | from qiskit.transpiler.exceptions import TranspilerError 17 | 18 | from qiskit_cold_atom.spins.spins_gate_library import ( 19 | RLXGate, 20 | RLYGate, 21 | RLZGate, 22 | RLZ2Gate, 23 | ) 24 | 25 | 26 | class Optimize1SpinGates(TransformationPass): 27 | """Simplify single-spin spin gates. 28 | 29 | The simplification of these gates is done by adding the angle of identical and 30 | consecutive gates together. For example, the circuit 31 | 32 | .. parsed-literal:: 33 | 34 | ┌──────────┐┌──────────┐┌──────────┐┌──────────┐┌──────────┐┌──────────┐ 35 | spin_0: ┤ Rlx(π/2) ├┤ Rlx(π/2) ├┤ Rlx(π/2) ├┤ Rly(π/2) ├┤ Rly(π/2) ├┤ Rlx(π/2) ├ 36 | └──────────┘└──────────┘└──────────┘└──────────┘└──────────┘└──────────┘ 37 | 38 | will be transpiled to 39 | 40 | .. parsed-literal:: 41 | 42 | ┌───────────┐┌────────┐┌──────────┐ 43 | spin_0: ┤ Rlx(3π/2) ├┤ Rly(π) ├┤ Rlx(π/2) ├ 44 | └───────────┘└────────┘└──────────┘ 45 | """ 46 | 47 | def __init__(self): 48 | super().__init__() 49 | self._gate_names = ["rlx", "rly", "rlz", "rlz2"] 50 | 51 | def run(self, dag): 52 | """Run the Optimize1qSpinGates pass on `dag`. 53 | 54 | Args: 55 | dag (DAGCircuit): the DAG to be optimized. 56 | 57 | Returns: 58 | DAGCircuit: the optimized DAG. 59 | 60 | Raises: 61 | TranspilerError: if the considered gate is not supported. 62 | """ 63 | 64 | for gate_name in self._gate_names: 65 | runs = dag.collect_runs(gate_name) 66 | 67 | for run in runs: 68 | if len(run) == 1: 69 | continue 70 | 71 | # Sum the angles of the same single-qubit gates. 72 | total_angle = 0.0 73 | for dag_node in run: 74 | total_angle += dag_node.op.params[0] 75 | 76 | if gate_name == "rlx": 77 | new_op = RLXGate(total_angle) 78 | elif gate_name == "rly": 79 | new_op = RLYGate(total_angle) 80 | elif gate_name == "rlz": 81 | new_op = RLZGate(total_angle) 82 | elif gate_name == "rlz2": 83 | new_op = RLZ2Gate(total_angle) 84 | else: 85 | raise TranspilerError(f"Could not use the basis {self._gate_names}.") 86 | 87 | dag.substitute_node(run[0], new_op, inplace=True) 88 | 89 | # Delete the remaining nodes 90 | for current_node in run[1:]: 91 | dag.remove_op_node(current_node) 92 | 93 | return dag 94 | -------------------------------------------------------------------------------- /test/test_fermionic_basis.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2021. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """ Fermionic basis tests.""" 14 | 15 | from qiskit.test import QiskitTestCase 16 | from qiskit_cold_atom.fermions.fermion_circuit_solver import ( 17 | FermionicState, 18 | FermionicBasis, 19 | ) 20 | 21 | 22 | class TestFermionicBasis(QiskitTestCase): 23 | """Class to test the fermionic basis class.""" 24 | 25 | def test_initialize(self): 26 | """Test the initialization of fermionic basis.""" 27 | with self.subTest("particle and spin conserved"): 28 | basis = FermionicBasis(sites=3, n_particles=[1, 2, 3]) 29 | self.assertEqual(basis.num_species, 3) 30 | self.assertEqual(basis.dimension, 9) 31 | with self.subTest("particle number not conserved"): 32 | basis = FermionicBasis(sites=3, n_particles=[1, 2, 3], particle_conservation=False) 33 | self.assertEqual(basis.dimension, 512) 34 | with self.subTest("particle number not conserved"): 35 | basis = FermionicBasis(sites=3, n_particles=[1, 2, 3], spin_conservation=False) 36 | self.assertEqual(basis.dimension, 84) 37 | 38 | def test_string(self): 39 | """Test the string representation.""" 40 | basis = FermionicBasis(sites=2, n_particles=[1, 1]) 41 | expect = "\n 0. |0, 1>|0, 1>\n 1. |0, 1>|1, 0>\n 2. |1, 0>|0, 1>\n 3. |1, 0>|1, 0>" 42 | self.assertEqual(str(basis), expect) 43 | 44 | def test_init_from_state(self): 45 | """Test the initialization from a fermionic state""" 46 | 47 | state = FermionicState([[0, 1, 1], [1, 0, 0]]) 48 | with self.subTest("particle and spin conserved"): 49 | basis = FermionicBasis.from_state(state, spin_conservation=True) 50 | self.assertEqual(basis.num_species, 2) 51 | self.assertEqual(basis.dimension, 9) 52 | with self.subTest("particle number not conserved"): 53 | basis = FermionicBasis.from_state(state, spin_conservation=False) 54 | self.assertEqual(basis.dimension, 20) 55 | with self.subTest("particle number not conserved"): 56 | basis = FermionicBasis.from_state( 57 | state, spin_conservation=False, particle_conservation=False 58 | ) 59 | self.assertEqual(basis.dimension, 64) 60 | 61 | def test_get_occupations(self): 62 | """Test the representation as flat occupations lists.""" 63 | 64 | basis = FermionicBasis(sites=2, n_particles=[1, 1]) 65 | self.assertEqual( 66 | basis.get_occupations(), 67 | [[0, 1, 0, 1], [0, 1, 1, 0], [1, 0, 0, 1], [1, 0, 1, 0]], 68 | ) 69 | 70 | def test_index_of_measurement(self): 71 | """Test the allocation of the basis state index for a given measurement.""" 72 | basis = FermionicBasis(sites=2, n_particles=[1, 1]) 73 | self.assertEqual(basis.get_index_of_measurement("0110"), 1) 74 | -------------------------------------------------------------------------------- /qiskit_cold_atom/__init__.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2021. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | r""" 14 | .. image:: ../images/qiskit_cold_atom_logo_with_text.svg 15 | :alt: Missing Logo 16 | 17 | ================================================== 18 | Qiskit Cold Atom module (:mod:`qiskit_cold_atom`) 19 | ================================================== 20 | 21 | .. currentmodule:: qiskit_cold_atom 22 | 23 | The Qiskit Cold Atom module provides functionality to describe quantum systems of trapped cold atoms 24 | in a gate- and circuit-based framework. 25 | 26 | Traditionally, each wire in a quantum circuit represents one qubit as the fundamental unit of information 27 | processing. Here, we extend this concept and allow wires to represent individual internal states of 28 | trapped cold atoms. This currently covers two settings, one for fermionic modes and one for spin 29 | modes. 30 | 31 | In a fermionic setting, each wire of a quantum circuit represents an abstract fermionic mode in second 32 | quantization which can either be occupied (1) or empty (0). Such systems are realized experimentally by 33 | individual fermionic atoms trapped in arrays of optical tweezers. Circuit instructions and backends 34 | to interact with and simulate such circuits are given by the :mod:`qiskit_cold_atom.fermions` module. 35 | 36 | In a spin setting, each wire of a quantum circuit represents a quantum mechanical spin of a given length 37 | :math:`S`. Upon measurement, each spin is measured in one of its :math:`2S+1` internal basis states 38 | labelled :math:`0` to :math:`2S`, thus it can be thought of as a qudit with dimension :math:`d = 2S+1`. 39 | This setting describes the collective spin of bosonic atoms trapped in a Bose-Einstein-condensate. 40 | Circuit instructions and backends to interact with and simulate such circuits are provided by the 41 | :mod:`qiskit_cold_atom.spins` module. 42 | 43 | The quantum circuits that these systems can implement thus utilize a fundamentally different form of 44 | quantum information processing compared to qubits. Therefore, the typical qubit gates can not be applied 45 | to these circuits. Instead, the fermions and spin modules define their own gate sets which are defined 46 | by their second-quantized Hamiltonians that generate the unitary gate. Note that loading the 47 | :mod:`qiskit_cold_atom.fermions` or :mod:`qiskit_cold_atom.spins` module will decorate the 48 | :class:`QuantumCircuit` class in Qiskit by adding methods to call pre-defined fermionic and spin gates, 49 | respectively. 50 | 51 | To enable the control of real quantum hardware, the :mod:`qiskit_cold_atom.providers` 52 | module contains a provider which enables access to cold atomic device backends. 53 | 54 | The top-level classes and submodules of qiskit_cold_atom are: 55 | 56 | .. autosummary:: 57 | :toctree: ../stubs/ 58 | :nosignatures: 59 | 60 | QiskitColdAtomError 61 | 62 | Submodules 63 | ========== 64 | 65 | .. autosummary:: 66 | :toctree: 67 | 68 | applications 69 | fermions 70 | providers 71 | spins 72 | 73 | """ 74 | from functools import wraps 75 | from qiskit import QuantumCircuit 76 | from qiskit_cold_atom.exceptions import QiskitColdAtomError 77 | 78 | 79 | def add_gate(func): 80 | """Decorator to add a gate method to the QuantumCircuit class""" 81 | 82 | @wraps(func) 83 | def wrapper(*args, **kwargs): 84 | return func(*args, **kwargs) 85 | 86 | setattr(QuantumCircuit, func.__name__, wrapper) 87 | 88 | return func 89 | 90 | 91 | __version__ = "0.1.0" 92 | 93 | __all__ = ["__version__", "QiskitColdAtomError", "add_gate"] 94 | -------------------------------------------------------------------------------- /qiskit_cold_atom/spins/__init__.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2021. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """ 14 | Module to support spin circuits. 15 | 16 | The spins module holds the circuit instructions and simulators needed for cold-atom setups which 17 | control the collective spin of an ensemble of atoms in a Bose-Einstein-condensate. 18 | 19 | In this setting, each wire in a quantum circuit describes a single quantum mechanical angular momentum 20 | (or spin) with principal quantum number :math:`S`. Upon measurement, each wire can be found in a state 21 | ranging from :math:`0` to :math:`2S`. 22 | Backends to describe such spin circuits are subclasses of the :class:`BaseSpinBackend` class. 23 | The :class:`SpinSimulator` backend is a general purpose simulator backend that simulates spin 24 | circuits. 25 | 26 | Spin backends 27 | ------------------- 28 | .. autosummary:: 29 | :toctree: ../stubs/ 30 | 31 | BaseSpinBackend 32 | SpinSimulator 33 | 34 | At the start of the circuit each spin is taken to be initialized in the 0 state, in analogy to qubits. 35 | 36 | 37 | Spin gates 38 | ========== 39 | 40 | Spin gates are quantum circuit instructions designed specifically for cold-atom based setups that 41 | control large spin ensembles. These gates are characterized by their effect on the spin ensemble. 42 | Spin gates are subclasses or instances of the :class:`SpinGate` class. All of these gates define a 43 | ``generator`` property used to compute the time-evolution. These generators are second quantized 44 | operators (:class:`SpinOp` from Qiskit Nature) that describe the gate Hamiltonian acting on the spins. 45 | When an entry of the :mod:`qiskit_cold_atom.spins` module is imported, the Spin gates are added to the 46 | :class:`QuantumCircuit` class in Qiskit. 47 | 48 | The module includes a number of gates suitable to a platform that implements rotations and squeezing of 49 | collective spins. 50 | 51 | .. autosummary:: 52 | :toctree: ../stubs/ 53 | 54 | SpinGate 55 | RLXGate 56 | RLYGate 57 | RLZGate 58 | RLZ2Gate 59 | OATGate 60 | RLZLZGate 61 | RLXLYGate 62 | 63 | These gates should serve as an example of how a concrete collective spin platform can be described 64 | through :mod:`qiskit_cold_atom`. 65 | Users are encouraged to define their own gates to describe different collective spin experiments. 66 | If these gates define a ``generator`` property as laid out above, the :class:SpinSimulator 67 | can be used to simulate circuits with such custom gates. 68 | 69 | 70 | Circuit solvers 71 | =============== 72 | Circuit solvers are classes that allow users to simulate a quantum circuit 73 | for cold-atom based setups. They are subclasses of 74 | :class:`BaseCircuitSolver` and can be called on quantum circuits to solve them. 75 | The numerical simulation of spin circuits is carried out by the :class:`SpinCircuitSolver` class. 76 | This simulates the circuits via exact diagonalization and provides access to the unitary, 77 | the statevector and simulated measurement outcomes of the circuit. 78 | 79 | .. autosummary:: 80 | :toctree: ../stubs/ 81 | 82 | SpinCircuitSolver 83 | """ 84 | 85 | from .spin_simulator_backend import SpinSimulator 86 | from .base_spin_backend import BaseSpinBackend 87 | from .spin_circuit_solver import SpinCircuitSolver 88 | 89 | # Gate imports 90 | from .spins_gate_library import ( 91 | SpinGate, 92 | RLXGate, 93 | RLYGate, 94 | RLZGate, 95 | RLZ2Gate, 96 | OATGate, 97 | RLZLZGate, 98 | RLXLYGate, 99 | ) 100 | -------------------------------------------------------------------------------- /tools/verify_headers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # This code is part of Qiskit. 3 | # 4 | # (C) Copyright IBM 2021 5 | # 6 | # This code is licensed under the Apache License, Version 2.0. You may 7 | # obtain a copy of this license in the LICENSE.txt file in the root directory 8 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 9 | # 10 | # Any modifications or derivative works of this code must retain this 11 | # copyright notice, and modified files need to carry a notice indicating 12 | # that they have been altered from the originals. 13 | 14 | """ 15 | Verify that headers of Python files comply with the rules 16 | """ 17 | 18 | import argparse 19 | import multiprocessing 20 | import os 21 | import sys 22 | import re 23 | 24 | # regex for character encoding from PEP 263 25 | pep263 = re.compile(r"^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)") 26 | 27 | 28 | def discover_files(code_paths): 29 | """ 30 | Find all Python files in the given paths 31 | """ 32 | out_paths = [] 33 | for path in code_paths: 34 | if os.path.isfile(path): 35 | out_paths.append(path) 36 | else: 37 | for directory in os.walk(path): 38 | dir_path = directory[0] 39 | for subfile in directory[2]: 40 | if subfile.endswith(".py") or subfile.endswith(".pyx"): 41 | out_paths.append(os.path.join(dir_path, subfile)) 42 | return out_paths 43 | 44 | 45 | def validate_header(file_path): 46 | """ 47 | Check if the file header complies with the rules 48 | """ 49 | header = """# This code is part of Qiskit. 50 | # 51 | """ 52 | apache_text = """# 53 | # This code is licensed under the Apache License, Version 2.0. You may 54 | # obtain a copy of this license in the LICENSE.txt file in the root directory 55 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 56 | # 57 | # Any modifications or derivative works of this code must retain this 58 | # copyright notice, and modified files need to carry a notice indicating 59 | # that they have been altered from the originals. 60 | """ 61 | count = 0 62 | with open(file_path, encoding="utf8") as fd: 63 | lines = fd.readlines() 64 | start = 0 65 | for index, line in enumerate(lines): 66 | count += 1 67 | if count > 5: 68 | return file_path, False, "Header not found in first 5 lines" 69 | if count <= 2 and pep263.match(line): 70 | return file_path, False, "Unnecessary encoding specification (PEP 263, 3120)" 71 | if line == "# This code is part of Qiskit.\n": 72 | start = index 73 | break 74 | if "".join(lines[start : start + 2]) != header: 75 | return (file_path, False, "Header up to copyright line does not match: %s" % header) 76 | if not lines[start + 2].startswith("# (C) Copyright IBM 20"): 77 | return (file_path, False, "Header copyright line not found") 78 | if "".join(lines[start + 3 : start + 11]) != apache_text: 79 | return (file_path, False, "Header apache text string doesn't match:\n %s" % apache_text) 80 | return (file_path, True, None) 81 | 82 | 83 | def main(): 84 | """ 85 | Run the verifier 86 | """ 87 | root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 88 | default_paths = [os.path.join(root_dir, "qiskit_cold_atom"), os.path.join(root_dir, "test")] 89 | parser = argparse.ArgumentParser(description="Check file headers.") 90 | parser.add_argument("paths", type=str, nargs="*", default=default_paths) 91 | args = parser.parse_args() 92 | files = discover_files(args.paths) 93 | pool = multiprocessing.Pool() 94 | res = pool.map(validate_header, files) 95 | failed_files = [x for x in res if x[1] is False] 96 | if len(failed_files) > 0: 97 | for failed_file in failed_files: 98 | sys.stderr.write("%s failed header check because:\n" % failed_file[0]) 99 | sys.stderr.write("%s\n\n" % failed_file[2]) 100 | sys.exit(1) 101 | sys.exit(0) 102 | 103 | 104 | if __name__ == "__main__": 105 | main() 106 | -------------------------------------------------------------------------------- /test/test_cold_atom_provider.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2021. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """Cold atom provider tests""" 14 | 15 | import os 16 | import uuid 17 | 18 | from qiskit.test import QiskitTestCase 19 | from qiskit_cold_atom.providers import ColdAtomProvider 20 | 21 | 22 | class TestHeidelbergProvider(QiskitTestCase): 23 | """HeidelbergColdAtomProvider tests.""" 24 | 25 | def setUp(self): 26 | super().setUp() 27 | 28 | # create directory for credentials if it doesn't already exist 29 | self._uuid = str(uuid.uuid4()) 30 | 31 | self.path = os.path.join(os.path.expanduser("~"), ".qiskit" + self._uuid) 32 | self.path_exists = os.path.isdir(self.path) 33 | if not self.path_exists: 34 | os.mkdir(self.path) 35 | 36 | # Store some credentials 37 | self.username = "test_user" 38 | self.token = "test_token" 39 | self.url = "http://localhost:9000/shots" 40 | 41 | self.filename = os.path.join(self.path, "cold_atom_provider_test" + self._uuid) 42 | 43 | def test_credential_management(self): 44 | """Test the management of locally stored credential data""" 45 | with self.subTest("test save account"): 46 | ColdAtomProvider.save_account( 47 | url=self.url, 48 | username=self.username, 49 | token=self.token, 50 | filename=self.filename, 51 | ) 52 | stored_credentials = ColdAtomProvider.stored_account(self.filename) 53 | target = { 54 | "urls": [self.url], 55 | "username": self.username, 56 | "token": self.token, 57 | } 58 | self.assertTrue(stored_credentials == target) 59 | 60 | with self.subTest("test overwrite warning"): 61 | with self.assertWarns(UserWarning): 62 | ColdAtomProvider.save_account( 63 | url=self.url, 64 | username=self.username, 65 | token=self.token, 66 | filename=self.filename, 67 | ) 68 | 69 | with self.subTest("test add url"): 70 | ColdAtomProvider.add_url("second_url", filename=self.filename) 71 | stored_credentials = ColdAtomProvider.stored_account(self.filename) 72 | target = { 73 | "urls": [self.url, "second_url"], 74 | "username": self.username, 75 | "token": self.token, 76 | } 77 | self.assertTrue(stored_credentials == target) 78 | 79 | with self.subTest("test delete account"): 80 | with self.assertWarns(UserWarning): 81 | ColdAtomProvider.delete_account( 82 | filename=os.path.join(os.path.expanduser("~"), ".qiskit", "wrong_filename") 83 | ) 84 | ColdAtomProvider.delete_account(filename=self.filename) 85 | stored_credentials = ColdAtomProvider.stored_account(self.filename) 86 | self.assertTrue(stored_credentials == {}) 87 | 88 | def test_provider_initialization(self): 89 | """Test the initialization of a cold atom provider from invalid credentials""" 90 | ColdAtomProvider.save_account( 91 | url=self.url, 92 | username=self.username, 93 | token=self.token, 94 | filename=self.filename, 95 | ) 96 | with self.subTest("test load account"): 97 | # test that a warning is raised when initializing with invalid credentials 98 | with self.assertWarns(UserWarning): 99 | provider = ColdAtomProvider.load_account(filename=self.filename) 100 | target = { 101 | "urls": [self.url], 102 | "username": self.username, 103 | "token": self.token, 104 | } 105 | self.assertTrue(provider.active_account() == target) 106 | 107 | def tearDown(self): 108 | """Clean-up after the test.""" 109 | super().tearDown() 110 | 111 | if os.path.exists(self.filename): 112 | os.remove(self.filename) 113 | 114 | if not self.path_exists: 115 | os.rmdir(self.path) 116 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | branches: [ main, 'stable/*' ] 5 | pull_request: 6 | branches: [ main, 'stable/*' ] 7 | concurrency: 8 | group: ${{ github.repository }}-${{ github.ref }}-${{ github.head_ref }} 9 | cancel-in-progress: true 10 | jobs: 11 | tests: 12 | name: tests-python${{ matrix.python-version }}-${{ matrix.os }} 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | python-version: [3.8, 3.11] 17 | os: ["ubuntu-latest", "macOS-latest", "windows-latest"] 18 | steps: 19 | - name: Print Concurrency Group 20 | env: 21 | CONCURRENCY_GROUP: ${{ github.repository }}-${{ github.ref }}-${{ github.head_ref }} 22 | run: | 23 | echo -e "\033[31;1;4mConcurrency Group\033[0m" 24 | echo -e "$CONCURRENCY_GROUP\n" 25 | shell: bash 26 | - uses: actions/checkout@v2 27 | - name: Set up Python ${{ matrix.python-version }} 28 | uses: actions/setup-python@v2 29 | with: 30 | python-version: ${{ matrix.python-version }} 31 | - name: Pip cache 32 | uses: actions/cache@v2 33 | with: 34 | path: ~/.cache/pip 35 | key: ${{ runner.os }}-${{ matrix.python-version }}-pip-tests-${{ hashFiles('setup.py','requirements-dev.txt','constraints.txt') }} 36 | restore-keys: | 37 | ${{ runner.os }}-${{ matrix.python-version }}-pip-tests- 38 | ${{ runner.os }}-${{ matrix.python-version }}-pip- 39 | ${{ runner.os }}-${{ matrix.python-version }} 40 | - name: Install Deps 41 | run: python -m pip install -U tox setuptools virtualenv wheel 42 | - name: Install and Run Tests 43 | run: tox -e py 44 | 45 | lint: 46 | name: lint 47 | runs-on: ubuntu-latest 48 | steps: 49 | - uses: actions/checkout@v2 50 | - name: Set up Python 3.8 51 | uses: actions/setup-python@v2 52 | with: 53 | python-version: 3.8 54 | - name: Pip cache 55 | uses: actions/cache@v2 56 | with: 57 | path: ~/.cache/pip 58 | key: ${{ runner.os }}-${{ matrix.python-version }}-pip-lint-${{ hashFiles('setup.py','requirements-dev.txt','constraints.txt') }} 59 | restore-keys: | 60 | ${{ runner.os }}-${{ matrix.python-version }}-pip-lint- 61 | ${{ runner.os }}-${{ matrix.python-version }}-pip- 62 | ${{ runner.os }}-${{ matrix.python-version }}- 63 | - name: Install Deps 64 | run: python -m pip install -U tox 65 | - name: Run lint 66 | run: tox -elint 67 | docs: 68 | name: docs 69 | runs-on: ubuntu-latest 70 | steps: 71 | - uses: actions/checkout@v2 72 | with: 73 | fetch-depth: 0 74 | - name: Set up Python 3.8 75 | uses: actions/setup-python@v2 76 | with: 77 | python-version: 3.8 78 | - name: Pip cache 79 | uses: actions/cache@v2 80 | with: 81 | path: ~/.cache/pip 82 | key: ${{ runner.os }}-${{ matrix.python-version }}-pip-docs-${{ hashFiles('setup.py','requirements-dev.txt','constraints.txt') }} 83 | restore-keys: | 84 | ${{ runner.os }}-pip-docs- 85 | ${{ runner.os }}-pip- 86 | ${{ runner.os }}- 87 | - name: Install Deps 88 | run: | 89 | python -m pip install -U tox 90 | sudo apt-get install -y pandoc graphviz 91 | - name: Build Docs 92 | run: tox -edocs 93 | - uses: actions/upload-artifact@v2 94 | with: 95 | name: html_docs 96 | path: docs/_build/html 97 | ffsim: 98 | name: tests-ffsim-python${{ matrix.python-version }}-${{ matrix.os }} 99 | runs-on: ${{ matrix.os }} 100 | strategy: 101 | matrix: 102 | python-version: [3.8, 3.11] 103 | os: ["ubuntu-latest", "macOS-latest"] 104 | steps: 105 | - uses: actions/checkout@v2 106 | - name: Set up Python ${{ matrix.python-version }} 107 | uses: actions/setup-python@v2 108 | with: 109 | python-version: ${{ matrix.python-version }} 110 | - name: Pip cache 111 | uses: actions/cache@v2 112 | with: 113 | path: ~/.cache/pip 114 | key: ${{ runner.os }}-${{ matrix.python-version }}-pip-tests-${{ hashFiles('setup.py','requirements-dev.txt','constraints.txt') }} 115 | restore-keys: | 116 | ${{ runner.os }}-${{ matrix.python-version }}-pip-tests- 117 | ${{ runner.os }}-${{ matrix.python-version }}-pip- 118 | ${{ runner.os }}-${{ matrix.python-version }} 119 | - name: Install Deps 120 | run: python -m pip install -U tox setuptools virtualenv wheel 121 | - name: Run ffsim tests 122 | run: tox -e ffsim -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Qiskit Cold Atom 2 | 3 | Qiskit cold atom logo 4 | 5 | **Qiskit** is an open-source SDK for working with quantum computers at the level of circuits, algorithms, and application modules. 6 | 7 | This project builds on this functionality to describe programmable quantum simulators of trapped cold atoms 8 | in a gate- and circuit-based framework. This includes a provider that allows access to cold atomic 9 | quantum devices located at Heidelberg University. 10 | 11 | Traditionally, each wire in a quantum circuit represents one qubit as the fundamental unit of information processing. 12 | Here, we extend this concept and allow wires to represent individual internal states of trapped cold atoms. 13 | This currently covers two settings, one for fermionic modes and one for spin modes, 14 | demonstrating that a broad range of hardware can be accommodated in Qiskit. 15 | 16 | We encourage new users to familiarize themselves with the basic functionalities through the tutorial-style python notebooks in [`docs/tutorials`](docs/tutorials). 17 | These require an environment to execute `.ipynb` notebooks such as jupyter lab. 18 | 19 | ## Installation 20 | 21 | To install Qiskit Cold Atom from source and further develop the package, clone this repository and install from the project root using pip: 22 | ```bash 23 | pip install -e . 24 | ``` 25 | To use the repository you can also install using either 26 | ```bash 27 | pip install git+https://github.com/qiskit-community/qiskit-cold-atom.git 28 | ``` 29 | or Pypi 30 | ```bash 31 | pip install qiskit-cold-atom 32 | ``` 33 | To install Qiskit Cold Atom with the [ffsim fermion simulator backend](#cold-atomic-circuits) (not supported on Windows), specify the `ffsim` extra in the `pip` install command, e.g. 34 | ```bash 35 | pip install "qiskit-cold-atom[ffsim]" 36 | ``` 37 | 38 | ## Setting up the Cold Atom Provider 39 | Qiskit Cold Atom includes local simulator backends to explore the cold atomic hardware. In order to access 40 | remote device backends, you will need valid user credentials. 41 | To this end, the user has to specify the `url` of the desired backend and a valid `username` and `token` as a password, 42 | which can be saved as an account: 43 | 44 | ```python 45 | from qiskit_cold_atom.providers import ColdAtomProvider 46 | 47 | # save an account to disk 48 | ColdAtomProvider.save_account(urls = ["url_backend_1", "url_backend_2"], username="my_name", token="my_password") 49 | ``` 50 | 51 | Loading the account instantiates the provider from which the included backends can be accessed. 52 | 53 | ```python 54 | # load the stored account 55 | provider = ColdAtomProvider.load_account() 56 | 57 | # get available backends 58 | print(provider.backends()) 59 | 60 | # Example: Get a simulator backend 61 | spin_simulator_backend = provider.get_backend("collective_spin_simulator") 62 | ``` 63 | 64 | ## Cold atomic circuits 65 | 66 | The circuits that can be run on the cold atomic hardware explored in this project use different gates 67 | from the circuits typically employed in Qiskit, because these hardware are not built from qubits, 68 | but from fermions or spins. 69 | Qiskit Cold Atom includes basic simulators for both the fermion and spin settings that can be used 70 | to simulate small circuits. See [Introduction & Fermionic Circuits](docs/tutorials/01_introduction_and_fermionic_circuits.ipynb) 71 | and [Spin circuits](docs/tutorials/02_spin_circuits.ipynb) for tutorials on how to define and run gates through 72 | quantum circuits in these settings. 73 | 74 | Qiskit Cold Atom also includes a high-performance simulator for fermionic circuits based on 75 | [ffsim](https://github.com/qiskit-community/ffsim), which can handle much larger circuits than the basic simulator mentioned before. The ffsim simulator is not supported on Windows, and in order 76 | for it to be available, Qiskit Cold Atom must be installed with the `ffsim` extra, e.g. 77 | ```bash 78 | pip install "qiskit-cold-atom[ffsim]" 79 | ``` 80 | 81 | ## Documentation 82 | 83 | The documentation can be found as Github pages here [https://qiskit-community.github.io/qiskit-cold-atom/](https://qiskit-community.github.io/qiskit-cold-atom/). 84 | To build the API reference locally, run: 85 | 86 | ```bash 87 | pip install -r requirements-dev.txt 88 | make -C docs html 89 | open docs/_build/html/index.html 90 | ``` 91 | 92 | ## Tests 93 | Test are located in the `test` folder. All contributions should come with test files that are named `test_*.py` which test the new functionalities. 94 | To execute the test suite locally, run 95 | ```bash 96 | python -m unittest 97 | ``` 98 | from the project root. 99 | 100 | ## License 101 | 102 | [Apache License 2.0]. 103 | 104 | [Apache License 2.0]: https://github.com/qiskit-community/qiskit-cold-atom/blob/master/LICENSE.txt 105 | -------------------------------------------------------------------------------- /releasenotes/config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | encoding: utf8 3 | default_branch: main 4 | sections: 5 | - [features, New Features] 6 | - [issues, Known Issues] 7 | - [upgrade, Upgrade Notes] 8 | - [deprecations, Deprecation Notes] 9 | - [critical, Critical Issues] 10 | - [security, Security Issues] 11 | - [fixes, Bug Fixes] 12 | - [developer, API Changes for Experiment Authors] 13 | - [other, Other Notes] 14 | template: | 15 | --- 16 | prelude: > 17 | Replace this text with content to appear at the top of the section for this 18 | release. All of the prelude content is merged together and then rendered 19 | separately from the items listed in other parts of the file, so the text 20 | needs to be worded so that both the prelude and the other items make sense 21 | when read independently. This may mean repeating some details. Not every 22 | release note requires a prelude. Usually only notes describing major 23 | features or adding release theme details should have a prelude. 24 | features: 25 | - | 26 | List new features here, or remove this section. All of the list items in 27 | this section are combined when the release notes are rendered, so the text 28 | needs to be worded so that it does not depend on any information only 29 | available in another section, such as the prelude. This may mean repeating 30 | some details. 31 | issues: 32 | - | 33 | List known issues here, or remove this section. All of the list items in 34 | this section are combined when the release notes are rendered, so the text 35 | needs to be worded so that it does not depend on any information only 36 | available in another section, such as the prelude. This may mean repeating 37 | some details. 38 | upgrade: 39 | - | 40 | List upgrade notes here, or remove this section. All of the list items in 41 | this section are combined when the release notes are rendered, so the text 42 | needs to be worded so that it does not depend on any information only 43 | available in another section, such as the prelude. This may mean repeating 44 | some details. 45 | deprecations: 46 | - | 47 | List deprecations notes here, or remove this section. All of the list 48 | items in this section are combined when the release notes are rendered, so 49 | the text needs to be worded so that it does not depend on any information 50 | only available in another section, such as the prelude. This may mean 51 | repeating some details. 52 | critical: 53 | - | 54 | Add critical notes here, or remove this section. All of the list items in 55 | this section are combined when the release notes are rendered, so the text 56 | needs to be worded so that it does not depend on any information only 57 | available in another section, such as the prelude. This may mean repeating 58 | some details. 59 | security: 60 | - | 61 | Add security notes here, or remove this section. All of the list items in 62 | this section are combined when the release notes are rendered, so the text 63 | needs to be worded so that it does not depend on any information only 64 | available in another section, such as the prelude. This may mean repeating 65 | some details. 66 | fixes: 67 | - | 68 | Add normal bug fixes here, or remove this section. All of the list items 69 | in this section are combined when the release notes are rendered, so the 70 | text needs to be worded so that it does not depend on any information only 71 | available in another section, such as the prelude. This may mean repeating 72 | some details. 73 | developer: 74 | - | 75 | Add upgrade of protected-level API (e.g. hooks). All of the list items 76 | in this section are combined when the release notes are rendered, so the 77 | text needs to be worded so that it does not depend on any information only 78 | available in another section, such as the prelude. This may mean repeating 79 | some details. 80 | other: 81 | - | 82 | Add other notes here, or remove this section. All of the list items in 83 | this section are combined when the release notes are rendered, so the text 84 | needs to be worded so that it does not depend on any information only 85 | available in another section, such as the prelude. This may mean repeating 86 | some details. 87 | earliest_version: 0.0.1 88 | -------------------------------------------------------------------------------- /qiskit_cold_atom/fermions/__init__.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2021. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """ 14 | Module to support fermionic circuits. 15 | 16 | The fermions module holds the circuit instructions and simulators needed 17 | by cold-atom setups that trap fermionic atoms in optical tweezer arrays or lattices. 18 | 19 | In this setting, each wire in a quantum circuit describes a fermionic mode. Upon measurement, each mode 20 | can be found to be occupied by a single particle (1) or be empty (0). 21 | Backends to describe such fermionic circuits are subclasses of the :class:`BaseFermionBackend` class. 22 | The :class:`FermionSimulator` backend is a general purpose simulator backend that simulates fermionic 23 | circuits similar to the QasmSimulator for qubits. 24 | 25 | For higher-performance simulation, you can use :class:`FfsimBackend`, 26 | which is much more efficient than :class:`FermionSimulator` and can handle larger circuits. 27 | :class:`FfsimBackend` is not supported on Windows, and a special ``pip`` command is 28 | needed to install it: 29 | 30 | .. code:: 31 | 32 | pip install "qiskit-cold-atom[ffsim]" 33 | 34 | Fermionic backends 35 | ------------------- 36 | .. autosummary:: 37 | :toctree: ../stubs/ 38 | 39 | BaseFermionBackend 40 | FermionSimulator 41 | FfsimBackend 42 | 43 | The fermions might also come in several distinguishable species, as is the case when they carry a spin 44 | degree of freedom. In this case, each spatial mode of an experiment can be occupied by a particle 45 | of each spin state. In the circuit description, each individual mode is assigned its own wire. 46 | For example, a system of spin-1/2 fermions in four spatial modes is described by a circuit with eight 47 | wires where the first four wires denote the spin-up and the last four wires denote the spin-down modes. 48 | 49 | Prior to applying gates, the fermionic modes in a quantum circuit need to be initialized with particles, 50 | which defines the total number of particles (excitations) in the circuit. This initial occupation number 51 | state can then be manipulated by applying Fermionic gates. 52 | 53 | Fermionic gates 54 | ---------------- 55 | Fermionic gates are quantum circuit instructions designed specifically for 56 | cold-atom based setups that control fermionic atoms in tweezers. These gates are 57 | characterized by their effect on the fermions. Fermionic gates are subclasses or instances of the 58 | :class:`FermionicGate` class. All of these gates define a ``generator`` property used to compute 59 | the time-evolution. These generators are second quantized operators (:class:`FermionicOp` from Qiskit 60 | Nature) that describe the gate Hamiltonian acting on the register. 61 | When an entry of the :mod:`qiskit_cold_atom.fermions` module is imported, the Fermionic Gates are added 62 | to the :class:`QuantumCircuit` class in Qiskit. 63 | 64 | The module includes a number of gates suitable to a platform that natively implements Fermi-Hubbard type 65 | dynamics. 66 | 67 | .. autosummary:: 68 | :toctree: ../stubs/ 69 | 70 | FermionicGate 71 | FermiHubbard 72 | Hop 73 | Interaction 74 | Phase 75 | FRXGate 76 | FRYGate 77 | FRZGate 78 | LoadFermions 79 | 80 | These gates should serve as an example of how a concrete fermionic platform can be described 81 | through :mod:`qiskit_cold_atom`. 82 | Users are encouraged to define their own gates to describe different fermionic hardware. 83 | If these gates define a ``generator`` property as laid out above, the :class:`FermionSimulator` 84 | can be used to simulate circuits with such custom gates. 85 | 86 | Circuit Solver 87 | ---------------- 88 | Circuit solvers are classes that allow users to simulate a quantum circuit 89 | for cold-atom based setups. They are subclasses of 90 | :class:`BaseCircuitSolver` and can be called on quantum circuits to solve them. 91 | The numerical simulation of fermionic circuits is carried out by the :class:`FermionCircuitSolver` class. 92 | This simulates the circuits via exact diagonalization and provides access to the unitary, 93 | the statevector and simulated measurement outcomes of the circuit. 94 | 95 | .. autosummary:: 96 | :toctree: ../stubs/ 97 | 98 | FermionCircuitSolver 99 | 100 | """ 101 | 102 | from qiskit_cold_atom.fermions.base_fermion_backend import BaseFermionBackend 103 | from qiskit_cold_atom.fermions.fermion_circuit_solver import FermionCircuitSolver 104 | from qiskit_cold_atom.fermions.fermion_gate_library import ( 105 | FermiHubbard, 106 | FermionicGate, 107 | FRXGate, 108 | FRYGate, 109 | FRZGate, 110 | Hop, 111 | Interaction, 112 | LoadFermions, 113 | Phase, 114 | ) 115 | from qiskit_cold_atom.fermions.fermion_simulator_backend import FermionSimulator 116 | 117 | try: 118 | from qiskit_cold_atom.fermions.ffsim_backend import FfsimBackend 119 | except ImportError: 120 | pass 121 | -------------------------------------------------------------------------------- /qiskit_cold_atom/providers/collective_spin_backend.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2021. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """Backend to emulate collective spin experiments.""" 14 | 15 | from typing import Optional 16 | 17 | from qiskit.providers import ProviderV1 as Provider 18 | 19 | from qiskit_cold_atom.spins.spin_simulator_backend import SpinSimulator 20 | 21 | 22 | class CollectiveSpinSimulator(SpinSimulator): 23 | """Emulator backend of a spin device with collective spins of trapped BECs in optical tweezers. 24 | 25 | This backend will define a configuration with a set of supported gates and a line coupling map. 26 | At instantiation users can chose the length of the coupling map which by default is three. This 27 | backend is intended to be a realistic representation of a collective spin experiment with BECs. 28 | For a general spin simulator backend, use the SpinSimulator base class. 29 | """ 30 | 31 | def __init__(self, n_tweezers: int = 3, provider: Optional[Provider] = None): 32 | """Create a new collective spin simulator backend. 33 | 34 | Args: 35 | n_tweezers: The number of optical tweezers. 36 | provider: The provider to which the backend may be added. 37 | """ 38 | 39 | single_spin_coupl = [[i] for i in range(n_tweezers)] 40 | two_spin_coupl = [[i, i + 1] for i in range(n_tweezers - 1)] + [ 41 | [i + 1, i] for i in range(n_tweezers - 1) 42 | ] 43 | 44 | configuration = { 45 | "backend_name": "collective_spin_simulator", 46 | "cold_atom_type": "spin", 47 | "backend_version": "0.0.1", 48 | "atomic_species": ["na"], 49 | "simulator": True, 50 | "local": True, 51 | "coupling_map": None, 52 | "description": "simulator of a collective hardware. Each wire in the circuit denotes the " 53 | "orientation of one collective spin in the hardware", 54 | "basis_gates": ["rlx", "rly", "rlz", "rlz2", "OAT", "rlzlz", "rlxly"], 55 | "memory": True, 56 | "n_qubits": 3, 57 | "conditional": False, 58 | "max_shots": 1e6, 59 | "max_experiments": 10, 60 | "open_pulse": False, 61 | "gates": [ 62 | { 63 | "coupling_map": single_spin_coupl, 64 | "description": "local rotation of the coherent spin around x", 65 | "name": "rlx", 66 | "parameters": ["omega"], 67 | "qasm_def": "gate rlx(omega) {}", 68 | }, 69 | { 70 | "coupling_map": single_spin_coupl, 71 | "description": "local rotation of the coherent spin around y", 72 | "name": "rly", 73 | "parameters": ["delta"], 74 | "qasm_def": "gate rly(delta) {}", 75 | }, 76 | { 77 | "coupling_map": single_spin_coupl, 78 | "description": "local rotation of the coherent spin around z", 79 | "name": "rlz", 80 | "parameters": ["delta"], 81 | "qasm_def": "gate rlz(delta) {}", 82 | }, 83 | { 84 | "coupling_map": single_spin_coupl, 85 | "description": "local spin evolution generated by one-axis " 86 | "twisting Hamiltonian Lz^2", 87 | "name": "rlz2", 88 | "parameters": ["chi"], 89 | "qasm_def": "gate rlz2(chi) {}", 90 | }, 91 | { 92 | "coupling_map": single_spin_coupl, 93 | "description": "local spin evolution generated by (chi*Lz^2 + " 94 | "delta*Lz + omega*Lx)", 95 | "name": "oat", 96 | "parameters": ["chi", "delta", "omega"], 97 | "qasm_def": "gate rot(chi, delta, omega) {}", 98 | }, 99 | { 100 | "coupling_map": two_spin_coupl, 101 | "description": "ZZ-interaction coupling two collective spins", 102 | "name": "rlzlz", 103 | "parameters": ["gamma"], 104 | "qasm_def": "gate rlzlz(gamma) {}", 105 | }, 106 | { 107 | "coupling_map": two_spin_coupl, 108 | "description": "LXLY-interaction coupling two collective spins", 109 | "name": "rlxly", 110 | "parameters": ["gamma"], 111 | "qasm_def": "gate rlxly(gamma) {}", 112 | }, 113 | ], 114 | "supported_instructions": [ 115 | "rlz", 116 | "rlx", 117 | "rly", 118 | "rlz2", 119 | "OAT", 120 | "rlzlz", 121 | "rlxly", 122 | "measure", 123 | "barrier", 124 | ], 125 | } 126 | 127 | super().__init__(config_dict=configuration, provider=provider) 128 | -------------------------------------------------------------------------------- /qiskit_cold_atom/providers/cold_atom_job.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2021. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """Job for cold atom backends.""" 14 | 15 | from typing import Dict, Optional 16 | import time 17 | import json 18 | import requests 19 | 20 | from qiskit.providers import BackendV1 as Backend 21 | from qiskit.providers import JobV1 as Job 22 | from qiskit.providers import JobTimeoutError, JobError 23 | from qiskit.providers import JobStatus 24 | from qiskit.result import Result 25 | 26 | 27 | class ColdAtomJob(Job): 28 | """Class of jobs returned by cold atom backends.""" 29 | 30 | def __init__(self, backend: Backend, job_id: str): 31 | """ 32 | Args: 33 | backend: The backend on which the job was run. 34 | job_id: The ID of the job. 35 | """ 36 | super().__init__(backend, job_id) 37 | 38 | self.token = self._backend.token 39 | self.user = self._backend.username 40 | 41 | def _wait_for_result(self, timeout: float = None, wait: float = 5.0) -> Dict: 42 | """ 43 | Query the backend to get the result. 44 | 45 | Args: 46 | timeout: time after which the server is no longer queried for the result 47 | wait: waiting time between queries for the result of the backend 48 | 49 | Returns: 50 | result dictionary formatted according to Qiskit schemas. 51 | 52 | Raises: 53 | JobTimeoutError: If the timeout is reached without receiving the result 54 | JobError: If the returned status reports an error 55 | """ 56 | start_time = time.time() 57 | 58 | while True: 59 | elapsed = time.time() - start_time 60 | if timeout and elapsed >= timeout: 61 | raise JobTimeoutError("Timed out waiting for result") 62 | 63 | result = requests.get( 64 | self._backend.url + "/get_job_result", 65 | params={ 66 | "job_id": self._job_id, 67 | "username": self.user, 68 | "token": self.token, 69 | }, 70 | ).json() 71 | 72 | if result["status"] == "finished": 73 | break 74 | if result["status"] == "error": 75 | raise JobError(f"{self._backend.name} returned error:\n" + str(result)) 76 | time.sleep(wait) 77 | 78 | return result 79 | 80 | # pylint: disable=arguments-differ 81 | def result(self, timeout: Optional[float] = None, wait: float = 5.0) -> Result: 82 | """ 83 | Retrieve a qiskit result object from the backend. 84 | 85 | Args: 86 | timeout: time after which the server is no longer queried for the result 87 | wait: waiting time between queries for the result of the backend 88 | 89 | Returns: 90 | qiskit.result object that contains the outcome of the job 91 | """ 92 | result_dict = self._wait_for_result(timeout, wait=wait) 93 | 94 | return Result.from_dict(result_dict) 95 | 96 | def status(self): 97 | """ 98 | Retrieve the status from the backend. 99 | 100 | Returns: 101 | status: A string describing the status of the job. 102 | """ 103 | 104 | r = requests.get( 105 | self._backend.url + "/get_job_status", 106 | params={ 107 | "job_id": self.job_id(), 108 | "username": self.user, 109 | "token": self.token, 110 | }, 111 | ) 112 | 113 | status_string = r.json()["status"] 114 | 115 | # If the backend can not be reached return ERROR as a status 116 | if r.status_code != 200: 117 | status = JobStatus.ERROR 118 | elif status_string == "INITIALIZING": 119 | status = JobStatus.INITIALIZING 120 | elif status_string == "QUEUED": 121 | status = JobStatus.QUEUED 122 | elif status_string == "VALIDATING": 123 | status = JobStatus.VALIDATING 124 | elif status_string == "RUNNING": 125 | status = JobStatus.RUNNING 126 | elif status_string == "CANCELLED": 127 | status = JobStatus.CANCELLED 128 | elif status_string == "DONE": 129 | status = JobStatus.DONE 130 | else: 131 | status = JobStatus.ERROR 132 | 133 | return status 134 | 135 | def error_message(self) -> Optional[str]: 136 | """ 137 | Retrieve the error message from the backend. 138 | 139 | Returns: 140 | error: A string describing the error that happened on the backend. 141 | """ 142 | status_payload = {"job_id": self.job_id()} 143 | 144 | r = requests.get( 145 | self._backend.url + "/get_job_status", 146 | params={ 147 | "json": json.dumps(status_payload), 148 | "username": self.user, 149 | "token": self.token, 150 | }, 151 | ) 152 | 153 | return r.json().get("error_message", None) 154 | 155 | def cancel(self): 156 | raise NotImplementedError 157 | 158 | def submit(self): 159 | raise NotImplementedError 160 | -------------------------------------------------------------------------------- /qiskit_cold_atom/applications/fermionic_evolution_problem.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2021. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """Class that holds a fermionic time-evolution problem.""" 14 | 15 | from typing import List, Union 16 | 17 | from qiskit import QuantumCircuit 18 | from qiskit_nature.second_q.operators import FermionicOp 19 | 20 | from qiskit_cold_atom.fermions.fermionic_state import FermionicState 21 | from qiskit_cold_atom.fermions.fermionic_basis import FermionicBasis 22 | from qiskit_cold_atom.fermions.fermion_gate_library import FermionicGate 23 | from qiskit_cold_atom.exceptions import QiskitColdAtomError 24 | from qiskit_cold_atom.applications.fermi_hubbard import FermionicLattice 25 | 26 | 27 | class FermionicEvolutionProblem: 28 | """ 29 | Problem class corresponding to evaluating an observable of a fermionic system after a time 30 | evolution under a hamiltonian from an initial state in an occupation number representation. 31 | """ 32 | 33 | def __init__( 34 | self, 35 | system: FermionicLattice, 36 | initial_state: FermionicState, 37 | evolution_times: Union[float, List[float]], 38 | observable: FermionicOp, 39 | ): 40 | """ 41 | Initialize a fermionic time evolution problem. 42 | 43 | Args: 44 | system: The fermionic system under which the initial state will evolve. 45 | initial_state: The fermionic state at time t=0. 46 | evolution_times: List of times (or single time) after which the observable is measured. 47 | observable: The observable to measure after the time evolution, given as a FermionicOp. 48 | The observable must be diagonal in the fermionic occupation number basis. 49 | 50 | Raises: 51 | QiskitColdAtomError: - If the sizes of the system, initial state and the observable 52 | do not match. 53 | - If the observables is not diagonal in the fermionic occupation number 54 | basis 55 | """ 56 | 57 | if system.size != initial_state.sites: 58 | raise QiskitColdAtomError( 59 | f"The size of the system {system.size} does not match " 60 | f"the size of the initial state {initial_state.sites}." 61 | ) 62 | 63 | if 2 * system.size != observable.register_length: 64 | raise QiskitColdAtomError( 65 | f"The fermionic modes of the system {2*system.size} do not match " 66 | f"the size of the observable {observable.register_length}." 67 | ) 68 | 69 | # check if matrix is diagonal 70 | # can later be replaced when the FermionicOp from qiskit-nature has its own .to_matrix() method 71 | basis = FermionicBasis.from_fermionic_op(observable) 72 | observable_mat = FermionicGate.operator_to_mat(observable, num_species=1, basis=basis) 73 | 74 | if list(observable_mat.nonzero()[0]) != list(observable_mat.nonzero()[1]): 75 | raise QiskitColdAtomError( 76 | "The fermionic observable needs to be diagonal in the computational basis, " 77 | "as measuring general, non-diagonal observables is not yet implemented for " 78 | "fermionic backends. This requires non-trivial basis transformations that " 79 | "are in general difficult to find and depend on the backend's native gate set." 80 | ) 81 | 82 | self._system = system 83 | self._initial_state = initial_state 84 | self._evolution_times = evolution_times 85 | self._observable = observable 86 | 87 | @property 88 | def system(self) -> FermionicLattice: 89 | """Return the system of the problem.""" 90 | return self._system 91 | 92 | @property 93 | def initial_state(self) -> FermionicState: 94 | """Return the initial state of the system.""" 95 | return self._initial_state 96 | 97 | @property 98 | def evolution_times(self) -> List[float]: 99 | """Return the evolution times to simulate.""" 100 | return self._evolution_times 101 | 102 | @property 103 | def observable(self) -> FermionicOp: 104 | """Return the observable as a FermionicOp.""" 105 | return self._observable 106 | 107 | def circuits(self, initial_state: QuantumCircuit) -> List[QuantumCircuit]: 108 | """ 109 | The problem embedded in a quantum circuit. 110 | 111 | Args: 112 | initial_state: A quantum circuit which corresponds to the initial state for the 113 | time-evolution problem. 114 | 115 | Return: 116 | A list of quantum circuits. Circuit :math:`i` is a single instruction which 117 | corresponds to :math:`exp(-i*H*t_i)` where :math:`t_i` is the time of the 118 | the ith evolution time. 119 | """ 120 | circuits = [] 121 | 122 | for time in self.evolution_times: 123 | circ = QuantumCircuit(initial_state.num_qubits) 124 | circ.compose(initial_state, inplace=True) 125 | circ.compose(self.system.to_circuit(time), inplace=True) 126 | circuits.append(circ) 127 | 128 | return circuits 129 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2021. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | # pylint: disable=invalid-name 14 | # Configuration file for the Sphinx documentation builder. 15 | # 16 | # This file does only contain a selection of the most common options. For a 17 | # full list see the documentation: 18 | # http://www.sphinx-doc.org/en/master/config 19 | 20 | # -- Path setup -------------------------------------------------------------- 21 | 22 | # If extensions (or modules to document with autodoc) are in another directory, 23 | # add these directories to sys.path here. If the directory is relative to the 24 | # documentation root, use os.path.abspath to make it absolute, like shown here. 25 | # 26 | import os 27 | import sys 28 | from datetime import date 29 | 30 | sys.path.insert(0, os.path.abspath("..")) 31 | sys.path.append(os.path.abspath(".")) 32 | 33 | """ 34 | Sphinx documentation builder 35 | """ 36 | 37 | import qiskit_sphinx_theme 38 | 39 | # Set env flag so that we can doc functions that may otherwise not be loaded 40 | # see for example interactive visualizations in qiskit.visualization. 41 | os.environ["QISKIT_DOCS"] = "TRUE" 42 | 43 | # -- Project information ----------------------------------------------------- 44 | project = "Qiskit Cold Atom" 45 | copyright = "2021 - 2023, Qiskit Cold Atom Development Team" # pylint: disable=redefined-builtin 46 | author = "Qiskit Cold Atom Development Team" 47 | 48 | # The short X.Y version 49 | version = "0.2" 50 | # The full version, including alpha/beta/rc tags 51 | release = "0.2.1" 52 | 53 | rst_prolog = """ 54 | .. raw:: html 55 | 56 |


57 | 58 | .. |version| replace:: {0} 59 | """.format( 60 | release 61 | ) 62 | 63 | nbsphinx_prolog = """ 64 | {% set docname = env.doc2path(env.docname, base=None) %} 65 | .. only:: html 66 | 67 | .. role:: raw-html(raw) 68 | :format: html 69 | 70 | .. raw:: html 71 | 72 |


73 | 74 | .. note:: 75 | Run interactively in jupyter notebook. 76 | """ 77 | 78 | # -- General configuration --------------------------------------------------- 79 | 80 | extensions = [ 81 | "sphinx.ext.napoleon", 82 | "sphinx.ext.autodoc", 83 | "sphinx.ext.autosummary", 84 | "sphinx.ext.mathjax", 85 | "sphinx.ext.viewcode", 86 | "sphinx.ext.extlinks", 87 | "sphinx_tabs.tabs", 88 | "jupyter_sphinx", 89 | "sphinx_autodoc_typehints", 90 | "reno.sphinxext", 91 | "sphinx.ext.doctest", 92 | "nbsphinx", 93 | "qiskit_sphinx_theme", 94 | ] 95 | templates_path = ["_templates"] 96 | 97 | nbsphinx_timeout = 360 98 | nbsphinx_execute = os.getenv("QISKIT_DOCS_BUILD_TUTORIALS", "never") 99 | nbsphinx_widgets_path = "" 100 | exclude_patterns = ["_build", "**.ipynb_checkpoints"] 101 | nbsphinx_thumbnails = {} 102 | 103 | # ----------------------------------------------------------------------------- 104 | # Autosummary 105 | # ----------------------------------------------------------------------------- 106 | 107 | autosummary_generate = True 108 | autosummary_generate_overwrite = False 109 | 110 | # ----------------------------------------------------------------------------- 111 | # Autodoc 112 | # ----------------------------------------------------------------------------- 113 | 114 | autodoc_default_options = { 115 | "inherited-members": None, 116 | } 117 | 118 | autoclass_content = "both" 119 | 120 | # If true, figures, tables and code-blocks are automatically numbered if they 121 | # have a caption. 122 | numfig = True 123 | 124 | # A dictionary mapping 'figure', 'table', 'code-block' and 'section' to 125 | # strings that are used for format of figure numbers. As a special character, 126 | # %s will be replaced to figure number. 127 | numfig_format = {"table": "Table %s"} 128 | # The language for content autogenerated by Sphinx. Refer to documentation 129 | # for a list of supported languages. 130 | # 131 | # This is also used if you do content translation via gettext catalogs. 132 | # Usually you set "language" from the command line for these cases. 133 | language = "en" 134 | 135 | # List of patterns, relative to source directory, that match files and 136 | # directories to ignore when looking for source files. 137 | # This pattern also affects html_static_path and html_extra_path. 138 | exclude_patterns = ["_build", "**.ipynb_checkpoints"] 139 | 140 | # The name of the Pygments (syntax highlighting) style to use. 141 | pygments_style = "colorful" 142 | 143 | # A boolean that decides whether module names are prepended to all object names 144 | # (for object types where a “module” of some kind is defined), e.g. for 145 | # py:function directives. 146 | add_module_names = False 147 | 148 | # A list of prefixes that are ignored for sorting the Python module index 149 | # (e.g., if this is set to ['foo.'], then foo.bar is shown under B, not F). 150 | # This can be handy if you document a project that consists of a single 151 | # package. Works only for the HTML builder currently. 152 | modindex_common_prefix = ["qiskit_cold_atom."] 153 | 154 | # -- Configuration for extlinks extension ------------------------------------ 155 | # Refer to https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html 156 | 157 | 158 | # -- Options for HTML output ------------------------------------------------- 159 | 160 | html_theme = "qiskit-ecosystem" 161 | html_title = f"{project} {release}" 162 | -------------------------------------------------------------------------------- /qiskit_cold_atom/providers/fermionic_tweezer_backend.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2021. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """Backend to emulate fermionic tweezer experiments.""" 14 | 15 | from qiskit_cold_atom.fermions.fermion_simulator_backend import FermionSimulator 16 | 17 | 18 | class FermionicTweezerSimulator(FermionSimulator): 19 | """Emulator backend of a fermionic device with n tweezer sites that uses two spin species. 20 | 21 | This backend will define a configuration with a set of supported gates and a line coupling map. 22 | At instantiation users can chose the length of the coupling map which by default is four sites. 23 | This backend is intended to be a realistic representation of a ferminoic tweezer experiment. 24 | For a general fermionic simulator backend, use the FermionSimulator base class. 25 | """ 26 | 27 | def __init__(self, n_tweezers: int = 4, provider=None): 28 | """Create a new fermionic tweezer simulator backend. 29 | 30 | Args: 31 | n_tweezers: The number of optical tweezers. 32 | provider: The provider to which the backend may be added. 33 | """ 34 | 35 | # define coupling maps for the gates 36 | sites = n_tweezers 37 | self._neighbouring_sites_couplings = [ 38 | list(range(i, i + size)) + list(range(i + sites, i + sites + size)) 39 | for size in range(2, sites + 1) 40 | for i in range(sites + 1 - size) 41 | ] 42 | self._global_site_couplings = [list(range(2 * sites))] 43 | self._single_site_couplings = [[i, i + sites] for i in range(sites)] 44 | 45 | configuration = { 46 | "backend_name": "fermionic_tweezer_simulator", 47 | "cold_atom_type": "fermion", 48 | "backend_version": "0.0.1", 49 | "simulator": True, 50 | "local": True, 51 | "coupling_map": None, 52 | "description": "Mock backend of a fermionic tweezer hardware. The first half of wires in a " 53 | "circuit denote the occupations of the spin-up fermions and the last half " 54 | "of wires denote the spin-down fermions", 55 | "basis_gates": [ 56 | "fhop", 57 | "fint", 58 | "fphase", 59 | "fhubbard", 60 | "frx", 61 | "fry", 62 | "frz", 63 | "load", 64 | ], 65 | "num_species": 2, 66 | "memory": True, 67 | "n_qubits": 2 * sites, 68 | "conditional": False, 69 | "max_shots": 1e6, 70 | "max_experiments": 10, 71 | "open_pulse": False, 72 | "gates": [ 73 | { 74 | "name": "fhop", 75 | "parameters": ["j_i"], 76 | "qasm_def": "{}", 77 | "description": "hopping of atoms to neighboring tweezers", 78 | "coupling_map": self._neighbouring_sites_couplings, 79 | }, 80 | { 81 | "name": "fint", 82 | "parameters": ["u"], 83 | "qasm_def": "{}", 84 | "description": "on-site interaction of atoms of opposite spin state", 85 | "coupling_map": self._global_site_couplings, 86 | }, 87 | { 88 | "name": "fphase", 89 | "parameters": ["mu_i"], 90 | "qasm_def": "{}", 91 | "description": "Applying a local phase to tweezers through an external potential", 92 | "coupling_map": self._single_site_couplings, 93 | }, 94 | { 95 | "name": "frx", 96 | "parameters": ["phi"], 97 | "qasm_def": "{}", 98 | "description": "x-rotation between the spin-up and spin-down state at one " 99 | "tweezer site", 100 | "coupling_map": self._single_site_couplings, 101 | }, 102 | { 103 | "name": "fry", 104 | "parameters": ["phi"], 105 | "qasm_def": "{}", 106 | "description": "y-rotation between the spin-up and spin-down state at one " 107 | "tweezer site", 108 | "coupling_map": self._single_site_couplings, 109 | }, 110 | { 111 | "name": "frz", 112 | "parameters": ["phi"], 113 | "qasm_def": "{}", 114 | "description": "z-rotation between the spin-up and spin-down state at one " 115 | "tweezer site", 116 | "coupling_map": self._single_site_couplings, 117 | }, 118 | ], 119 | "supported_instructions": [ 120 | "load", 121 | "measure", 122 | "barrier", 123 | "fhop", 124 | "fint", 125 | "fphase", 126 | "fhubbard", 127 | "frx", 128 | "fry", 129 | "frz", 130 | ], 131 | } 132 | 133 | super().__init__(config_dict=configuration, provider=provider) 134 | -------------------------------------------------------------------------------- /qiskit_cold_atom/applications/time_evolution_solver.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2021. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """A solver for time-evolution problems.""" 14 | 15 | from typing import List 16 | 17 | from qiskit_nature.second_q.operators import FermionicOp 18 | from qiskit_nature.second_q.mappers import ( 19 | JordanWignerMapper, 20 | BravyiKitaevMapper, 21 | ParityMapper, 22 | ) 23 | 24 | from qiskit import QuantumRegister 25 | from qiskit import QuantumCircuit 26 | from qiskit.algorithms import TimeEvolutionProblem 27 | from qiskit.algorithms.time_evolvers import TrotterQRTE 28 | from qiskit.quantum_info import Statevector 29 | 30 | from qiskit_cold_atom.applications.fermionic_evolution_problem import ( 31 | FermionicEvolutionProblem, 32 | ) 33 | from qiskit_cold_atom.fermions.base_fermion_backend import BaseFermionBackend 34 | 35 | 36 | class TimeEvolutionSolver: 37 | """ 38 | Solver class that solves time evolution problem by either analog simulation on fermionic 39 | hardware or trotterized time evolution on qubit hardware. The computation that this time 40 | evolution solver will do depends on the type of the backend. 41 | """ 42 | 43 | MAPPER_DICT = { 44 | "bravyi_kitaev": BravyiKitaevMapper(), 45 | "jordan_wigner": JordanWignerMapper(), 46 | "parity": ParityMapper(), 47 | } 48 | 49 | def __init__( 50 | self, 51 | backend, 52 | map_type: str = None, 53 | trotter_steps: int = None, 54 | shots: int = 1000, 55 | ): 56 | """ 57 | Initialize a time evolution solver 58 | 59 | Args: 60 | backend: The backend on which to execute the problem, may be qubit or fermionic. 61 | map_type: The fermion-to-qubit mapping required if a qubit backend is given 62 | trotter_steps: The amount of trotter steps to approximate time evolution on 63 | qubit backends 64 | shots: number of measurements taken of the constructed circuits 65 | """ 66 | 67 | self.backend = backend 68 | self.map_type = map_type 69 | self.trotter_steps = trotter_steps 70 | self.shots = shots 71 | 72 | def solve(self, problem: FermionicEvolutionProblem) -> List[float]: 73 | """Solve the problem using the provided backend 74 | 75 | Args: 76 | problem: The FermionicEvolutionProblem to solve. 77 | 78 | Returns: 79 | A list of expectation values of the observable of the problem. This list has the 80 | same length as the list of times for which to compute the time evolution. 81 | """ 82 | 83 | if isinstance(self.backend, BaseFermionBackend): 84 | qc_load = self.backend.initialize_circuit(problem.initial_state.occupations) 85 | circuits = problem.circuits(qc_load) 86 | 87 | observable_evs = self.backend.measure_observable_expectation( 88 | circuits, problem.observable, self.shots 89 | ) 90 | 91 | else: 92 | # use qubit pipeline 93 | circuits = self.construct_qubit_circuits(problem) 94 | 95 | mapper = self.MAPPER_DICT[self.map_type] 96 | qubit_observable = mapper.map(problem.observable) 97 | observable_evs = [ 98 | Statevector(qc).expectation_value(qubit_observable) for qc in circuits 99 | ] 100 | 101 | return observable_evs 102 | 103 | def construct_qubit_circuits(self, problem: FermionicEvolutionProblem) -> List[QuantumCircuit]: 104 | """Convert the problem to a trotterized qubit circuit using the specified map_type 105 | 106 | Args: 107 | problem: The fermionic evolution problem specifying the system, evolution-time 108 | and observable to be measured 109 | 110 | Returns: 111 | a list of quantum circuits that simulate the time evolution. 112 | There is one circuit for each evolution time specified in the problem' 113 | """ 114 | 115 | psi_0 = problem.initial_state 116 | system = problem.system 117 | hamiltonian = system.to_fermionic_op() 118 | 119 | mapper = self.MAPPER_DICT[self.map_type] 120 | 121 | circuits = [] 122 | 123 | # construct circuit of initial state: 124 | label = {f"+_{i}": 1.0 for i, bit in enumerate(psi_0.occupations_flat) if bit} 125 | bitstr_op = FermionicOp(label, num_spin_orbitals=len(psi_0.occupations_flat)) 126 | qubit_op = mapper.map(bitstr_op)[0] 127 | init_circ = QuantumCircuit(QuantumRegister(qubit_op.num_qubits, "q")) 128 | 129 | for i, pauli_label in enumerate(qubit_op.paulis.to_labels()[0][::-1]): 130 | if pauli_label == "X": 131 | init_circ.x(i) 132 | elif pauli_label == "Y": 133 | init_circ.y(i) 134 | elif pauli_label == "Z": 135 | init_circ.z(i) 136 | 137 | for time in problem.evolution_times: 138 | # map fermionic hamiltonian to qubits 139 | qubit_hamiltonian = mapper.map(hamiltonian) 140 | # construct trotterization circuits 141 | evolution_problem = TimeEvolutionProblem(qubit_hamiltonian, time, init_circ) 142 | trotter_qrte = TrotterQRTE(num_timesteps=self.trotter_steps) 143 | evolved_state = trotter_qrte.evolve(evolution_problem).evolved_state 144 | circuits.append(evolved_state) 145 | 146 | return circuits 147 | -------------------------------------------------------------------------------- /qiskit_cold_atom/providers/remote_backend.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2021. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """Classes for remote cold atom backends.""" 14 | 15 | import json 16 | from typing import List, Dict, Union 17 | import requests 18 | 19 | from qiskit.providers.models import BackendConfiguration 20 | from qiskit.providers import Options 21 | from qiskit import QuantumCircuit 22 | from qiskit.providers import ProviderV1 as Provider 23 | from qiskit.providers import BackendV1 as Backend 24 | from qiskit.providers import JobStatus 25 | 26 | from qiskit_cold_atom.spins.base_spin_backend import BaseSpinBackend 27 | from qiskit_cold_atom.fermions.base_fermion_backend import BaseFermionBackend 28 | from qiskit_cold_atom.circuit_tools import CircuitTools 29 | from qiskit_cold_atom.providers.cold_atom_job import ColdAtomJob 30 | from qiskit_cold_atom.exceptions import QiskitColdAtomError 31 | 32 | 33 | class RemoteBackend(Backend): 34 | """Remote cold atom backend.""" 35 | 36 | def __init__(self, provider: Provider, url: str): 37 | """ 38 | Initialize the backend by querying the server for the backend configuration dictionary. 39 | 40 | Args: 41 | provider: The provider which need to have the correct credentials attributes in place 42 | url: The url of the backend server 43 | 44 | Raises: 45 | QiskitColdAtomError: If the connection to the backend server can not be established. 46 | 47 | """ 48 | 49 | self.url = url 50 | self.username = provider.credentials["username"] 51 | self.token = provider.credentials["token"] 52 | # Get the config file from the remote server 53 | try: 54 | r = requests.get( 55 | self.url + "/get_config", 56 | params={ 57 | "username": self.username, 58 | "token": self.token, 59 | }, 60 | ) 61 | except requests.exceptions.ConnectionError as err: 62 | raise QiskitColdAtomError( 63 | "connection to the backend server can not be established." 64 | ) from err 65 | 66 | super().__init__(configuration=BackendConfiguration.from_dict(r.json()), provider=provider) 67 | 68 | @classmethod 69 | def _default_options(cls) -> Options: 70 | """Return the default options. 71 | 72 | Returns: 73 | qiskit.providers.Options: A options object with default values set 74 | """ 75 | return Options(shots=1) 76 | 77 | @property 78 | def credentials(self) -> Dict[str, Union[str, List[str]]]: 79 | """Returns: the access credentials used.""" 80 | return self.provider().credentials 81 | 82 | # pylint: disable=arguments-differ, unused-argument 83 | def run( 84 | self, 85 | circuit: Union[QuantumCircuit, List[QuantumCircuit]], 86 | shots: int = 1, 87 | convert_wires: bool = True, 88 | **run_kwargs, 89 | ) -> ColdAtomJob: 90 | """ 91 | Run a quantum circuit or list of quantum circuits. 92 | 93 | Args: 94 | circuit: The quantum circuits to be executed on the device backend 95 | shots: The number of measurement shots to be measured for each given circuit 96 | convert_wires: If True (the default), the circuits are converted to the wiring convention 97 | of the backend. 98 | run_kwargs: Additional keyword arguments that might be passed down when calling 99 | qiskit.execute() which will have no effect on this backend. 100 | 101 | Raises: 102 | QiskitColdAtomError: If the response from the backend does not have a job_id. 103 | 104 | Returns: 105 | A Job object through the backend can be queried for status, result etc. 106 | """ 107 | 108 | job_payload = CircuitTools.circuit_to_cold_atom(circuit, self, shots=shots) 109 | 110 | res = requests.post( 111 | self.url + "/post_job", 112 | json={ 113 | "job": json.dumps(job_payload), 114 | "username": self.username, 115 | "token": self.token, 116 | }, 117 | ) 118 | 119 | res.raise_for_status() 120 | response = res.json() 121 | 122 | if "job_id" not in response: 123 | raise QiskitColdAtomError("The response has no job_id.") 124 | 125 | return ColdAtomJob(self, response["job_id"]) 126 | 127 | def retrieve_job(self, job_id: str) -> ColdAtomJob: 128 | """Return a single job submitted to this backend. 129 | 130 | Args: 131 | job_id: The ID of the job to retrieve. 132 | 133 | Returns: 134 | The job with the given ID. 135 | 136 | Raises: 137 | QiskitColdAtomError: If the job retrieval failed. 138 | """ 139 | 140 | retrieved_job = ColdAtomJob(backend=self, job_id=job_id) 141 | 142 | try: 143 | job_status = retrieved_job.status() 144 | except requests.exceptions.RequestException as request_error: 145 | raise QiskitColdAtomError( 146 | "connection to the remote backend could not be established" 147 | ) from request_error 148 | 149 | if job_status == JobStatus.ERROR: 150 | raise QiskitColdAtomError(f"Job with id {job_id} could not be retrieved") 151 | 152 | return retrieved_job 153 | 154 | 155 | class RemoteSpinBackend(RemoteBackend, BaseSpinBackend): 156 | """Remote backend which runs spin circuits.""" 157 | 158 | 159 | class RemoteFermionBackend(RemoteBackend, BaseFermionBackend): 160 | """Remote backend which runs fermionic circuits.""" 161 | -------------------------------------------------------------------------------- /qiskit_cold_atom/applications/fermi_hubbard.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2021. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """Module to build a Fermi-Hubbard problem.""" 14 | 15 | from abc import ABC, abstractmethod 16 | from typing import List 17 | 18 | from qiskit import QuantumCircuit 19 | from qiskit_nature.second_q.operators import FermionicOp 20 | from qiskit_cold_atom.fermions.fermion_circuit_solver import FermionicBasis 21 | from qiskit_cold_atom.fermions.fermion_gate_library import FermiHubbard 22 | from qiskit_cold_atom.exceptions import QiskitColdAtomError 23 | 24 | 25 | class FermionicLattice(ABC): 26 | """Abstract base fermionic lattice.""" 27 | 28 | @property 29 | @abstractmethod 30 | def size(self) -> int: 31 | """The number of lattice sites of the system.""" 32 | 33 | @abstractmethod 34 | def to_fermionic_op(self) -> FermionicOp: 35 | """Creates the Hamiltonian of the lattice in second quantization. 36 | 37 | Returns: 38 | The Hamiltonian as a FermionicOp. 39 | """ 40 | 41 | @abstractmethod 42 | def to_circuit(self, time: float = 1.0) -> QuantumCircuit: 43 | """ 44 | Wrap the generator of the system in a QuantumCircuit. 45 | 46 | Args: 47 | time: Duration of the time evolution. 48 | 49 | Returns: 50 | A quantum circuit which corresponds to the time-evolved Hamiltonian. 51 | """ 52 | 53 | 54 | class FermiHubbard1D(FermionicLattice): 55 | """Describes a one-dimensional Fermi-Hubbard model with open boundary conditions.""" 56 | 57 | def __init__( 58 | self, 59 | num_sites: int, 60 | particles_up: int, 61 | particles_down: int, 62 | hop_strength: float, 63 | int_strength: float, 64 | potential: List[float], 65 | ): 66 | r""" 67 | Initialize a one-dimensional fermi-hubbard system. In second quantization this system is 68 | described by the Hamiltonian 69 | 70 | :math:`H = \sum_{i=1,\sigma}^{L-1} - J_i (f^\dagger_{i,\sigma} f_{i+1,\sigma} + 71 | f^\dagger_{i+1,\sigma} f_{i,\sigma}) + U \sum_{i=1}^{L} n_{i,\uparrow} n_{i,\downarrow} 72 | + \sum_{i=1,\sigma}^{L} \mu_i n_{i,\sigma}` 73 | 74 | Args: 75 | num_sites: number of lattice sites in the 1D chain. 76 | particles_up: total number of spin-up particles in the lattice 77 | particles_down: total number of spin-down particles in the lattice 78 | hop_strength: strength of hopping between sites 79 | int_strength: strength of the local interaction 80 | potential: list of local phases, must be on length num_wires 81 | 82 | Raises: 83 | QiskitColdAtomError: if the length of the potential does not match the system size. 84 | """ 85 | 86 | # pylint: disable=invalid-name 87 | self._size = num_sites 88 | self.particles_up = particles_up 89 | self.particles_down = particles_down 90 | self.J = hop_strength 91 | self.U = int_strength 92 | self.basis = FermionicBasis(self.size, n_particles=[self.particles_up, self.particles_down]) 93 | 94 | if not len(potential) == self.size: 95 | raise QiskitColdAtomError( 96 | f"The length of the potentials {len(potential)} must match system size {self.size}" 97 | ) 98 | 99 | self.mu = potential 100 | 101 | @property 102 | def size(self) -> int: 103 | """Return the number of sites of the problem.""" 104 | return self._size 105 | 106 | def to_fermionic_op(self) -> FermionicOp: 107 | """Construct the hamiltonian of the lattice as a FermionicOp. 108 | 109 | Returns: 110 | A FermionicOp defining the systems Hamiltonian 111 | """ 112 | 113 | operator_labels = {} 114 | 115 | # add hopping terms 116 | for idx in range(self.size - 1): 117 | right_to_left_up = f"+_{idx} -_{idx+1}" 118 | operator_labels[right_to_left_up] = -self.J 119 | left_to_right_up = f"-_{idx} +_{idx+1}" 120 | operator_labels[left_to_right_up] = self.J 121 | right_to_left_down = f"+_{self.size + idx} -_{self.size + idx+1}" 122 | operator_labels[right_to_left_down] = -self.J 123 | left_to_right_down = f"-_{self.size + idx} +_{self.size + idx+1}" 124 | operator_labels[left_to_right_down] = self.J 125 | 126 | # add interaction terms 127 | for idx in range(self.size): 128 | opstring = f"+_{idx} -_{idx} +_{self.size + idx} -_{self.size + idx}" 129 | operator_labels[opstring] = self.U 130 | 131 | # add potential terms 132 | for idx in range(self.size): 133 | op_up = f"+_{idx} -_{idx}" 134 | operator_labels[op_up] = self.mu[idx] 135 | op_down = f"+_{self.size + idx} -_{self.size + idx}" 136 | operator_labels[op_down] = self.mu[idx] 137 | 138 | return FermionicOp(operator_labels, num_spin_orbitals=2 * self.size) 139 | 140 | def to_circuit(self, time: float = 1.0) -> QuantumCircuit: 141 | """ 142 | Wrap the generator of the system in a QuantumCircuit. 143 | 144 | Args: 145 | time: Duration of the time evolution. 146 | 147 | Returns: 148 | A quantum circuit which corresponds to the time-evolved Hamiltonian. 149 | """ 150 | circ = QuantumCircuit(2 * self.size) 151 | circ.append( 152 | FermiHubbard( 153 | num_modes=2 * self.size, 154 | j=[self.J * time] * (self.size - 1), 155 | u=self.U * time, 156 | mu=[mu_i * time for mu_i in self.mu], 157 | ), 158 | qargs=range(2 * self.size), 159 | ) 160 | 161 | return circ 162 | -------------------------------------------------------------------------------- /qiskit_cold_atom/fermions/fermionic_basis.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2021. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """Module to describe a basis of fermionic states in occupation number representation.""" 14 | 15 | from typing import List, Union 16 | from itertools import combinations, chain, product 17 | import numpy as np 18 | 19 | from qiskit_nature.second_q.operators import FermionicOp 20 | 21 | from qiskit_cold_atom.fermions.fermionic_state import FermionicState 22 | 23 | 24 | class FermionicBasis: 25 | """Class that represents the basis states of the fermionic fock space in occupation number 26 | representation for given particle numbers. The ordering of states complies with Qiskit bitstring 27 | ordering where the smaller number in binary representation has a lower index in the basis""" 28 | 29 | def __init__( 30 | self, 31 | sites: int, 32 | n_particles: Union[int, List[int]], 33 | particle_conservation: bool = True, 34 | spin_conservation: bool = True, 35 | ): 36 | """ 37 | Args: 38 | sites: number of spatial fermionic modes 39 | n_particles: the total number of particles. If given as a list, the entries of the list 40 | give the particles per spin species, where the length of the list defines the number 41 | of different fermionic species 42 | particle_conservation: Boolean flag for the conservation of the total particle number 43 | spin_conservation: Boolean flag for conservation of the particle number per spin species 44 | """ 45 | 46 | self.sites = sites 47 | 48 | if isinstance(n_particles, int): 49 | n_particles = [n_particles] 50 | 51 | self.n_particles = n_particles 52 | self.n_tot = sum(n_particles) 53 | self.num_species = len(n_particles) 54 | 55 | states = [] 56 | 57 | if particle_conservation: 58 | if spin_conservation: 59 | indices = [] 60 | for i, n in enumerate(self.n_particles): 61 | indices.append(list(combinations(np.arange(sites) + i * sites, n))) 62 | 63 | for combination in product(*indices): 64 | particle_indices = list(_ for _ in chain.from_iterable(combination)) 65 | 66 | occupations = [0] * self.sites * self.num_species 67 | for idx in particle_indices: 68 | occupations[idx] = 1 69 | 70 | states.append( 71 | FermionicState.from_total_occupations(occupations, self.num_species) 72 | ) 73 | 74 | else: 75 | for indices_tot in list(combinations(range(self.num_species * sites), self.n_tot)): 76 | occupations_tot = [0] * sites * self.num_species 77 | for i in indices_tot: 78 | occupations_tot[i] = 1 79 | states.append( 80 | FermionicState.from_total_occupations(occupations_tot, self.num_species) 81 | ) 82 | else: 83 | for occs in product("10", repeat=(self.num_species * self.sites)): 84 | occupations_tot = [int(n) for n in occs] 85 | states.append(FermionicState(list(occupations_tot))) 86 | 87 | # reverse order of states to comply with Qiskit bitstring ordering 88 | self.states = states[::-1] 89 | 90 | self.dimension = len(self.states) 91 | 92 | def __str__(self): 93 | string = "" 94 | if self.dimension < 30: 95 | for i in range(self.dimension): 96 | if i < 10: 97 | string += "\n {}. ".format(i) + self.states[i].__str__() 98 | else: 99 | string += "\n {}. ".format(i) + self.states[i].__str__() 100 | else: 101 | for i in range(5): 102 | string += "\n {}. ".format(i) + self.states[i].__str__() 103 | string += "\n . \n . \n ." 104 | for i in range(self.dimension - 5, self.dimension): 105 | string += "\n {}. ".format(i) + self.states[i].__str__() 106 | 107 | return string 108 | 109 | @classmethod 110 | def from_state( 111 | cls, 112 | state: FermionicState, 113 | spin_conservation: bool, 114 | particle_conservation: bool = True, 115 | ): 116 | """Helper function to create the basis corresponding to a given occupation number state with 117 | particle number conservation and optionally spin conservation.""" 118 | sites = state.sites 119 | n_particles = [] 120 | for occs in state.occupations: 121 | n_particles.append(sum(occs)) 122 | 123 | return cls(sites, n_particles, particle_conservation, spin_conservation) 124 | 125 | @classmethod 126 | def from_fermionic_op(cls, fer_op: FermionicOp): 127 | """Helper function to create the full Fock space basis corresponding to a given FermionicOp.""" 128 | sites = fer_op.register_length 129 | n_particles = fer_op.register_length 130 | return cls(sites, n_particles, particle_conservation=False, spin_conservation=False) 131 | 132 | def get_occupations(self) -> List[List[int]]: 133 | """Get a list of the flattened occupations of the individual basis states.""" 134 | return [state.occupations_flat for state in self.states] 135 | 136 | def get_index_of_measurement(self, bitstring: str) -> int: 137 | """ 138 | For a binary string of occupations, e.g. '10100011', return the index of the basis state 139 | that corresponds to these occupations. In contrast to qubits, this index is not given by 2 140 | to the power of the bitstring as fermionic bases do not always include all states due to 141 | particle and spin conservation rules. 142 | 143 | Args: 144 | bitstring: A binary string of occupations. 145 | 146 | Returns: 147 | The index of the basis state corresponding to the bitstring. 148 | """ 149 | occupation_strings = ["".join(map(str, k)) for k in self.get_occupations()] 150 | index = occupation_strings.index(bitstring) 151 | return index 152 | -------------------------------------------------------------------------------- /test/test_spin_simulator_backend.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2021. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """General spin simulator backend tests.""" 14 | 15 | from time import sleep 16 | import numpy as np 17 | 18 | from qiskit import QuantumCircuit 19 | from qiskit.providers import JobStatus 20 | from qiskit.result import Result 21 | from qiskit.test import QiskitTestCase 22 | from qiskit_aer import AerJob 23 | 24 | from qiskit_cold_atom.exceptions import QiskitColdAtomError 25 | from qiskit_cold_atom.spins import SpinSimulator 26 | from qiskit_cold_atom.spins.base_spin_backend import BaseSpinBackend 27 | 28 | 29 | class TestSpinSimulatorBackend(QiskitTestCase): 30 | """class to test the FermionSimulatorBackend class.""" 31 | 32 | def setUp(self): 33 | super().setUp() 34 | self.backend = SpinSimulator() 35 | 36 | def test_initialization(self): 37 | """test the initialization of the backend""" 38 | target_config = { 39 | "backend_name": "spin_simulator", 40 | "backend_version": "0.0.1", 41 | "n_qubits": None, 42 | "basis_gates": None, 43 | "gates": [], 44 | "local": True, 45 | "simulator": True, 46 | "conditional": False, 47 | "open_pulse": False, 48 | "memory": True, 49 | "max_shots": 1e5, 50 | "coupling_map": None, 51 | "description": "a base simulator for spin circuits. Instead of a qubit, each wire " 52 | "represents a single high-dimensional spin", 53 | } 54 | 55 | backend = SpinSimulator() 56 | self.assertIsInstance(backend, BaseSpinBackend) 57 | self.assertTrue(target_config.items() <= backend.configuration().to_dict().items()) 58 | 59 | def test_run_method(self): 60 | """Test the run method of the backend simulator""" 61 | 62 | with self.subTest("test call"): 63 | circ = QuantumCircuit(2) 64 | job = self.backend.run(circ) 65 | self.assertIsInstance(job, AerJob) 66 | self.assertIsInstance(job.job_id(), str) 67 | self.assertIsInstance(job.result(), Result) 68 | self.assertEqual(job.status(), JobStatus.DONE) 69 | 70 | circ1 = QuantumCircuit(2) 71 | circ2 = QuantumCircuit(3) 72 | 73 | with self.subTest("test call with multiple circuits"): 74 | job = self.backend.run([circ1, circ2]) 75 | self.assertIsInstance(job, AerJob) 76 | 77 | with self.subTest("test shot number"): 78 | target_shots = 123 79 | job = self.backend.run([circ1, circ2], shots=target_shots) 80 | for exp in job.result().results: 81 | self.assertEqual(exp.shots, target_shots) 82 | 83 | with self.subTest("test seed of RNG"): 84 | target_seed = 123 85 | job = self.backend.run([circ1, circ2], seed=target_seed) 86 | for exp in job.result().results: 87 | self.assertEqual(exp.header.random_seed, target_seed) 88 | 89 | with self.subTest("test dimension of simulation"): 90 | test_circ = QuantumCircuit(2) 91 | test_circ.rlx(np.pi / 2, 0) 92 | test_circ.rly(np.pi / 4, [0, 1]) 93 | 94 | statevector_1 = self.backend.run(test_circ, spin=1).result().get_statevector() 95 | self.assertEqual(len(statevector_1), 3**2) 96 | 97 | statevector_2 = self.backend.run(test_circ, spin=5 / 2).result().get_statevector() 98 | self.assertEqual(len(statevector_2), 6**2) 99 | 100 | with self.subTest("test irregular spin values"): 101 | test_circ = QuantumCircuit(2) 102 | job = self.backend.run(test_circ, spin=5 / 4) 103 | sleep(0.01) 104 | self.assertIs(job.status(), JobStatus.ERROR) 105 | with self.assertRaises(QiskitColdAtomError): 106 | job.result() 107 | 108 | def test_execute(self): 109 | """test the ._execute() method internally called by .run()""" 110 | 111 | test_circ = QuantumCircuit(2) 112 | test_circ.rly(np.pi / 2, 0) 113 | test_circ.rlx(np.pi / 2, 1) 114 | test_circ.measure_all() 115 | 116 | result = self.backend.run(test_circ, spin=1, seed=45, shots=5).result() 117 | 118 | with self.subTest("test simulation counts"): 119 | self.assertEqual(result.get_counts(), {"0 1": 1, "2 2": 1, "1 1": 2, "1 0": 1}) 120 | 121 | with self.subTest("test simulation memory"): 122 | self.assertEqual(result.get_memory(), ["2 2", "1 1", "0 1", "1 0", "1 1"]) 123 | 124 | with self.subTest("test simulation statevector"): 125 | self.assertTrue( 126 | np.allclose( 127 | result.get_statevector(), 128 | np.array( 129 | [ 130 | 1 / 4, 131 | -1j / np.sqrt(8), 132 | -1 / 4, 133 | 1 / np.sqrt(8), 134 | -1j / 2, 135 | -1 / np.sqrt(8), 136 | 1 / 4, 137 | -1j / np.sqrt(8), 138 | -1 / 4, 139 | ] 140 | ), 141 | ) 142 | ) 143 | 144 | with self.subTest("test simulation unitary"): 145 | # test the unitary on a single spin-2 example 146 | test_circ = QuantumCircuit(1) 147 | test_circ.rlx(np.pi / 2, 0) 148 | test_circ.rlz(np.pi / 2, 0) 149 | test_circ.measure_all() 150 | 151 | result = self.backend.run(test_circ, spin=2, seed=45, shots=5).result() 152 | 153 | self.assertTrue( 154 | np.allclose( 155 | result.get_unitary(), 156 | np.array( 157 | [ 158 | [-0.25, 0.5j, np.sqrt(3 / 8), -0.5j, -0.25], 159 | [-0.5, 0.5j, 0.0, 0.5j, 0.5], 160 | [-np.sqrt(3 / 8), 0.0, -0.5, 0.0, -np.sqrt(3 / 8)], 161 | [-0.5, -0.5j, 0.0, -0.5j, 0.5], 162 | [-0.25, -0.5j, np.sqrt(3 / 8), 0.5j, -0.25], 163 | ] 164 | ), 165 | ) 166 | ) 167 | 168 | with self.subTest("test time taken"): 169 | self.assertTrue(result.to_dict()["time_taken"] < 0.5) 170 | 171 | with self.subTest("test result success"): 172 | self.assertTrue(result.to_dict()["success"]) 173 | -------------------------------------------------------------------------------- /qiskit_cold_atom/fermions/fermionic_state.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2021. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """Module to describe fermionic states in occupation number basis""" 14 | 15 | from typing import List, Union 16 | import warnings 17 | import numpy as np 18 | 19 | from qiskit import QuantumCircuit 20 | 21 | from qiskit_cold_atom.exceptions import QiskitColdAtomError 22 | 23 | 24 | class FermionicState: 25 | """Fermionic states in an occupation number representation.""" 26 | 27 | def __init__(self, occupations: Union[List[int], List[List[int]]]): 28 | """Create a :class:`FermionicState` from the given occupations. 29 | 30 | Args: 31 | occupations: List of occupation numbers. When List[int] is given, the occupations 32 | correspond to the number of indistinguishable fermionic particles in each mode, 33 | e.g. [0, 1, 1, 0] implies that sites one and two are occupied by a fermion. 34 | When List[List[int]] is given, the occupations describe the number of particles in 35 | fermionic modes with different (distinguishable) species of fermions. Each 36 | inner list gives the occupations of one fermionic species. 37 | 38 | Raises: 39 | QiskitColdAtomError: 40 | - If the inner lists do not have the same length 41 | - If the occupations are not 0 or 1 42 | """ 43 | 44 | if isinstance(occupations[0], (int, np.integer)): 45 | occupations = [occupations] 46 | 47 | self._sites = len(occupations[0]) 48 | self._occupations = occupations 49 | self._num_species = len(occupations) 50 | 51 | self._occupations_flat = [] 52 | for occs in self.occupations: 53 | self._occupations_flat += occs 54 | 55 | for occs in self.occupations[0:]: 56 | if len(occs) != self._sites: 57 | raise QiskitColdAtomError( 58 | f"All occupations of different fermionic species must have " 59 | f"same length, received {self.occupations[0]} and {occs}." 60 | ) 61 | for n in occs: 62 | if n not in (0, 1): 63 | raise QiskitColdAtomError(f"Fermionic occupations must be 0 or 1, got {n}.") 64 | 65 | @property 66 | def occupations(self) -> List[List[int]]: 67 | """Return the occupation number of each fermionic mode.""" 68 | return self._occupations 69 | 70 | @property 71 | def occupations_flat(self) -> List[int]: 72 | """Return the occupations of each fermionic mode in a flat list.""" 73 | return self._occupations_flat 74 | 75 | @property 76 | def sites(self) -> int: 77 | """Return the number of fermionic sites.""" 78 | return self._sites 79 | 80 | @property 81 | def num_species(self) -> int: 82 | """Return the number of species of fermions, e.g. 2 for spin up/down systems.""" 83 | return self._num_species 84 | 85 | def __str__(self): 86 | output = "" 87 | for i in range(self.num_species): 88 | output += "|" + str(self.occupations[i])[1:-1] + ">" 89 | return output 90 | 91 | @classmethod 92 | def from_total_occupations(cls, occupations: List[int], num_species: int) -> "FermionicState": 93 | """ 94 | Create a fermionic state from a single (flat) list of total occupations. 95 | 96 | Args: 97 | occupations: a list of occupations of all fermionic modes, e.g. [0, 1, 1, 0, 1, 0]. 98 | num_species: number of fermionic species. If > 1, the total occupation list is cast 99 | into a nested list where each inner list describes one fermionic species. In the 100 | above example, for num_species = 2, this becomes 101 | FermionicState([[0, 1, 1], [0, 1, 0]]). 102 | 103 | Returns: 104 | A fermionic state initialized with the given input. 105 | 106 | Raises: 107 | QiskitColdAtomError: If the length of occupations is not a multiple of num_species. 108 | """ 109 | if len(occupations) % num_species != 0: 110 | raise QiskitColdAtomError( 111 | "The state must have a number of occupations that is a multiple of the" 112 | "number of fermionic species." 113 | ) 114 | 115 | sites = int(len(occupations) / num_species) 116 | return cls(np.reshape(occupations, (num_species, sites)).tolist()) 117 | 118 | @classmethod 119 | def initial_state(cls, circuit: QuantumCircuit, num_species: int = 1) -> "FermionicState": 120 | """ 121 | Create a fermionic state from a quantum circuit that uses the `LoadFermion` instruction. 122 | This instruction must be the first instructions of the circuit and no further LoadFermion 123 | instruction can be applied, even after other instructions such as gates have been applied. 124 | 125 | Args: 126 | circuit: a quantum circuit with LoadFermions instructions that initialize fermionic 127 | particles. 128 | num_species: number of different fermionic species, e.g. 1 for a single 129 | type of spinless fermions (default), 2 for spin-1/2 fermions etc. 130 | 131 | Returns: 132 | A FermionicState initialized from the given circuit. 133 | 134 | Raises: 135 | QiskitColdAtomError: 136 | - If the number of wires in the circuit is not a multiple of num_species, 137 | - If LoadFermions instructions come after other instructions. 138 | """ 139 | if num_species > 1: 140 | if circuit.num_qubits % num_species != 0: 141 | raise QiskitColdAtomError( 142 | "The circuit must have a number of wires that is a multiple of the" 143 | "number of fermionic species." 144 | ) 145 | 146 | occupations = [0] * circuit.num_qubits 147 | gates_applied = [False] * circuit.num_qubits 148 | 149 | if not circuit.data[0][0].name == "load": 150 | warnings.warn( 151 | "No particles have been initialized, the circuit will return a trivial result." 152 | ) 153 | 154 | # check that there are no more 'LoadFermions' instructions 155 | for instruction in circuit.data: 156 | qargs = [circuit.qubits.index(qubit) for qubit in instruction[1]] 157 | 158 | if instruction[0].name == "load": 159 | for idx in qargs: 160 | if gates_applied[idx]: 161 | raise QiskitColdAtomError( 162 | f"State preparation instruction in circuit after gates on wire {idx}" 163 | ) 164 | occupations[idx] = 1 165 | else: 166 | for idx in qargs: 167 | gates_applied[idx] = True 168 | 169 | return cls.from_total_occupations(occupations, num_species) 170 | -------------------------------------------------------------------------------- /qiskit_cold_atom/spins/spin_circuit_solver.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2021. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """Module to simulate spin circuits.""" 14 | 15 | import math 16 | from typing import List, Union, Optional 17 | from fractions import Fraction 18 | import numpy as np 19 | from scipy.sparse import csc_matrix 20 | from qiskit import QuantumCircuit 21 | from qiskit_nature.second_q.operators import SpinOp 22 | from qiskit_cold_atom.base_circuit_solver import BaseCircuitSolver 23 | from qiskit_cold_atom.exceptions import QiskitColdAtomError 24 | 25 | 26 | class SpinCircuitSolver(BaseCircuitSolver): 27 | """Performs numerical simulations of spin systems by exactly computing the time 28 | evolution under unitary operations generated by exponentiating spin Hamiltonians.""" 29 | 30 | def __init__( 31 | self, 32 | spin: Union[float, Fraction] = Fraction(1, 2), 33 | shots: Optional[int] = None, 34 | seed: Optional[int] = None, 35 | ): 36 | """ 37 | Initialize a spin circuit solver. 38 | 39 | Args: 40 | spin: The length of the spin of each wire in the circuit. 41 | shots: Amount of shots for the measurement simulation; 42 | if not None, measurements are performed. 43 | seed: The seed for the RNG for the measurement simulation. 44 | 45 | Raises: 46 | QiskitColdAtomError: if the spin is not a positive integer or half-integer. 47 | """ 48 | 49 | self.spin = Fraction(spin) 50 | if self.spin.denominator not in (1, 2): 51 | raise QiskitColdAtomError( 52 | f"spin must be a positive half-integer (integer or half-odd-integer), " 53 | f"not {self.spin}." 54 | ) 55 | 56 | super().__init__(shots=shots, seed=seed) 57 | 58 | def get_initial_state(self, circuit: QuantumCircuit) -> csc_matrix: 59 | """ 60 | Return the initial state as a sparse column vector. 61 | 62 | Args: 63 | circuit: A circuit that tells us the dimension of the initial state to return. 64 | 65 | Returns: 66 | initial state: A sparse column vector of the initial state. 67 | """ 68 | 69 | dim = int((2 * self.spin + 1) ** circuit.num_qubits) 70 | 71 | initial_state = csc_matrix(([1 + 0j], ([0], [0])), shape=(dim, 1), dtype=complex) 72 | return initial_state 73 | 74 | def _embed_operator(self, operator: SpinOp, num_wires: int, qargs: List[int]) -> SpinOp: 75 | """ 76 | Turning a SpinOp operator that acts onto the wires given in qargs into an operator 77 | that acts on the entire register of the circuit by manipulating the indices of the 78 | sparse labels of the SpinOps. 79 | 80 | Args: 81 | operator: SpinOp describing the generating Hamiltonian of a gate 82 | num_wires: The total number of wires in which the operator should be embedded into 83 | qargs: The wire indices the gate acts on 84 | 85 | Returns: 86 | A SpinOp acting on the entire quantum register of the Circuit 87 | 88 | Raises: 89 | QiskitColdAtomError: - If the given operator is not a SpinOp 90 | - If the size of the operator does not match the given qargs 91 | """ 92 | if not isinstance(operator, SpinOp): 93 | raise QiskitColdAtomError(f"Expected SpinOp; got {type(operator).__name__} instead") 94 | 95 | if operator.num_spins != len(qargs): 96 | raise QiskitColdAtomError( 97 | f"operator size {operator.num_spins} does not match qargs {qargs} of the gates." 98 | ) 99 | 100 | embedded_op_dict = {} 101 | for label, factor in operator._data.items(): 102 | old_labels = label.split() 103 | new_labels = [term[:2] + str(qargs[int(term[2])]) + term[3:] for term in old_labels] 104 | embedded_op_dict[" ".join(map(str, new_labels))] = factor 105 | 106 | return SpinOp(embedded_op_dict, spin=self.spin, num_spins=num_wires) 107 | 108 | def operator_to_mat(self, operator: SpinOp) -> csc_matrix: 109 | """ 110 | Convert a SpinOp describing a gate generator to a sparse matrix. 111 | 112 | Args: 113 | operator: spin operator of which to compute the matrix representation 114 | 115 | Returns: 116 | scipy.sparse matrix of the Hamiltonian 117 | """ 118 | return csc_matrix(operator.to_matrix()) 119 | 120 | def preprocess_circuit(self, circuit: QuantumCircuit): 121 | r""" 122 | Compute the Hilbert space dimension of the given quantum circuit as :math:`(2S+1)^N` 123 | where :math:`S` is the length of the spin and :math:`N` is the number of spins in 124 | the quantum circuit. 125 | 126 | Args: 127 | circuit: The circuit to pre-process. 128 | """ 129 | self._dim = int((2 * self.spin + 1) ** circuit.num_qubits) 130 | 131 | def draw_shots(self, measurement_distribution: List[float]) -> List[str]: 132 | r"""A helper function to draw counts from a given distribution of measurement outcomes. 133 | 134 | Args: 135 | measurement_distribution: List of probabilities of the individual measurement outcomes. 136 | 137 | Returns: 138 | outcome_memory: A list of individual measurement results, e.g. ["12 3 4", "0 4 9", ...] 139 | The outcome of each shot is denoted by a space-delimited string "a1 a2 a3 ..." where 140 | :math:`a_i` is the measured level of the spin with possible values ranging from 0 to 2S 141 | The :math:`a_i` are in reverse order of the spins of the register to comply with qiskit's 142 | little endian convention. 143 | 144 | Raises: 145 | QiskitColdAtomError: 146 | - If the length of the given probabilities does not math the expected Hilbert 147 | space dimension. 148 | - If the dimension is not a power of the spin length of the solver. 149 | - If the number of shots self.shots has not been specified. 150 | """ 151 | 152 | meas_dim = len(measurement_distribution) 153 | 154 | if meas_dim != self.dim: 155 | raise QiskitColdAtomError( 156 | f"Dimension of the measurement probabilities {meas_dim} does not " 157 | f"match the dimension expected by the solver, {self.dim}" 158 | ) 159 | 160 | if self.shots is None: 161 | raise QiskitColdAtomError( 162 | "The number of shots has to be set before drawing measurements" 163 | ) 164 | 165 | # Draw measurements as the indices of the basis states: 166 | meas_results = np.random.choice(range(meas_dim), self.shots, p=measurement_distribution) 167 | base = int(2 * self.spin + 1) 168 | num_wires = math.log(meas_dim, base) 169 | 170 | if num_wires.is_integer(): 171 | num_wires = int(num_wires) 172 | else: 173 | raise QiskitColdAtomError( 174 | "The length of given measurement distribution it not compatible with " 175 | "the spin-length of the solver." 176 | ) 177 | 178 | outcome_memory = [] 179 | for meas_idx in meas_results: 180 | digits = [0] * num_wires 181 | for i in range(num_wires): 182 | digits[i] = meas_idx % base 183 | meas_idx //= base 184 | 185 | outcome_memory.append(" ".join(map(str, digits))) 186 | 187 | return outcome_memory 188 | -------------------------------------------------------------------------------- /qiskit_cold_atom/spins/spin_simulator_backend.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2021. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """General spin simulator backend.""" 14 | 15 | from typing import Union, List, Dict, Any, Optional 16 | import uuid 17 | from fractions import Fraction 18 | import warnings 19 | import time 20 | import datetime 21 | 22 | from qiskit.providers.models import BackendConfiguration 23 | from qiskit.providers import Options 24 | from qiskit import QuantumCircuit 25 | from qiskit.result import Result 26 | from qiskit.circuit.measure import Measure 27 | from qiskit_aer import AerJob 28 | 29 | from qiskit_cold_atom.spins.spin_circuit_solver import SpinCircuitSolver 30 | from qiskit_cold_atom.spins.base_spin_backend import BaseSpinBackend 31 | from qiskit_cold_atom.circuit_tools import CircuitTools 32 | 33 | 34 | class SpinSimulator(BaseSpinBackend): 35 | """A simulator to simulate general spin circuits. 36 | 37 | This general spin simulator backend simulates spin circuits with gates that have 38 | generators described by spin Hamiltonians. It computes the statevector and unitary 39 | of a circuit and simulates measurements. 40 | """ 41 | 42 | # Default configuration of the backend if the user does not provide one. 43 | __DEFAULT_CONFIGURATION__ = { 44 | "backend_name": "spin_simulator", 45 | "backend_version": "0.0.1", 46 | "n_qubits": None, 47 | "basis_gates": None, 48 | "gates": [], 49 | "local": True, 50 | "simulator": True, 51 | "conditional": False, 52 | "open_pulse": False, 53 | "memory": True, 54 | "max_shots": 1e5, 55 | "coupling_map": None, 56 | "description": "a base simulator for spin circuits. Instead of a qubit, each wire represents a " 57 | "single high-dimensional spin", 58 | } 59 | 60 | def __init__(self, config_dict: Optional[Dict[str, Any]] = None, provider=None): 61 | """ 62 | Initialize the backend from a configuration dictionary. 63 | 64 | Args: 65 | config_dict: Configuration dictionary of the backend. If None is given 66 | a default is assumed. 67 | """ 68 | 69 | if config_dict is None: 70 | config_dict = self.__DEFAULT_CONFIGURATION__ 71 | 72 | super().__init__( 73 | configuration=BackendConfiguration.from_dict(config_dict), provider=provider 74 | ) 75 | 76 | @classmethod 77 | def _default_options(cls): 78 | return Options(shots=1) 79 | 80 | def _execute(self, data: Dict[str, Any], job_id: str = "") -> Result: 81 | """ 82 | Helper function to execute a job. The circuit and all relevant parameters are 83 | given in the data dict. Performs validation checks on the received circuits 84 | and utilizes the SpinCircuitSolver to perform the numerical simulations. 85 | 86 | Args: 87 | data: Data dictionary that that contains the experiments to simulate, given in the shape: 88 | data = { 89 | "num_species": int, 90 | "shots": int, 91 | "seed": int, 92 | "experiments": Dict[str, QuantumCircuit], 93 | } 94 | job_id: The job id assigned by the run method 95 | 96 | Returns: 97 | result: A qiskit job result. 98 | """ 99 | # Start timer 100 | start = time.time() 101 | 102 | output = {"results": []} 103 | 104 | spin = data["spin"] 105 | shots = data["shots"] 106 | seed = data["seed"] 107 | 108 | solver = SpinCircuitSolver(spin, shots, seed) 109 | 110 | for exp_i, exp_name in enumerate(data["experiments"]): 111 | experiment = data["experiments"][exp_name] 112 | circuit = experiment["circuit"] 113 | 114 | # perform compatibility checks with the backend configuration in case gates and supported 115 | # instructions are constrained by the backend's configuration 116 | if self.configuration().gates and self.configuration().supported_instructions: 117 | CircuitTools.validate_circuits(circuits=circuit, backend=self, shots=shots) 118 | 119 | # check whether all wires are measured 120 | measured_wires = set() 121 | 122 | for inst in circuit.data: 123 | if isinstance(inst[0], Measure): 124 | for wire in inst[1]: 125 | index = circuit.qubits.index(wire) 126 | if index in measured_wires: 127 | warnings.warn( 128 | f"Wire {index} has already been measured, " 129 | f"second measurement is ignored" 130 | ) 131 | else: 132 | measured_wires.add(index) 133 | 134 | if measured_wires and len(measured_wires) != len(circuit.qubits): 135 | warnings.warn( 136 | f"Number of wires in the circuit ({len(circuit.qubits)}) does not equal the " 137 | f"number of wires with measurement instructions ({len(measured_wires)}). " 138 | f"{self.__class__.__name__} only supports measurement of the entire quantum " 139 | "register which will be performed instead." 140 | ) 141 | 142 | if not measured_wires: 143 | solver.shots = None 144 | 145 | simulation_result = solver(circuit) 146 | 147 | output["results"].append( 148 | { 149 | "header": {"name": exp_name, "random_seed": seed}, 150 | "shots": shots, 151 | "spin": spin, 152 | "status": "DONE", 153 | "success": True, 154 | } 155 | ) 156 | # add the simulation result at the correct place in the result dictionary 157 | output["results"][exp_i]["data"] = simulation_result 158 | 159 | output["job_id"] = job_id 160 | output["date"] = datetime.datetime.now().isoformat() 161 | output["backend_name"] = self.name() 162 | output["backend_version"] = self.configuration().backend_version 163 | output["time_taken"] = time.time() - start 164 | output["success"] = True 165 | output["qobj_id"] = None 166 | 167 | return Result.from_dict(output) 168 | 169 | # pylint: disable=arguments-differ, unused-argument 170 | def run( 171 | self, 172 | circuits: Union[QuantumCircuit, List[QuantumCircuit]], 173 | shots: int = 1000, 174 | spin: Union[float, Fraction] = Fraction(1, 2), 175 | seed: Optional[int] = None, 176 | **run_kwargs, 177 | ) -> AerJob: 178 | """ 179 | Run the simulator with a variable length of the individual spins. 180 | 181 | Args: 182 | circuits: A list of quantum circuits. 183 | shots: The number of shots to measure. 184 | spin: The spin length of the simulated system which must be a positive 185 | integer or half-integer. Defaults to 1/2 which is equivalent to qubits. 186 | seed: The seed for the simulator. 187 | run_kwargs: Additional keyword arguments that might be passed down when calling 188 | qiskit.execute() which will have no effect on this backend. 189 | 190 | Returns: 191 | aer_job: a job object containing the result of the simulation 192 | """ 193 | 194 | if isinstance(circuits, QuantumCircuit): 195 | circuits = [circuits] 196 | 197 | data = {"spin": spin, "shots": shots, "seed": seed, "experiments": {}} 198 | 199 | for idx, circuit in enumerate(circuits): 200 | data["experiments"]["experiment_%i" % idx] = { 201 | "circuit": circuit, 202 | } 203 | 204 | job_id = str(uuid.uuid4()) 205 | aer_job = AerJob(self, job_id, self._execute, data) 206 | aer_job.submit() 207 | return aer_job 208 | -------------------------------------------------------------------------------- /qiskit_cold_atom/fermions/base_fermion_backend.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2021. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """Module for cold-atom fermion backends.""" 14 | 15 | from abc import ABC 16 | from typing import Union, List, Optional 17 | import numpy as np 18 | 19 | from qiskit.providers import BackendV1 as Backend 20 | from qiskit import QuantumCircuit 21 | from qiskit_nature.second_q.operators import FermionicOp 22 | 23 | from qiskit_cold_atom.fermions.fermion_gate_library import LoadFermions 24 | from qiskit_cold_atom.fermions.fermion_circuit_solver import FermionCircuitSolver 25 | from qiskit_cold_atom.fermions.fermionic_state import FermionicState 26 | from qiskit_cold_atom.exceptions import QiskitColdAtomError 27 | 28 | 29 | class BaseFermionBackend(Backend, ABC): 30 | """Abstract base class for fermionic tweezer backends.""" 31 | 32 | def initialize_circuit(self, occupations: Union[List[int], List[List[int]]]): 33 | """ 34 | Initialize a fermionic quantum circuit with the given occupations. 35 | 36 | Args: 37 | occupations: List of occupation numbers. When ``List[int]`` is given, the occupations 38 | correspond to the number of indistinguishable fermionic particles in each mode, 39 | e.g. ``[0, 1, 1, 0]`` implies that sites one and two are occupied by a fermion. 40 | When ``List[List[int]]`` is given, the occupations describe the number of particles in 41 | fermionic modes with different (distinguishable) species of fermions. Each 42 | inner list gives the occupations of one fermionic species. 43 | 44 | Returns: 45 | circuit: Qiskit QuantumCircuit with a quantum register for each fermionic species 46 | initialized with the ``load`` instructions corresponding to the given occupations 47 | 48 | Raises: 49 | QiskitColdAtomError: If occupations do not match the backend 50 | """ 51 | try: 52 | backend_size = self.configuration().to_dict()["n_qubits"] 53 | except NameError as name_error: 54 | raise QiskitColdAtomError( 55 | f"Number of tweezers not specified for {self.name()}" 56 | ) from name_error 57 | 58 | initial_state = FermionicState(occupations) 59 | 60 | n_wires = initial_state.sites * initial_state.num_species 61 | 62 | if n_wires > backend_size: 63 | raise QiskitColdAtomError( 64 | f"{self.name()} supports up to {backend_size} sites, {n_wires} were given" 65 | ) 66 | 67 | # if num_species is specified by the backend, the wires describe different atomic species 68 | # and the circuit must exactly match the expected wire count of the backend. 69 | if "num_species" in self.configuration().to_dict().keys(): 70 | num_species = self.configuration().num_species 71 | if num_species > 1 and n_wires < self.configuration().num_qubits: 72 | raise QiskitColdAtomError( 73 | f"{self.name()} requires circuits with exactly " 74 | f"{self.configuration().num_qubits} wires, but an initial occupation of size " 75 | f"{n_wires} was given." 76 | ) 77 | 78 | from qiskit.circuit import QuantumRegister 79 | 80 | if initial_state.num_species > 1: 81 | registers = [] 82 | for i in range(initial_state.num_species): 83 | registers.append(QuantumRegister(initial_state.sites, f"spin_{i}")) 84 | circuit = QuantumCircuit(*registers) 85 | 86 | else: 87 | circuit = QuantumCircuit(QuantumRegister(initial_state.sites, "fer_mode")) 88 | 89 | for i, occupation_list in enumerate(initial_state.occupations): 90 | for j, occ in enumerate(occupation_list): 91 | if occ: 92 | circuit.append(LoadFermions(), qargs=[i * initial_state.sites + j]) 93 | 94 | return circuit 95 | 96 | def measure_observable_expectation( 97 | self, 98 | circuits: Union[QuantumCircuit, List[QuantumCircuit]], 99 | observable: FermionicOp, 100 | shots: int, 101 | seed: Optional[int] = None, 102 | num_species: int = 1, 103 | get_variance: bool = False, 104 | ): 105 | """Measure the expectation value of an observable in a state prepared by a given quantum circuit 106 | that uses fermionic gates. Measurements are added to the entire register if they are not yet 107 | applied in the circuit. 108 | 109 | Args: 110 | circuits: QuantumCircuit applying gates with fermionic generators 111 | observable: A FermionicOp describing an observable of which the expectation value is sampled 112 | shots: Number of measurement shots taken in case the circuit has measure instructions 113 | seed: seed for the random number generator of the measurement simulation 114 | num_species: number of different fermionic species described by the circuits 115 | get_variance: If True, also returns an estimate of the variance of the observable 116 | 117 | Raises: 118 | QiskitColdAtomError: if the observable is non-diagonal 119 | 120 | Returns: 121 | observable_ev: List of the measured expectation values of the observables in given circuits 122 | variance: List of the estimated variances of of the observables (if get_variance is True) 123 | """ 124 | 125 | if isinstance(circuits, QuantumCircuit): 126 | circuits = [circuits] 127 | 128 | observable_evs = [0] * len(circuits) 129 | observable_vars = [0] * len(circuits) 130 | 131 | for idx, circuit in enumerate(circuits): 132 | # check whether the observable is diagonal in the computational basis. 133 | solver = FermionCircuitSolver(num_species=2) 134 | solver.preprocess_circuit(circuit) 135 | observable_mat = solver.operator_to_mat(observable) 136 | 137 | if list(observable_mat.nonzero()[0]) != list(observable_mat.nonzero()[1]): 138 | raise QiskitColdAtomError( 139 | "Measuring general observables that are non-diagonal in the " 140 | "computational basis is not yet implemented for " 141 | "fermionic backends. This requires non-trivial basis " 142 | "transformations that are in general difficult to find and " 143 | "depend on the backend's native gate set." 144 | ) 145 | 146 | circuit.remove_final_measurements() 147 | circuit.measure_all() 148 | 149 | # pylint: disable=unexpected-keyword-arg 150 | job = self.run(circuit, shots=shots, seed=seed, num_species=num_species) 151 | counts = job.result().get_counts() 152 | 153 | for bitstring in counts: 154 | # Extract the index of the measured count-bitstring in the fermionic basis. 155 | # In contrast to qubits, this is not trivial and requires an additional step. 156 | ind = solver.basis.get_index_of_measurement(bitstring) 157 | 158 | # contribution to the operator estimate of this outcome 159 | p = counts[bitstring] / shots 160 | observable_evs[idx] += p * observable_mat[ind, ind].real 161 | 162 | if get_variance: 163 | # contribution to the variance of the operator 164 | observable_vars[idx] += ( 165 | np.sqrt(p * (1 - p) / shots) * observable_mat[ind, ind] 166 | ) ** 2 167 | 168 | if get_variance: 169 | return observable_evs, observable_vars 170 | else: 171 | return observable_evs 172 | 173 | def draw(self, qc: QuantumCircuit, **draw_options): 174 | """Modified circuit drawer to better display atomic mixture quantum circuits. 175 | 176 | Note that in the future this method may be modified and tailored to fermionic quantum circuits. 177 | 178 | Args: 179 | qc: The quantum circuit to draw. 180 | draw_options: Key word arguments for the drawing of circuits. 181 | """ 182 | qc.draw(**draw_options) 183 | -------------------------------------------------------------------------------- /test/test_circuit_tools.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2021. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """tests for circuit_to_cold_atom functions""" 14 | 15 | from typing import Dict 16 | 17 | from qiskit import QuantumCircuit 18 | from qiskit.test import QiskitTestCase 19 | from qiskit.circuit import Parameter 20 | from qiskit.providers import BackendV1 as Backend 21 | from qiskit.providers.models import BackendConfiguration 22 | from qiskit_cold_atom.exceptions import QiskitColdAtomError 23 | from qiskit_cold_atom.circuit_tools import CircuitTools, WireOrder 24 | 25 | # These imports are needed to decorate the quantum circuit 26 | import qiskit_cold_atom.spins # pylint: disable=unused-import 27 | import qiskit_cold_atom.fermions # pylint: disable=unused-import 28 | 29 | 30 | class DummyBackend(Backend): 31 | """dummy backend class for test purposes only""" 32 | 33 | def __init__(self, config_dict: Dict): 34 | super().__init__(configuration=BackendConfiguration.from_dict(config_dict)) 35 | 36 | def run(self, run_input, **options): 37 | pass 38 | 39 | @classmethod 40 | def _default_options(cls): 41 | pass 42 | 43 | 44 | class TestCircuitToColdAtom(QiskitTestCase): 45 | """circuit to cold atom tests.""" 46 | 47 | def setUp(self): 48 | super().setUp() 49 | # Set up a dummy backend from a configuration dictionary 50 | 51 | test_config = { 52 | "backend_name": "test_backend", 53 | "backend_version": "0.0.1", 54 | "simulator": True, 55 | "local": True, 56 | "coupling_map": None, 57 | "description": "dummy backend for testing purposes only", 58 | "basis_gates": ["hop, int"], 59 | "memory": False, 60 | "n_qubits": 5, 61 | "conditional": False, 62 | "max_shots": 100, 63 | "max_experiments": 2, 64 | "open_pulse": False, 65 | "gates": [ 66 | { 67 | "coupling_map": [[0], [1], [2], [3], [4]], 68 | "name": "rlz", 69 | "parameters": ["delta"], 70 | "qasm_def": "gate rLz(delta) {}", 71 | }, 72 | { 73 | "coupling_map": [[0], [1], [2]], 74 | "name": "rlz2", 75 | "parameters": ["chi"], 76 | "qasm_def": "gate rlz2(chi) {}", 77 | }, 78 | { 79 | "coupling_map": [[0], [1], [2], [3], [4]], 80 | "name": "rlx", 81 | "parameters": ["omega"], 82 | "qasm_def": "gate rx(omega) {}", 83 | }, 84 | ], 85 | "supported_instructions": [ 86 | "delay", 87 | "rlx", 88 | "rlz", 89 | "rlz2", 90 | "measure", 91 | "barrier", 92 | ], 93 | } 94 | 95 | self.dummy_backend = DummyBackend(test_config) 96 | 97 | def test_circuit_to_cold_atom(self): 98 | """test the circuit_to_cold_atom function""" 99 | 100 | circ1 = QuantumCircuit(3) 101 | circ1.rlx(0.5, [0, 1]) 102 | circ1.rlz(0.3, [1, 2]) 103 | circ1.measure_all() 104 | 105 | circ2 = QuantumCircuit(2) 106 | circ2.rlz2(0.5, 1) 107 | circ2.measure_all() 108 | 109 | shots = 10 110 | 111 | target_output = { 112 | "experiment_0": { 113 | "instructions": [ 114 | ["rlx", [0], [0.5]], 115 | ["rlx", [1], [0.5]], 116 | ["rlz", [1], [0.3]], 117 | ["rlz", [2], [0.3]], 118 | ["barrier", [0, 1, 2], []], 119 | ["measure", [0], []], 120 | ["measure", [1], []], 121 | ["measure", [2], []], 122 | ], 123 | "num_wires": 3, 124 | "shots": shots, 125 | "wire_order": "sequential", 126 | }, 127 | "experiment_1": { 128 | "instructions": [ 129 | ["rlz2", [1], [0.5]], 130 | ["barrier", [0, 1], []], 131 | ["measure", [0], []], 132 | ["measure", [1], []], 133 | ], 134 | "num_wires": 2, 135 | "shots": shots, 136 | "wire_order": "sequential", 137 | }, 138 | } 139 | 140 | actual_output = CircuitTools.circuit_to_cold_atom( 141 | [circ1, circ2], backend=self.dummy_backend, shots=shots 142 | ) 143 | 144 | self.assertEqual(actual_output, target_output) 145 | 146 | def test_validate_circuits(self): 147 | """test the validation of circuits against the backend configuration""" 148 | 149 | with self.subTest("test size of circuit"): 150 | circ = QuantumCircuit(6) 151 | circ.rlx(0.4, 2) 152 | with self.assertRaises(QiskitColdAtomError): 153 | CircuitTools.validate_circuits(circ, backend=self.dummy_backend) 154 | 155 | with self.subTest("test support of native instructions"): 156 | circ = QuantumCircuit(4) 157 | # add gate that is not supported by the backend 158 | circ.fhop([0.5], [0, 1, 2, 3]) 159 | with self.assertRaises(QiskitColdAtomError): 160 | CircuitTools.validate_circuits(circ, backend=self.dummy_backend) 161 | 162 | with self.subTest("check gate coupling map"): 163 | circ = QuantumCircuit(5) 164 | circ.rlz2(0.5, 4) 165 | with self.assertRaises(QiskitColdAtomError): 166 | CircuitTools.validate_circuits(circ, backend=self.dummy_backend) 167 | 168 | with self.subTest("test max. allowed circuits"): 169 | circuits = [QuantumCircuit(2)] * 3 170 | with self.assertRaises(QiskitColdAtomError): 171 | CircuitTools.circuit_to_cold_atom(circuits=circuits, backend=self.dummy_backend) 172 | 173 | with self.subTest("test max. allowed shots"): 174 | circuits = QuantumCircuit(2) 175 | with self.assertRaises(QiskitColdAtomError): 176 | CircuitTools.circuit_to_cold_atom( 177 | circuits=circuits, backend=self.dummy_backend, shots=1000 178 | ) 179 | 180 | with self.subTest("test running with unbound parameters"): 181 | theta = Parameter("θ") 182 | circ = QuantumCircuit(1) 183 | circ.rlx(theta, 0) 184 | with self.assertRaises(QiskitColdAtomError): 185 | CircuitTools.validate_circuits(circ, backend=self.dummy_backend) 186 | 187 | def test_circuit_to_data(self): 188 | """test the circuit to data method""" 189 | 190 | circ = QuantumCircuit(3) 191 | circ.rlx(0.5, [0, 1]) 192 | circ.rlz(0.3, [1, 2]) 193 | circ.measure_all() 194 | 195 | target_output = [ 196 | ["rlx", [0], [0.5]], 197 | ["rlx", [1], [0.5]], 198 | ["rlz", [1], [0.3]], 199 | ["rlz", [2], [0.3]], 200 | ["barrier", [0, 1, 2], []], 201 | ["measure", [0], []], 202 | ["measure", [1], []], 203 | ["measure", [2], []], 204 | ] 205 | 206 | actual_output = CircuitTools.circuit_to_data(circ, backend=self.dummy_backend) 207 | 208 | self.assertEqual(actual_output, target_output) 209 | 210 | def test_convert_wire_order(self): 211 | """test the convert_wire_order method""" 212 | 213 | num_sites = 4 214 | num_species = 3 215 | # conversion rule: i -> (i % num_sites) * num_species + i // num_sites 216 | wires_sequential = [0, 1, 4, 5, 8, 9] 217 | wires_interleaved = [0, 3, 1, 4, 2, 5] 218 | 219 | with self.subTest("test sequential to interleaved"): 220 | wires_converted = CircuitTools.convert_wire_order( 221 | wires=wires_sequential, 222 | convention_from=WireOrder.SEQUENTIAL, 223 | convention_to=WireOrder.INTERLEAVED, 224 | num_sites=num_sites, 225 | num_species=num_species, 226 | ) 227 | self.assertEqual(wires_converted, wires_interleaved) 228 | 229 | with self.subTest("test interleaved to sequential"): 230 | wires_converted = CircuitTools.convert_wire_order( 231 | wires=wires_interleaved, 232 | convention_from=WireOrder.INTERLEAVED, 233 | convention_to=WireOrder.SEQUENTIAL, 234 | num_sites=num_sites, 235 | num_species=num_species, 236 | ) 237 | self.assertEqual(wires_converted, wires_sequential) 238 | -------------------------------------------------------------------------------- /qiskit_cold_atom/fermions/fermion_simulator_backend.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2021. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """General Fermion simulator backend.""" 14 | 15 | from typing import Union, List, Dict, Any, Optional 16 | import uuid 17 | import warnings 18 | import time 19 | import datetime 20 | 21 | from qiskit.providers.models import BackendConfiguration 22 | from qiskit.providers import Options 23 | from qiskit import QuantumCircuit 24 | from qiskit.result import Result 25 | from qiskit_aer import AerJob 26 | 27 | from qiskit_cold_atom.fermions.fermion_circuit_solver import FermionCircuitSolver 28 | from qiskit_cold_atom.fermions.base_fermion_backend import BaseFermionBackend 29 | from qiskit_cold_atom.circuit_tools import CircuitTools 30 | 31 | 32 | class FermionSimulator(BaseFermionBackend): 33 | """A simulator to simulate general fermionic circuits. 34 | 35 | This general fermion simulator backend simulates fermionic circuits with gates that have 36 | generators described by fermionic Hamiltonians. It computes the statevector and unitary 37 | of a circuit and simulates measurements. 38 | """ 39 | 40 | _DEFAULT_CONFIGURATION = { 41 | "backend_name": "fermion_simulator", 42 | "backend_version": "0.0.1", 43 | "n_qubits": 20, 44 | "basis_gates": None, 45 | "gates": [], 46 | "local": False, 47 | "simulator": True, 48 | "conditional": False, 49 | "open_pulse": False, 50 | "memory": True, 51 | "max_shots": 1e5, 52 | "coupling_map": None, 53 | "description": "a base simulator for fermionic circuits. Instead of qubits, each wire represents" 54 | " a single fermionic mode", 55 | "supported_instructions": None, 56 | } 57 | 58 | def __init__(self, config_dict: Dict[str, Any] = None, provider=None): 59 | """Initializing the backend from a configuration dictionary""" 60 | 61 | if config_dict is None: 62 | config_dict = self._DEFAULT_CONFIGURATION 63 | 64 | super().__init__( 65 | configuration=BackendConfiguration.from_dict(config_dict), provider=provider 66 | ) 67 | 68 | @classmethod 69 | def _default_options(cls): 70 | return Options(shots=1) 71 | 72 | def _execute(self, data: Dict[str, Any], job_id: str = ""): 73 | """Helper function to execute a job. The circuit and all relevant parameters are given in the 74 | data dict. Performs validation checks on the received circuits and utilizes 75 | the FermionCircuitSolver to perform the numerical simulations. 76 | 77 | Args: 78 | data: Data dictionary that that contains the experiments to simulate, given in the shape: 79 | data = { 80 | "num_species": int, 81 | "shots": int, 82 | "seed": int, 83 | "experiments": Dict[str, QuantumCircuit], 84 | } 85 | job_id: The job id assigned by the run method 86 | 87 | Returns: 88 | result: A qiskit job result. 89 | """ 90 | # Start timer 91 | start = time.time() 92 | 93 | output = {"results": []} 94 | 95 | num_species = data["num_species"] 96 | shots = data["shots"] 97 | seed = data["seed"] 98 | 99 | solver = FermionCircuitSolver(num_species=num_species, shots=shots, seed=seed) 100 | 101 | for exp_i, exp_name in enumerate(data["experiments"]): 102 | experiment = data["experiments"][exp_name] 103 | circuit = experiment["circuit"] 104 | 105 | # perform compatibility checks with the backend configuration in case gates and supported 106 | # instructions are constrained by the backend's configuration 107 | if self.configuration().gates and self.configuration().supported_instructions: 108 | CircuitTools.validate_circuits(circuits=circuit, backend=self, shots=shots) 109 | 110 | # check whether all wires are measured 111 | measured_wires = [] 112 | 113 | for inst in circuit.data: 114 | name = inst[0].name 115 | 116 | if name == "measure": 117 | for wire in inst[1]: 118 | index = circuit.qubits.index(wire) 119 | if index in measured_wires: 120 | warnings.warn( 121 | f"Wire {index} has already been measured, " 122 | f"second measurement is ignored" 123 | ) 124 | else: 125 | measured_wires.append(index) 126 | 127 | if measured_wires and len(measured_wires) != len(circuit.qubits): 128 | warnings.warn( 129 | f"Number of wires in circuit ({len(circuit.qubits)}) exceeds number of wires " 130 | + f" with assigned measurement instructions ({len(measured_wires)}). " 131 | + "This simulator backend only supports measurement of the entire quantum register " 132 | "which will instead be performed." 133 | ) 134 | 135 | # If there are no measurements, set shots to None 136 | if not measured_wires: 137 | solver.shots = None 138 | 139 | simulation_result = solver(circuit) 140 | 141 | output["results"].append( 142 | { 143 | "header": {"name": exp_name, "random_seed": seed}, 144 | "shots": shots, 145 | "status": "DONE", 146 | "success": True, 147 | } 148 | ) 149 | # add the simulation result at the correct place in the result dictionary 150 | output["results"][exp_i]["data"] = simulation_result 151 | 152 | output["job_id"] = job_id 153 | output["date"] = datetime.datetime.now().isoformat() 154 | output["backend_name"] = self.name() 155 | output["backend_version"] = self.configuration().backend_version 156 | output["time_taken"] = time.time() - start 157 | output["success"] = True 158 | output["qobj_id"] = None 159 | 160 | return Result.from_dict(output) 161 | 162 | # pylint: disable=arguments-differ, unused-argument 163 | def run( 164 | self, 165 | circuits: Union[QuantumCircuit, List[QuantumCircuit]], 166 | shots: int = 1000, 167 | seed: Optional[int] = None, 168 | num_species: int = 1, 169 | **run_kwargs, 170 | ) -> AerJob: 171 | """ 172 | Method to run circuits on the backend. 173 | 174 | Args: 175 | circuits: QuantumCircuit applying fermionic gates to run on the backend 176 | shots: Number of measurement shots taken in case the circuit has measure instructions 177 | seed: seed for the random number generator of the measurement simulation 178 | num_species: number of different fermionic species described by the circuits 179 | run_kwargs: Additional keyword arguments that might be passed down when calling 180 | qiskit.execute() which will have no effect on this backend. 181 | 182 | Returns: 183 | aer_job: a job object containing the result of the simulation 184 | """ 185 | 186 | if isinstance(circuits, QuantumCircuit): 187 | circuits = [circuits] 188 | 189 | data = { 190 | "num_species": num_species, 191 | "shots": shots, 192 | "seed": seed, 193 | "experiments": {}, 194 | } 195 | 196 | for idx, circuit in enumerate(circuits): 197 | data["experiments"][f"experiment_{idx}"] = { 198 | "circuit": circuit, 199 | } 200 | 201 | job_id = str(uuid.uuid4()) 202 | aer_job = AerJob(self, job_id, self._execute, data) 203 | aer_job.submit() 204 | return aer_job 205 | 206 | @staticmethod 207 | def get_basis(circuit: QuantumCircuit, num_species: int = 1): 208 | """Get the basis of fermionic states in occupation number representation for the simulation 209 | of a given quantum circuit. 210 | 211 | Args: 212 | circuit: A quantum circuit using Fermionic Gates. 213 | num_species: Number of different fermionic species described by the circuit. 214 | 215 | Returns: 216 | basis: the fermionic basis in which the simulation of the circuit is performed. 217 | """ 218 | solver = FermionCircuitSolver(num_species=num_species) 219 | solver.preprocess_circuit(circuit) 220 | basis = solver.basis 221 | 222 | return basis 223 | 224 | def draw(self, qc: QuantumCircuit, **draw_options): 225 | """Draw the circuit by defaulting to the draw method of QuantumCircuit. 226 | 227 | Note that in the future this method may be modified and tailored to fermion 228 | quantum circuits. 229 | 230 | Args: 231 | qc: The quantum circuit to draw. 232 | draw_options: Key word arguments for the drawing of circuits. 233 | """ 234 | qc.draw(**draw_options) 235 | -------------------------------------------------------------------------------- /test/test_fermionic_gates.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2021. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """ Fermionic gate tests """ 14 | 15 | import numpy as np 16 | from scipy.linalg import expm 17 | from qiskit.test import QiskitTestCase 18 | from qiskit_nature.second_q.operators import FermionicOp 19 | from qiskit_cold_atom.fermions.fermion_simulator_backend import FermionSimulator 20 | from qiskit_cold_atom.fermions.fermion_gate_library import ( 21 | Hop, 22 | Interaction, 23 | Phase, 24 | FRXGate, 25 | FRYGate, 26 | FRZGate, 27 | FermiHubbard, 28 | ) 29 | 30 | 31 | class TestFermionicGates(QiskitTestCase): 32 | """Tests for the fermionic gates""" 33 | 34 | def setUp(self): 35 | super().setUp() 36 | self.backend = FermionSimulator() 37 | 38 | def test_interaction_gate(self): 39 | """check matrix form of interaction gate in a two-tweezer example""" 40 | u_val = np.pi / 2 41 | circ = self.backend.initialize_circuit([[0, 1], [1, 0]]) 42 | circ.append(Interaction(4, u_val), qargs=[0, 1, 2, 3]) 43 | # add gate to circuit via the @add_gate-decorated method 44 | circ_decorated = self.backend.initialize_circuit([[0, 1], [1, 0]]) 45 | circ_decorated.fint(u_val, [0, 1, 2, 3]) 46 | 47 | for circuit in [circ, circ_decorated]: 48 | unitary = self.backend.run(circuit, num_species=2).result().get_unitary() 49 | 50 | self.assertTrue( 51 | np.allclose( 52 | unitary, 53 | expm( 54 | -1j 55 | * np.array( 56 | [ 57 | [u_val, 0, 0, 0], 58 | [0, 0, 0, 0], 59 | [0, 0, 0, 0], 60 | [0, 0, 0, u_val], 61 | ] 62 | ) 63 | ), 64 | ) 65 | ) 66 | 67 | def test_hopping_gate(self): 68 | """check matrix form of hopping gate in a two-tweezer example""" 69 | j = np.pi / 4 70 | circ = self.backend.initialize_circuit([[0, 1], [1, 0]]) 71 | circ.append(Hop(4, [j]), qargs=[0, 1, 2, 3]) 72 | # add gate to circuit via the @add_gate-decorated method 73 | circ_decorated = self.backend.initialize_circuit([[0, 1], [1, 0]]) 74 | circ_decorated.fhop([j], [0, 1, 2, 3]) 75 | 76 | for circuit in [circ, circ_decorated]: 77 | unitary = self.backend.run(circuit, num_species=2).result().get_unitary() 78 | self.assertTrue( 79 | np.allclose( 80 | unitary, 81 | expm( 82 | -1j 83 | * np.array( 84 | [ 85 | [0, -j, -j, 0], 86 | [-j, 0, 0, -j], 87 | [-j, 0, 0, -j], 88 | [0, -j, -j, 0], 89 | ] 90 | ) 91 | ), 92 | ) 93 | ) 94 | 95 | def test_phase_gate(self): 96 | """check matrix form of phase gate in a two-tweezer example""" 97 | phi1, phi2 = np.pi / 4, np.pi / 8 98 | circ = self.backend.initialize_circuit([[0, 1], [1, 0]]) 99 | circ.append(Phase(4, [phi1, phi2]), qargs=[0, 1, 2, 3]) 100 | # add gate to circuit via the @add_gate-decorated method 101 | circ_decorated = self.backend.initialize_circuit([[0, 1], [1, 0]]) 102 | circ_decorated.fphase([phi1, phi2], [0, 1, 2, 3]) 103 | 104 | for circuit in [circ, circ_decorated]: 105 | unitary = self.backend.run(circuit, num_species=2).result().get_unitary() 106 | 107 | self.assertTrue( 108 | np.allclose( 109 | unitary, 110 | expm( 111 | -1j 112 | * np.array( 113 | [ 114 | [2 * phi2, 0, 0, 0], 115 | [0, phi2 + phi1, 0, 0], 116 | [0, 0, phi2 + phi1, 0], 117 | [0, 0, 0, 2 * phi1], 118 | ] 119 | ) 120 | ), 121 | ) 122 | ) 123 | 124 | def test_spin_rx_gate(self): 125 | """check matrix form of spin_rx gate in a two-tweezer example""" 126 | phi = np.pi / 4 127 | circ = self.backend.initialize_circuit([[0, 1], [1, 0]]) 128 | circ.append(FRXGate(phi), qargs=[0, 2]) 129 | # add gate to circuit via the @add_gate-decorated method 130 | circ_decorated = self.backend.initialize_circuit([[0, 1], [1, 0]]) 131 | circ_decorated.frx(phi, [0, 2]) 132 | 133 | for circuit in [circ, circ_decorated]: 134 | unitary = self.backend.run(circuit, num_species=2).result().get_unitary() 135 | 136 | self.assertTrue( 137 | np.allclose( 138 | unitary, 139 | expm( 140 | -1j 141 | * np.array( 142 | [ 143 | [0, 0, 0, phi, 0, 0], 144 | [0, 0, 0, 0, 0, 0], 145 | [0, 0, 0, 0, 0, -phi], 146 | [phi, 0, 0, 0, 0, 0], 147 | [0, 0, 0, 0, 0, 0], 148 | [0, 0, -phi, 0, 0, 0], 149 | ] 150 | ) 151 | ), 152 | ) 153 | ) 154 | 155 | def test_spin_ry_gate(self): 156 | """check matrix form of spin_ry gate in a two-tweezer example""" 157 | phi = np.pi / 4 158 | circ = self.backend.initialize_circuit([[0, 1], [1, 0]]) 159 | circ.append(FRYGate(phi), qargs=[0, 2]) 160 | # add gate to circuit via the @add_gate-decorated method 161 | circ_decorated = self.backend.initialize_circuit([[0, 1], [1, 0]]) 162 | circ_decorated.fry(phi, [0, 2]) 163 | 164 | for circuit in [circ, circ_decorated]: 165 | unitary = self.backend.run(circuit, num_species=2).result().get_unitary() 166 | 167 | self.assertTrue( 168 | np.allclose( 169 | unitary, 170 | expm( 171 | -1j 172 | * np.array( 173 | [ 174 | [0, 0, 0, 1j * phi, 0, 0], 175 | [0, 0, 0, 0, 0, 0], 176 | [0, 0, 0, 0, 0, -1j * phi], 177 | [-1j * phi, 0, 0, 0, 0, 0], 178 | [0, 0, 0, 0, 0, 0], 179 | [0, 0, 1j * phi, 0, 0, 0], 180 | ] 181 | ) 182 | ), 183 | ) 184 | ) 185 | 186 | def test_spin_rz_gate(self): 187 | """check matrix form of spin_rz gate in a two-tweezer example""" 188 | phi = np.pi / 4 189 | circ = self.backend.initialize_circuit([[0, 1], [1, 0]]) 190 | circ.append(FRZGate(phi), qargs=[0, 2]) 191 | # add gate to circuit via the @add_gate-decorated method 192 | circ_decorated = self.backend.initialize_circuit([[0, 1], [1, 0]]) 193 | circ_decorated.frz(phi, [0, 2]) 194 | 195 | for circuit in [circ, circ_decorated]: 196 | unitary = self.backend.run(circuit, num_species=2).result().get_unitary() 197 | 198 | self.assertTrue( 199 | np.allclose( 200 | unitary, 201 | expm( 202 | -1j 203 | * np.array( 204 | [ 205 | [0, 0, 0, 0], 206 | [0, -phi, 0, 0], 207 | [0, 0, phi, 0], 208 | [0, 0, 0, 0], 209 | ] 210 | ) 211 | ), 212 | ) 213 | ) 214 | 215 | def test_fermionic_gate_class(self): 216 | """test the functionality of the base class for fermionic gates""" 217 | 218 | test_gates = [ 219 | Hop(num_modes=4, j=[0.5]), 220 | Interaction(num_modes=8, u=2.0), 221 | Phase(num_modes=2, mu=[1.0]), 222 | FRXGate(phi=0.5), 223 | FRYGate(phi=-0.7), 224 | FRZGate(phi=1.3), 225 | FermiHubbard(num_modes=4, j=[0.5], u=1.0, mu=[0.4, 1.2]), 226 | ] 227 | 228 | with self.subTest("test to_matrix and power"): 229 | for gate in test_gates: 230 | exp_matrix = gate.to_matrix() @ gate.to_matrix() 231 | exp_gate = gate.power(2) 232 | self.assertTrue(np.allclose(exp_matrix, exp_gate.to_matrix())) 233 | 234 | with self.subTest("test generation of operator"): 235 | from qiskit.quantum_info import Operator 236 | 237 | for gate in test_gates: 238 | self.assertTrue(isinstance(Operator(gate), Operator)) 239 | 240 | def test_identity_gates(self): 241 | """test that gates with parameters equal to zero still have a well-defined generator.""" 242 | test_gates = [ 243 | Hop(num_modes=4, j=[0.0]), 244 | Interaction(num_modes=4, u=0.0), 245 | Phase(num_modes=2, mu=[0.0]), 246 | FRXGate(phi=0.0), 247 | FRYGate(phi=-0.0), 248 | FRZGate(phi=0.0), 249 | FermiHubbard(num_modes=4, j=[0.0], u=0.0, mu=[0.0, 0.0]), 250 | ] 251 | 252 | for gate in test_gates: 253 | self.assertIsInstance(gate.generator, FermionicOp) 254 | -------------------------------------------------------------------------------- /docs/images/qiskit_cold_atom_logo_with_text.svg: -------------------------------------------------------------------------------- 1 | QiskitCold Atom --------------------------------------------------------------------------------