From 982c8c995c9226f8f47907063112a858e055323f Mon Sep 17 00:00:00 2001 From: Jan Unsleber Date: Mon, 4 May 2026 09:08:34 +0000 Subject: [PATCH] draft qre algorithm --- .../source/_static/examples/python/circuit.py | 10 +- .../user/comprehensive/data/circuit.rst | 2 - examples/qpe_stretched_n2.ipynb | 10 +- examples/state_prep_energy.ipynb | 918 +++++++++--------- .../src/qdk_chemistry/algorithms/__init__.py | 4 + .../src/qdk_chemistry/algorithms/registry.py | 4 + .../algorithms/resource_estimator/__init__.py | 12 + .../algorithms/resource_estimator/base.py | 63 ++ .../algorithms/resource_estimator/qdk.py | 150 +++ python/src/qdk_chemistry/data/__init__.py | 16 + python/src/qdk_chemistry/data/circuit.py | 28 - .../data/resource_estimator_data.py | 502 ++++++++++ python/tests/test_circuit.py | 22 +- python/tests/test_resource_estimator.py | 264 +++++ 14 files changed, 1502 insertions(+), 503 deletions(-) create mode 100644 python/src/qdk_chemistry/algorithms/resource_estimator/__init__.py create mode 100644 python/src/qdk_chemistry/algorithms/resource_estimator/base.py create mode 100644 python/src/qdk_chemistry/algorithms/resource_estimator/qdk.py create mode 100644 python/src/qdk_chemistry/data/resource_estimator_data.py create mode 100644 python/tests/test_resource_estimator.py diff --git a/docs/source/_static/examples/python/circuit.py b/docs/source/_static/examples/python/circuit.py index a2685f5f5..1f193512e 100644 --- a/docs/source/_static/examples/python/circuit.py +++ b/docs/source/_static/examples/python/circuit.py @@ -53,10 +53,12 @@ print(f"Qubits: {len(circuit_json['qubits'])}") # 5. Resource estimation -estimate_result = circuit.estimate() -formatted = estimate_result["physicalCountsFormatted"] -print(f"Physical qubits: {formatted['physicalQubits']}") -print(f"Runtime: {formatted['runtime']}") +from qdk_chemistry.algorithms import create as create_algorithm + +estimator = create_algorithm("resource_estimator") +estimate_result = estimator.run(circuit) +print(f"Physical qubits: {estimate_result.physical_counts.physical_qubits}") +print(f"Runtime: {estimate_result.physical_counts.runtime}") # end-cell-qsharp-workflow ################################################################################ diff --git a/docs/source/user/comprehensive/data/circuit.rst b/docs/source/user/comprehensive/data/circuit.rst index 82d59dc2b..f4fede4dd 100644 --- a/docs/source/user/comprehensive/data/circuit.rst +++ b/docs/source/user/comprehensive/data/circuit.rst @@ -110,8 +110,6 @@ Each method returns the circuit in the requested format, converting from whateve - Returns the OpenQASM string. Converts from :term:`QIR` via Qiskit if only :term:`QIR` is available. * - :meth:`~qdk_chemistry.data.Circuit.get_qiskit_circuit` - Returns a Qiskit ``QuantumCircuit``. Requires ``qiskit`` to be installed. - * - :meth:`~qdk_chemistry.data.Circuit.estimate` - - Runs Q#'s resource estimator on the circuit. Accepts optional ``params`` for estimation configuration. Example: Convert state preparation circuits to different formats ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/examples/qpe_stretched_n2.ipynb b/examples/qpe_stretched_n2.ipynb index d26c0554b..70b2f5eb0 100644 --- a/examples/qpe_stretched_n2.ipynb +++ b/examples/qpe_stretched_n2.ipynb @@ -56,7 +56,7 @@ "source": [ "from qdk_chemistry.data import Structure\n", "\n", - "# Stretched N2 structure at 1.270025 Å bond length\n", + "# Stretched N2 structure at 1.270025 \u00c5 bond length\n", "structure = Structure.from_xyz_file(Path(\"data/stretched_n2.structure.xyz\"))" ] }, @@ -334,7 +334,9 @@ "\n", "# Print logical qubit counts estimated from the circuit\n", "df = pd.DataFrame(\n", - " regular_isometry_circuit.estimate().logical_counts.items(),\n", + "from qdk_chemistry.algorithms import create as create_algorithm\n", + "estimator = create_algorithm('resource_estimator')\n", + " estimator.run(regular_isometry_circuit).logical_counts.items(),\n", " columns=['Logical Estimate', 'Counts']\n", ")\n", "display(df)" @@ -367,7 +369,9 @@ "\n", "# Print logical qubit counts estimated from the circuit\n", "df = pd.DataFrame(\n", - " sparse_isometry_circuit.estimate().logical_counts.items(),\n", + "from qdk_chemistry.algorithms import create as create_algorithm\n", + "estimator = create_algorithm('resource_estimator')\n", + " estimator.run(sparse_isometry_circuit).logical_counts.items(),\n", " columns=['Logical Estimate', 'Counts']\n", ")\n", "display(df)" diff --git a/examples/state_prep_energy.ipynb b/examples/state_prep_energy.ipynb index 4343b5134..b2387bdc8 100644 --- a/examples/state_prep_energy.ipynb +++ b/examples/state_prep_energy.ipynb @@ -1,459 +1,463 @@ { - "cells": [ - { - "cell_type": "markdown", - "id": "115ee185", - "metadata": {}, - "source": [ - "# Using `qdk-chemistry` for multi-reference quantum chemistry state preparation and energy estimation\n", - "\n", - "This notebook demonstrates an end-to-end multi-configurational quantum chemistry workflow using `qdk-chemistry`.\n", - "It covers molecule loading and visualization, self-consistent-field (SCF) calculation, active-space selection, multi-configurational wavefunction generation, quantum state-preparation circuit construction, and measurement circuits for energy estimation.\n", - "\n", - "**Prerequisites:** In addition to [installing `qdk-chemistry`](https://github.com/microsoft/qdk-chemistry/blob/main/INSTALL.md), you will need to install the `jupyter` extra to run this notebook:\n", - "\n", - "```bash\n", - "pip install 'qdk-chemistry[jupyter]'\n", - "```\n", - "\n", - "This installs the additional dependencies required by this notebook (ipykernel, pandas, and pyscf).\n", - "\n", - "---\n", - "\n", - "In many molecular systems—such as bond dissociation or transition-metal complexes—a single electronic configuration cannot describe the true electronic structure.\n", - "These multi-configurational systems exhibit strong electron correlation that challenges mean-field and single-determinant methods like [Hartree–Fock](https://en.wikipedia.org/wiki/Hartree%E2%80%93Fock_method) or standard [coupled cluster theory](https://en.wikipedia.org/wiki/Coupled_cluster).\n", - "\n", - "While classical multi-configurational approaches can capture these effects, their computational cost grows exponentially with system size.\n", - "Quantum computers offer a complementary route: they can represent superpositions of many configurations natively and solve these problems with polynomial scaling.\n", - "\n", - "However, near-term fault-tolerant quantum hardware is still in the early stages of growth and scaling.\n", - "To use it effectively, we must compress and optimize chemistry problems before they reach the quantum device.\n", - "Classical methods enable this by identifying essential orbitals through active-space selection, generating approximate wavefunctions for state preparation, and supplying data to optimize quantum circuits for energy estimation.\n", - "\n", - "This notebook focuses on state preparation, where a multi-configurational wavefunction from classical computation is transformed into a quantum circuit.\n", - "State preparation is central to quantum chemistry algorithms such as [Quantum Phase Estimation (QPE)](https://en.wikipedia.org/wiki/Quantum_phase_estimation_algorithm) and also serves as a practical hardware benchmark: preparing complex multi-configurational states tests the fidelity and coherence of quantum hardware.\n", - "\n", - "In the example below, we show how to generate and optimize state preparation circuits, from active-space selection to energy measurement, demonstrating how chemical insight can reduce quantum resource requirements for near-term devices." - ] - }, - { - "cell_type": "markdown", - "id": "d0a5a02f", - "metadata": {}, - "source": [ - "## Loading and visualizing the molecular structure\n", - "\n", - "For this example, we will use the benzene diradical molecule.\n", - "The benzene diradical has two unpaired electrons, making it a good candidate for multi-reference quantum chemistry methods.\n", - "This molecule is also an important intermediate in the [Bergman cyclization reaction](https://en.wikipedia.org/wiki/Bergman_cyclization), a popular reaction in synthetic organic chemistry.\n", - "\n", - "The molecular structure is provided in the [XYZ file format](https://en.wikipedia.org/wiki/XYZ_file_format).\n", - "This cell demonstrates how to load the molecule and visualize its structure." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5436376a", - "metadata": {}, - "outputs": [], - "source": [ - "from pathlib import Path\n", - "\n", - "from qdk.widgets import MoleculeViewer\n", - "\n", - "from qdk_chemistry.data import Structure\n", - "\n", - "# Read molecular structure from XYZ file\n", - "structure = Structure.from_xyz_file(\n", - " Path(\".\") / \"data/benzene_diradical.structure.xyz\"\n", - ")\n", - "\n", - "# Visualize the molecular structure\n", - "display(MoleculeViewer(molecule_data=structure.to_xyz()))" - ] - }, - { - "cell_type": "markdown", - "id": "f01be634", - "metadata": {}, - "source": [ - "## Generating the molecular orbitals\n", - "\n", - "This step performs a [Hartree-Fock](https://en.wikipedia.org/wiki/Hartree%E2%80%93Fock_method) (HF) SCF calculation to generate an approximate initial wavefunction and ground-state energy guess.\n", - "The wavefunction and energy returned by this initial calculation do not provide an accurate description of the system electronic structure; however, they are useful for constructing molecular orbitals.\n", - "The resulting molecular orbitals will be used in subsequent steps for active space selection and multi-configuration calculations." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "75d71220", - "metadata": {}, - "outputs": [], - "source": [ - "from qdk_chemistry.algorithms import create\n", - "\n", - "# Perform an SCF calculation, returning the energy and wavefunction\n", - "scf_solver = create(\"scf_solver\")\n", - "E_hf, wfn_hf = scf_solver.run(structure, charge=0, spin_multiplicity=1, basis_or_guess=\"cc-pvdz\")\n", - "print(f\"SCF energy is {E_hf:.3f} Hartree\")\n", - "\n", - "# Display a summary of the molecular orbitals obtained from the SCF calculation\n", - "print(\"SCF Orbitals:\\n\", wfn_hf.get_orbitals().get_summary())" - ] - }, - { - "cell_type": "markdown", - "id": "ec9aef75", - "metadata": {}, - "source": [ - "## Selecting an active space and calculating the multi-configuration wavefunction" - ] - }, - { - "cell_type": "markdown", - "id": "0b3bac6a", - "metadata": {}, - "source": [ - "### Active space selection\n", - "\n", - "Most chemistry applications on quantum computers will require the use of [active spaces](https://en.wikipedia.org/wiki/Complete_active_space) to focus the quantum calculation on a subset of the electrons and orbitals in the system.\n", - "For example, the benzene diradical with the default basis set specified above results in ~100 molecular orbitals, requiring ~200 qubits to represent the full electronic structure problem.\n", - "\n", - "This cell shows how to optimize this calculation by selecting an active space from the valence molecular orbitals calculated in the previous SCF step, focusing on the [frontier orbitals](https://en.wikipedia.org/wiki/Frontier_molecular_orbital_theory) that are most relevant to molecular reactivity." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2f5ea942", - "metadata": {}, - "outputs": [], - "source": [ - "# Select active space (6 electrons in 6 orbitals for benzene diradical) to choose most chemically relevant orbitals\n", - "active_space_selector = create(\"active_space_selector\", algorithm_name=\"qdk_valence\",\n", - " num_active_electrons=6, num_active_orbitals=6)\n", - "active_wfn = active_space_selector.run(wfn_hf)\n", - "active_orbitals = active_wfn.get_orbitals()\n", - "\n", - "# Print a summary of the active space orbitals\n", - "print(\"Active Space Orbitals:\\n\", active_orbitals.get_summary())" - ] - }, - { - "cell_type": "markdown", - "id": "dd42b1f0", - "metadata": {}, - "source": [ - "The next cell shows how to visualize the selected active orbitals.\n", - "The drop-down menu provides the ability to select different occupied and virtual orbitals in the active space to visualize their shapes, while the isovalue slider adjusts the surface representation of the orbitals for different electron density levels.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4d8850fc", - "metadata": {}, - "outputs": [], - "source": [ - "from qdk_chemistry.utils.cubegen import generate_cubefiles_from_orbitals\n", - "\n", - "# Generate cube files for the active orbitals\n", - "cube_data = generate_cubefiles_from_orbitals(\n", - " orbitals=active_orbitals,\n", - " grid_size=(40, 40, 40),\n", - " margin=10.0,\n", - " indices=active_orbitals.get_active_space_indices()[0],\n", - " label_maker=lambda p: f\"{'occupied' if p < 20 else 'virtual'}_{p + 1:04d}\"\n", - ")\n", - "\n", - "# Visualize the molecular orbitals together with the structure\n", - "MoleculeViewer(molecule_data=structure.to_xyz(), cube_data=cube_data)" - ] - }, - { - "cell_type": "markdown", - "id": "f8b8e498", - "metadata": {}, - "source": [ - "### Calculate the multi-configuration wavefunction for the active space\n", - "\n", - "Once the active space has been selected, we are ready to solve the electronic structure problem (e.g., [Schrodinger's equation](https://en.wikipedia.org/wiki/Schr%C3%B6dinger_equation#Time-independent_equation)) more accurately than our initial SCF guess.\n", - "However, this requires two steps illustrated in this cell:\n", - "\n", - "1. First, we need to construct the [Hamiltonian](https://en.wikipedia.org/wiki/Hamiltonian_(quantum_mechanics)), which provides the mathematical description of the energy and interactions of the electrons in the active space.\n", - "\n", - "2. Second, we need to construct an improved estimate of the multi-configuration wavefunction and ground-state energy for this Hamiltonian.\n", - "The benzene diradical system in this demonstration is small enough that we can use a Complete Active Space [Configuration Interaction](https://en.wikipedia.org/wiki/Configuration_interaction) (CAS-CI) calculation to obtain the exact quantum mechanical energy and wavefunction for the active space.\n", - "However, for larger systems, the exact solution will not be feasible classically, and approximate methods such as [selected configuration interaction](https://arxiv.org/abs/2303.05688) or [density matrix renormalization group (DMRG)](https://en.wikipedia.org/wiki/Density_matrix_renormalization_group) are required.\n", - "\n", - "Unlike the mean-field Hartree-Fock method, which approximates the wavefunction as a single [Slater determinant](https://en.wikipedia.org/wiki/Slater_determinant), these multi-configuration methods consider all possible electron configurations within the active space, capturing electron correlation effects.\n", - "By subtracting the mean-field Hartree-Fock energy from the correlated multi-configuration energy, we obtain the [correlation energy](https://en.wikipedia.org/wiki/Electronic_correlation) for this active space." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "980dae08", - "metadata": {}, - "outputs": [], - "source": [ - "# Construct Hamiltonian in the active space and print its summary\n", - "hamiltonian_constructor = create(\"hamiltonian_constructor\")\n", - "hamiltonian = hamiltonian_constructor.run(active_orbitals)\n", - "print(\"Active Space Hamiltonian:\\n\", hamiltonian.get_summary())\n", - "\n", - "# Perform CASCI calculation to get the wavefunction and exact energy for the active space\n", - "mc = create(\"multi_configuration_calculator\")\n", - "E_cas, wfn_cas = mc.run(\n", - " hamiltonian, n_active_alpha_electrons=3, n_active_beta_electrons=3\n", - ")\n", - "print(f\"CASCI energy is {E_cas:.3f} Hartree, and the electron correlation energy is {E_cas - E_hf:.3f} Hartree\")" - ] - }, - { - "cell_type": "markdown", - "id": "623cae91", - "metadata": {}, - "source": [ - "## Loading the wavefunction onto a quantum computer\n", - "\n", - "Now that we have calculated the multi-configuration wavefunction for the active space, we can generate a quantum circuit to prepare this state on a quantum computer.\n", - "However, not all parts of the multi-configuration wavefunction contribute equally to the overall state, creating an opportunity for optimization." - ] - }, - { - "cell_type": "markdown", - "id": "ecd4c6d0", - "metadata": {}, - "source": [ - "### Identifying the dominant configurations in the wavefunction\n", - "\n", - "The first task is to understand the sparsity of the wavefunction: how many configurations contribute significantly to the overall state?\n", - "\n", - "This cell demonstrates how to analyze the wavefunction and identify the dominant configurations based on their amplitudes." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ebba573c", - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "from qdk.widgets import Histogram\n", - "\n", - "# Plot top determinant weights from the CASCI wavefunction\n", - "NUM_DETERMINANTS = 10\n", - "print(f\"Total determinants in the CASCI wavefunction: {len(wfn_cas.get_active_determinants())}\")\n", - "print(f\"Plotting the top {NUM_DETERMINANTS} determinants by weight.\")\n", - "top_configurations = wfn_cas.get_top_determinants(max_determinants=NUM_DETERMINANTS)\n", - "display(Histogram(bar_values={k.to_string(): np.abs(v)**2 for k, v in top_configurations.items()},))" - ] - }, - { - "cell_type": "markdown", - "id": "c73ad541", - "metadata": {}, - "source": [ - "Reducing the wavefunction to these determinants allows us to optimize the computational requirements for loading the quantum computer with a state that has high overlap with the true wavefunction—an important metric for quantum algorithms like QPE.\n", - "However, this reduction of the wavefunction also changes our description of the quantum system, particularly its energy.\n", - "Therefore, for the purposes of benchmarking, we need to recalculate the energy of the truncated wavefunction classically to provide a reference for evaluating the accuracy of the quantum calculation.\n", - "This cell shows how to recalculate this energy." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "560f8554", - "metadata": {}, - "outputs": [], - "source": [ - "# Get top 2 determinants from the CASCI wavefunction to form a sparse wavefunction\n", - "top_configurations = wfn_cas.get_top_determinants(max_determinants=2)\n", - "\n", - "# Compute the reference energy of the sparse wavefunction\n", - "pmc_calculator = create(\"projected_multi_configuration_calculator\")\n", - "E_sparse, wfn_sparse = pmc_calculator.run(hamiltonian, list(top_configurations.keys()))\n", - "\n", - "print(f\"Reference energy for top 2 determinants is {E_sparse:.6f} Hartree\")" - ] - }, - { - "cell_type": "markdown", - "id": "e133afd5", - "metadata": {}, - "source": [ - "### Loading the wavefunction using general state preparation methods\n", - "\n", - "One possibility for loading the multi-configuration wavefunction onto a quantum computer is to use general state preparation approaches such as the [isometry method](https://arxiv.org/abs/1501.06911), as offered in software such as [Qiskit](https://qiskit.org/documentation/stubs/qiskit.circuit.library.Isometry.html).\n", - "While this is a very powerful general-purpose approach, it can be resource intensive, requiring very deep circuits even for modest-sized wavefunctions due to its exponential scaling in the number of qubits.\n", - "This approach also requires numerous fine rotations—operations that can be challenging for near-term fault-tolerant quantum hardware.\n", - "This cell demonstrates how to use the isometry method to generate a quantum circuit for preparing the multi-configuration wavefunction on a quantum computer.\n", - "\n", - "**Note**: the generated circuits are so deep that you will need to adjust the \"zoom\" selection in the visualization window to see the detailed operations." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a1bb218f", - "metadata": {}, - "outputs": [], - "source": [ - "import pandas as pd\n", - "from qdk.widgets import Circuit\n", - "\n", - "# Generate state preparation circuit for the sparse state using the regular isometry method (Qiskit)\n", - "state_prep = create(\"state_prep\", \"qiskit_regular_isometry\", transpile=False)\n", - "regular_isometry_circuit = state_prep.run(wfn_sparse)\n", - "\n", - "# Visualize the regular isometry circuit\n", - "display(Circuit(regular_isometry_circuit.get_qsharp_circuit()))\n", - "\n", - "# Print logical qubit counts estimated from the circuit\n", - "df = pd.DataFrame(regular_isometry_circuit.estimate().logical_counts.items(), columns=['Logical Estimate', 'Counts'])\n", - "display(df)" - ] - }, - { - "cell_type": "markdown", - "id": "f577f124", - "metadata": {}, - "source": [ - "### Loading the wavefunction using optimized state preparation methods\n", - "\n", - "As the cell above illustrates, the general isometry method for state preparation can be very resource intensive—requiring thousands of fine rotations for this benzene diradical example.\n", - "However, we can optimize this process by taking advantage of the sparse multi-configuration wavefunction structure, generating much more efficient quantum circuits for state preparation.\n", - "The cell below demonstrates how the `qdk-chemistry` library can be used for optimized wavefunction loading, producing a circuit that is orders of magnitude more efficient than the general isometry method.\n", - "\n", - "The underlying approach is based on a variation of the [sparse isometry method](https://quantum-journal.org/papers/q-2021-03-15-412/pdf/), with new updates specific to `qdk-chemistry` that avoid the use of multi-controlled gates (also challenging for near-term fault-tolerant quantum computers)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9baaf705", - "metadata": {}, - "outputs": [], - "source": [ - "import pandas as pd\n", - "from qdk.widgets import Circuit\n", - "\n", - "# Generate state preparation circuit for the sparse state via sparse isometry (GF2 + X)\n", - "state_prep = create(\"state_prep\", \"sparse_isometry_gf2x\")\n", - "sparse_isometry_circuit = state_prep.run(wfn_sparse)\n", - "\n", - "# Visualize the sparse isometry circuit\n", - "display(Circuit(sparse_isometry_circuit.get_qsharp_circuit(prune_classical_qubits=True)))\n", - "\n", - "# Print logical qubit counts estimated from the circuit\n", - "df = pd.DataFrame(\n", - " sparse_isometry_circuit.estimate().logical_counts.items(), columns=['Logical Estimate', 'Counts'])\n", - "display(df)" - ] - }, - { - "cell_type": "markdown", - "id": "6635d626", - "metadata": {}, - "source": [ - "Rather than requiring thousands of fine rotations, this optimized approach requires only a single fine rotation for the two-determinant benzene diradical wavefunction—demonstrating the power of chemistry-informed optimizations for quantum state preparation.\n", - "\n", - "Close inspection of the generated circuit shows that it has also reduced our qubit count: several of the qubits have been converted to classical bits, which can be post-processed after measurement.\n", - "We will revisit these classical bits in the next section on energy measurement." - ] - }, - { - "cell_type": "markdown", - "id": "4588419b", - "metadata": {}, - "source": [ - "## Estimating the energy on a quantum computer\n", - "\n", - "For the final stage of this state preparation application benchmark workflow, we estimate the energy of the optimized multi-configuration wavefunction prepared on a quantum computer.\n", - "The first step in this process is mapping the classical Hamiltonian for the active space to a qubit Hamiltonian that can be measured on a quantum computer.\n", - "For this example, we use the [Jordan-Wigner transformation](https://en.wikipedia.org/wiki/Jordan%E2%80%93Wigner_transformation) to perform this mapping." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2f856f41", - "metadata": {}, - "outputs": [], - "source": [ - "# Prepare qubit Hamiltonian\n", - "qubit_mapper = create(\"qubit_mapper\", algorithm_name=\"qiskit\", encoding=\"jordan-wigner\")\n", - "qubit_hamiltonian = qubit_mapper.run(hamiltonian)" - ] - }, - { - "cell_type": "markdown", - "id": "0dc7b827", - "metadata": {}, - "source": [ - "### Estimating the energy on a quantum computer (simulator)\n", - "\n", - "Finally, we need to generate the measurement circuits required to estimate the energy of the prepared multi-configuration wavefunction on a quantum computer.\n", - "The cell below demonstrates how to generate these measurement circuits using the `qdk-chemistry` library and how to use the QDK simulator to execute them.\n", - "\n", - "This cell provides a set budget of measurements (\"shots\") to be evenly divided between the measurement circuits.\n", - "The measurement process is probabilistic, so we obtain a distribution of results from each circuit.\n", - "These distributions are then combined to produce a final energy estimate, along with an uncertainty (variance) as reported below.\n", - "The uncertainty is directly related to the number of shots used in the measurement process: more shots lead to lower uncertainty." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3e9b6616", - "metadata": {}, - "outputs": [], - "source": [ - "# Estimate energy using the optimized circuit and the qubit Hamiltonian\n", - "estimator = create(\"energy_estimator\", algorithm_name=\"qdk\")\n", - "circuit_executor = create(\"circuit_executor\", algorithm_name=\"qdk_full_state_simulator\")\n", - "energy_results, simulation_data = estimator.run(\n", - " circuit=sparse_isometry_circuit,\n", - " qubit_hamiltonian=qubit_hamiltonian,\n", - " circuit_executor=circuit_executor,\n", - " total_shots=1500000,\n", - ")\n", - "\n", - "# Print statistic for measured energy\n", - "energy_mean = energy_results.energy_expectation_value + hamiltonian.get_core_energy()\n", - "energy_stddev = np.sqrt(energy_results.energy_variance)\n", - "print(\n", - " f\"Estimated energy from quantum circuit: {energy_mean:.3f} ± {energy_stddev:.3f} Hartree\"\n", - ")\n", - "\n", - "# Print comparison with reference energy\n", - "print(f\"Difference from reference energy: {energy_mean - E_sparse} Hartree\")" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "qdk_chemistry_venv (3.12.3)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.3" - } - }, - "nbformat": 4, - "nbformat_minor": 5 + "cells": [ + { + "cell_type": "markdown", + "id": "115ee185", + "metadata": {}, + "source": [ + "# Using `qdk-chemistry` for multi-reference quantum chemistry state preparation and energy estimation\n", + "\n", + "This notebook demonstrates an end-to-end multi-configurational quantum chemistry workflow using `qdk-chemistry`.\n", + "It covers molecule loading and visualization, self-consistent-field (SCF) calculation, active-space selection, multi-configurational wavefunction generation, quantum state-preparation circuit construction, and measurement circuits for energy estimation.\n", + "\n", + "**Prerequisites:** In addition to [installing `qdk-chemistry`](https://github.com/microsoft/qdk-chemistry/blob/main/INSTALL.md), you will need to install the `jupyter` extra to run this notebook:\n", + "\n", + "```bash\n", + "pip install 'qdk-chemistry[jupyter]'\n", + "```\n", + "\n", + "This installs the additional dependencies required by this notebook (ipykernel, pandas, and pyscf).\n", + "\n", + "---\n", + "\n", + "In many molecular systems\u2014such as bond dissociation or transition-metal complexes\u2014a single electronic configuration cannot describe the true electronic structure.\n", + "These multi-configurational systems exhibit strong electron correlation that challenges mean-field and single-determinant methods like [Hartree\u2013Fock](https://en.wikipedia.org/wiki/Hartree%E2%80%93Fock_method) or standard [coupled cluster theory](https://en.wikipedia.org/wiki/Coupled_cluster).\n", + "\n", + "While classical multi-configurational approaches can capture these effects, their computational cost grows exponentially with system size.\n", + "Quantum computers offer a complementary route: they can represent superpositions of many configurations natively and solve these problems with polynomial scaling.\n", + "\n", + "However, near-term fault-tolerant quantum hardware is still in the early stages of growth and scaling.\n", + "To use it effectively, we must compress and optimize chemistry problems before they reach the quantum device.\n", + "Classical methods enable this by identifying essential orbitals through active-space selection, generating approximate wavefunctions for state preparation, and supplying data to optimize quantum circuits for energy estimation.\n", + "\n", + "This notebook focuses on state preparation, where a multi-configurational wavefunction from classical computation is transformed into a quantum circuit.\n", + "State preparation is central to quantum chemistry algorithms such as [Quantum Phase Estimation (QPE)](https://en.wikipedia.org/wiki/Quantum_phase_estimation_algorithm) and also serves as a practical hardware benchmark: preparing complex multi-configurational states tests the fidelity and coherence of quantum hardware.\n", + "\n", + "In the example below, we show how to generate and optimize state preparation circuits, from active-space selection to energy measurement, demonstrating how chemical insight can reduce quantum resource requirements for near-term devices." + ] + }, + { + "cell_type": "markdown", + "id": "d0a5a02f", + "metadata": {}, + "source": [ + "## Loading and visualizing the molecular structure\n", + "\n", + "For this example, we will use the benzene diradical molecule.\n", + "The benzene diradical has two unpaired electrons, making it a good candidate for multi-reference quantum chemistry methods.\n", + "This molecule is also an important intermediate in the [Bergman cyclization reaction](https://en.wikipedia.org/wiki/Bergman_cyclization), a popular reaction in synthetic organic chemistry.\n", + "\n", + "The molecular structure is provided in the [XYZ file format](https://en.wikipedia.org/wiki/XYZ_file_format).\n", + "This cell demonstrates how to load the molecule and visualize its structure." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5436376a", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "\n", + "from qdk.widgets import MoleculeViewer\n", + "\n", + "from qdk_chemistry.data import Structure\n", + "\n", + "# Read molecular structure from XYZ file\n", + "structure = Structure.from_xyz_file(\n", + " Path(\".\") / \"data/benzene_diradical.structure.xyz\"\n", + ")\n", + "\n", + "# Visualize the molecular structure\n", + "display(MoleculeViewer(molecule_data=structure.to_xyz()))" + ] + }, + { + "cell_type": "markdown", + "id": "f01be634", + "metadata": {}, + "source": [ + "## Generating the molecular orbitals\n", + "\n", + "This step performs a [Hartree-Fock](https://en.wikipedia.org/wiki/Hartree%E2%80%93Fock_method) (HF) SCF calculation to generate an approximate initial wavefunction and ground-state energy guess.\n", + "The wavefunction and energy returned by this initial calculation do not provide an accurate description of the system electronic structure; however, they are useful for constructing molecular orbitals.\n", + "The resulting molecular orbitals will be used in subsequent steps for active space selection and multi-configuration calculations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "75d71220", + "metadata": {}, + "outputs": [], + "source": [ + "from qdk_chemistry.algorithms import create\n", + "\n", + "# Perform an SCF calculation, returning the energy and wavefunction\n", + "scf_solver = create(\"scf_solver\")\n", + "E_hf, wfn_hf = scf_solver.run(structure, charge=0, spin_multiplicity=1, basis_or_guess=\"cc-pvdz\")\n", + "print(f\"SCF energy is {E_hf:.3f} Hartree\")\n", + "\n", + "# Display a summary of the molecular orbitals obtained from the SCF calculation\n", + "print(\"SCF Orbitals:\\n\", wfn_hf.get_orbitals().get_summary())" + ] + }, + { + "cell_type": "markdown", + "id": "ec9aef75", + "metadata": {}, + "source": [ + "## Selecting an active space and calculating the multi-configuration wavefunction" + ] + }, + { + "cell_type": "markdown", + "id": "0b3bac6a", + "metadata": {}, + "source": [ + "### Active space selection\n", + "\n", + "Most chemistry applications on quantum computers will require the use of [active spaces](https://en.wikipedia.org/wiki/Complete_active_space) to focus the quantum calculation on a subset of the electrons and orbitals in the system.\n", + "For example, the benzene diradical with the default basis set specified above results in ~100 molecular orbitals, requiring ~200 qubits to represent the full electronic structure problem.\n", + "\n", + "This cell shows how to optimize this calculation by selecting an active space from the valence molecular orbitals calculated in the previous SCF step, focusing on the [frontier orbitals](https://en.wikipedia.org/wiki/Frontier_molecular_orbital_theory) that are most relevant to molecular reactivity." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2f5ea942", + "metadata": {}, + "outputs": [], + "source": [ + "# Select active space (6 electrons in 6 orbitals for benzene diradical) to choose most chemically relevant orbitals\n", + "active_space_selector = create(\"active_space_selector\", algorithm_name=\"qdk_valence\",\n", + " num_active_electrons=6, num_active_orbitals=6)\n", + "active_wfn = active_space_selector.run(wfn_hf)\n", + "active_orbitals = active_wfn.get_orbitals()\n", + "\n", + "# Print a summary of the active space orbitals\n", + "print(\"Active Space Orbitals:\\n\", active_orbitals.get_summary())" + ] + }, + { + "cell_type": "markdown", + "id": "dd42b1f0", + "metadata": {}, + "source": [ + "The next cell shows how to visualize the selected active orbitals.\n", + "The drop-down menu provides the ability to select different occupied and virtual orbitals in the active space to visualize their shapes, while the isovalue slider adjusts the surface representation of the orbitals for different electron density levels.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4d8850fc", + "metadata": {}, + "outputs": [], + "source": [ + "from qdk_chemistry.utils.cubegen import generate_cubefiles_from_orbitals\n", + "\n", + "# Generate cube files for the active orbitals\n", + "cube_data = generate_cubefiles_from_orbitals(\n", + " orbitals=active_orbitals,\n", + " grid_size=(40, 40, 40),\n", + " margin=10.0,\n", + " indices=active_orbitals.get_active_space_indices()[0],\n", + " label_maker=lambda p: f\"{'occupied' if p < 20 else 'virtual'}_{p + 1:04d}\"\n", + ")\n", + "\n", + "# Visualize the molecular orbitals together with the structure\n", + "MoleculeViewer(molecule_data=structure.to_xyz(), cube_data=cube_data)" + ] + }, + { + "cell_type": "markdown", + "id": "f8b8e498", + "metadata": {}, + "source": [ + "### Calculate the multi-configuration wavefunction for the active space\n", + "\n", + "Once the active space has been selected, we are ready to solve the electronic structure problem (e.g., [Schrodinger's equation](https://en.wikipedia.org/wiki/Schr%C3%B6dinger_equation#Time-independent_equation)) more accurately than our initial SCF guess.\n", + "However, this requires two steps illustrated in this cell:\n", + "\n", + "1. First, we need to construct the [Hamiltonian](https://en.wikipedia.org/wiki/Hamiltonian_(quantum_mechanics)), which provides the mathematical description of the energy and interactions of the electrons in the active space.\n", + "\n", + "2. Second, we need to construct an improved estimate of the multi-configuration wavefunction and ground-state energy for this Hamiltonian.\n", + "The benzene diradical system in this demonstration is small enough that we can use a Complete Active Space [Configuration Interaction](https://en.wikipedia.org/wiki/Configuration_interaction) (CAS-CI) calculation to obtain the exact quantum mechanical energy and wavefunction for the active space.\n", + "However, for larger systems, the exact solution will not be feasible classically, and approximate methods such as [selected configuration interaction](https://arxiv.org/abs/2303.05688) or [density matrix renormalization group (DMRG)](https://en.wikipedia.org/wiki/Density_matrix_renormalization_group) are required.\n", + "\n", + "Unlike the mean-field Hartree-Fock method, which approximates the wavefunction as a single [Slater determinant](https://en.wikipedia.org/wiki/Slater_determinant), these multi-configuration methods consider all possible electron configurations within the active space, capturing electron correlation effects.\n", + "By subtracting the mean-field Hartree-Fock energy from the correlated multi-configuration energy, we obtain the [correlation energy](https://en.wikipedia.org/wiki/Electronic_correlation) for this active space." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "980dae08", + "metadata": {}, + "outputs": [], + "source": [ + "# Construct Hamiltonian in the active space and print its summary\n", + "hamiltonian_constructor = create(\"hamiltonian_constructor\")\n", + "hamiltonian = hamiltonian_constructor.run(active_orbitals)\n", + "print(\"Active Space Hamiltonian:\\n\", hamiltonian.get_summary())\n", + "\n", + "# Perform CASCI calculation to get the wavefunction and exact energy for the active space\n", + "mc = create(\"multi_configuration_calculator\")\n", + "E_cas, wfn_cas = mc.run(\n", + " hamiltonian, n_active_alpha_electrons=3, n_active_beta_electrons=3\n", + ")\n", + "print(f\"CASCI energy is {E_cas:.3f} Hartree, and the electron correlation energy is {E_cas - E_hf:.3f} Hartree\")" + ] + }, + { + "cell_type": "markdown", + "id": "623cae91", + "metadata": {}, + "source": [ + "## Loading the wavefunction onto a quantum computer\n", + "\n", + "Now that we have calculated the multi-configuration wavefunction for the active space, we can generate a quantum circuit to prepare this state on a quantum computer.\n", + "However, not all parts of the multi-configuration wavefunction contribute equally to the overall state, creating an opportunity for optimization." + ] + }, + { + "cell_type": "markdown", + "id": "ecd4c6d0", + "metadata": {}, + "source": [ + "### Identifying the dominant configurations in the wavefunction\n", + "\n", + "The first task is to understand the sparsity of the wavefunction: how many configurations contribute significantly to the overall state?\n", + "\n", + "This cell demonstrates how to analyze the wavefunction and identify the dominant configurations based on their amplitudes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ebba573c", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from qdk.widgets import Histogram\n", + "\n", + "# Plot top determinant weights from the CASCI wavefunction\n", + "NUM_DETERMINANTS = 10\n", + "print(f\"Total determinants in the CASCI wavefunction: {len(wfn_cas.get_active_determinants())}\")\n", + "print(f\"Plotting the top {NUM_DETERMINANTS} determinants by weight.\")\n", + "top_configurations = wfn_cas.get_top_determinants(max_determinants=NUM_DETERMINANTS)\n", + "display(Histogram(bar_values={k.to_string(): np.abs(v)**2 for k, v in top_configurations.items()},))" + ] + }, + { + "cell_type": "markdown", + "id": "c73ad541", + "metadata": {}, + "source": [ + "Reducing the wavefunction to these determinants allows us to optimize the computational requirements for loading the quantum computer with a state that has high overlap with the true wavefunction\u2014an important metric for quantum algorithms like QPE.\n", + "However, this reduction of the wavefunction also changes our description of the quantum system, particularly its energy.\n", + "Therefore, for the purposes of benchmarking, we need to recalculate the energy of the truncated wavefunction classically to provide a reference for evaluating the accuracy of the quantum calculation.\n", + "This cell shows how to recalculate this energy." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "560f8554", + "metadata": {}, + "outputs": [], + "source": [ + "# Get top 2 determinants from the CASCI wavefunction to form a sparse wavefunction\n", + "top_configurations = wfn_cas.get_top_determinants(max_determinants=2)\n", + "\n", + "# Compute the reference energy of the sparse wavefunction\n", + "pmc_calculator = create(\"projected_multi_configuration_calculator\")\n", + "E_sparse, wfn_sparse = pmc_calculator.run(hamiltonian, list(top_configurations.keys()))\n", + "\n", + "print(f\"Reference energy for top 2 determinants is {E_sparse:.6f} Hartree\")" + ] + }, + { + "cell_type": "markdown", + "id": "e133afd5", + "metadata": {}, + "source": [ + "### Loading the wavefunction using general state preparation methods\n", + "\n", + "One possibility for loading the multi-configuration wavefunction onto a quantum computer is to use general state preparation approaches such as the [isometry method](https://arxiv.org/abs/1501.06911), as offered in software such as [Qiskit](https://qiskit.org/documentation/stubs/qiskit.circuit.library.Isometry.html).\n", + "While this is a very powerful general-purpose approach, it can be resource intensive, requiring very deep circuits even for modest-sized wavefunctions due to its exponential scaling in the number of qubits.\n", + "This approach also requires numerous fine rotations\u2014operations that can be challenging for near-term fault-tolerant quantum hardware.\n", + "This cell demonstrates how to use the isometry method to generate a quantum circuit for preparing the multi-configuration wavefunction on a quantum computer.\n", + "\n", + "**Note**: the generated circuits are so deep that you will need to adjust the \"zoom\" selection in the visualization window to see the detailed operations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a1bb218f", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "from qdk.widgets import Circuit\n", + "\n", + "# Generate state preparation circuit for the sparse state using the regular isometry method (Qiskit)\n", + "state_prep = create(\"state_prep\", \"qiskit_regular_isometry\", transpile=False)\n", + "regular_isometry_circuit = state_prep.run(wfn_sparse)\n", + "\n", + "# Visualize the regular isometry circuit\n", + "display(Circuit(regular_isometry_circuit.get_qsharp_circuit()))\n", + "\n", + "# Print logical qubit counts estimated from the circuit\n", + "from qdk_chemistry.algorithms import create as create_algorithm\n", + "estimator = create_algorithm('resource_estimator')\n", + "df = pd.DataFrame(estimator.run(regular_isometry_circuit).logical_counts.items(), columns=['Logical Estimate', 'Counts'])\n", + "display(df)" + ] + }, + { + "cell_type": "markdown", + "id": "f577f124", + "metadata": {}, + "source": [ + "### Loading the wavefunction using optimized state preparation methods\n", + "\n", + "As the cell above illustrates, the general isometry method for state preparation can be very resource intensive\u2014requiring thousands of fine rotations for this benzene diradical example.\n", + "However, we can optimize this process by taking advantage of the sparse multi-configuration wavefunction structure, generating much more efficient quantum circuits for state preparation.\n", + "The cell below demonstrates how the `qdk-chemistry` library can be used for optimized wavefunction loading, producing a circuit that is orders of magnitude more efficient than the general isometry method.\n", + "\n", + "The underlying approach is based on a variation of the [sparse isometry method](https://quantum-journal.org/papers/q-2021-03-15-412/pdf/), with new updates specific to `qdk-chemistry` that avoid the use of multi-controlled gates (also challenging for near-term fault-tolerant quantum computers)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9baaf705", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "from qdk.widgets import Circuit\n", + "\n", + "# Generate state preparation circuit for the sparse state via sparse isometry (GF2 + X)\n", + "state_prep = create(\"state_prep\", \"sparse_isometry_gf2x\")\n", + "sparse_isometry_circuit = state_prep.run(wfn_sparse)\n", + "\n", + "# Visualize the sparse isometry circuit\n", + "display(Circuit(sparse_isometry_circuit.get_qsharp_circuit(prune_classical_qubits=True)))\n", + "\n", + "# Print logical qubit counts estimated from the circuit\n", + "df = pd.DataFrame(\n", + "from qdk_chemistry.algorithms import create as create_algorithm\n", + "estimator = create_algorithm('resource_estimator')\n", + " estimator.run(sparse_isometry_circuit).logical_counts.items(), columns=['Logical Estimate', 'Counts'])\n", + "display(df)" + ] + }, + { + "cell_type": "markdown", + "id": "6635d626", + "metadata": {}, + "source": [ + "Rather than requiring thousands of fine rotations, this optimized approach requires only a single fine rotation for the two-determinant benzene diradical wavefunction\u2014demonstrating the power of chemistry-informed optimizations for quantum state preparation.\n", + "\n", + "Close inspection of the generated circuit shows that it has also reduced our qubit count: several of the qubits have been converted to classical bits, which can be post-processed after measurement.\n", + "We will revisit these classical bits in the next section on energy measurement." + ] + }, + { + "cell_type": "markdown", + "id": "4588419b", + "metadata": {}, + "source": [ + "## Estimating the energy on a quantum computer\n", + "\n", + "For the final stage of this state preparation application benchmark workflow, we estimate the energy of the optimized multi-configuration wavefunction prepared on a quantum computer.\n", + "The first step in this process is mapping the classical Hamiltonian for the active space to a qubit Hamiltonian that can be measured on a quantum computer.\n", + "For this example, we use the [Jordan-Wigner transformation](https://en.wikipedia.org/wiki/Jordan%E2%80%93Wigner_transformation) to perform this mapping." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2f856f41", + "metadata": {}, + "outputs": [], + "source": [ + "# Prepare qubit Hamiltonian\n", + "qubit_mapper = create(\"qubit_mapper\", algorithm_name=\"qiskit\", encoding=\"jordan-wigner\")\n", + "qubit_hamiltonian = qubit_mapper.run(hamiltonian)" + ] + }, + { + "cell_type": "markdown", + "id": "0dc7b827", + "metadata": {}, + "source": [ + "### Estimating the energy on a quantum computer (simulator)\n", + "\n", + "Finally, we need to generate the measurement circuits required to estimate the energy of the prepared multi-configuration wavefunction on a quantum computer.\n", + "The cell below demonstrates how to generate these measurement circuits using the `qdk-chemistry` library and how to use the QDK simulator to execute them.\n", + "\n", + "This cell provides a set budget of measurements (\"shots\") to be evenly divided between the measurement circuits.\n", + "The measurement process is probabilistic, so we obtain a distribution of results from each circuit.\n", + "These distributions are then combined to produce a final energy estimate, along with an uncertainty (variance) as reported below.\n", + "The uncertainty is directly related to the number of shots used in the measurement process: more shots lead to lower uncertainty." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3e9b6616", + "metadata": {}, + "outputs": [], + "source": [ + "# Estimate energy using the optimized circuit and the qubit Hamiltonian\n", + "estimator = create(\"energy_estimator\", algorithm_name=\"qdk\")\n", + "circuit_executor = create(\"circuit_executor\", algorithm_name=\"qdk_full_state_simulator\")\n", + "energy_results, simulation_data = estimator.run(\n", + " circuit=sparse_isometry_circuit,\n", + " qubit_hamiltonian=qubit_hamiltonian,\n", + " circuit_executor=circuit_executor,\n", + " total_shots=1500000,\n", + ")\n", + "\n", + "# Print statistic for measured energy\n", + "energy_mean = energy_results.energy_expectation_value + hamiltonian.get_core_energy()\n", + "energy_stddev = np.sqrt(energy_results.energy_variance)\n", + "print(\n", + " f\"Estimated energy from quantum circuit: {energy_mean:.3f} \u00b1 {energy_stddev:.3f} Hartree\"\n", + ")\n", + "\n", + "# Print comparison with reference energy\n", + "print(f\"Difference from reference energy: {energy_mean - E_sparse} Hartree\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "qdk_chemistry_venv (3.12.3)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 } diff --git a/python/src/qdk_chemistry/algorithms/__init__.py b/python/src/qdk_chemistry/algorithms/__init__.py index 86af9352f..74319a587 100644 --- a/python/src/qdk_chemistry/algorithms/__init__.py +++ b/python/src/qdk_chemistry/algorithms/__init__.py @@ -48,6 +48,8 @@ ) from qdk_chemistry.algorithms.qubit_hamiltonian_solver import QubitHamiltonianSolver from qdk_chemistry.algorithms.qubit_mapper import QdkQubitMapper, QubitMapper +from qdk_chemistry.algorithms.resource_estimator.base import ResourceEstimator +from qdk_chemistry.algorithms.resource_estimator.qdk import QdkQreV1 from qdk_chemistry.algorithms.scf_solver import QdkScfSolver, ScfSolver from qdk_chemistry.algorithms.stability_checker import QdkStabilityChecker, StabilityChecker from qdk_chemistry.algorithms.state_preparation import StatePreparation @@ -79,6 +81,7 @@ "QdkMacisPmc", "QdkOccupationActiveSpaceSelector", "QdkPipekMezeyLocalizer", + "QdkQreV1", "QdkQubitMapper", "QdkScfSolver", "QdkStabilityChecker", @@ -86,6 +89,7 @@ "QdkValenceActiveSpaceSelector", "QubitHamiltonianSolver", "QubitMapper", + "ResourceEstimator", "ScfSolver", "StabilityChecker", "StatePreparation", diff --git a/python/src/qdk_chemistry/algorithms/registry.py b/python/src/qdk_chemistry/algorithms/registry.py index 84e072711..4c370f869 100644 --- a/python/src/qdk_chemistry/algorithms/registry.py +++ b/python/src/qdk_chemistry/algorithms/registry.py @@ -509,6 +509,7 @@ def _register_python_factories(): from qdk_chemistry.algorithms.phase_estimation import PhaseEstimationFactory # noqa: PLC0415 from qdk_chemistry.algorithms.qubit_hamiltonian_solver import QubitHamiltonianSolverFactory # noqa: PLC0415 from qdk_chemistry.algorithms.qubit_mapper import QubitMapperFactory # noqa: PLC0415 + from qdk_chemistry.algorithms.resource_estimator import ResourceEstimatorFactory # noqa: PLC0415 from qdk_chemistry.algorithms.state_preparation import StatePreparationFactory # noqa: PLC0415 from qdk_chemistry.algorithms.time_evolution.builder import TimeEvolutionBuilderFactory # noqa: PLC0415 from qdk_chemistry.algorithms.time_evolution.controlled_circuit_mapper import ( # noqa: PLC0415 @@ -523,6 +524,7 @@ def _register_python_factories(): register_factory(ControlledEvolutionCircuitMapperFactory()) register_factory(CircuitExecutorFactory()) register_factory(PhaseEstimationFactory()) + register_factory(ResourceEstimatorFactory()) _ = _register_cpp_factories() @@ -584,6 +586,7 @@ def _register_python_algorithms(): ) from qdk_chemistry.algorithms.qubit_hamiltonian_solver import DenseMatrixSolver, SparseMatrixSolver # noqa: PLC0415 from qdk_chemistry.algorithms.qubit_mapper import QdkQubitMapper # noqa: PLC0415 + from qdk_chemistry.algorithms.resource_estimator.qdk import QdkQreV1 # noqa: PLC0415 from qdk_chemistry.algorithms.state_preparation import SparseIsometryGF2XStatePreparation # noqa: PLC0415 from qdk_chemistry.algorithms.time_evolution.builder.partially_randomized import ( # noqa: PLC0415 PartiallyRandomized, @@ -610,6 +613,7 @@ def _register_python_algorithms(): register(lambda: QdkFullStateSimulator()) register(lambda: QdkSparseStateSimulator()) register(lambda: IterativePhaseEstimation()) + register(lambda: QdkQreV1()) _register_python_algorithms() diff --git a/python/src/qdk_chemistry/algorithms/resource_estimator/__init__.py b/python/src/qdk_chemistry/algorithms/resource_estimator/__init__.py new file mode 100644 index 000000000..5c6c93cd4 --- /dev/null +++ b/python/src/qdk_chemistry/algorithms/resource_estimator/__init__.py @@ -0,0 +1,12 @@ +"""QDK/Chemistry resource estimator module. + +This module provides algorithm support for quantum resource estimation. +""" + +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +from .base import ResourceEstimatorFactory + +__all__: list[str] = ["ResourceEstimatorFactory"] diff --git a/python/src/qdk_chemistry/algorithms/resource_estimator/base.py b/python/src/qdk_chemistry/algorithms/resource_estimator/base.py new file mode 100644 index 000000000..a394f980c --- /dev/null +++ b/python/src/qdk_chemistry/algorithms/resource_estimator/base.py @@ -0,0 +1,63 @@ +"""QDK/Chemistry resource estimator abstractions. + +This module defines the abstract base class for resource estimator algorithms +that estimate quantum resources required to execute a circuit. +""" + +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from abc import abstractmethod + +from qdk_chemistry.algorithms.base import Algorithm, AlgorithmFactory +from qdk_chemistry.data import Circuit +from qdk_chemistry.data.resource_estimator_data import ResourceEstimatorData + +__all__: list[str] = ["ResourceEstimator", "ResourceEstimatorFactory"] + + +class ResourceEstimator(Algorithm): + """Abstract base class for quantum resource estimator algorithms.""" + + def __init__(self): + """Initialize the ResourceEstimator with default settings.""" + super().__init__() + + def type_name(self) -> str: + """Return the algorithm type name as resource_estimator.""" + return "resource_estimator" + + @abstractmethod + def _run_impl( + self, + circuit: Circuit, + ) -> ResourceEstimatorData: + """Estimate the quantum resources required for the given circuit. + + Estimation parameters are provided via ``self.settings()``. + + Args: + circuit: The quantum circuit to estimate resources for. + + Returns: + ResourceEstimatorData: The estimated resources. + + """ + + +class ResourceEstimatorFactory(AlgorithmFactory): + """Factory class for creating ResourceEstimator instances.""" + + def __init__(self): + """Initialize the ResourceEstimatorFactory.""" + super().__init__() + + def algorithm_type_name(self) -> str: + """Return the algorithm type name as resource_estimator.""" + return "resource_estimator" + + def default_algorithm_name(self) -> str: + """Return the qdk_qre_v1 as default algorithm name.""" + return "qdk_qre_v1" diff --git a/python/src/qdk_chemistry/algorithms/resource_estimator/qdk.py b/python/src/qdk_chemistry/algorithms/resource_estimator/qdk.py new file mode 100644 index 000000000..19b6929bf --- /dev/null +++ b/python/src/qdk_chemistry/algorithms/resource_estimator/qdk.py @@ -0,0 +1,150 @@ +"""QDK/Chemistry Resource Estimator implementation using QDK. + +This module provides a ResourceEstimator implementation that uses the QDK +resource estimation backend to estimate quantum resources required for a circuit. +It supports circuits provided as Q# factory data or QASM. +""" + +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +import qsharp +import qsharp.estimator +import qsharp.openqasm + +from qdk_chemistry.algorithms.resource_estimator.base import ResourceEstimator +from qdk_chemistry.data import Circuit, Settings +from qdk_chemistry.data.resource_estimator_data import ( + ErrorBudget, + EstimationConfig, + LogicalCounts, + LogicalQubit, + PhysicalCounts, + ResourceEstimatorData, +) +from qdk_chemistry.utils import Logger + +__all__: list[str] = ["QdkQreV1", "QdkQreV1Settings"] + + +class QdkQreV1Settings(Settings): + """Settings for the QDK QRE v1 Resource Estimator.""" + + def __init__(self) -> None: + """Initialize QDK QRE v1 settings.""" + Logger.trace_entering() + super().__init__() + self._set_default( + "error_budget", "double", 0.001, + "Total error budget for the estimation" + ) + + +class QdkQreV1(ResourceEstimator): + """QDK QRE v1 Resource Estimator algorithm implementation. + + Uses the Q# resource estimator (v1/v2 API) to estimate physical + resources required for a quantum circuit. + """ + + def __init__(self) -> None: + """Initialize the QDK QRE v1 Resource Estimator.""" + Logger.trace_entering() + super().__init__() + self._settings = QdkQreV1Settings() + + def name(self) -> str: + """Return the algorithm name as qdk_qre_v1.""" + return "qdk_qre_v1" + + def _run_impl( + self, + circuit: Circuit, + ) -> ResourceEstimatorData: + """Estimate the quantum resources required for the given circuit. + + Estimation parameters are taken from ``self.settings()``. + + Args: + circuit: The quantum circuit to estimate resources for. + + Returns: + ResourceEstimatorData: The estimated resources. + + Raises: + RuntimeError: If no suitable circuit representation is available for estimation. + + """ + Logger.trace_entering() + + params = {"errorBudget": self._settings.get("error_budget")} + + if circuit._qsharp_factory is not None: + result = qsharp.estimate( + circuit._qsharp_factory.program, + params, + *circuit._qsharp_factory.parameter.values(), + ) + elif circuit.qasm is not None: + result = qsharp.openqasm.estimate(circuit.qasm, params) + else: + raise RuntimeError( + "Cannot estimate resources: no Q# factory data or QASM representation is available." + ) + + # Convert the EstimatorResult (dict subclass) to typed data + raw = dict(result) if isinstance(result, dict) else result + + lc_raw = raw.get("logicalCounts", {}) + pc_raw = raw.get("physicalCounts", {}) + bd_raw = pc_raw.get("breakdown", {}) + lq_raw = raw.get("logicalQubit", {}) + eb_raw = raw.get("errorBudget", {}) + jp_raw = raw.get("jobParams", {}) + + # Build provenance config from jobParams + qp = jp_raw.get("qubitParams", {}) + qec = jp_raw.get("qecScheme", {}) + config = EstimationConfig( + qubit_model=qp.get("name", qp.get("instructionSet", "")), + qec_scheme=qec.get("name", ""), + error_budget=float(jp_raw.get("errorBudget", 0.0)), + ) + + return ResourceEstimatorData( + logical_counts=LogicalCounts( + num_qubits=lc_raw.get("numQubits", 0), + t_count=lc_raw.get("tCount", 0), + rotation_count=lc_raw.get("rotationCount", 0), + rotation_depth=lc_raw.get("rotationDepth", 0), + ccz_count=lc_raw.get("cczCount", 0), + ccix_count=lc_raw.get("ccixCount", 0), + measurement_count=lc_raw.get("measurementCount", 0), + ), + physical_counts=PhysicalCounts( + physical_qubits=pc_raw.get("physicalQubits", 0), + runtime=pc_raw.get("runtime", 0), + runtime_unit="ns", + rqops=pc_raw.get("rqops", 0), + algorithm_qubits=bd_raw.get("physicalQubitsForAlgorithm", 0), + factory_qubits=bd_raw.get("physicalQubitsForTfactories", 0), + algorithmic_logical_depth=bd_raw.get("algorithmicLogicalDepth", 0), + logical_depth=bd_raw.get("logicalDepth", 0), + ), + logical_qubit=LogicalQubit( + code_distance=lq_raw.get("codeDistance", 0), + logical_cycle_time=lq_raw.get("logicalCycleTime", 0), + logical_error_rate=lq_raw.get("logicalErrorRate", 0.0), + physical_qubits=lq_raw.get("physicalQubits", 0), + ), + error_budget=ErrorBudget( + logical=eb_raw.get("logical", 0.0), + rotations=eb_raw.get("rotations", 0.0), + tstates=eb_raw.get("tstates", 0.0), + ), + estimator=self.name(), + status=raw.get("status", "unknown"), + error=eb_raw.get("logical", 0.0) + eb_raw.get("rotations", 0.0) + eb_raw.get("tstates", 0.0), + config=config, + ) diff --git a/python/src/qdk_chemistry/data/__init__.py b/python/src/qdk_chemistry/data/__init__.py index deb1e7801..46688f589 100644 --- a/python/src/qdk_chemistry/data/__init__.py +++ b/python/src/qdk_chemistry/data/__init__.py @@ -113,6 +113,15 @@ from qdk_chemistry.data.noise_models import QuantumErrorProfile from qdk_chemistry.data.qpe_result import QpeResult from qdk_chemistry.data.qubit_hamiltonian import QubitHamiltonian +from qdk_chemistry.data.resource_estimator_data import ( + CircuitCounts, + ErrorBudget, + EstimationConfig, + LogicalCounts, + LogicalQubit, + PhysicalCounts, + ResourceEstimatorData, +) from qdk_chemistry.data.symmetries import Symmetries from qdk_chemistry.data.time_evolution.base import TimeEvolutionUnitary from qdk_chemistry.data.time_evolution.containers.base import TimeEvolutionUnitaryContainer @@ -134,6 +143,7 @@ "CasWavefunctionContainer", "CholeskyHamiltonianContainer", "Circuit", + "CircuitCounts", "CircuitExecutorData", "Configuration", "ConfigurationSet", @@ -144,11 +154,15 @@ "Element", "EncodingMismatchError", "EnergyExpectationResult", + "ErrorBudget", + "EstimationConfig", "FermionModeOrder", "Hamiltonian", "HamiltonianContainer", "HamiltonianType", "LatticeGraph", + "LogicalCounts", + "LogicalQubit", "MP2Container", "MeasurementData", "ModelOrbitals", @@ -157,9 +171,11 @@ "PauliOperator", "PauliProductFormulaContainer", "PauliTermAccumulator", + "PhysicalCounts", "QpeResult", "QuantumErrorProfile", "QubitHamiltonian", + "ResourceEstimatorData", "SciWavefunctionContainer", "SettingNotFound", "SettingNotFoundError", diff --git a/python/src/qdk_chemistry/data/circuit.py b/python/src/qdk_chemistry/data/circuit.py index 9404b507b..2da50fe91 100644 --- a/python/src/qdk_chemistry/data/circuit.py +++ b/python/src/qdk_chemistry/data/circuit.py @@ -19,7 +19,6 @@ import h5py import qsharp._native -import qsharp.estimator import qsharp.openqasm from qsharp.openqasm import OutputSemantics @@ -193,33 +192,6 @@ def get_qsharp_circuit(self, prune_classical_qubits: bool = False) -> qsharp._na raise RuntimeError("The quantum circuit is not set in a Q# format.") - def estimate( - self, - params: dict[str, Any] | list[Any] | qsharp.estimator.EstimatorParams | None = None, - ) -> qsharp.estimator.EstimatorResult: - """Estimate resources for the quantum circuit. - - Args: - params: Resource estimation parameters. Accepts a dict, list, or ``qsharp.estimator.EstimatorParams``. - - Returns: - qsharp.estimator.EstimatorResult: The estimated resources. - - Raises: - RuntimeError: If no suitable circuit representation is available for estimation. - - """ - if self._qsharp_factory is not None: - return qsharp.estimate( - self._qsharp_factory.program, - params, - *self._qsharp_factory.parameter.values(), - ) - if self.qasm is not None: - return qsharp.openqasm.estimate(self.qasm, params) - - raise RuntimeError("Cannot estimate resources: no Q# factory data or QASM representation is available.") - def get_qiskit_circuit(self): """Convert the Circuit to a Qiskit QuantumCircuit. diff --git a/python/src/qdk_chemistry/data/resource_estimator_data.py b/python/src/qdk_chemistry/data/resource_estimator_data.py new file mode 100644 index 000000000..cb74695dc --- /dev/null +++ b/python/src/qdk_chemistry/data/resource_estimator_data.py @@ -0,0 +1,502 @@ +"""QDK/Chemistry Resource Estimator Data module.""" + +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from typing import Any + +import h5py + +from qdk_chemistry.data.base import DataClass + +__all__: list[str] = [] + + +def _typed_obj_to_dict(obj: object) -> dict[str, Any]: + """Serialize a __slots__-based object to a dict.""" + return {s: getattr(obj, s) for s in obj.__slots__} + + +def _typed_obj_to_hdf5(obj: object, group: h5py.Group) -> None: + """Write a __slots__-based object as HDF5 attributes.""" + for s in obj.__slots__: + group.attrs[s] = getattr(obj, s) + + +def _make_eq(cls): + """Add __eq__ and __repr__ based on __slots__.""" + def __eq__(self, other): + if not isinstance(other, cls): + return NotImplemented + return all(getattr(self, s) == getattr(other, s) for s in self.__slots__) + + def __repr__(self): + fields = ", ".join(f"{s}={getattr(self, s)}" for s in self.__slots__) + return f"{cls.__name__}({fields})" + + cls.__eq__ = __eq__ + cls.__repr__ = __repr__ + return cls + + +# --------------------------------------------------------------------------- +# Circuit-level metrics (backend-agnostic, pre-QEC) +# --------------------------------------------------------------------------- +@_make_eq +class CircuitCounts: + """Circuit-level gate and depth counts. + + These metrics are available from any backend (Qiskit, Cirq, Q#) + without requiring a QEC model. + """ + + __slots__ = ( + "depth", + "num_gates", + "num_single_qubit_clifford", + "num_two_qubit_clifford", + "num_non_clifford", + ) + + def __init__( + self, + depth: int = 0, + num_gates: int = 0, + num_single_qubit_clifford: int = 0, + num_two_qubit_clifford: int = 0, + num_non_clifford: int = 0, + ) -> None: + self.depth = depth + self.num_gates = num_gates + self.num_single_qubit_clifford = num_single_qubit_clifford + self.num_two_qubit_clifford = num_two_qubit_clifford + self.num_non_clifford = num_non_clifford + + +# --------------------------------------------------------------------------- +# Logical-level counts (application profile, QEC-agnostic) +# --------------------------------------------------------------------------- +@_make_eq +class LogicalCounts: + """Logical resource counts from a quantum resource estimation.""" + + __slots__ = ( + "num_qubits", + "t_count", + "rotation_count", + "rotation_depth", + "ccz_count", + "ccix_count", + "measurement_count", + ) + + def __init__( + self, + num_qubits: int = 0, + t_count: int = 0, + rotation_count: int = 0, + rotation_depth: int = 0, + ccz_count: int = 0, + ccix_count: int = 0, + measurement_count: int = 0, + ) -> None: + self.num_qubits = num_qubits + self.t_count = t_count + self.rotation_count = rotation_count + self.rotation_depth = rotation_depth + self.ccz_count = ccz_count + self.ccix_count = ccix_count + self.measurement_count = measurement_count + + +# --------------------------------------------------------------------------- +# Physical-level counts (post-QEC, architecture-dependent) +# --------------------------------------------------------------------------- +@_make_eq +class PhysicalCounts: + """Physical resource counts from a quantum resource estimation.""" + + __slots__ = ( + "physical_qubits", + "runtime", + "runtime_unit", + "rqops", + "algorithm_qubits", + "factory_qubits", + "algorithmic_logical_depth", + "logical_depth", + ) + + def __init__( + self, + physical_qubits: int = 0, + runtime: int = 0, + runtime_unit: str = "ns", + rqops: int = 0, + algorithm_qubits: int = 0, + factory_qubits: int = 0, + algorithmic_logical_depth: int = 0, + logical_depth: int = 0, + ) -> None: + self.physical_qubits = physical_qubits + self.runtime = runtime + self.runtime_unit = runtime_unit + self.rqops = rqops + self.algorithm_qubits = algorithm_qubits + self.factory_qubits = factory_qubits + self.algorithmic_logical_depth = algorithmic_logical_depth + self.logical_depth = logical_depth + + +# --------------------------------------------------------------------------- +# Logical qubit properties +# --------------------------------------------------------------------------- +@_make_eq +class LogicalQubit: + """Logical qubit properties from a quantum resource estimation.""" + + __slots__ = ( + "code_distance", + "logical_cycle_time", + "logical_error_rate", + "physical_qubits", + ) + + def __init__( + self, + code_distance: int = 0, + logical_cycle_time: int = 0, + logical_error_rate: float = 0.0, + physical_qubits: int = 0, + ) -> None: + self.code_distance = code_distance + self.logical_cycle_time = logical_cycle_time + self.logical_error_rate = logical_error_rate + self.physical_qubits = physical_qubits + + +# --------------------------------------------------------------------------- +# Error budget +# --------------------------------------------------------------------------- +@_make_eq +class ErrorBudget: + """Error budget breakdown from a quantum resource estimation.""" + + __slots__ = ("logical", "rotations", "tstates") + + def __init__( + self, + logical: float = 0.0, + rotations: float = 0.0, + tstates: float = 0.0, + ) -> None: + self.logical = logical + self.rotations = rotations + self.tstates = tstates + + +# --------------------------------------------------------------------------- +# Estimation configuration / provenance +# --------------------------------------------------------------------------- +@_make_eq +class EstimationConfig: + """Configuration that produced a resource estimation result. + + Captures the architecture/QEC combination so that individual results + within a Pareto set (or across different backends) can be traced back + to the parameters that generated them. + """ + + __slots__ = ( + "qubit_model", + "qec_scheme", + "error_budget", + "description", + ) + + def __init__( + self, + qubit_model: str = "", + qec_scheme: str = "", + error_budget: float = 0.0, + description: str = "", + ) -> None: + self.qubit_model = qubit_model + self.qec_scheme = qec_scheme + self.error_budget = error_budget + self.description = description + + +# --------------------------------------------------------------------------- +# Top-level DataClass +# --------------------------------------------------------------------------- +class ResourceEstimatorData(DataClass): + """Resource estimation results from quantum resource estimation algorithms. + + The :attr:`config` describing the provenance. + """ + + # Class attribute for filename validation + _data_type_name = "resource_estimator_data" + + # Serialization version for this class + _serialization_version = "0.1.0" + + def __init__( + self, + logical_counts: LogicalCounts, + physical_counts: PhysicalCounts, + logical_qubit: LogicalQubit, + error_budget: ErrorBudget, + estimator: str, + status: str = "success", + error: float = 0.0, + circuit_counts: CircuitCounts | None = None, + config: EstimationConfig | None = None, + ) -> None: + """Initialize resource estimator data. + + Args: + logical_counts: Logical resource counts (qubits, T-gates, rotations, etc.). + physical_counts: Physical resource counts (physical qubits, runtime, RQOPS). + logical_qubit: Logical qubit properties (code distance, cycle time, error rate). + error_budget: Error budget breakdown (logical, rotations, T-states). + estimator: Name of the estimator algorithm that produced this result. + status: Status of the estimation (e.g. ``"success"``). + error: Achieved total error probability of the estimation. + circuit_counts: Optional circuit-level gate/depth counts. + config: Optional estimation configuration describing the + architecture/QEC combination that produced this result. + + """ + self.logical_counts = logical_counts + self.physical_counts = physical_counts + self.logical_qubit = logical_qubit + self.error_budget = error_budget + self.estimator = estimator + self.status = status + self.error = error + self.circuit_counts = circuit_counts + self.config = config + super().__init__() + + # DataClass interface implementation + + def get_summary(self) -> str: + """Get a human-readable summary of the resource estimation. + + Returns: + str: Summary string describing the estimation results. + + """ + lc = self.logical_counts + pc = self.physical_counts + lines = [ + f"Resource Estimator Data (estimator: {self.estimator})", + f" Status: {self.status}", + f" Error: {self.error}", + f" Logical qubits: {lc.num_qubits}", + f" T-count: {lc.t_count}", + f" Physical qubits: {pc.physical_qubits}", + f" Runtime: {pc.runtime} {pc.runtime_unit}", + ] + if self.config is not None: + cfg = self.config + lines.append(f" Qubit model: {cfg.qubit_model}") + lines.append(f" QEC scheme: {cfg.qec_scheme}") + if self.circuit_counts is not None: + cc = self.circuit_counts + lines.append(f" Circuit depth: {cc.depth}") + lines.append(f" Circuit gates: {cc.num_gates}") + return "\n".join(lines) + + def to_json(self) -> dict[str, Any]: + """Convert resource estimator data to a dictionary for JSON serialization. + + Returns: + dict[str, Any]: Dictionary representation of the estimation data. + + """ + result: dict[str, Any] = { + "estimator": self.estimator, + "status": self.status, + "error": self.error, + "logical_counts": _typed_obj_to_dict(self.logical_counts), + "physical_counts": _typed_obj_to_dict(self.physical_counts), + "logical_qubit": _typed_obj_to_dict(self.logical_qubit), + "error_budget": _typed_obj_to_dict(self.error_budget), + } + if self.circuit_counts is not None: + result["circuit_counts"] = _typed_obj_to_dict(self.circuit_counts) + if self.config is not None: + result["config"] = _typed_obj_to_dict(self.config) + return self._add_json_version(result) + + def to_hdf5(self, group: h5py.Group) -> None: + """Save the estimation data to an HDF5 group. + + Args: + group: HDF5 group or file to write the estimation data to. + + """ + self._add_hdf5_version(group) + group.attrs["estimator"] = self.estimator + group.attrs["status"] = self.status + group.attrs["error"] = self.error + + _typed_obj_to_hdf5(self.logical_counts, group.create_group("logical_counts")) + _typed_obj_to_hdf5(self.physical_counts, group.create_group("physical_counts")) + _typed_obj_to_hdf5(self.logical_qubit, group.create_group("logical_qubit")) + _typed_obj_to_hdf5(self.error_budget, group.create_group("error_budget")) + + if self.circuit_counts is not None: + _typed_obj_to_hdf5(self.circuit_counts, group.create_group("circuit_counts")) + if self.config is not None: + _typed_obj_to_hdf5(self.config, group.create_group("config")) + + @classmethod + def from_json(cls, json_data: dict[str, Any]) -> "ResourceEstimatorData": + """Create resource estimator data from a JSON dictionary. + + Args: + json_data: Dictionary containing the serialized data. + + Returns: + ResourceEstimatorData: New instance reconstructed from JSON data. + + """ + cls._validate_json_version(cls._serialization_version, json_data) + lc_d = json_data.get("logical_counts", {}) + pc_d = json_data.get("physical_counts", {}) + lq_d = json_data.get("logical_qubit", {}) + eb_d = json_data.get("error_budget", {}) + cc_d = json_data.get("circuit_counts") + cfg_d = json_data.get("config") + + return cls( + logical_counts=LogicalCounts( + num_qubits=lc_d.get("num_qubits", 0), + t_count=lc_d.get("t_count", 0), + rotation_count=lc_d.get("rotation_count", 0), + rotation_depth=lc_d.get("rotation_depth", 0), + ccz_count=lc_d.get("ccz_count", 0), + ccix_count=lc_d.get("ccix_count", 0), + measurement_count=lc_d.get("measurement_count", 0), + ), + physical_counts=PhysicalCounts( + physical_qubits=pc_d.get("physical_qubits", 0), + runtime=pc_d.get("runtime", 0), + runtime_unit=pc_d.get("runtime_unit", "ns"), + rqops=pc_d.get("rqops", 0), + algorithm_qubits=pc_d.get("algorithm_qubits", 0), + factory_qubits=pc_d.get("factory_qubits", 0), + algorithmic_logical_depth=pc_d.get("algorithmic_logical_depth", 0), + logical_depth=pc_d.get("logical_depth", 0), + ), + logical_qubit=LogicalQubit( + code_distance=lq_d.get("code_distance", 0), + logical_cycle_time=lq_d.get("logical_cycle_time", 0), + logical_error_rate=lq_d.get("logical_error_rate", 0.0), + physical_qubits=lq_d.get("physical_qubits", 0), + ), + error_budget=ErrorBudget( + logical=eb_d.get("logical", 0.0), + rotations=eb_d.get("rotations", 0.0), + tstates=eb_d.get("tstates", 0.0), + ), + estimator=json_data.get("estimator", ""), + status=json_data.get("status", "unknown"), + error=json_data.get("error", 0.0), + circuit_counts=CircuitCounts( + depth=cc_d.get("depth", 0), + num_gates=cc_d.get("num_gates", 0), + num_single_qubit_clifford=cc_d.get("num_single_qubit_clifford", 0), + num_two_qubit_clifford=cc_d.get("num_two_qubit_clifford", 0), + num_non_clifford=cc_d.get("num_non_clifford", 0), + ) if cc_d is not None else None, + config=EstimationConfig( + qubit_model=cfg_d.get("qubit_model", ""), + qec_scheme=cfg_d.get("qec_scheme", ""), + error_budget=cfg_d.get("error_budget", 0.0), + description=cfg_d.get("description", ""), + ) if cfg_d is not None else None, + ) + + @classmethod + def from_hdf5(cls, group: h5py.Group) -> "ResourceEstimatorData": + """Load resource estimator data from an HDF5 group. + + Args: + group: HDF5 group or file containing the data. + + Returns: + ResourceEstimatorData: New instance reconstructed from HDF5 data. + + """ + cls._validate_hdf5_version(cls._serialization_version, group) + + lc_grp = group["logical_counts"] + pc_grp = group["physical_counts"] + lq_grp = group["logical_qubit"] + eb_grp = group["error_budget"] + + circuit_counts = None + if "circuit_counts" in group: + cc_grp = group["circuit_counts"] + circuit_counts = CircuitCounts( + depth=int(cc_grp.attrs["depth"]), + num_gates=int(cc_grp.attrs["num_gates"]), + num_single_qubit_clifford=int(cc_grp.attrs["num_single_qubit_clifford"]), + num_two_qubit_clifford=int(cc_grp.attrs["num_two_qubit_clifford"]), + num_non_clifford=int(cc_grp.attrs["num_non_clifford"]), + ) + + config = None + if "config" in group: + cfg_grp = group["config"] + config = EstimationConfig( + qubit_model=str(cfg_grp.attrs["qubit_model"]), + qec_scheme=str(cfg_grp.attrs["qec_scheme"]), + error_budget=float(cfg_grp.attrs["error_budget"]), + description=str(cfg_grp.attrs["description"]), + ) + + return cls( + logical_counts=LogicalCounts( + num_qubits=int(lc_grp.attrs["num_qubits"]), + t_count=int(lc_grp.attrs["t_count"]), + rotation_count=int(lc_grp.attrs["rotation_count"]), + rotation_depth=int(lc_grp.attrs["rotation_depth"]), + ccz_count=int(lc_grp.attrs["ccz_count"]), + ccix_count=int(lc_grp.attrs["ccix_count"]), + measurement_count=int(lc_grp.attrs["measurement_count"]), + ), + physical_counts=PhysicalCounts( + physical_qubits=int(pc_grp.attrs["physical_qubits"]), + runtime=int(pc_grp.attrs["runtime"]), + runtime_unit=str(pc_grp.attrs["runtime_unit"]), + rqops=int(pc_grp.attrs["rqops"]), + algorithm_qubits=int(pc_grp.attrs["algorithm_qubits"]), + factory_qubits=int(pc_grp.attrs["factory_qubits"]), + algorithmic_logical_depth=int(pc_grp.attrs["algorithmic_logical_depth"]), + logical_depth=int(pc_grp.attrs["logical_depth"]), + ), + logical_qubit=LogicalQubit( + code_distance=int(lq_grp.attrs["code_distance"]), + logical_cycle_time=int(lq_grp.attrs["logical_cycle_time"]), + logical_error_rate=float(lq_grp.attrs["logical_error_rate"]), + physical_qubits=int(lq_grp.attrs["physical_qubits"]), + ), + error_budget=ErrorBudget( + logical=float(eb_grp.attrs["logical"]), + rotations=float(eb_grp.attrs["rotations"]), + tstates=float(eb_grp.attrs["tstates"]), + ), + estimator=str(group.attrs.get("estimator", "")), + status=str(group.attrs.get("status", "unknown")), + error=float(group.attrs.get("error", 0.0)), + circuit_counts=circuit_counts, + config=config, + ) diff --git a/python/tests/test_circuit.py b/python/tests/test_circuit.py index 58637a038..cc2afca50 100644 --- a/python/tests/test_circuit.py +++ b/python/tests/test_circuit.py @@ -14,6 +14,7 @@ import pytest import qsharp +from qdk_chemistry.algorithms import create as create_algorithm from qdk_chemistry.data import Circuit from qdk_chemistry.data.circuit import QsharpFactoryData from qdk_chemistry.plugins.qiskit import QDK_CHEMISTRY_HAS_QISKIT @@ -411,10 +412,10 @@ def test_cannot_add_new_attributes(self): class TestCircuitEstimate: - """Test cases for Circuit.estimate method.""" + """Test cases for Circuit resource estimation via the algorithm.""" def test_estimate_from_factory(self): - """Test that estimate works with Q# factory data.""" + """Test that resource estimation works with Q# factory data.""" state_prep_params = { "rowMap": [1, 0], "stateVector": [0.6, 0.0, 0.0, 0.8], @@ -426,12 +427,13 @@ def test_estimate_from_factory(self): parameter=state_prep_params, ) circuit = Circuit(qsharp_factory=qsharp_factory) - result = circuit.estimate() + estimator = create_algorithm("resource_estimator") + result = estimator.run(circuit) assert result is not None - assert hasattr(result, "logical_counts") + assert result.logical_counts.num_qubits >= 0 def test_estimate_from_qasm(self): - """Test that estimate works with QASM representation.""" + """Test that resource estimation works with QASM representation.""" qasm_with_t = """ OPENQASM 3.0; include "stdgates.inc"; @@ -444,12 +446,13 @@ def test_estimate_from_qasm(self): c[1] = measure q[1]; """ circuit = Circuit(qasm=qasm_with_t) - result = circuit.estimate() + estimator = create_algorithm("resource_estimator") + result = estimator.run(circuit) assert result is not None - assert hasattr(result, "logical_counts") + assert result.logical_counts.t_count >= 0 def test_estimate_raises_with_qir_only(self): - """Test that estimate raises when only QIR representation is available.""" + """Test that estimation raises when only QIR representation is available.""" qir = qsharp.openqasm.compile(""" OPENQASM 3.0; include "stdgates.inc"; @@ -461,5 +464,6 @@ def test_estimate_raises_with_qir_only(self): c[1] = measure q[1]; """) circuit = Circuit(qir=qir) + estimator = create_algorithm("resource_estimator") with pytest.raises(RuntimeError, match="Cannot estimate resources"): - circuit.estimate() + estimator.run(circuit) diff --git a/python/tests/test_resource_estimator.py b/python/tests/test_resource_estimator.py new file mode 100644 index 000000000..48a8e16ea --- /dev/null +++ b/python/tests/test_resource_estimator.py @@ -0,0 +1,264 @@ +"""Tests for the ResourceEstimator algorithm in QDK/Chemistry.""" + +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import tempfile +from pathlib import Path + +import h5py +import pytest +import qsharp + +from qdk_chemistry.algorithms import create +from qdk_chemistry.algorithms.resource_estimator.base import ResourceEstimator +from qdk_chemistry.algorithms.resource_estimator.qdk import QdkQreV1 +from qdk_chemistry.data import Circuit +from qdk_chemistry.data.circuit import QsharpFactoryData +from qdk_chemistry.data.resource_estimator_data import ( + CircuitCounts, + ErrorBudget, + EstimationConfig, + LogicalCounts, + LogicalQubit, + PhysicalCounts, + ResourceEstimatorData, +) +from qdk_chemistry.utils.qsharp import QSHARP_UTILS + + +class TestResourceEstimatorRegistry: + """Test that the ResourceEstimator is properly registered.""" + + def test_create_default_resource_estimator(self): + """Test creating the default resource estimator via the registry.""" + estimator = create("resource_estimator") + assert isinstance(estimator, ResourceEstimator) + assert isinstance(estimator, QdkQreV1) + + def test_create_by_name(self): + """Test creating a resource estimator by explicit name.""" + estimator = create("resource_estimator", "qdk_qre_v1") + assert isinstance(estimator, QdkQreV1) + + def test_algorithm_name(self): + """Test the algorithm name property.""" + estimator = QdkQreV1() + assert estimator.name() == "qdk_qre_v1" + + def test_algorithm_type_name(self): + """Test the algorithm type name property.""" + estimator = QdkQreV1() + assert estimator.type_name() == "resource_estimator" + + def test_default_settings(self): + """Test that default settings are applied.""" + estimator = QdkQreV1() + assert estimator.settings().get("error_budget") == 0.001 + + +class TestQdkQreV1: + """Test cases for QdkQreV1 execution.""" + + def test_estimate_from_factory(self): + """Test resource estimation with Q# factory data.""" + state_prep_params = { + "rowMap": [1, 0], + "stateVector": [0.6, 0.0, 0.0, 0.8], + "expansionOps": [], + "numQubits": 2, + } + qsharp_factory = QsharpFactoryData( + program=QSHARP_UTILS.StatePreparation.MakeStatePreparationCircuit, + parameter=state_prep_params, + ) + circuit = Circuit(qsharp_factory=qsharp_factory) + estimator = QdkQreV1() + result = estimator.run(circuit) + assert isinstance(result, ResourceEstimatorData) + assert result.estimator == "qdk_qre_v1" + assert result.logical_counts is not None + assert result.logical_counts.num_qubits >= 0 + assert result.physical_counts.physical_qubits >= 0 + + def test_estimate_from_qasm(self): + """Test resource estimation with QASM representation.""" + qasm_with_t = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[2] q; + bit[2] c; + h q[0]; + t q[0]; + cx q[0], q[1]; + c[0] = measure q[0]; + c[1] = measure q[1]; + """ + circuit = Circuit(qasm=qasm_with_t) + estimator = QdkQreV1() + result = estimator.run(circuit) + assert isinstance(result, ResourceEstimatorData) + assert result.logical_counts.t_count >= 0 + assert result.status == "success" + # New fields populated by QdkQreV1 + assert result.physical_counts.algorithm_qubits > 0 + assert result.physical_counts.factory_qubits >= 0 + assert result.physical_counts.runtime_unit == "ns" + assert result.error > 0.0 + assert result.config is not None + assert result.config.qec_scheme == "surface_code" + assert result.config.qubit_model != "" + + def test_estimate_raises_with_qir_only(self): + """Test that estimation raises when only QIR representation is available.""" + qir = qsharp.openqasm.compile(""" + OPENQASM 3.0; + include "stdgates.inc"; + qubit[2] q; + bit[2] c; + h q[0]; + cx q[0], q[1]; + c[0] = measure q[0]; + c[1] = measure q[1]; + """) + circuit = Circuit(qir=qir) + estimator = QdkQreV1() + with pytest.raises(RuntimeError, match="Cannot estimate resources"): + estimator.run(circuit) + + def test_estimate_with_custom_error_budget(self): + """Test resource estimation with custom error budget setting.""" + qasm_with_t = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[2] q; + bit[2] c; + h q[0]; + t q[0]; + cx q[0], q[1]; + c[0] = measure q[0]; + c[1] = measure q[1]; + """ + circuit = Circuit(qasm=qasm_with_t) + estimator = QdkQreV1() + estimator.settings().set("error_budget", 0.01) + result = estimator.run(circuit) + assert isinstance(result, ResourceEstimatorData) + + +class TestResourceEstimatorData: + """Test cases for ResourceEstimatorData DataClass.""" + + def _make_sample_data(self) -> ResourceEstimatorData: + return ResourceEstimatorData( + logical_counts=LogicalCounts( + num_qubits=4, t_count=10, rotation_count=0, + rotation_depth=0, ccz_count=0, ccix_count=0, measurement_count=2, + ), + physical_counts=PhysicalCounts( + physical_qubits=1000, runtime=500, runtime_unit="ns", + rqops=100, algorithm_qubits=600, factory_qubits=400, + algorithmic_logical_depth=3, logical_depth=10, + ), + logical_qubit=LogicalQubit( + code_distance=7, logical_cycle_time=2800, + logical_error_rate=3e-6, physical_qubits=98, + ), + error_budget=ErrorBudget( + logical=0.0005, rotations=0.0, tstates=0.0005, + ), + estimator="qdk_qre_v1", + status="success", + error=0.001, + circuit_counts=CircuitCounts( + depth=5, num_gates=8, + num_single_qubit_clifford=3, num_two_qubit_clifford=2, + num_non_clifford=3, + ), + config=EstimationConfig( + qubit_model="qubit_gate_ns_e3", + qec_scheme="surface_code", + error_budget=0.001, + ), + ) + + def test_properties(self): + """Test data class properties.""" + data = self._make_sample_data() + assert data.status == "success" + assert data.error == 0.001 + assert data.logical_counts.num_qubits == 4 + assert data.logical_counts.t_count == 10 + assert data.physical_counts.physical_qubits == 1000 + assert data.physical_counts.runtime == 500 + assert data.physical_counts.runtime_unit == "ns" + assert data.physical_counts.algorithm_qubits == 600 + assert data.physical_counts.factory_qubits == 400 + assert data.physical_counts.algorithmic_logical_depth == 3 + assert data.physical_counts.logical_depth == 10 + assert data.logical_qubit.code_distance == 7 + assert data.error_budget.logical == 0.0005 + assert data.estimator == "qdk_qre_v1" + assert data.circuit_counts is not None + assert data.circuit_counts.depth == 5 + assert data.circuit_counts.num_non_clifford == 3 + assert data.config is not None + assert data.config.qec_scheme == "surface_code" + + def test_immutability(self): + """Test that the data class is immutable after init.""" + data = self._make_sample_data() + with pytest.raises(AttributeError): + data.estimator = "changed" + + def test_get_summary(self): + """Test human-readable summary.""" + data = self._make_sample_data() + summary = data.get_summary() + assert "Resource Estimator Data" in summary + assert "qdk_qre_v1" in summary + assert "success" in summary + assert "4" in summary # logical qubits + assert "10" in summary # T-count + + def test_json_roundtrip(self): + """Test JSON serialization/deserialization roundtrip.""" + original = self._make_sample_data() + json_data = original.to_json() + restored = ResourceEstimatorData.from_json(json_data) + assert restored.logical_counts == original.logical_counts + assert restored.physical_counts == original.physical_counts + assert restored.logical_qubit == original.logical_qubit + assert restored.error_budget == original.error_budget + assert restored.estimator == original.estimator + assert restored.status == original.status + assert restored.error == original.error + assert restored.circuit_counts == original.circuit_counts + assert restored.config == original.config + + def test_hdf5_roundtrip(self): + """Test HDF5 serialization/deserialization roundtrip.""" + original = self._make_sample_data() + with tempfile.TemporaryDirectory() as tmpdir: + filepath = Path(tmpdir) / "test.resource_estimator_data.h5" + with h5py.File(filepath, "w") as f: + original.to_hdf5(f) + with h5py.File(filepath, "r") as f: + restored = ResourceEstimatorData.from_hdf5(f) + assert restored.logical_counts == original.logical_counts + assert restored.physical_counts == original.physical_counts + assert restored.logical_qubit == original.logical_qubit + assert restored.error_budget == original.error_budget + assert restored.estimator == original.estimator + + def test_json_file_roundtrip(self): + """Test to_json_file and from_json_file.""" + original = self._make_sample_data() + with tempfile.TemporaryDirectory() as tmpdir: + filepath = Path(tmpdir) / "test.resource_estimator_data.json" + original.to_json_file(str(filepath)) + restored = ResourceEstimatorData.from_json_file(str(filepath)) + assert restored.logical_counts == original.logical_counts + assert restored.estimator == original.estimator