diff --git a/docs/source/_static/examples/python/circuit.py b/docs/source/_static/examples/python/circuit.py index 3f1b6ef5b..bc40c8891 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)[0] +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 2d17d6055..32802ab45 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 c7211b0f0..532d4d85b 100644 --- a/examples/qpe_stretched_n2.ipynb +++ b/examples/qpe_stretched_n2.ipynb @@ -1,675 +1,669 @@ { - "cells": [ - { - "cell_type": "markdown", - "id": "3a823352", - "metadata": {}, - "source": [ - "# Iterative phase estimation using `qdk-chemistry`\n", - "\n", - "This notebook provides an example of `qdk-chemistry` functionality through an end-to-end workflow estimating the ground state energy of a multi-configurational quantum chemistry system. This is one example of a wide range of functionality in `qdk-chemistry`. Please see for the full documentation.\n", - "\n", - "In addition to [installing `qdk-chemistry`](https://github.com/microsoft/qdk-chemistry/blob/main/INSTALL.md), you will need to install the `jupyter` and `qiskit-extras` extras to run this notebook:\n", - "\n", - "```bash\n", - "pip install 'qdk-chemistry[jupyter,qiskit-extras]'\n", - "```\n", - "\n", - "This installs the additional dependencies required by this notebook (ipykernel, pandas, pyscf, qiskit)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "adbe6e61", - "metadata": {}, - "outputs": [], - "source": [ - "# Load frequently used external packages\n", - "from pathlib import Path\n", - "from collections import Counter\n", - "\n", - "import numpy as np\n", - "import pandas as pd\n", - "\n", - "# Reduce logging output for demo\n", - "from qdk_chemistry.utils import Logger\n", - "Logger.set_global_level(Logger.LogLevel.off)" - ] - }, - { - "cell_type": "markdown", - "id": "8f6c1d96", - "metadata": {}, - "source": [ - "## Loading the stretched N2 structure\n", - "\n", - "This example uses a *stretched* N2 molecule, which introduces multi-reference character in the wavefunction. The structure is loaded from a [XYZ-format](https://en.wikipedia.org/wiki/XYZ_file_format) file." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "baff7f4f", - "metadata": {}, - "outputs": [], - "source": [ - "from qdk_chemistry.data import Structure\n", - "\n", - "# Stretched N2 structure at 1.270025 Å bond length\n", - "structure = Structure.from_xyz_file(Path(\"data/stretched_n2.structure.xyz\"))" - ] - }, - { - "cell_type": "markdown", - "id": "be49f433", - "metadata": {}, - "source": [ - "## Generating and optimizing the molecular orbitals\n", - "\n", - "This step performs a Hartree-Fock (HF) self-consistent field (SCF) calculation to generate an approximate initial wavefunction and ground-state energy guess." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "792e2fa1", - "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(\n", - " structure,\n", - " charge=0,\n", - " spin_multiplicity=1,\n", - " basis_or_guess=\"cc-pvdz\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "55d1da6b", - "metadata": {}, - "source": [ - "Unlike the basis functions, canonical molecular orbitals from SCF calculations are often delocalized over the entire molecule. As an example, we use the MP2 natural orbital localization method in `qdk-chemistry` to generate orbitals that tend to yield more chemically meaningful representations.\n", - "\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": "4c072e66", - "metadata": {}, - "outputs": [], - "source": [ - "from qdk_chemistry.utils import compute_valence_space_parameters\n", - "\n", - "# Reduce the number of orbitals\n", - "num_val_e, num_val_o = compute_valence_space_parameters(wfn_hf, charge=0)\n", - "active_space_selector = create(\n", - " \"active_space_selector\",\n", - " \"qdk_valence\",\n", - " num_active_electrons=num_val_e,\n", - " num_active_orbitals=num_val_o\n", - ")\n", - "valence_wf = active_space_selector.run(wfn_hf)\n", - "\n", - "# Localize the orbitals\n", - "localizer = create(\"orbital_localizer\", \"qdk_mp2_natural_orbitals\")\n", - "valence_indices = valence_wf.get_orbitals().get_active_space_indices()\n", - "loc_wfn = localizer.run(valence_wf, *valence_indices)\n", - "print(\"Localized orbitals:\\n\", loc_wfn.get_orbitals().get_summary())" - ] - }, - { - "cell_type": "markdown", - "id": "de0b4c16", - "metadata": {}, - "source": [ - "## Optimizing problem size with active space selection\n", - "\n", - "Active space selection focuses the quantum calculation on a subset of the electrons and orbitals in the system.\n", - "\n", - "This example uses `qdk_autocas_eos`, an automated entropy-based active-space selection method to identify strongly\n", - "correlated orbitals." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f961642c", - "metadata": {}, - "outputs": [], - "source": [ - "# Construct a Hamiltonian from the localized orbitals\n", - "hamiltonian_constructor = create(\"hamiltonian_constructor\")\n", - "loc_orbitals = loc_wfn.get_orbitals()\n", - "loc_hamiltonian = hamiltonian_constructor.run(loc_orbitals)\n", - "num_alpha_electrons, num_beta_electrons = loc_wfn.get_active_num_electrons()\n", - "\n", - "# Compute the selected configuration interaction wavefunction\n", - "macis_mc = create(\n", - " \"multi_configuration_calculator\",\n", - " \"macis_asci\",\n", - " calculate_one_rdm=True,\n", - " calculate_two_rdm=True,\n", - " )\n", - "_, wfn_sci = macis_mc.run(loc_hamiltonian, num_alpha_electrons, num_beta_electrons)\n", - "\n", - "# Optimize the problem with autoCAS-EOS active space selection\n", - "autocas = create(\"active_space_selector\", \"qdk_autocas_eos\")\n", - "autocas_wfn = autocas.run(wfn_sci)\n", - "indices, _ = autocas_wfn.get_orbitals().get_active_space_indices()\n", - "print(f\"autoCAS-EOS selected {len(indices)} of {num_val_o} orbitals for the active space: indices={list(indices)}\")" - ] - }, - { - "cell_type": "markdown", - "id": "accc09ed", - "metadata": {}, - "source": [ - "The next step constructs the active-space Hamiltonian and computes a multi-configuration wavefunction for the selected\n", - "active space.\n", - "This step also provides a reference energy for the active space system that can be used to benchmark the iQPE result." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cccc74f4", - "metadata": {}, - "outputs": [], - "source": [ - "# Construct the active space Hamiltonian\n", - "hamiltonian_constructor = create(\"hamiltonian_constructor\")\n", - "refined_orbitals = autocas_wfn.get_orbitals()\n", - "active_hamiltonian = hamiltonian_constructor.run(refined_orbitals)\n", - "\n", - "# Calculate the exact wavefunction and energy with CASCI\n", - "alpha_electrons, beta_electrons = autocas_wfn.get_active_num_electrons()\n", - "mc = create(\"multi_configuration_calculator\", \"macis_cas\")\n", - "e_cas, wfn_cas = mc.run(active_hamiltonian, alpha_electrons, beta_electrons)\n", - "print(f\"Active space system energy: {e_cas:.6f} Hartree\")" - ] - }, - { - "cell_type": "markdown", - "id": "c811f60a", - "metadata": {}, - "source": [ - "Visualizing the selected active space is an important step to ensure that the selected orbitals are chemically meaningful.\n", - "This step uses visualization tools built into `qdk` to display the selected active space orbitals." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3e5c51c8", - "metadata": {}, - "outputs": [], - "source": [ - "from qdk.widgets import MoleculeViewer\n", - "from qdk_chemistry.utils.cubegen import generate_cubefiles_from_orbitals\n", - "\n", - "# Make a general guess about the number of occupied orbitals\n", - "(num_alpha, num_beta) = wfn_cas.get_total_num_electrons()\n", - "num_occupied = (num_alpha + num_beta) // 2\n", - "\n", - "active_orbitals = wfn_cas.get_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 < num_occupied 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": "dc9835f3", - "metadata": {}, - "source": [ - "## Optimizing trial wavefunction loading onto a quantum computer\n", - "\n", - "The multi-configuration wavefunction in the active space can serve as the trial state for the iQPE algorithm.\n", - "However, this information needs to be loaded as a state on a quantum computer.\n", - "\n", - "The amount of data loaded onto the quantum computer can be optimized by exploiting the sparsity of the wavefunction.\n", - "This step identifies the dominant configurations in the wavefunction using visualization tools provided by `qdk`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "64effd7f", - "metadata": {}, - "outputs": [], - "source": [ - "from qdk.widgets import Histogram\n", - "\n", - "# Plot top configuration weights from the CASCI wavefunction\n", - "num_configurations = len(wfn_cas.get_active_determinants())\n", - "print(f\"Total configurations in the CASCI wavefunction: {num_configurations}\")\n", - "print(\"Plotting the configurations by weight.\")\n", - "top_configurations = wfn_cas.get_top_determinants()\n", - "display(\n", - " Histogram(\n", - " bar_values={k.to_string(): np.abs(v)**2 for k, v in top_configurations.items()}, \n", - " items=\"top-25\", \n", - " sort=\"high-to-low\",\n", - " )\n", - " )" - ] - }, - { - "cell_type": "markdown", - "id": "51d9685c", - "metadata": {}, - "source": [ - "To run quantum phase estimation, we need to prepare an initial trial state for the calculation.\n", - "In this example, we will take the first two terms of the multi-configuration wavefunction, add a small amount of noise, and check their overlap with the full wavefunction.\n", - "\n", - "Choosing fewer terms still gives us good overlap in the trial state, and also illustrates QPE output with imperfect starting information." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "62520449", - "metadata": {}, - "outputs": [], - "source": [ - "from utils.qpe_utils import prepare_top_dets_trial_state\n", - "\n", - "# Prepare a trial state with two determinants. Compute its overlap with the CASCI wavefunction.\n", - "wfn_trial, fidelity = prepare_top_dets_trial_state(wfn_cas, active_hamiltonian, num_dets=2)\n", - "print(f\"Overlap of trial state with CASCI wavefunction: {fidelity:.2%}\")\n", - "\n", - "# Generate a plot of the configurations in the trial wavefunction\n", - "configurations = wfn_trial.get_top_determinants()\n", - "display(\n", - " Histogram(\n", - " bar_values={k.to_string(): np.abs(v)**2 for k, v in configurations.items()},\n", - " sort=\"high-to-low\",\n", - " )\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "b5349c2c", - "metadata": {}, - "source": [ - "There are many ways to \"load\" this state onto a quantum computer.\n", - "This example uses a popular method as a basis for comparison with our chemistry-aware optimized approach.\n", - "Circuit statistics are shown and the circuit is visualized using built-in `qdk` functions." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "455d4494", - "metadata": {}, - "outputs": [], - "source": [ - "import qdk_chemistry.plugins.qiskit # Enable Qiskit plugin capabilities # noqa: F401\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\")\n", - "regular_isometry_circuit = state_prep.run(wfn_trial)\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(\n", - " regular_isometry_circuit.estimate().logical_counts.items(),\n", - " columns=['Logical Estimate', 'Counts']\n", - ")\n", - "display(df)" - ] - }, - { - "cell_type": "markdown", - "id": "a5d97513", - "metadata": {}, - "source": [ - "The popular approach for state preparation requires a larger number of operations with numerous fine rotations.\n", - "However, `qdk-chemistry` provides optimized state preparation methods that exploit the structure of chemistry wavefunctions to reduce the number of operations and improve noise resilience." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fbc8ea70", - "metadata": {}, - "outputs": [], - "source": [ - "from qdk.widgets import Circuit\n", - "\n", - "# Generate state preparation circuit for the sparse state via GF2+X sparse isometry\n", - "state_prep = create(\"state_prep\", \"sparse_isometry_gf2x\")\n", - "sparse_isometry_circuit = state_prep.run(wfn_trial)\n", - "\n", - "# Visualize the sparse isometry circuit, idle and classical qubits are removed\n", - "display(Circuit(sparse_isometry_circuit.get_qsharp_circuit()))\n", - "\n", - "# Print logical qubit counts estimated from the circuit\n", - "df = pd.DataFrame(\n", - " sparse_isometry_circuit.estimate().logical_counts.items(),\n", - " columns=['Logical Estimate', 'Counts']\n", - ")\n", - "display(df)" - ] - }, - { - "cell_type": "markdown", - "id": "15ed2623", - "metadata": {}, - "source": [ - "## Estimating the ground state energy with iterative quantum phase estimation\n", - "\n", - "Kitaev-style iterative quantum phase estimation (iQPE) estimates an eigenphase of the time-evolution operator $U = e^{-iHt}$ using one ancilla qubit and a sequence of controlled-$U^{2^k}$ applications.\n", - " \n", - "Each iteration measures one bit of the phase (from most-significant to least-significant) and uses phase feedback to refine the estimate." - ] - }, - { - "cell_type": "markdown", - "id": "f345165d", - "metadata": {}, - "source": [ - "The classical Hamiltonian for the active space must be mapped to a qubit Hamiltonian that can be measured on a quantum computer.\n", - "The Jordan-Wigner transformation is a popular mapping that is used in this example." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d50081e5", - "metadata": {}, - "outputs": [], - "source": [ - "from qdk_chemistry.data import MajoranaMapping\n", - "\n", - "# Prepare the qubit-mapped Hamiltonian\n", - "n_spin_orbitals = 2 * len(active_hamiltonian.get_orbitals().get_active_space_indices()[0])\n", - "qubit_mapper = create(\"qubit_mapper\", \"qiskit\")\n", - "qubit_hamiltonian = qubit_mapper.run(active_hamiltonian, MajoranaMapping.jordan_wigner(n_spin_orbitals))\n", - "print(\"Qubit Hamiltonian:\\n\", qubit_hamiltonian.get_summary())" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "67327154", - "metadata": {}, - "outputs": [], - "source": [ - "# Set up parameters for iQPE\n", - "from utils.qpe_utils import compute_evolution_time\n", - "\n", - "M_PRECISION = 10\n", - "SHOTS_PER_BIT = 3\n", - "SIMULATOR_SEED = 42\n", - "\n", - "# Propose evolution time given the qubit Hamiltonian and number of precision bits\n", - "evolution_time = compute_evolution_time(qubit_hamiltonian, num_bits=M_PRECISION)\n", - "print(f\"Proposed evolution time: {evolution_time:.4f} Hartree^-1\")" - ] - }, - { - "cell_type": "markdown", - "id": "a43d9a9b", - "metadata": {}, - "source": [ - "The circuit for iQPE consists of initial trial state preparation followed by multiple controlled time-evolution operations.\n", - "This cell visualizes the one iteration of the iQPE circuit in QASM format using built-in `qdk` visualization tools." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b4a368e3", - "metadata": {}, - "outputs": [], - "source": [ - "from qdk_chemistry.data import AlgorithmRef\n", - "\n", - "unitary_builder = AlgorithmRef(\"hamiltonian_unitary_builder\", \"trotter\", time=evolution_time)\n", - "circuit_mapper = AlgorithmRef(\"controlled_circuit_mapper\", \"pauli_sequence\")\n", - "\n", - "# Use the circuit builder to generate a single iteration circuit for visualization\n", - "circuit_builder = create(\n", - " \"qpe_circuit_builder\", \n", - " \"qdk_iterative\",\n", - " num_bits=M_PRECISION,\n", - " num_iteration=M_PRECISION - 3, # 3rd from last iteration\n", - " unitary_builder=unitary_builder,\n", - " controlled_circuit_mapper=circuit_mapper,\n", - ")\n", - "iqpe_iter_circuits = circuit_builder.run(\n", - " state_preparation=sparse_isometry_circuit,\n", - " qubit_hamiltonian=qubit_hamiltonian,\n", - ")\n", - "\n", - "# Visualize the iQPE iteration circuit\n", - "display(Circuit(iqpe_iter_circuits[0].get_qsharp_circuit()))" - ] - }, - { - "cell_type": "markdown", - "id": "7b85ca39", - "metadata": {}, - "source": [ - "This real-time example performs a single-trial low-precision iQPE run on the `qdk` full state simulator." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5d1c6526", - "metadata": {}, - "outputs": [], - "source": [ - "# Execute the iQPE algorithm for a single trial\n", - "circuit_executor = AlgorithmRef(\"circuit_executor\", \"qdk_full_state_simulator\", seed=SIMULATOR_SEED)\n", - "iqpe_circuit_builder_low = AlgorithmRef(\n", - " \"qpe_circuit_builder\",\n", - " \"qdk_iterative\",\n", - " num_bits=6,\n", - " controlled_circuit_mapper=circuit_mapper,\n", - " unitary_builder=unitary_builder,\n", - ")\n", - "\n", - "iqpe_low = create(\n", - " \"phase_estimation\",\n", - " \"qdk_iterative\",\n", - " shots_per_bit=SHOTS_PER_BIT,\n", - ")\n", - "iqpe_low.settings().set(\"qpe_circuit_builder\", iqpe_circuit_builder_low)\n", - "iqpe_low.settings().set(\"circuit_executor\", circuit_executor)\n", - "result = iqpe_low.run(\n", - " state_preparation=sparse_isometry_circuit,\n", - " qubit_hamiltonian=qubit_hamiltonian,\n", - ")\n", - "\n", - "# Summarize the QPE results\n", - "estimated_energy = result.raw_energy + active_hamiltonian.get_core_energy()\n", - "estimated_error = abs(estimated_energy - e_cas)\n", - "print(\"QPE Results for 6-bit precision:\")\n", - "print(f\"Reference CASCI energy: {e_cas:.6f} Hartree\")\n", - "print(f\"Total energy from phase estimation: {estimated_energy:.6f} Hartree\")\n", - "print(f\"Energy difference with CASCI energy: {estimated_error:.3e} Hartree\")" - ] - }, - { - "cell_type": "markdown", - "id": "452d0cc2", - "metadata": {}, - "source": [ - "The above cell used a low-precision single trial for a real-time example.\n", - "However, iQPE generally requires multiple trials to establish confidence in the resulting estimate.\n", - "The following cell performs a multi-trial iQPE run with high precision using the same simulator." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "76891f8b", - "metadata": {}, - "outputs": [], - "source": [ - "# Large number of trials with results previously calculated and saved\n", - "NUM_TRIALS = 20\n", - "RESULTS_DIR = Path(\n", - " f\"results_iqpe/precision_{M_PRECISION}/time_{round(evolution_time, 12)}\"\n", - ")\n", - "\n", - "iqpe_circuit_builder = AlgorithmRef(\n", - " \"qpe_circuit_builder\",\n", - " \"qdk_iterative\",\n", - " num_bits=M_PRECISION,\n", - " controlled_circuit_mapper=circuit_mapper,\n", - " unitary_builder=unitary_builder,\n", - ")\n", - "\n", - "# Run iQPE if results do not already exist\n", - "RESULTS_DIR.mkdir(parents=True, exist_ok=True)\n", - "\n", - "for trial in range(NUM_TRIALS):\n", - " trial_seed = SIMULATOR_SEED + trial\n", - " json_path = RESULTS_DIR / f\"iqpe_result_{trial_seed}.qpe_result.json\"\n", - " if not json_path.exists():\n", - " print(f\"Running trial {trial + 1} of {NUM_TRIALS}...\")\n", - " trial_executor = AlgorithmRef(\"circuit_executor\", \"qdk_full_state_simulator\", seed=trial_seed)\n", - " iqpe = create(\n", - " \"phase_estimation\",\n", - " \"qdk_iterative\",\n", - " shots_per_bit=SHOTS_PER_BIT,\n", - " )\n", - " iqpe.settings().set(\"qpe_circuit_builder\", iqpe_circuit_builder)\n", - " iqpe.settings().set(\"circuit_executor\", trial_executor)\n", - " result = iqpe.run(\n", - " state_preparation=sparse_isometry_circuit,\n", - " qubit_hamiltonian=qubit_hamiltonian,\n", - " )\n", - " result.to_json_file(json_path)" - ] - }, - { - "cell_type": "markdown", - "id": "f0f7da6f", - "metadata": {}, - "source": [ - "For a system with noise or an imperfect trial state, multiple trials of iQPE are needed to obtain a reliable estimate of the ground state energy.\n", - "This estimate is typically taken as the most frequently observed energy from multiple trials (\"majority vote\")." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ac88390f", - "metadata": {}, - "outputs": [], - "source": [ - "from qdk_chemistry.data import QpeResult\n", - "\n", - "# Load the results\n", - "results_loaded = []\n", - "for json_file in RESULTS_DIR.glob(\"*qpe_result.json\"):\n", - " result = QpeResult.from_json_file(json_file)\n", - " results_loaded.append(result)\n", - "\n", - "# Count the energy values\n", - "energy_counts = Counter(\n", - " [\n", - " result.raw_energy + active_hamiltonian.get_core_energy()\n", - " for result in results_loaded\n", - " ]\n", - ")\n", - "print(f\"QPE Results of {M_PRECISION} bit precision from {NUM_TRIALS} trials:\")\n", - "display(pd.DataFrame(energy_counts.items(), columns=['Energy (Hartree)', 'Counts']))\n", - "\n", - "# Use the most frequently observed energy across all trials as the overall estimate\n", - "estimated_energy, _ = energy_counts.most_common(1)[0]\n" - ] - }, - { - "cell_type": "markdown", - "id": "20f13908", - "metadata": {}, - "source": [ - "The iQPE energy estimate accuracy is useful for benchmarking the impact of precision, evolution time, and other parameters on the final result.\n", - "The following cell summarizes energy errors from the multiple trials." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "67509ed7", - "metadata": {}, - "outputs": [], - "source": [ - "# Print summary of results\n", - "print(f\"Reference CASCI energy: {e_cas:.6f} Hartree\")\n", - "print(f\"Total energy from phase estimation: {estimated_energy:.6f} Hartree\")\n", - "print(f\"Energy difference with CASCI energy: {abs(estimated_energy - e_cas):.3e} Hartree\")\n", - "\n", - "# Summarize the energy errors\n", - "energy_errors = {\n", - " qpe_e - e_cas: count\n", - " for qpe_e, count in sorted(energy_counts.items())\n", - "}\n", - "\n", - "# Plot distribution of energy differences\n", - "print(\"Energy difference (Hartree) distribution:\")\n", - "display(\n", - " Histogram(\n", - " bar_values={f\"{err:.3e}\": count for err, count in energy_errors.items()}\n", - " )\n", - " )" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "049f2290", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "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": "3a823352", + "metadata": {}, + "source": [ + "# Iterative phase estimation using `qdk-chemistry`\n", + "\n", + "This notebook provides an example of `qdk-chemistry` functionality through an end-to-end workflow estimating the ground state energy of a multi-configurational quantum chemistry system. This is one example of a wide range of functionality in `qdk-chemistry`. Please see for the full documentation.\n", + "\n", + "In addition to [installing `qdk-chemistry`](https://github.com/microsoft/qdk-chemistry/blob/main/INSTALL.md), you will need to install the `jupyter` and `qiskit-extras` extras to run this notebook:\n", + "\n", + "```bash\n", + "pip install 'qdk-chemistry[jupyter,qiskit-extras]'\n", + "```\n", + "\n", + "This installs the additional dependencies required by this notebook (ipykernel, pandas, pyscf, qiskit)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "adbe6e61", + "metadata": {}, + "outputs": [], + "source": [ + "# Load frequently used external packages\n", + "from pathlib import Path\n", + "from collections import Counter\n", + "\n", + "import numpy as np\n", + "import pandas as pd\n", + "\n", + "# Reduce logging output for demo\n", + "from qdk_chemistry.utils import Logger\n", + "Logger.set_global_level(Logger.LogLevel.off)" + ] + }, + { + "cell_type": "markdown", + "id": "8f6c1d96", + "metadata": {}, + "source": [ + "## Loading the stretched N2 structure\n", + "\n", + "This example uses a *stretched* N2 molecule, which introduces multi-reference character in the wavefunction. The structure is loaded from a [XYZ-format](https://en.wikipedia.org/wiki/XYZ_file_format) file." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "baff7f4f", + "metadata": {}, + "outputs": [], + "source": [ + "from qdk_chemistry.data import Structure\n", + "\n", + "# Stretched N2 structure at 1.270025 Å bond length\n", + "structure = Structure.from_xyz_file(Path(\"data/stretched_n2.structure.xyz\"))" + ] + }, + { + "cell_type": "markdown", + "id": "be49f433", + "metadata": {}, + "source": [ + "## Generating and optimizing the molecular orbitals\n", + "\n", + "This step performs a Hartree-Fock (HF) self-consistent field (SCF) calculation to generate an approximate initial wavefunction and ground-state energy guess." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "792e2fa1", + "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(\n", + " structure,\n", + " charge=0,\n", + " spin_multiplicity=1,\n", + " basis_or_guess=\"cc-pvdz\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "55d1da6b", + "metadata": {}, + "source": [ + "Unlike the basis functions, canonical molecular orbitals from SCF calculations are often delocalized over the entire molecule. As an example, we use the MP2 natural orbital localization method in `qdk-chemistry` to generate orbitals that tend to yield more chemically meaningful representations.\n", + "\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": "4c072e66", + "metadata": {}, + "outputs": [], + "source": [ + "from qdk_chemistry.utils import compute_valence_space_parameters\n", + "\n", + "# Reduce the number of orbitals\n", + "num_val_e, num_val_o = compute_valence_space_parameters(wfn_hf, charge=0)\n", + "active_space_selector = create(\n", + " \"active_space_selector\",\n", + " \"qdk_valence\",\n", + " num_active_electrons=num_val_e,\n", + " num_active_orbitals=num_val_o\n", + ")\n", + "valence_wf = active_space_selector.run(wfn_hf)\n", + "\n", + "# Localize the orbitals\n", + "localizer = create(\"orbital_localizer\", \"qdk_mp2_natural_orbitals\")\n", + "valence_indices = valence_wf.get_orbitals().get_active_space_indices()\n", + "loc_wfn = localizer.run(valence_wf, *valence_indices)\n", + "print(\"Localized orbitals:\\n\", loc_wfn.get_orbitals().get_summary())" + ] + }, + { + "cell_type": "markdown", + "id": "de0b4c16", + "metadata": {}, + "source": [ + "## Optimizing problem size with active space selection\n", + "\n", + "Active space selection focuses the quantum calculation on a subset of the electrons and orbitals in the system.\n", + "\n", + "This example uses `qdk_autocas_eos`, an automated entropy-based active-space selection method to identify strongly\n", + "correlated orbitals." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f961642c", + "metadata": {}, + "outputs": [], + "source": [ + "# Construct a Hamiltonian from the localized orbitals\n", + "hamiltonian_constructor = create(\"hamiltonian_constructor\")\n", + "loc_orbitals = loc_wfn.get_orbitals()\n", + "loc_hamiltonian = hamiltonian_constructor.run(loc_orbitals)\n", + "num_alpha_electrons, num_beta_electrons = loc_wfn.get_active_num_electrons()\n", + "\n", + "# Compute the selected configuration interaction wavefunction\n", + "macis_mc = create(\n", + " \"multi_configuration_calculator\",\n", + " \"macis_asci\",\n", + " calculate_one_rdm=True,\n", + " calculate_two_rdm=True,\n", + " )\n", + "_, wfn_sci = macis_mc.run(loc_hamiltonian, num_alpha_electrons, num_beta_electrons)\n", + "\n", + "# Optimize the problem with autoCAS-EOS active space selection\n", + "autocas = create(\"active_space_selector\", \"qdk_autocas_eos\")\n", + "autocas_wfn = autocas.run(wfn_sci)\n", + "indices, _ = autocas_wfn.get_orbitals().get_active_space_indices()\n", + "print(f\"autoCAS-EOS selected {len(indices)} of {num_val_o} orbitals for the active space: indices={list(indices)}\")" + ] + }, + { + "cell_type": "markdown", + "id": "accc09ed", + "metadata": {}, + "source": [ + "The next step constructs the active-space Hamiltonian and computes a multi-configuration wavefunction for the selected\n", + "active space.\n", + "This step also provides a reference energy for the active space system that can be used to benchmark the iQPE result." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cccc74f4", + "metadata": {}, + "outputs": [], + "source": [ + "# Construct the active space Hamiltonian\n", + "hamiltonian_constructor = create(\"hamiltonian_constructor\")\n", + "refined_orbitals = autocas_wfn.get_orbitals()\n", + "active_hamiltonian = hamiltonian_constructor.run(refined_orbitals)\n", + "\n", + "# Calculate the exact wavefunction and energy with CASCI\n", + "alpha_electrons, beta_electrons = autocas_wfn.get_active_num_electrons()\n", + "mc = create(\"multi_configuration_calculator\", \"macis_cas\")\n", + "e_cas, wfn_cas = mc.run(active_hamiltonian, alpha_electrons, beta_electrons)\n", + "print(f\"Active space system energy: {e_cas:.6f} Hartree\")" + ] + }, + { + "cell_type": "markdown", + "id": "c811f60a", + "metadata": {}, + "source": [ + "Visualizing the selected active space is an important step to ensure that the selected orbitals are chemically meaningful.\n", + "This step uses visualization tools built into `qdk` to display the selected active space orbitals." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3e5c51c8", + "metadata": {}, + "outputs": [], + "source": [ + "from qdk.widgets import MoleculeViewer\n", + "from qdk_chemistry.utils.cubegen import generate_cubefiles_from_orbitals\n", + "\n", + "# Make a general guess about the number of occupied orbitals\n", + "(num_alpha, num_beta) = wfn_cas.get_total_num_electrons()\n", + "num_occupied = (num_alpha + num_beta) // 2\n", + "\n", + "active_orbitals = wfn_cas.get_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 < num_occupied 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": "dc9835f3", + "metadata": {}, + "source": [ + "## Optimizing trial wavefunction loading onto a quantum computer\n", + "\n", + "The multi-configuration wavefunction in the active space can serve as the trial state for the iQPE algorithm.\n", + "However, this information needs to be loaded as a state on a quantum computer.\n", + "\n", + "The amount of data loaded onto the quantum computer can be optimized by exploiting the sparsity of the wavefunction.\n", + "This step identifies the dominant configurations in the wavefunction using visualization tools provided by `qdk`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "64effd7f", + "metadata": {}, + "outputs": [], + "source": [ + "from qdk.widgets import Histogram\n", + "\n", + "# Plot top configuration weights from the CASCI wavefunction\n", + "num_configurations = len(wfn_cas.get_active_determinants())\n", + "print(f\"Total configurations in the CASCI wavefunction: {num_configurations}\")\n", + "print(\"Plotting the configurations by weight.\")\n", + "top_configurations = wfn_cas.get_top_determinants()\n", + "display(\n", + " Histogram(\n", + " bar_values={k.to_string(): np.abs(v)**2 for k, v in top_configurations.items()}, \n", + " items=\"top-25\", \n", + " sort=\"high-to-low\",\n", + " )\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "51d9685c", + "metadata": {}, + "source": [ + "To run quantum phase estimation, we need to prepare an initial trial state for the calculation.\n", + "In this example, we will take the first two terms of the multi-configuration wavefunction, add a small amount of noise, and check their overlap with the full wavefunction.\n", + "\n", + "Choosing fewer terms still gives us good overlap in the trial state, and also illustrates QPE output with imperfect starting information." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "62520449", + "metadata": {}, + "outputs": [], + "source": [ + "from utils.qpe_utils import prepare_top_dets_trial_state\n", + "\n", + "# Prepare a trial state with two determinants. Compute its overlap with the CASCI wavefunction.\n", + "wfn_trial, fidelity = prepare_top_dets_trial_state(wfn_cas, active_hamiltonian, num_dets=2)\n", + "print(f\"Overlap of trial state with CASCI wavefunction: {fidelity:.2%}\")\n", + "\n", + "# Generate a plot of the configurations in the trial wavefunction\n", + "configurations = wfn_trial.get_top_determinants()\n", + "display(\n", + " Histogram(\n", + " bar_values={k.to_string(): np.abs(v)**2 for k, v in configurations.items()},\n", + " sort=\"high-to-low\",\n", + " )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "b5349c2c", + "metadata": {}, + "source": [ + "There are many ways to \"load\" this state onto a quantum computer.\n", + "This example uses a popular method as a basis for comparison with our chemistry-aware optimized approach.\n", + "Circuit statistics are shown and the circuit is visualized using built-in `qdk` functions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "455d4494", + "metadata": {}, + "outputs": [], + "source": [ + "import qdk_chemistry.plugins.qiskit # Enable Qiskit plugin capabilities # noqa: F401\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\")\n", + "regular_isometry_circuit = state_prep.run(wfn_trial)\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(\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)" + ] + }, + { + "cell_type": "markdown", + "id": "a5d97513", + "metadata": {}, + "source": [ + "The popular approach for state preparation requires a larger number of operations with numerous fine rotations.\n", + "However, `qdk-chemistry` provides optimized state preparation methods that exploit the structure of chemistry wavefunctions to reduce the number of operations and improve noise resilience." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fbc8ea70", + "metadata": {}, + "outputs": [], + "source": [ + "from qdk.widgets import Circuit\n", + "\n", + "# Generate state preparation circuit for the sparse state via GF2+X sparse isometry\n", + "state_prep = create(\"state_prep\", \"sparse_isometry_gf2x\")\n", + "sparse_isometry_circuit = state_prep.run(wfn_trial)\n", + "\n", + "# Visualize the sparse isometry circuit, idle and classical qubits are removed\n", + "display(Circuit(sparse_isometry_circuit.get_qsharp_circuit()))\n", + "\n", + "# Print logical qubit counts estimated from the circuit\n", + "df = pd.DataFrame(\n", + " sparse_isometry_circuit.estimate().logical_counts.items(),\n", + " columns=['Logical Estimate', 'Counts']\n", + ")\n", + "display(df)" + ] + }, + { + "cell_type": "markdown", + "id": "15ed2623", + "metadata": {}, + "source": [ + "## Estimating the ground state energy with iterative quantum phase estimation\n", + "\n", + "Kitaev-style iterative quantum phase estimation (iQPE) estimates an eigenphase of the time-evolution operator $U = e^{-iHt}$ using one ancilla qubit and a sequence of controlled-$U^{2^k}$ applications.\n", + " \n", + "Each iteration measures one bit of the phase (from most-significant to least-significant) and uses phase feedback to refine the estimate." + ] + }, + { + "cell_type": "markdown", + "id": "f345165d", + "metadata": {}, + "source": [ + "The classical Hamiltonian for the active space must be mapped to a qubit Hamiltonian that can be measured on a quantum computer.\n", + "The Jordan-Wigner transformation is a popular mapping that is used in this example." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d50081e5", + "metadata": {}, + "outputs": [], + "source": [ + "from qdk_chemistry.data import MajoranaMapping\n", + "\n", + "# Prepare the qubit-mapped Hamiltonian\n", + "n_spin_orbitals = 2 * len(active_hamiltonian.get_orbitals().get_active_space_indices()[0])\n", + "qubit_mapper = create(\"qubit_mapper\", \"qiskit\")\n", + "qubit_hamiltonian = qubit_mapper.run(active_hamiltonian, MajoranaMapping.jordan_wigner(n_spin_orbitals))\n", + "print(\"Qubit Hamiltonian:\\n\", qubit_hamiltonian.get_summary())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "67327154", + "metadata": {}, + "outputs": [], + "source": [ + "# Set up parameters for iQPE\n", + "from utils.qpe_utils import compute_evolution_time\n", + "\n", + "M_PRECISION = 10\n", + "SHOTS_PER_BIT = 3\n", + "SIMULATOR_SEED = 42\n", + "\n", + "# Propose evolution time given the qubit Hamiltonian and number of precision bits\n", + "evolution_time = compute_evolution_time(qubit_hamiltonian, num_bits=M_PRECISION)\n", + "print(f\"Proposed evolution time: {evolution_time:.4f} Hartree^-1\")" + ] + }, + { + "cell_type": "markdown", + "id": "a43d9a9b", + "metadata": {}, + "source": [ + "The circuit for iQPE consists of initial trial state preparation followed by multiple controlled time-evolution operations.\n", + "This cell visualizes the one iteration of the iQPE circuit in QASM format using built-in `qdk` visualization tools." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b4a368e3", + "metadata": {}, + "outputs": [], + "source": [ + "from qdk_chemistry.data import AlgorithmRef\n", + "\n", + "unitary_builder = AlgorithmRef(\"hamiltonian_unitary_builder\", \"trotter\", time=evolution_time)\n", + "circuit_mapper = AlgorithmRef(\"controlled_circuit_mapper\", \"pauli_sequence\")\n", + "\n", + "# Use the circuit builder to generate a single iteration circuit for visualization\n", + "circuit_builder = create(\n", + " \"qpe_circuit_builder\", \n", + " \"qdk_iterative\",\n", + " num_bits=M_PRECISION,\n", + " num_iteration=M_PRECISION - 3, # 3rd from last iteration\n", + " unitary_builder=unitary_builder,\n", + " controlled_circuit_mapper=circuit_mapper,\n", + ")\n", + "iqpe_iter_circuits = circuit_builder.run(\n", + " state_preparation=sparse_isometry_circuit,\n", + " qubit_hamiltonian=qubit_hamiltonian,\n", + ")\n", + "\n", + "# Visualize the iQPE iteration circuit\n", + "display(Circuit(iqpe_iter_circuits[0].get_qsharp_circuit()))" + ] + }, + { + "cell_type": "markdown", + "id": "7b85ca39", + "metadata": {}, + "source": [ + "This real-time example performs a single-trial low-precision iQPE run on the `qdk` full state simulator." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5d1c6526", + "metadata": {}, + "outputs": [], + "source": [ + "# Execute the iQPE algorithm for a single trial\n", + "circuit_executor = AlgorithmRef(\"circuit_executor\", \"qdk_full_state_simulator\", seed=SIMULATOR_SEED)\n", + "iqpe_circuit_builder_low = AlgorithmRef(\n", + " \"qpe_circuit_builder\",\n", + " \"qdk_iterative\",\n", + " num_bits=6,\n", + " controlled_circuit_mapper=circuit_mapper,\n", + " unitary_builder=unitary_builder,\n", + ")\n", + "\n", + "iqpe_low = create(\n", + " \"phase_estimation\",\n", + " \"qdk_iterative\",\n", + " shots_per_bit=SHOTS_PER_BIT,\n", + ")\n", + "iqpe_low.settings().set(\"qpe_circuit_builder\", iqpe_circuit_builder_low)\n", + "iqpe_low.settings().set(\"circuit_executor\", circuit_executor)\n", + "result = iqpe_low.run(\n", + " state_preparation=sparse_isometry_circuit,\n", + " qubit_hamiltonian=qubit_hamiltonian,\n", + ")\n", + "\n", + "# Summarize the QPE results\n", + "estimated_energy = result.raw_energy + active_hamiltonian.get_core_energy()\n", + "estimated_error = abs(estimated_energy - e_cas)\n", + "print(\"QPE Results for 6-bit precision:\")\n", + "print(f\"Reference CASCI energy: {e_cas:.6f} Hartree\")\n", + "print(f\"Total energy from phase estimation: {estimated_energy:.6f} Hartree\")\n", + "print(f\"Energy difference with CASCI energy: {estimated_error:.3e} Hartree\")" + ] + }, + { + "cell_type": "markdown", + "id": "452d0cc2", + "metadata": {}, + "source": [ + "The above cell used a low-precision single trial for a real-time example.\n", + "However, iQPE generally requires multiple trials to establish confidence in the resulting estimate.\n", + "The following cell performs a multi-trial iQPE run with high precision using the same simulator." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "76891f8b", + "metadata": {}, + "outputs": [], + "source": [ + "# Large number of trials with results previously calculated and saved\n", + "NUM_TRIALS = 20\n", + "RESULTS_DIR = Path(\n", + " f\"results_iqpe/precision_{M_PRECISION}/time_{round(evolution_time, 12)}\"\n", + ")\n", + "\n", + "iqpe_circuit_builder = AlgorithmRef(\n", + " \"qpe_circuit_builder\",\n", + " \"qdk_iterative\",\n", + " num_bits=M_PRECISION,\n", + " controlled_circuit_mapper=circuit_mapper,\n", + " unitary_builder=unitary_builder,\n", + ")\n", + "\n", + "# Run iQPE if results do not already exist\n", + "RESULTS_DIR.mkdir(parents=True, exist_ok=True)\n", + "\n", + "for trial in range(NUM_TRIALS):\n", + " trial_seed = SIMULATOR_SEED + trial\n", + " json_path = RESULTS_DIR / f\"iqpe_result_{trial_seed}.qpe_result.json\"\n", + " if not json_path.exists():\n", + " print(f\"Running trial {trial + 1} of {NUM_TRIALS}...\")\n", + " trial_executor = AlgorithmRef(\"circuit_executor\", \"qdk_full_state_simulator\", seed=trial_seed)\n", + " iqpe = create(\n", + " \"phase_estimation\",\n", + " \"qdk_iterative\",\n", + " shots_per_bit=SHOTS_PER_BIT,\n", + " )\n", + " iqpe.settings().set(\"qpe_circuit_builder\", iqpe_circuit_builder)\n", + " iqpe.settings().set(\"circuit_executor\", trial_executor)\n", + " result = iqpe.run(\n", + " state_preparation=sparse_isometry_circuit,\n", + " qubit_hamiltonian=qubit_hamiltonian,\n", + " )\n", + " result.to_json_file(json_path)" + ] + }, + { + "cell_type": "markdown", + "id": "f0f7da6f", + "metadata": {}, + "source": [ + "For a system with noise or an imperfect trial state, multiple trials of iQPE are needed to obtain a reliable estimate of the ground state energy.\n", + "This estimate is typically taken as the most frequently observed energy from multiple trials (\"majority vote\")." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ac88390f", + "metadata": {}, + "outputs": [], + "source": [ + "from qdk_chemistry.data import QpeResult\n", + "\n", + "# Load the results\n", + "results_loaded = []\n", + "for json_file in RESULTS_DIR.glob(\"*qpe_result.json\"):\n", + " result = QpeResult.from_json_file(json_file)\n", + " results_loaded.append(result)\n", + "\n", + "# Count the energy values\n", + "energy_counts = Counter(\n", + " [\n", + " result.raw_energy + active_hamiltonian.get_core_energy()\n", + " for result in results_loaded\n", + " ]\n", + ")\n", + "print(f\"QPE Results of {M_PRECISION} bit precision from {NUM_TRIALS} trials:\")\n", + "display(pd.DataFrame(energy_counts.items(), columns=['Energy (Hartree)', 'Counts']))\n", + "\n", + "# Use the most frequently observed energy across all trials as the overall estimate\n", + "estimated_energy, _ = energy_counts.most_common(1)[0]\n" + ] + }, + { + "cell_type": "markdown", + "id": "20f13908", + "metadata": {}, + "source": [ + "The iQPE energy estimate accuracy is useful for benchmarking the impact of precision, evolution time, and other parameters on the final result.\n", + "The following cell summarizes energy errors from the multiple trials." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "67509ed7", + "metadata": {}, + "outputs": [], + "source": [ + "# Print summary of results\n", + "print(f\"Reference CASCI energy: {e_cas:.6f} Hartree\")\n", + "print(f\"Total energy from phase estimation: {estimated_energy:.6f} Hartree\")\n", + "print(f\"Energy difference with CASCI energy: {abs(estimated_energy - e_cas):.3e} Hartree\")\n", + "\n", + "# Summarize the energy errors\n", + "energy_errors = {\n", + " qpe_e - e_cas: count\n", + " for qpe_e, count in sorted(energy_counts.items())\n", + "}\n", + "\n", + "# Plot distribution of energy differences\n", + "print(\"Energy difference (Hartree) distribution:\")\n", + "display(\n", + " Histogram(\n", + " bar_values={f\"{err:.3e}\": count for err, count in energy_errors.items()}\n", + " )\n", + " )" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "qdk-chem-prod (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/examples/state_prep_energy.ipynb b/examples/state_prep_energy.ipynb index bcaf3bfd8..2306099b7 100644 --- a/examples/state_prep_energy.ipynb +++ b/examples/state_prep_energy.ipynb @@ -20,8 +20,8 @@ "\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", + "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", @@ -258,7 +258,7 @@ "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", + "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." @@ -290,7 +290,7 @@ "\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 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." @@ -325,7 +325,7 @@ "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", + "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", @@ -360,7 +360,7 @@ "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", + "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." @@ -437,7 +437,7 @@ "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", + " f\"Estimated energy from quantum circuit: {energy_mean:.3f} ± {energy_stddev:.3f} Hartree\"\n", ")\n", "\n", "# Print comparison with reference energy\n", diff --git a/python/src/qdk_chemistry/algorithms/__init__.py b/python/src/qdk_chemistry/algorithms/__init__.py index 91423195a..442e05a33 100644 --- a/python/src/qdk_chemistry/algorithms/__init__.py +++ b/python/src/qdk_chemistry/algorithms/__init__.py @@ -51,6 +51,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_v1 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 @@ -81,6 +83,7 @@ "QdkMacisPmc", "QdkOccupationActiveSpaceSelector", "QdkPipekMezeyLocalizer", + "QdkQreV1", "QdkQubitMapper", "QdkScfSolver", "QdkStabilityChecker", @@ -89,6 +92,7 @@ "QpeCircuitBuilder", "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 afa591928..ae97aabc4 100644 --- a/python/src/qdk_chemistry/algorithms/registry.py +++ b/python/src/qdk_chemistry/algorithms/registry.py @@ -514,6 +514,7 @@ def _register_python_factories(): from qdk_chemistry.algorithms.propagator import PropagatorFactory # 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.term_grouper import TermGrouperFactory # noqa: PLC0415 from qdk_chemistry.algorithms.time_evolution.hamiltonian_simulation import ( # noqa: PLC0415 @@ -532,6 +533,7 @@ def _register_python_factories(): register_factory(CircuitExecutorFactory()) register_factory(QpeCircuitBuilderFactory()) register_factory(PhaseEstimationFactory()) + register_factory(ResourceEstimatorFactory()) register_factory(PropagatorFactory()) @@ -608,6 +610,8 @@ def _register_python_algorithms(): from qdk_chemistry.algorithms.propagator import MagnusPropagator # noqa: PLC0415 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_v1 import QdkQreV1 # noqa: PLC0415 + from qdk_chemistry.algorithms.resource_estimator.qdk_v3 import QdkQreV3 # noqa: PLC0415 from qdk_chemistry.algorithms.state_preparation import SparseIsometryGF2XStatePreparation # noqa: PLC0415 from qdk_chemistry.algorithms.term_grouper import ( # noqa: PLC0415 FullCommutingTermGrouper, @@ -635,6 +639,8 @@ def _register_python_algorithms(): register(lambda: QdkSparseStateSimulator()) register(lambda: QdkIterativeQpeCircuitBuilder()) register(lambda: IterativePhaseEstimation()) + register(lambda: QdkQreV1()) + register(lambda: QdkQreV3()) register(lambda: StandardPhaseEstimation()) 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..bd2ddfd07 --- /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, + ) -> list[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: + list[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_v1.py b/python/src/qdk_chemistry/algorithms/resource_estimator/qdk_v1.py new file mode 100644 index 000000000..cd7d6cb3e --- /dev/null +++ b/python/src/qdk_chemistry/algorithms/resource_estimator/qdk_v1.py @@ -0,0 +1,132 @@ +"""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. +# -------------------------------------------------------------------------------------------- +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, + ) -> list[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: + list[ResourceEstimatorData]: A single-entry list containing the estimated resources. + + Raises: + RuntimeError: If no suitable circuit representation is available for estimation. + + """ + Logger.trace_entering() + + result = circuit.estimate({"errorBudget": self._settings.get("error_budget")}) + + # 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/algorithms/resource_estimator/qdk_v3.py b/python/src/qdk_chemistry/algorithms/resource_estimator/qdk_v3.py new file mode 100644 index 000000000..484822b78 --- /dev/null +++ b/python/src/qdk_chemistry/algorithms/resource_estimator/qdk_v3.py @@ -0,0 +1,310 @@ +"""QDK/Chemistry Resource Estimator v3 using the QRE Pareto-front API. + +This module provides a ResourceEstimator implementation that uses the QDK +QRE v3 estimation backend to produce a Pareto-optimal frontier of resource +estimation results exploring the qubits-vs-runtime tradeoff space. +""" + +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +import random +import time +from typing import Any + +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] = ["QdkQreV3", "QdkQreV3Settings"] + + +class QdkQreV3Settings(Settings): + """Settings for the QDK QRE v3 Resource Estimator.""" + + def __init__(self) -> None: + """Initialize QDK QRE v3 settings.""" + Logger.trace_entering() + super().__init__() + self._set_default("error_budget", "double", 0.01, "Maximum total error budget for the estimation") + self._set_default( + "qubit_model", + "string", + "gate_based", + "Qubit technology model: 'gate_based' or 'majorana'", + ) + self._set_default("qec_scheme", "string", "surface_code", "QEC scheme: 'surface_code'") + self._set_default("factory", "string", "round_based", "Magic state factory model: 'round_based'") + self._set_default( + "gate_time", + "int", + 50, + "Single-qubit gate time in nanoseconds (gate_based only)", + ) + self._set_default( + "measurement_time", + "int", + 100, + "Measurement time in nanoseconds (gate_based only)", + ) + self._set_default( + "use_graph", + "bool", + False, + "Use graph-based pruning (faster but may miss some Pareto points)", + ) + self._set_default( + "slow_down_factors", + "vector", + [1.0, 2.0, 3.0, 5.0, 8.0, 10.0, 15.0, 20.0, 25.0, 30.0], + "LatticeSurgery slow-down factors for Pareto exploration", + ) + + +class QdkQreV3(ResourceEstimator): + """QDK QRE v3 Resource Estimator producing a Pareto frontier.""" + + def __init__(self) -> None: + """Initialize the QDK QRE v3 Resource Estimator.""" + Logger.trace_entering() + super().__init__() + self._settings = QdkQreV3Settings() + + def name(self) -> str: + """Return the algorithm name as qdk_qre_v3.""" + return "qdk_qre_v3" + + def _run_impl( + self, + circuit: Circuit, + ) -> list[ResourceEstimatorData]: + """Estimate the Pareto-optimal quantum resource frontier for the circuit.""" + Logger.trace_entering() + + from qdk.qre import PSSPC, LatticeSurgery # noqa: PLC0415 + from qdk.qre import estimate as qre_estimate # noqa: PLC0415 + from qdk.qre.models import GateBased, Majorana, RoundBasedFactory, SurfaceCode # noqa: PLC0415 + + max_error = self._settings.get("error_budget") + use_graph = self._settings.get("use_graph") + qubit_model_name = self._settings.get("qubit_model") + qec_scheme_name = self._settings.get("qec_scheme") + factory_name = self._settings.get("factory") + + app = self._make_application(circuit) + + if qubit_model_name == "majorana": + arch = Majorana() + else: + arch = GateBased( + gate_time=int(self._settings.get("gate_time")), + measurement_time=int(self._settings.get("measurement_time")), + ) + + if qec_scheme_name != "surface_code": + raise ValueError(f"Unsupported QRE v3 QEC scheme: {qec_scheme_name}") + if factory_name != "round_based": + raise ValueError(f"Unsupported QRE v3 factory model: {factory_name}") + + isa_query = SurfaceCode.q() * RoundBasedFactory.q() + trace_query = PSSPC.q() * LatticeSurgery.q(slow_down_factor=self._settings.get("slow_down_factors")) + + table = qre_estimate( + application=app, + architecture=arch, + isa_query=isa_query, + trace_query=trace_query, + max_error=max_error, + use_graph=use_graph, + ) + + results = [ + self._to_resource_estimator_data( + entry, + qubit_model_name=qubit_model_name, + qec_scheme_name=qec_scheme_name, + factory_name=factory_name, + max_error=max_error, + ) + for entry in table + ] + results.sort(key=lambda result: result.physical_counts.physical_qubits) + return results + + @staticmethod + def _make_application(circuit: Circuit) -> Any: + """Create a qdk.qre Application from a qdk_chemistry Circuit.""" + if circuit._qsharp_factory is not None: # noqa: SLF001 + return QdkQreV3._make_qsharp_application( + circuit._qsharp_factory.program, # noqa: SLF001 + args=tuple(circuit._qsharp_factory.parameter.values()), # noqa: SLF001 + ) + if circuit.qasm is not None: + return QdkQreV3._make_openqasm_application(circuit.qasm) + if circuit.qir is not None: + return QdkQreV3._make_openqasm_application(circuit.get_qasm()) + + raise RuntimeError("Cannot estimate resources: no Q# factory data, QASM, or QIR representation is available.") + + @staticmethod + def _make_qsharp_application(program: Any, *, args: tuple[Any, ...]) -> Any: + """Create a lightweight QRE application for Q# entry expressions.""" + from qdk.qre._application import Application # noqa: PLC0415 + + class QSharpApplication(Application[None]): + def __init__(self, entry_expr: Any, args: tuple[Any, ...]) -> None: + self.entry_expr = entry_expr + self.args = args + + def get_trace(self, _parameters: None = None) -> Any: + return QdkQreV3._trace_from_entry_expr(self.entry_expr, *self.args) + + return QSharpApplication(program, args) + + @staticmethod + def _make_openqasm_application(program: str) -> Any: + """Create a lightweight QRE application for OpenQASM programs.""" + from qdk.qre._application import Application # noqa: PLC0415 + + class OpenQASMApplication(Application[None]): + def __init__(self, program: str) -> None: + self.program = program + + def get_trace(self, _parameters: None = None) -> Any: + from qdk import code # noqa: PLC0415 + from qdk.openqasm import ProgramType, import_openqasm # noqa: PLC0415 + + program = self.program + if isinstance(program, str): + for _ in range(1_000): + name = f"openqasm{random.randint(0, 1_000_000)}" + if not hasattr(code, "qasm_import") or not hasattr(code.qasm_import, name): + break + else: + raise RuntimeError("Failed to find a unique name for the OpenQASM program.") + + import_openqasm(program, name=name, program_type=ProgramType.File) + program = getattr(code.qasm_import, name) + + return QdkQreV3._trace_from_entry_expr(program) + + return OpenQASMApplication(program) + + @staticmethod + def _trace_from_entry_expr(entry_expr: Any, *args: Any) -> Any: + """Convert QDK logical counts into a QRE trace.""" + from qdk._interpreter import logical_counts # noqa: PLC0415 + from qdk.estimator import LogicalCounts as QdkLogicalCounts # noqa: PLC0415 + from qdk.qre._qre import Trace # noqa: PLC0415 + from qdk.qre.instruction_ids import CCX, MEAS_Z, READ_FROM_MEMORY, RZ, WRITE_TO_MEMORY, T # noqa: PLC0415 + from qdk.qre.property_keys import ( # noqa: PLC0415 + ALGORITHM_COMPUTE_QUBITS, + ALGORITHM_MEMORY_QUBITS, + EVALUATION_TIME, + ) + + start = time.time_ns() + counts = entry_expr if isinstance(entry_expr, QdkLogicalCounts) else logical_counts(entry_expr, *args) + evaluation_time = time.time_ns() - start + + ccx_count = counts.get("cczCount", 0) + counts.get("ccixCount", 0) + num_qubits = counts.get("numQubits", 0) + compute_qubits = counts.get("numComputeQubits", num_qubits) + memory_qubits = num_qubits - compute_qubits + trace = Trace(compute_qubits) + + rotation_count = counts.get("rotationCount", 0) + rotation_depth = counts.get("rotationDepth", rotation_count) + if rotation_count != 0 and rotation_depth != 0: + for count, depth in QdkQreV3._bucketize_rotation_counts(rotation_count, rotation_depth): + block = trace.add_block(repetitions=depth) + for index in range(count): + block.add_operation(RZ, [index]) + + if t_count := counts.get("tCount", 0): + block = trace.add_block(repetitions=t_count) + block.add_operation(T, [0]) + + if ccx_count: + block = trace.add_block(repetitions=ccx_count) + block.add_operation(CCX, [0, 1, 2]) + + if meas_count := counts.get("measurementCount", 0): + block = trace.add_block(repetitions=meas_count) + block.add_operation(MEAS_Z, [0]) + + if memory_qubits != 0: + trace.memory_qubits = memory_qubits + + if rfm_count := counts.get("readFromMemoryCount", 0): + block = trace.add_block(repetitions=rfm_count) + block.add_operation(READ_FROM_MEMORY, [0, compute_qubits]) + + if wtm_count := counts.get("writeToMemoryCount", 0): + block = trace.add_block(repetitions=wtm_count) + block.add_operation(WRITE_TO_MEMORY, [0, compute_qubits]) + + trace.set_property(EVALUATION_TIME, evaluation_time) + trace.set_property(ALGORITHM_COMPUTE_QUBITS, compute_qubits) + trace.set_property(ALGORITHM_MEMORY_QUBITS, memory_qubits) + return trace + + @staticmethod + def _bucketize_rotation_counts(rotation_count: int, rotation_depth: int) -> list[tuple[int, int]]: + """Return rotation count/depth buckets for QRE trace construction.""" + if rotation_depth == 0: + return [] + + base = rotation_count // rotation_depth + extra = rotation_count % rotation_depth + + result: list[tuple[int, int]] = [] + if extra > 0: + result.append((base + 1, extra)) + if rotation_depth - extra > 0: + result.append((base, rotation_depth - extra)) + return result + + def _to_resource_estimator_data( + self, + entry: Any, + *, + qubit_model_name: str, + qec_scheme_name: str, + factory_name: str, + max_error: float, + ) -> ResourceEstimatorData: + """Convert one QRE v3 Pareto entry into ResourceEstimatorData.""" + return ResourceEstimatorData( + logical_counts=LogicalCounts(num_qubits=0), + physical_counts=PhysicalCounts( + physical_qubits=int(getattr(entry, "qubits", 0)), + runtime=int(getattr(entry, "runtime", 0)), + runtime_unit="ns", + ), + logical_qubit=LogicalQubit(), + error_budget=ErrorBudget(), + estimator=self.name(), + status="success", + error=float(getattr(entry, "error", 0.0)), + config=EstimationConfig( + qubit_model=qubit_model_name, + qec_scheme=qec_scheme_name, + error_budget=max_error, + description=str(getattr(entry, "source", "")), + gate_time_ns=int(self._settings.get("gate_time")) if qubit_model_name == "gate_based" else 0, + measurement_time_ns=( + int(self._settings.get("measurement_time")) if qubit_model_name == "gate_based" else 0 + ), + factory=factory_name, + ), + ) diff --git a/python/src/qdk_chemistry/data/__init__.py b/python/src/qdk_chemistry/data/__init__.py index 6becdf5a7..33519996b 100644 --- a/python/src/qdk_chemistry/data/__init__.py +++ b/python/src/qdk_chemistry/data/__init__.py @@ -118,6 +118,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.term_partition import FlatPartition, LayeredPartition, TermPartition from qdk_chemistry.data.time_dependent_qubit_hamiltonian.base import TimeDependentQubitHamiltonian @@ -143,6 +152,7 @@ "CasWavefunctionContainer", "CholeskyHamiltonianContainer", "Circuit", + "CircuitCounts", "CircuitExecutorData", "Configuration", "ConfigurationSet", @@ -154,6 +164,8 @@ "ElectronicStructureSettings", "Element", "EnergyExpectationResult", + "ErrorBudget", + "EstimationConfig", "FermionModeOrder", "FlatPartition", "Hamiltonian", @@ -161,6 +173,8 @@ "HamiltonianType", "LatticeGraph", "LayeredPartition", + "LogicalCounts", + "LogicalQubit", "MP2Container", "MajoranaMapping", "MeasurementData", @@ -170,9 +184,11 @@ "PauliOperator", "PauliProductFormulaContainer", "PauliTermAccumulator", + "PhysicalCounts", "QpeResult", "QuantumErrorProfile", "QubitHamiltonian", + "ResourceEstimatorData", "SciWavefunctionContainer", "SettingNotFound", "SettingNotFoundError", 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..839526c09 --- /dev/null +++ b/python/src/qdk_chemistry/data/resource_estimator_data.py @@ -0,0 +1,530 @@ +"""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 _slot_names(obj: object) -> tuple[str, ...]: + """Return slot names for a __slots__-based object.""" + slots = getattr(obj, "__slots__", ()) + if isinstance(slots, str): + return (slots,) + return tuple(slots) + + +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 _slot_names(obj)} + + +def _typed_obj_to_hdf5(obj: object, group: h5py.Group) -> None: + """Write a __slots__-based object as HDF5 attributes.""" + for s in _slot_names(obj): + 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 _slot_names(self)) + + def _repr(self): + fields = ", ".join(f"{s}={getattr(self, s)}" for s in _slot_names(self)) + 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_non_clifford", + "num_single_qubit_clifford", + "num_two_qubit_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__ = ( + "ccix_count", + "ccz_count", + "measurement_count", + "num_qubits", + "rotation_count", + "rotation_depth", + "t_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__ = ( + "algorithm_qubits", + "algorithmic_logical_depth", + "factory_qubits", + "logical_depth", + "physical_qubits", + "rqops", + "runtime", + "runtime_unit", + ) + + 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__ = ( + "description", + "error_budget", + "factory", + "gate_time_ns", + "measurement_time_ns", + "qec_scheme", + "qubit_model", + ) + + def __init__( + self, + qubit_model: str = "", + qec_scheme: str = "", + error_budget: float = 0.0, + description: str = "", + gate_time_ns: int = 0, + measurement_time_ns: int = 0, + factory: str = "", + ) -> None: + self.qubit_model = qubit_model + self.qec_scheme = qec_scheme + self.error_budget = error_budget + self.description = description + self.gate_time_ns = gate_time_ns + self.measurement_time_ns = measurement_time_ns + self.factory = factory + + +# --------------------------------------------------------------------------- +# 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", ""), + gate_time_ns=cfg_d.get("gate_time_ns", 0), + measurement_time_ns=cfg_d.get("measurement_time_ns", 0), + factory=cfg_d.get("factory", ""), + ) + 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"]), + gate_time_ns=int(cfg_grp.attrs.get("gate_time_ns", 0)), + measurement_time_ns=int(cfg_grp.attrs.get("measurement_time_ns", 0)), + factory=str(cfg_grp.attrs.get("factory", "")), + ) + + 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 de393d2a5..a952c2d90 100644 --- a/python/tests/test_circuit.py +++ b/python/tests/test_circuit.py @@ -23,6 +23,7 @@ from qsharp._native import Circuit as QdkCircuitType from qsharp._qsharp import QirInputData +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 @@ -420,10 +421,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], @@ -435,12 +436,13 @@ def test_estimate_from_factory(self): parameter=state_prep_params, ) circuit = Circuit(qsharp_factory=qsharp_factory) - result = circuit.estimate() - assert result is not None - assert hasattr(result, "logical_counts") + estimator = create_algorithm("resource_estimator") + results = estimator.run(circuit) + assert len(results) == 1 + assert results[0].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"; @@ -453,9 +455,10 @@ def test_estimate_from_qasm(self): c[1] = measure q[1]; """ circuit = Circuit(qasm=qasm_with_t) - result = circuit.estimate() - assert result is not None - assert hasattr(result, "logical_counts") + estimator = create_algorithm("resource_estimator") + results = estimator.run(circuit) + assert len(results) == 1 + assert results[0].logical_counts.t_count >= 0 def test_estimate_raises_with_qir_only(self): """Test that estimate raises when only QIR representation is available.""" @@ -470,5 +473,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..497a2c1d5 --- /dev/null +++ b/python/tests/test_resource_estimator.py @@ -0,0 +1,295 @@ +"""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 +from qdk import openqasm + +from qdk_chemistry.algorithms import create +from qdk_chemistry.algorithms.resource_estimator.base import ResourceEstimator +from qdk_chemistry.algorithms.resource_estimator.qdk_v1 import QdkQreV1 +from qdk_chemistry.algorithms.resource_estimator.qdk_v3 import QdkQreV3 +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_create_qre_v3_by_name(self): + """Test creating the QRE v3 resource estimator by explicit name.""" + estimator = create("resource_estimator", "qdk_qre_v3") + assert isinstance(estimator, QdkQreV3) + assert estimator.settings().get("error_budget") == 0.01 + + 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() + results = estimator.run(circuit) + assert len(results) == 1 + result = results[0] + 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() + results = estimator.run(circuit) + assert len(results) == 1 + result = results[0] + 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 = 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) + results = estimator.run(circuit) + assert len(results) == 1 + assert isinstance(results[0], 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, + gate_time_ns=50, + measurement_time_ns=100, + factory="round_based", + ), + ) + + 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