diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..286990d --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,69 @@ +name: Build Sphinx docs + +on: + workflow_dispatch: + push: + tags: + - "[0-9]+.[0-9]+.[0-9]+*" + branches: + - main + - 'stable/**' + pull_request: + branches: + - main + - 'stable/**' +jobs: + build: + name: Build docs + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v7 + with: + # reno reads the tags and their history to assemble the release notes, and + # setup.py needs the vendored upstream headers to compile the extension. + fetch-depth: 0 + submodules: recursive + - uses: actions/setup-python@v7 + with: + python-version: '3.10' + - name: Install dependencies + # autodoc imports the package, so the extension has to compile here -- hence the + # same MPI and BLAS packages the test workflow installs, rather than the lighter + # set a pure-Python docs build would need. + run: | + python -m pip install --upgrade pip + pip install tox + sudo apt-get update + sudo apt-get install -y libopenmpi-dev openmpi-bin libopenblas-dev + - name: Build docs + shell: bash + # CPU only: the runner has no GPU toolchain, and the API reference does not + # depend on which backends were compiled. + env: + SBD_BUILD_BACKEND: cpu + run: | + tox -edocs + - name: Upload docs artifact + # Uploaded even on failure, which pairs with sphinx-build's --keep-going: the + # partial HTML is usually the fastest way to see what went wrong. + if: always() + uses: actions/upload-pages-artifact@v5 + with: + path: docs/_build/html + + deploy: + name: Deploy docs + if: ${{ github.ref == 'refs/heads/main' }} + needs: build + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 diff --git a/.gitignore b/.gitignore index 58e68a6..ef20cb7 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,10 @@ build/ dist/ *.egg-info/ +# Sphinx documentation +docs/_build/ +docs/stubs/ + # editor / OS noise .DS_Store *.swp diff --git a/docs/_static/.gitkeep b/docs/_static/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/_static/images/qiskit-dark-logo.svg b/docs/_static/images/qiskit-dark-logo.svg new file mode 100644 index 0000000..b520890 --- /dev/null +++ b/docs/_static/images/qiskit-dark-logo.svg @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_static/images/qiskit-light-logo.svg b/docs/_static/images/qiskit-light-logo.svg new file mode 100644 index 0000000..25b27dd --- /dev/null +++ b/docs/_static/images/qiskit-light-logo.svg @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_templates/autosummary/class.rst b/docs/_templates/autosummary/class.rst new file mode 100644 index 0000000..175b08f --- /dev/null +++ b/docs/_templates/autosummary/class.rst @@ -0,0 +1,33 @@ +{# + We show all the class's methods and attributes on the same page. By default, we document + all methods, including those defined by parent classes. +-#} + +{{ objname | escape | underline }} + +.. currentmodule:: {{ module }} + +.. autoclass:: {{ objname }} + :no-members: + :no-inherited-members: + :no-special-members: + :show-inheritance: + +{% block attributes_summary %} + {% if attributes %} + .. rubric:: Attributes + {% for item in attributes %} + .. autoattribute:: {{ item }} + {%- endfor %} + {% endif %} +{% endblock -%} + +{% block methods_summary %} + {% set wanted_methods = (methods | reject('==', '__init__') | list) %} + {% if wanted_methods %} + .. rubric:: Methods + {% for item in wanted_methods %} + .. automethod:: {{ item }} + {%- endfor %} + {% endif %} +{% endblock %} diff --git a/docs/apidocs/index.rst b/docs/apidocs/index.rst new file mode 100644 index 0000000..58288ee --- /dev/null +++ b/docs/apidocs/index.rst @@ -0,0 +1,10 @@ +********************************* +``sbd-eigensolver`` API reference +********************************* + +.. toctree:: + :maxdepth: 1 + + sbd + sbd.sbd_solver + sbd.device_config diff --git a/docs/apidocs/sbd.device_config.rst b/docs/apidocs/sbd.device_config.rst new file mode 100644 index 0000000..7c1a7d1 --- /dev/null +++ b/docs/apidocs/sbd.device_config.rst @@ -0,0 +1,8 @@ +================================================ +Device configuration (:mod:`sbd.device_config`) +================================================ + +.. automodule:: sbd.device_config + :members: + :no-inherited-members: + :no-special-members: diff --git a/docs/apidocs/sbd.rst b/docs/apidocs/sbd.rst new file mode 100644 index 0000000..2e256e8 --- /dev/null +++ b/docs/apidocs/sbd.rst @@ -0,0 +1,9 @@ +============================= +SBD bindings (:mod:`sbd`) +============================= + +.. automodule:: sbd + :members: + :exclude-members: sbd_solver + :no-inherited-members: + :no-special-members: diff --git a/docs/apidocs/sbd.sbd_solver.rst b/docs/apidocs/sbd.sbd_solver.rst new file mode 100644 index 0000000..65be11d --- /dev/null +++ b/docs/apidocs/sbd.sbd_solver.rst @@ -0,0 +1,8 @@ +============================================= +SQD-compatible solver (:mod:`sbd.sbd_solver`) +============================================= + +.. automodule:: sbd.sbd_solver + :members: + :no-inherited-members: + :no-special-members: diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..47eeaa6 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,180 @@ +# This code is a Qiskit project. +# +# (C) Copyright IBM 2026. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Sphinx configuration for the sbd-eigensolver documentation.""" + +import inspect +import os +import re +import sys +from importlib.metadata import version as metadata_version + +# Note there is deliberately no `sys.path` manipulation here. The import name of this +# package (`sbd`) does not match the directory it lives in (`python/`); the mapping is +# declared as `package-dir = {sbd = "python"}` in pyproject.toml and only setuptools +# knows about it. Autodoc therefore needs the package genuinely *installed*, which is +# also what makes the `metadata_version` call below work. + +project = "Selected Basis Diagonalization (SBD)" +project_copyright = "2026, IBM Quantum" +description = "Python bindings for the SBD eigensolver library" +author = "IBM Quantum" +language = "en" +# The *distribution* name ("sbd-eigensolver"), not the import name ("sbd"). +release = metadata_version("sbd-eigensolver") + +html_theme = "qiskit-ecosystem" + +html_theme_options = { + "dark_logo": "images/qiskit-dark-logo.svg", + "light_logo": "images/qiskit-light-logo.svg", + "sidebar_qiskit_ecosystem_member": False, +} +html_static_path = ["_static"] +templates_path = ["_templates"] + +# Sphinx should ignore these patterns when building. +exclude_patterns = [ + "_build", +] + +extensions = [ + "sphinx.ext.napoleon", + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.mathjax", + "sphinx.ext.linkcode", + "sphinx.ext.intersphinx", + "sphinx_copybutton", + "reno.sphinxext", + "qiskit_sphinx_theme", +] + +html_last_updated_fmt = "%Y/%m/%d" +html_title = f"{project} {release}" + +# This allows RST files to put `|version|` in their file and +# have it updated with the release set in conf.py. +rst_prolog = f""" +.. |version| replace:: {release} +""" + +# Options for autodoc. These reflect the values from Qiskit SDK and Runtime. +autosummary_generate = True +autosummary_generate_overwrite = False +autoclass_content = "both" +autodoc_typehints = "description" +autodoc_default_options = { + "inherited-members": None, + "show-inheritance": True, +} +napoleon_google_docstring = True +napoleon_numpy_docstring = False + +# This adds numbers to the captions for figures, tables, +# and code blocks. +numfig = True +numfig_format = {"table": "Table %s"} + +add_module_names = False + +modindex_common_prefix = ["sbd."] + +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), + "numpy": ("https://numpy.org/doc/stable/", None), +} + +# ---------------------------------------------------------------------------------- +# Source code links +# ---------------------------------------------------------------------------------- + +# The package is imported as `sbd` but stored in the repository under `python/`, so the +# two halves of a source link need different spellings: the installed file path is +# split on one, and the URL is built with the other. +_IMPORT_NAME = "sbd" +_REPO_SUBDIR = "python" + + +def determine_github_branch() -> str: + """Determine the GitHub branch name to use for source code links. + + We need to decide whether to use `stable/` vs. `main` for dev builds. + Refer to https://docs.github.com/en/actions/learn-github-actions/variables + for how we determine this with GitHub Actions. + """ + # If CI env vars not set, default to `main`. This is relevant for local builds. + if "GITHUB_REF_NAME" not in os.environ: + return "main" + + # PR workflows set the branch they're merging into. + if base_ref := os.environ.get("GITHUB_BASE_REF"): + return base_ref + + ref_name = os.environ["GITHUB_REF_NAME"] + + # Check if the ref_name is a tag like `1.0.0` or `1.0.0rc1`. If so, we need + # to transform it to a Git branch like `stable/1.0`. + version_without_patch = re.match(r"(\d+\.\d+)", ref_name) + return f"stable/{version_without_patch.group()}" if version_without_patch else ref_name + + +GITHUB_BRANCH = determine_github_branch() + + +def linkcode_resolve(domain, info): + """Point the "source" link of each documented object at GitHub.""" + if domain != "py": + return None + + module_name = info["module"] + module = sys.modules.get(module_name) + if module is None or _IMPORT_NAME not in module_name: + return None + + def is_valid_code_object(obj): + return inspect.isclass(obj) or inspect.ismethod(obj) or inspect.isfunction(obj) + + obj = module + for part in info["fullname"].split("."): + try: + obj = getattr(obj, part) + except AttributeError: + return None + if not is_valid_code_object(obj): + return None + + # Unwrap decorators. This requires they used `functools.wrap()`. + while hasattr(obj, "__wrapped__"): + obj = obj.__wrapped__ + if not is_valid_code_object(obj): + return None + + try: + full_file_name = inspect.getsourcefile(obj) + except TypeError: + return None + if full_file_name is None or f"/{_IMPORT_NAME}/" not in full_file_name: + return None + file_name = full_file_name.split(f"/{_IMPORT_NAME}/")[-1] + + try: + source, lineno = inspect.getsourcelines(obj) + except (OSError, TypeError): + linespec = "" + else: + ending_lineno = lineno + len(source) - 1 + linespec = f"#L{lineno}-L{ending_lineno}" + return ( + "https://github.com/Qiskit/sbd-eigensolver-python/tree/" + f"{GITHUB_BRANCH}/{_REPO_SUBDIR}/{file_name}{linespec}" + ) diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..4dcd859 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,56 @@ +##################################### +Selected Basis Diagonalization (SBD) +##################################### + +``sbd-eigensolver`` provides Python bindings for the SBD (Selected Basis +Diagonalization) library, which finds eigenvalues and eigenvectors of a +second-quantized Hamiltonian projected onto a subspace spanned by a selected set of +determinants. The bindings are MPI-parallel and can run on CPUs or, where a suitable +toolchain is available, on NVIDIA or AMD GPUs. + +The package also exposes a solver compatible with the ``qiskit-addon-sqd`` interface, +so SBD can be used as the diagonalization step of a sample-based quantum +diagonalization (SQD) workflow. See :mod:`sbd.sbd_solver`. + +Getting started +--------------- + +Installation, the environment variables that control which backends are compiled, and +runnable examples are documented in the `README +`__ in the root +of this project's repository. Example scripts and a notebook live in `python/examples +`__. + +A minimal diagonalization looks like this:: + + import sbd + + config = sbd.TPB_SBD() + results = sbd.tpb_diag_from_files("FCIDUMP", "adets.dat", config) + +The backend is initialized automatically on first use; :func:`sbd.init` only needs to +be called to select a device explicitly. + +Contributing +------------ + +The source code is available `on GitHub +`__. + +We use `GitHub issues +`__ for tracking requests and +bugs. + +License +------- + +`Apache License 2.0 +`__ + +.. toctree:: + :hidden: + + Documentation home + API reference + Release notes + GitHub diff --git a/docs/release-notes.rst b/docs/release-notes.rst new file mode 100644 index 0000000..9523d94 --- /dev/null +++ b/docs/release-notes.rst @@ -0,0 +1,3 @@ +.. _release notes: + +.. release-notes:: Release Notes diff --git a/pyproject.toml b/pyproject.toml index ab324c3..a235da2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,16 @@ nbtest = [ "sbd-eigensolver[basetest]", "nbmake>=1.5.0", ] +docs = [ + # The API reference is generated from the docstrings by autodoc, which imports the + # package -- so the docs build needs the compiled extension, and `sbd.sbd_solver` + # additionally needs qiskit-addon-sqd and pyscf importable. The `test` extra + # already pulls both in. + "sbd-eigensolver[test]", + "qiskit-sphinx-theme~=2.1.0", + "sphinx-copybutton", + "reno>=4.1", +] notebook-dependencies = [ "qiskit-addon-sqd>=0.13.1", "pyscf>=2.9", diff --git a/python/__init__.py b/python/__init__.py index c2a683c..33ac1b5 100644 --- a/python/__init__.py +++ b/python/__init__.py @@ -321,11 +321,11 @@ def init(device='cpu', comm_backend='mpi'): Args: device: Default compute device — 'cpu', 'gpu', 'gpu-omp', or 'auto'. - 'gpu' is the NVIDIA-only Thrust backend; 'gpu-omp' is OpenMP - target offload and serves NVIDIA and AMD alike. - Aliases: 'gpu-thrust' / 'gpu-nvidia' / 'cuda' (= 'gpu'); - 'gpu-omp-offload' / 'gpu-nvhpc-omp' / 'gpu-nvidia-omp' / - 'gpu-amd-omp' / 'gpu-rocm-omp' / 'rocm' (= 'gpu-omp'). + 'gpu' is the NVIDIA-only Thrust backend; 'gpu-omp' is OpenMP + target offload and serves NVIDIA and AMD alike. + Aliases for 'gpu': 'gpu-thrust', 'gpu-nvidia', 'cuda'. + Aliases for 'gpu-omp': 'gpu-omp-offload', 'gpu-nvhpc-omp', + 'gpu-nvidia-omp', 'gpu-amd-omp', 'gpu-rocm-omp', 'rocm'. comm_backend: Communication backend — 'mpi'. Raises: diff --git a/python/sbd_solver.py b/python/sbd_solver.py index e9fb489..d22b13e 100644 --- a/python/sbd_solver.py +++ b/python/sbd_solver.py @@ -285,9 +285,11 @@ def assemble_rdms(results: dict, norb: int) -> tuple[np.ndarray | None, np.ndarr qiskit-addon-sqd (e.g. ``fermion.py``'s own ``solve_fermion``). SBD's documented layout (sbd-ext docs/user-guide.md, matching the C++ - reference in apps/chemistry_tpb_selected_basis_diagonalization/main.cc): + reference in apps/chemistry_tpb_selected_basis_diagonalization/main.cc):: + one_p_rdm[s][i + L*j] = two_p_rdm[s+2t][i + L*j + L^2*k + L^3*l] = + A Fortran-order reshape implements those flat-index formulas directly (arr_F[i, j] / arr_F[i, j, k, l]); rdm1 needs no further transpose (it is symmetric here regardless), and rdm2's spin-summed block sum diff --git a/releasenotes/config.yaml b/releasenotes/config.yaml new file mode 100644 index 0000000..0513169 --- /dev/null +++ b/releasenotes/config.yaml @@ -0,0 +1,5 @@ +--- +encoding: utf8 +default_branch: main +unreleased_version_title: "Upcoming release" +earliest_version: 1.6.1 diff --git a/releasenotes/notes/.gitkeep b/releasenotes/notes/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tox.ini b/tox.ini index dd3ecb9..55f0681 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 4.4.3 -envlist = py{310,311,312,313,314}{,-notebook}, mpi +envlist = py{310,311,312,313,314}{,-notebook}, mpi, docs isolated_build = True [testenv] @@ -71,6 +71,31 @@ extras = commands = pytest --nbmake --nbmake-timeout=3000 {posargs} python/examples/ +[testenv:docs] +# Unlike a pure-Python project, the docs build cannot skip installing the package: +# autodoc imports `sbd` to read its docstrings, and conf.py reads the version from the +# installed distribution metadata. `package`/`wheel_build_env` and the MPI/BLAS +# `passenv` are therefore inherited from [testenv] -- the extension has to compile +# here just as it does for the tests, and it reuses the same wheel. +extras = + docs +passenv = + {[testenv]passenv} + # Consulted by conf.py's determine_github_branch() to aim the source-code links at + # the right branch. + CI + GITHUB_BASE_REF + GITHUB_REF_NAME +commands = + sphinx-build -j auto -W -T --keep-going -b html {posargs} {toxinidir}/docs/ {toxinidir}/docs/_build/html + +[testenv:docs-clean] +skip_install = true +allowlist_externals = + rm +commands = + rm -rf {toxinidir}/docs/stubs/ {toxinidir}/docs/_build/ + [testenv:slow] # The reference cases grow by roughly an order of magnitude in determinant count per # row of the upstream tables, and compute time scales worse than linearly in that, so