diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml index 1ca5496..2080915 100644 --- a/.github/actions/build/action.yml +++ b/.github/actions/build/action.yml @@ -7,7 +7,7 @@ runs: run: python3 ./.github/compiler_setup.py ${CXX} ${BUILD_OS} - name: Install Python dependencies shell: bash - run: python -m pip install cmake scikit-build-core + run: python -m pip install cmake scikit-build-core setuptools_scm build - name: Build shell: bash run: | @@ -24,5 +24,4 @@ runs: - name: Build Publishable release distributions for PyPI shell: bash run: | - python -m pip install build python -m build -s diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2f21a1e..7c6df98 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,7 +7,7 @@ on: permissions: id-token: write # This is required for requesting the JWT jobs: - main-build: + source_dist_build: runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -28,5 +28,23 @@ jobs: env: BUILD_OS: ${{ matrix.os }} CXX: ${{ matrix.compiler }} - - name: Publish release distributions to PyPI + - name: Publish source dist release distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 + wheels_build: + name: Build wheels on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + # macos-13 is an intel runner, macos-14 is apple silicon + # os: [ubuntu-latest, macos-13, macos-14] + os: [ubuntu-latest] + steps: + - uses: actions/checkout@v4 + - name: Build wheels + uses: pypa/cibuildwheel@v2.21.2 + with: + config-file: "{package}/pyproject.toml" + - name: Publish wheel release distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: wheelhouse diff --git a/CMakeLists.txt b/CMakeLists.txt index aad11ec..18d1243 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -74,7 +74,7 @@ find_package(OpenMP REQUIRED) # Setup Python find_package(Python 3.10 # This is a minimum version - COMPONENTS Interpreter Development REQUIRED) + COMPONENTS Interpreter Development.Module REQUIRED) # Our primary target add_library(fast_pauli INTERFACE) diff --git a/Makefile b/Makefile index a20403c..3fd31aa 100644 --- a/Makefile +++ b/Makefile @@ -99,7 +99,7 @@ format: .PHONY: clean clean: - rm -rf build dist + rm -rf build dist wheelhouse .PHONY: pre-commit-setup pre-commit-setup: diff --git a/docs/getting_started.rst b/docs/getting_started.rst index fa9a7b0..886d6df 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -3,17 +3,33 @@ Getting Started ===================== -Welcome to Fast-Pauli from `Qognitive `_, an open-source Python / C++ library for optimized operations on Pauli matrices and Pauli strings -based on `PauliComposer `_. In this guide, -we'll introduce some of the important operations to help users get started. For more details, -see the API documentation. +Welcome to :code:`fast-pauli` from `Qognitive `_, an open-source Python / C++ library for optimized operations on Pauli matrices and Pauli strings +based on `PauliComposer `_. +In this guide, we'll introduce some of the important operations to help users get started as well as some conceptual background on Pauli matrices and Pauli strings. -For tips on installing the library, check out the guide: :doc:`index`. +If you want to follow along with this guide, please follow the installation instructions in :doc:`index`. +For more details on our programmatic interface, see the :doc:`python_api` or :doc:`cpp_api` documentation. Pauli Matrices ------------------------ -For a conceptual overview of Pauli matrices, see `here `_. +For a more in-depth overview of Pauli matrices, see `here `_. + +In math and physics, a `Pauli matrix `_, named after the physicist Wolfgang Pauli, is any one of the special 2 x 2 complex matrices in the set (often denoted by the greek letter :math:`\sigma`) : + +.. math:: + + \sigma_x = \begin{bmatrix} 0 & 1 \\ 1 & 0 \end{bmatrix} + \sigma_y = \begin{bmatrix} 0 & -i \\ i & 0 \end{bmatrix} + \sigma_z = \begin{bmatrix} 1 & 0 \\ 0 & -1 \end{bmatrix} + +All the Pauli matrices share the properties that they are: + +1. Hermitian (equal to their own conjugate transpose) :math:`\sigma_i = \sigma_i^\dagger` for all :math:`i \in \{x, y, z\}` +2. Involutory (they are their own inverse) :math:`\sigma_i^2 = \sigma_0` for all :math:`i \in \{x, y, z\}` +3. Unitary (their inverse is equal to their conjugate transpose) :math:`\sigma_i^{-1} = \sigma_i^\dagger` for all :math:`i \in \{x, y, z\}` + +with the identity matrix :math:`\sigma_0` or the :math:`2 \times 2` Identity matrix :math:`I` being the trivial case. In ``fast_pauli``, we represent pauli matrices using the ``Pauli`` class. For example, to represent the Pauli matrices, we can do: @@ -26,13 +42,17 @@ In ``fast_pauli``, we represent pauli matrices using the ``Pauli`` class. For ex pauli_y = fp.Pauli('Y') pauli_z = fp.Pauli('Z') -We can also multiply two ``Pauli`` objects together to get a ``Pauli`` object representing the tensor product of the two pauli matrices. + str(pauli_0) # returns "I" + +We can also multiply two ``Pauli`` objects together to get a ``Pauli`` object representing the matrix product of the two pauli matrices. +The result includes a phase factor because the product of two Pauli matrices is not another Pauli matrix, but rather a Pauli matrix multiplied by either ``i`` or ``-i`` which we call a phase factor. + +For example, we can compute the resulting ``Pauli`` object from multiplying :math:`\sigma_x` and :math:`\sigma_y` as follows: .. code-block:: python + # phase = i, new_pauli = fp.Pauli('Z') phase, new_pauli = pauli_x @ pauli_y - # returns "I" - str(pauli_0) From here, we can also convert our ``Pauli`` object back to a dense numpy array if we'd like: @@ -43,7 +63,23 @@ From here, we can also convert our ``Pauli`` object back to a dense numpy array Pauli Strings ------------------------ -In ``fast_pauli``, we represent Pauli strings using the ``PauliString`` class. For example, to construct the Pauli string ``X, Y, Z``, we can do: +Pauli strings are tensor-product combinations of Pauli matrices. For example, the following is a valid Pauli string: + +.. math:: + + \mathcal{\hat{P}} = \sigma_x \otimes \sigma_y \otimes \sigma_z + +where :math:`\otimes` denotes the tensor or `Kronecker `_ product, and we can more simply denote by + +.. math:: + + \mathcal{\hat{P}} = XYZ + +A Pauli string of length ``N`` is a tensor product of ``N`` +Pauli matrices. We can represent a ``N``-length Pauli String as a :math:`2^N \times 2^N` operator or matrix (in physics, an operator is represented by a matrix in a given basis), so ``XYZ`` is a :math:`8 \times 8` matrix. +Since these operators can get large very quickly, ``fast_pauli`` represents Pauli strings sparsely as an array of ``Pauli`` objects. + +For example, to construct the Pauli string ``XYZ``, we can do: .. code-block:: python @@ -61,19 +97,17 @@ Pauli Strings also support operations like addition, multiplication, and more. F P1.dim P1.n_qubits - # Add two Pauli strings. Return type is a PauliOp because - # the product is not a Pauli string - P3 = P1 + P2 - # Multiply two Pauli strings. phase, new_string = P1 @ P2 -We can also do more complicated things, like compute the action of a Pauli string :math:`\mathcal{\hat{P}}` on a quantum state :math:`| \psi \rangle`, :math:`\mathcal{\hat{P}}| \psi \rangle`, or -compute the expectation value of a Pauli string with a state :math:`\langle \psi | \mathcal{\hat{P}} | \psi \rangle`: +We can also do more complicated things, like compute the action of a Pauli string :math:`\mathcal{\hat{P}}` on a vector :math:`| \psi \rangle`, :math:`\mathcal{\hat{P}}| \psi \rangle`, or +compute the expectation value of a Pauli string with a state :math:`\langle \psi | \mathcal{\hat{P}} | \psi \rangle`. As a side note, in this guide we will use state and vector interchangeably: .. code-block:: python + import numpy as np + # Apply P to a state P = fp.PauliString('XY') state = np.array([1, 0, 0, 1], dtype=complex) @@ -82,7 +116,7 @@ compute the expectation value of a Pauli string with a state :math:`\langle \psi # Compute the expected value of P with respect to a state or a batch of states value = P.expectation_value(state) - states = np.random.randn(8, 8) + 1j * np.random.randn(8, 8) + states = np.random.randn(4, 8) + 1j * np.random.randn(4, 8) values = P.expectation_value(states) We can also convert ``PauliString`` objects back to dense numpy arrays if we'd like, or extract their string representation: @@ -91,16 +125,16 @@ We can also convert ``PauliString`` objects back to dense numpy arrays if we'd l P = fp.PauliString('XYZ') P_np = P.to_tensor() - # Returns "XYZ" - P_str = str(P) -For more details on the ``PauliString`` class, see the Python or C++ API documentation. + P_str = str(P) # Returns "XYZ" + +For more details on the ``PauliString`` class, see the :doc:`python_api` or :doc:`cpp_api` documentation. Pauli Operators ------------------------ -The ``PauliOp`` class lets us represent operators that are linear combinations of Pauli strings with complex coefficients. More specifically, -we can represent an arbitrary operator :math:`A` as a sum of Pauli strings :math:`P_i` with complex coefficients :math:`c_i`: +The ``PauliOp`` class lets us represent operators that are linear combinations of Pauli strings with complex coefficients. +For example, we can represent any arbitrary operator :math:`A` as a sum of Pauli strings :math:`P_i` with complex coefficients :math:`c_i`: .. math:: @@ -111,8 +145,8 @@ that represents the operator :math:`A = 0.5 * XYZ + 0.5 * YYZ`, we can do: .. code-block:: python - coeffs = np.array([0.5, 0.5], dtype=complex) - pauli_strings = ['XYZ', 'YYZ'] + coeffs = np.array([0.5, 0.5], dtype=complex) # represent c_i in the sum above + pauli_strings = ['XYZ', 'YYZ'] # represent P_i in the sum above A = fp.PauliOp(coeffs, pauli_strings) # Get the number of qubits the operator acts on, @@ -122,8 +156,8 @@ that represents the operator :math:`A = 0.5 * XYZ + 0.5 * YYZ`, we can do: A.dim A.n_pauli_strings -Just like with ``PauliString`` objects, we can apply ``PauliOp`` objects to a set of quantum states or compute expectation values, as well as arithmetic -operations and dense matrix conversions. Just like with ``PauliString`` objects, we can also convert ``PauliOp`` objects back to dense numpy arrays if we'd like +Just like with ``PauliString`` objects, we can apply ``PauliOp`` objects to a set of vectors, or compute expectation values, as well as arithmetic +operations. Just like with ``PauliString`` objects, we can also convert ``PauliOp`` objects back to dense numpy arrays if we'd like or get their string representation, in this case a list of strings: .. code-block:: python @@ -132,6 +166,13 @@ or get their string representation, in this case a list of strings: pauli_strings = ['XYZ', 'YYZ'] A = fp.PauliOp(coeffs, pauli_strings) + # Adding two Pauli strings returns a PauliOp. + # The returned object is a PauliOp because + # the sum is a linear combination of Pauli strings + P1 = fp.PauliString('XYZ') + P2 = fp.PauliString('YZX') + O = P1 + P2 + # PauliOp supports addition, subtraction, multiplication, # scaling, as well as have PauliString objects # as the second operand. All valid operations: @@ -141,8 +182,8 @@ or get their string representation, in this case a list of strings: s = fp.PauliString('XYZ') A4 = A1 + s - # Apply A to a state or set of states - states = np.random.rand(10, 8) + 1j * np.random.rand(10, 8) + # Apply A to a single state / vector or set + states = np.random.rand(8, 10) + 1j * np.random.rand(8, 10) new_states = A.apply(states) # Compute the expectation value of A with respect to a state @@ -156,19 +197,19 @@ or get their string representation, in this case a list of strings: Qiskit Integration ------------------------ -``Fast-Pauli`` also has integration with `IBM's Qiskit SDK `_, allowing for easy interfacing with certain Qiskit objects. For example, we can convert +``fast_pauli`` also has integration with `IBM's Qiskit SDK `_, allowing for easy interfacing with certain Qiskit objects. For example, we can convert between ``PauliOp`` objects and ``SparsePauliOp`` objects from Qiskit: .. code-block:: python - # Convert a Fast-Pauli PauliOp to a Qiskit SparsePauliOp object and back + # Convert a fast_pauli PauliOp to a Qiskit SparsePauliOp object and back O = fp.PauliOp([1], ['XYZ']) qiskit_op = fp.to_qiskit(O) fast_pauli_op = fp.from_qiskit(qiskit_op) - # Convert a Fast-Pauli PauliString to a Qiskit Pauli object + # Convert a fast_pauli PauliString to a Qiskit Pauli object P = fp.PauliString('XYZ') qiskit_pauli = fp.to_qiskit(P) -For more details on Qiskit conversions, see the Python or C++ API documentation. +For more details on Qiskit conversions, see the :doc:`python_api` documentation. diff --git a/pyproject.toml b/pyproject.toml index 8ea1ba8..ae8ba40 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ requires = [ # scikit-build-core uses cmake as needed (do not list it here) "scikit-build-core", + "setuptools_scm[toml]>=8", ] build-backend = "scikit_build_core.build" @@ -41,17 +42,19 @@ authors = [ { name="Jeffrey Berger", email="jeff.berger@qognitive.io" }, ] dependencies = ["numpy", "qiskit", "qiskit-algorithms"] -version = "0.0.8" +requires-python = ">= 3.10" classifiers = [ "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ] +dynamic = ["version"] [project.optional-dependencies] dev = [ + "cibuildwheel", "clang-format", "cmake-format", "pre-commit", @@ -59,6 +62,7 @@ dev = [ "ruff", "pytest", "pytest-benchmark", + "setuptools_scm", "sphinx", "sphinx_rtd_theme", "sphinx-autobuild", @@ -178,5 +182,16 @@ docstring-code-format = true # pip install -e . --verbose -C cmake.args="-DCMAKE_CXX_COMPILER=clang++-18" [tool.scikit-build] cmake.build-type = "Release" -build-dir = "build" +build-dir = "build/{wheel_tag}" +metadata.version.provider = "scikit_build_core.metadata.setuptools_scm" +sdist.include = ["fast_pauli/__version__.py"] # TODO add more options here + +[tool.setuptools_scm] +write_to = "fast_pauli/__version__.py" + +[tool.cibuildwheel] +build = ["cp310-manylinux_x86_64", "cp311-manylinux_x86_64", "cp312-manylinux_x86_64", "cp313-manylinux_x86_64"] +build-frontend = "build" +test-requires = "pytest" +test-command = "pytest -s -vv --import-mode importlib {project}/tests/fast_pauli"