diff --git a/.claude/skills/cudaq-guide/SKILL.md b/.claude/skills/cudaq-guide/SKILL.md index 9dda4831f1d..c7b210e928f 100644 --- a/.claude/skills/cudaq-guide/SKILL.md +++ b/.claude/skills/cudaq-guide/SKILL.md @@ -3,13 +3,13 @@ name: "cudaq-guide" title: "Cuda Quantum" description: "CUDA-Q onboarding guide for installation, test programs, GPU simulation, QPU hardware, and quantum applications." version: "1.0.0" -author: "Sachin Pisal " +author: "CUDA-Q" tags: [cuda-quantum, quantum-computing, onboarding, getting-started, nvidia] tools: [Read, Glob, Grep, Bash] license: "Apache License 2.0" compatibility: "Python 3.10+, C++ 20" metadata: - author: "Sachin Pisal " + author: "CUDA-Q" tags: - cuda-quantum - quantum-computing diff --git a/.github/nv-slack-bot.yaml b/.github/nv-slack-bot.yaml new file mode 100644 index 00000000000..27bc9e65782 --- /dev/null +++ b/.github/nv-slack-bot.yaml @@ -0,0 +1,100 @@ +$schema: https://public.gha-runners.nvidia.com/nv-slack-bot/schemas/config-v1.json +enabled: true +notifications: + - name: "Publishing workflow failed" + event: workflow_run + slack: + nvidia: + channels: + - id: C0AT93CK1B9 # nvqpp-cudaq-notifications + match: workflow_run.name = "Publishing" and workflow_run.conclusion = "failure" + message: + body: | + <{{url}}|Publishing workflow> failed on `{{branch}}` (commit <{{commitUrl}}|{{sha}}>) + vars: + url: workflow_run.html_url + branch: workflow_run.head_branch + sha: $substring(workflow_run.head_sha, 0, 7) + commitUrl: workflow_run.head_repository.html_url & "/commit/" & workflow_run.head_sha + show_webhook_payload_on_error: false + + - name: "Stable publishing workflow failed" + event: workflow_run + slack: + nvidia: + channels: + - id: C0AT93CK1B9 # nvqpp-cudaq-notifications + match: workflow_run.name = "Push stable release" and workflow_run.conclusion = "failure" + message: + body: | + <{{url}}|Push stable release workflow> failed on `{{branch}}` (commit <{{commitUrl}}|{{sha}}>) + vars: + url: workflow_run.html_url + branch: workflow_run.head_branch + sha: $substring(workflow_run.head_sha, 0, 7) + commitUrl: workflow_run.head_repository.html_url & "/commit/" & workflow_run.head_sha + show_webhook_payload_on_error: false + + - name: "Deployments workflow failed" + event: workflow_run + slack: + nvidia: + channels: + - id: C0AT93CK1B9 # nvqpp-cudaq-notifications + match: workflow_run.name = "Deployments" and workflow_run.conclusion = "failure" + message: + body: | + <{{url}}|Deployments workflow> failed on `{{branch}}` (commit <{{commitUrl}}|{{sha}}>) + vars: + url: workflow_run.html_url + branch: workflow_run.head_branch + sha: $substring(workflow_run.head_sha, 0, 7) + commitUrl: workflow_run.head_repository.html_url & "/commit/" & workflow_run.head_sha + show_webhook_payload_on_error: false + + - name: "Merge queue CI failed" + event: workflow_run + slack: + nvidia: + channels: + - id: C0AT93CK1B9 # nvqpp-cudaq-notifications + match: workflow_run.name = "CI" and workflow_run.event = "merge_group" and workflow_run.conclusion = "failure" + message: + body: | + <{{url}}|Merge queue CI> failed on `{{branch}}` (commit <{{commitUrl}}|{{sha}}>) + vars: + url: workflow_run.html_url + branch: workflow_run.head_branch + sha: $substring(workflow_run.head_sha, 0, 7) + commitUrl: workflow_run.head_repository.html_url & "/commit/" & workflow_run.head_sha + show_webhook_payload_on_error: false + + - name: "Issue opened" + event: issues + slack: + nvidia: + channels: + - id: C0AT93CK1B9 # nvqpp-cudaq-notifications + match: action = "opened" + message: + body: | + New issue opened: <{{url}}|{{title}}> + vars: + url: issue.html_url + title: issue.title + show_webhook_payload_on_error: false + + - name: "Stale issue (no update in over a week)" + event: repository_dispatch + slack: + nvidia: + channels: + - id: C0AT93CK1B9 # nvqpp-cudaq-notifications + match: action = "stale-issue" + message: + body: | + Issue has had no activity in over a week: <{{url}}|{{title}}> + vars: + url: client_payload.url + title: client_payload.title + show_webhook_payload_on_error: false diff --git a/.github/pre-commit/spelling_allowlist.txt b/.github/pre-commit/spelling_allowlist.txt index 64c9c045bed..e4dea1a5777 100644 --- a/.github/pre-commit/spelling_allowlist.txt +++ b/.github/pre-commit/spelling_allowlist.txt @@ -108,6 +108,8 @@ Photonics PyPI Pygments QAOA +QASM +QBRAID QCI QCaaS QEC @@ -128,6 +130,7 @@ QuTiP Quake Quantinuum RDMA +REPL RHEL RPC RSA @@ -168,6 +171,7 @@ amongst ancilla ansatz ansatzes +api archiver arity auxillary @@ -300,6 +304,7 @@ lossy lvalue macOS makefiles +measurementCounts merchantability mps multinomial @@ -307,10 +312,12 @@ multithreaded mutex namespace namespaces +nanobind natively normalization nullary nvcc +nvq observables optimizer optimizers @@ -332,21 +339,26 @@ preprocessor probability programmatically pybind +qBraid qaoa +qbraid qed qio +qrn quantize quantized qubit qubits qudit qudits +queryable qumode qumodes reStructuredText realtime reconfigurable reproducibility +resultData reusability runtime runtimes diff --git a/.github/workflows/clean_up.yml b/.github/workflows/clean_up.yml index ebca2c0aaa2..a5bedab7c05 100644 --- a/.github/workflows/clean_up.yml +++ b/.github/workflows/clean_up.yml @@ -144,6 +144,8 @@ jobs: # Since we use the same workflows during CI, a default environment that defines # the necessary variables is used instead. Unfortunately, this automatically # also creates an (unwanted) deployment, which we delete with this job. + # The ghcr-ci environment similarly produces unwanted deployment entries + # from the dev_environment workflow during CI runs on pull requests. # See also https://github.com/actions/runner/issues/2120 deployments: name: Deployments @@ -155,26 +157,28 @@ jobs: - uses: actions/github-script@v7 with: script: | - const deployments = await github.rest.repos.listDeployments({ - owner: context.repo.owner, - repo: context.repo.repo, - environment: 'default' - }); - await Promise.all( - deployments.data.map(async (deployment) => { - await github.rest.repos.createDeploymentStatus({ - owner: context.repo.owner, - repo: context.repo.repo, - deployment_id: deployment.id, - state: 'inactive' - }); - return github.rest.repos.deleteDeployment({ + for (const environment of ['default', 'ghcr-ci']) { + const deployments = await github.rest.repos.listDeployments({ owner: context.repo.owner, repo: context.repo.repo, - deployment_id: deployment.id - }); - }) - ); + environment: environment + }); + await Promise.all( + deployments.data.map(async (deployment) => { + await github.rest.repos.createDeploymentStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: deployment.id, + state: 'inactive' + }); + return github.rest.repos.deleteDeployment({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: deployment.id + }); + }) + ); + } pr_cleanup: name: Clean up documentation previews diff --git a/.github/workflows/config/gitlab_commits.txt b/.github/workflows/config/gitlab_commits.txt index c912a492963..e3e19dbe3f3 100644 --- a/.github/workflows/config/gitlab_commits.txt +++ b/.github/workflows/config/gitlab_commits.txt @@ -1,2 +1,2 @@ nvidia-mgpu-repo: cuda-quantum/cuquantum-mgpu.git -nvidia-mgpu-commit: 52dbd7d31cf3c88c8e5a1de9bac6635a5b0c8309 +nvidia-mgpu-commit: 16b82e64ab6f1d14ed7162a8d2580b632271a89f diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index 47cce0eb1e3..7297e2faeba 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -28,6 +28,7 @@ on: - quantinuum - scaleway - tii + - qbraid single_test_name: type: string required: false @@ -191,7 +192,7 @@ jobs: run: | # Determine which providers to test based on inputs and event type if [[ "${{ github.event_name }}" == "schedule" || "${{ inputs.target }}" == "nightly" ]]; then - providers='["anyon", "fermioniq", "infleqtion", "ionq", "iqm", "oqc", "orca", "pasqal", "qci", "quantinuum", "scaleway", "tii"]' + providers='["anyon", "fermioniq", "infleqtion", "ionq", "iqm", "oqc", "orca", "pasqal", "qbraid", "qci", "quantinuum", "scaleway", "tii"]' else # Just run the specified target provider providers="[\"${{ inputs.target }}\"]" @@ -261,6 +262,9 @@ jobs: pasqal) filelist="docs/sphinx/targets/cpp/pasqal.cpp docs/sphinx/targets/python/pasqal.py" ;; + qbraid) + filelist="targettests/qbraid/*.cpp docs/sphinx/targets/cpp/qbraid.cpp docs/sphinx/targets/python/qbraid.py" + ;; qci) filelist="targettests/qci/*.cpp" ;; @@ -380,6 +384,11 @@ jobs: echo "PASQAL_PROJECT_ID=${{ secrets.PASQAL_PROJECT_ID }}" >> $GITHUB_ENV echo "PASQAL_MACHINE_TARGET=EMU_FREE" >> $GITHUB_ENV ;; + qbraid) + echo "### Setting up qBraid account" >> $GITHUB_STEP_SUMMARY + echo "::add-mask::${{ secrets.QBRAID_API_KEY }}" + echo "QBRAID_API_KEY=${{ secrets.QBRAID_API_KEY }}" >> $GITHUB_ENV + ;; qci) echo "### Setting up QCI account" >> $GITHUB_STEP_SUMMARY echo "::add-mask::${{ secrets.QCI_AUTH_TOKEN }}" @@ -671,6 +680,39 @@ jobs: fi ;; + qbraid) + if [[ "$filename" == *.cpp ]]; then + nvq++ -v $filename --target qbraid --qbraid-machine qbraid:qbraid:sim:qir-sv + test_status=$? + if [ $test_status -eq 0 ]; then + ./a.out + test_status=$? + if [ $test_status -eq 0 ]; then + echo ":white_check_mark: Successfully ran test: $filename" >> $GITHUB_STEP_SUMMARY + else + echo ":x: Test failed (failed to execute): $filename" >> $GITHUB_STEP_SUMMARY + test_err_sum=$((test_err_sum+1)) + fi + else + echo ":x: Test failed (failed to compile): $filename" >> $GITHUB_STEP_SUMMARY + test_err_sum=$((test_err_sum+1)) + fi + elif [[ "$filename" == *.py ]]; then + python3 $filename 1> /dev/null + test_status=$? + if [ $test_status -eq 0 ]; then + echo ":white_check_mark: Successfully ran test: $filename" >> $GITHUB_STEP_SUMMARY + else + echo ":x: Test failed (failed to execute): $filename" >> $GITHUB_STEP_SUMMARY + test_err_sum=$((test_err_sum+1)) + fi + else + echo "::warning::Unsupported file type: $filename" + echo ":warning: Test skipped (unsupported file type): $filename" >> $GITHUB_STEP_SUMMARY + test_skip_sum=$((test_skip_sum+1)) + fi + ;; + qci) nvq++ -v $filename --target qci test_status=$? diff --git a/.github/workflows/stale_issue_notifier.yml b/.github/workflows/stale_issue_notifier.yml new file mode 100644 index 00000000000..fefdacba09f --- /dev/null +++ b/.github/workflows/stale_issue_notifier.yml @@ -0,0 +1,107 @@ +name: Stale issue notifier + +on: + schedule: + - cron: '0 14 * * *' + workflow_dispatch: + issues: + types: [edited, reopened] + issue_comment: + types: [created] + +permissions: + contents: write + issues: write + +jobs: + notify: + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + steps: + - name: Dispatch stale-issue events + uses: actions/github-script@v7 + with: + script: | + const WEEK_MS = 7 * 24 * 60 * 60 * 1000; + const LABEL = 'stale-notified'; + const now = Date.now(); + + try { + await github.rest.issues.getLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: LABEL, + }); + } catch (e) { + if (e.status !== 404) throw e; + await github.rest.issues.createLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: LABEL, + color: 'ededed', + description: 'Stale notification has already fired for this issue', + }); + } + + const issues = await github.paginate(github.rest.issues.listForRepo, { + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + sort: 'updated', + direction: 'asc', + per_page: 100, + }); + + let dispatched = 0; + for (const issue of issues) { + if (issue.pull_request) continue; + const age = now - new Date(issue.updated_at).getTime(); + const alreadyNotified = issue.labels.some(l => (l.name || l) === LABEL); + if (age < WEEK_MS || alreadyNotified) continue; + + await github.rest.repos.createDispatchEvent({ + owner: context.repo.owner, + repo: context.repo.repo, + event_type: 'stale-issue', + client_payload: { + number: issue.number, + url: issue.html_url, + title: issue.title, + }, + }); + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + labels: [LABEL], + }); + core.info(`Dispatched stale-issue for #${issue.number}`); + dispatched++; + } + core.info(`Total dispatched: ${dispatched}`); + + refresh: + if: | + github.event_name == 'issues' || + (github.event_name == 'issue_comment' && github.event.issue.pull_request == null) + runs-on: ubuntu-latest + steps: + - name: Remove stale-notified label on activity + uses: actions/github-script@v7 + with: + script: | + const LABEL = 'stale-notified'; + const issue = context.payload.issue; + const hasLabel = (issue.labels || []).some(l => l.name === LABEL); + if (!hasLabel) return; + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + name: LABEL, + }); + core.info(`Removed ${LABEL} from #${issue.number}`); + } catch (e) { + if (e.status !== 404) throw e; + } diff --git a/.gitmodules b/.gitmodules index 622993890c6..644ab8cc24f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -48,3 +48,6 @@ [submodule "tpls/Stim"] path = tpls/Stim url = https://github.com/quantumlib/Stim +[submodule "tpls/nanobind"] + path = tpls/nanobind + url = https://github.com/wjakob/nanobind.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 548bd30adcf..0baf807653e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -135,6 +135,11 @@ if (NOT DEFINED CUDAQ_ENABLE_SCALEWAY_BACKEND) set(CUDAQ_ENABLE_SCALEWAY_BACKEND ON CACHE BOOL "Enable building the Scaleway target.") endif() +# Enable qBraid target by default. +if (NOT DEFINED CUDAQ_ENABLE_QBRAID_BACKEND) + set(CUDAQ_ENABLE_QBRAID_BACKEND ON CACHE BOOL "Enable building the qBraid target.") +endif() + # Generate a CompilationDatabase (compile_commands.json file) for our build, # for use by clang_complete, YouCompleteMe, etc. set(CMAKE_EXPORT_COMPILE_COMMANDS 1) @@ -713,6 +718,12 @@ if (CUDAQ_ENABLE_PYTHON) # Python bindings generated as part of the CUDA-Q build and bindings generated for # third party CUDA-Q libraries; see also https://github.com/pybind/pybind11/issues/1262 add_subdirectory(tpls/pybind11) + + # nanobind is used for all CUDA-Q Python bindings. pybind11 is retained only + # for upstream MLIR Python extensions (e.g., _mlirAsyncPasses) which use + # mlir/Bindings/Python/PybindAdaptors.h. + add_subdirectory(tpls/nanobind) + add_subdirectory(python) endif() diff --git a/docker/build/devcontainer.Dockerfile b/docker/build/devcontainer.Dockerfile index 51fa1c72ea5..4100c90e340 100644 --- a/docker/build/devcontainer.Dockerfile +++ b/docker/build/devcontainer.Dockerfile @@ -183,7 +183,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ else \ cupy_version=13.4.1; \ fi && \ - python3 -m pip install --break-system-packages cupy-cuda$(echo $CUDA_VERSION | cut -d . -f1)x==${cupy_version} cuquantum-cu$(echo $CUDA_VERSION | cut -d . -f1)==26.1.0 && \ + python3 -m pip install --break-system-packages cupy-cuda$(echo $CUDA_VERSION | cut -d . -f1)x==${cupy_version} cuquantum-cu$(echo $CUDA_VERSION | cut -d . -f1)==26.3.1 && \ if [ "$(python3 --version | grep -o [0-9\.]* | cut -d . -f -2)" != "3.12" ]; then \ echo "expecting Python version 3.12"; \ fi diff --git a/docs/sphinx/api/languages/python_api.rst b/docs/sphinx/api/languages/python_api.rst index 29644506d2d..a973997b7ce 100644 --- a/docs/sphinx/api/languages/python_api.rst +++ b/docs/sphinx/api/languages/python_api.rst @@ -76,6 +76,7 @@ Kernel Execution Backend Configuration ============================= +.. autofunction:: cudaq::parse_args .. autofunction:: cudaq::has_target .. autofunction:: cudaq::get_target .. autofunction:: cudaq::get_targets @@ -494,4 +495,3 @@ Trajectory and Selection Types .. autoclass:: cudaq.ptsbe.KrausSelection :members: - diff --git a/docs/sphinx/applications/python/divisive_clustering_coresets.ipynb b/docs/sphinx/applications/python/divisive_clustering_coresets.ipynb index b0b1fae4c5a..4ede8d321b7 100644 --- a/docs/sphinx/applications/python/divisive_clustering_coresets.ipynb +++ b/docs/sphinx/applications/python/divisive_clustering_coresets.ipynb @@ -651,7 +651,7 @@ "source": [ "threshold_height = 1\n", "clusters = dendo.get_clusters_using_height(threshold_height)\n", - "colors = [\"red\", \"blue\", \"green\", \"black\", \"purple\", \"orange\", \"yellow\"]\n", + "colors = [\"red\", \"blue\", \"green\", \"black\", \"purple\", \"orange\", \"yellow\", \"cyan\", \"magenta\", \"brown\"]\n", "dendo.plot_dendrogram(\n", " plot_title=\"Dendrogram of Coreset using VQE\",\n", " colors=colors,\n", diff --git a/docs/sphinx/applications/python/krylov.ipynb b/docs/sphinx/applications/python/krylov.ipynb index 711eb9d2c28..66b95331e95 100644 --- a/docs/sphinx/applications/python/krylov.ipynb +++ b/docs/sphinx/applications/python/krylov.ipynb @@ -60,7 +60,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "ba61665c-dc3b-4e43-b1cf-340855ea68fb", "metadata": {}, "outputs": [], @@ -100,7 +100,7 @@ "[pyscf] Total number of orbitals = 2\n", "[pyscf] Total number of electrons = 2\n", "[pyscf] HF energy = -1.116325564486115\n", - "[pyscf] Total R-CCSD energy = -1.1371758844013342\n", + "[pyscf] Total R-CCSD energy = -1.1371758844013327\n", "Ground state energy (classical simulation)= (-1.1371757102406845+0j) , index= 3\n" ] } @@ -167,17 +167,20 @@ "name": "stdout", "output_type": "stream", "text": [ - "[(-0.10647701149300526+0j), (0.17028010135220517+0j), (0.17028010135220514+0j), (-0.22004130022421745+0j), (-0.22004130022421745+0j), (0.1683359862516207+0j), (0.12020049071260122+0j), (0.1656068235817425+0j), (0.1656068235817425+0j), (0.12020049071260122+0j), (0.17407289249680213+0j), (-0.04540633286914128+0j), (0.04540633286914128+0j), (0.04540633286914128+0j), (-0.04540633286914128+0j)]\n", - "['IIII', 'ZIII', 'IZII', 'IIZI', 'IIIZ', 'ZZII', 'ZIZI', 'ZIIZ', 'IZZI', 'IZIZ', 'IIZZ', 'XXYY', 'XYYX', 'YXXY', 'YYXX']\n" + "[(0.17028010135220506+0j), (0.17028010135220503+0j), (-0.2200413002242175+0j), (-0.2200413002242175+0j), (0.1683359862516207+0j), (0.12020049071260122+0j), (0.1656068235817425+0j), (0.1656068235817425+0j), (0.12020049071260122+0j), (0.17407289249680213+0j), (-0.04540633286914128+0j), (0.04540633286914128+0j), (0.04540633286914128+0j), (-0.04540633286914128+0j)]\n", + "['ZIII', 'IZII', 'IIZI', 'IIIZ', 'ZZII', 'ZIZI', 'ZIIZ', 'IZZI', 'IZIZ', 'IIZZ', 'XXYY', 'XYYX', 'YXXY', 'YYXX']\n" ] } ], "source": [ - "\n", - "# Collect coefficients from a spin operator so we can pass them to a kernel\n", + "# Collect coefficients from a spin operator so we can pass them to a kernel.\n", + "# The identity term is excluded. Its contribution is added back to the \n", + "# Hamiltonian matrix classically below.\n", "def term_coefficients(ham: cudaq.SpinOperator) -> list[complex]:\n", " result = []\n", " for term in ham:\n", + " if term.is_identity():\n", + " continue\n", " result.append(term.evaluate_coefficient())\n", " return result\n", "\n", @@ -185,9 +188,10 @@ "def term_words(ham: cudaq.SpinOperator) -> list[str]:\n", " # Our kernel uses these words to apply exp_pauli to the entire state.\n", " # we hence ensure that each pauli word covers the entire space.\n", - " \n", " result = []\n", " for term in ham:\n", + " if term.is_identity():\n", + " continue\n", " result.append(term.get_pauli_word(qubits_num))\n", " return result\n", "\n", @@ -195,6 +199,13 @@ "coefficient = term_coefficients(hamiltonian)\n", "pauli_string = term_words(hamiltonian)\n", "\n", + "# Sum of identity-term coefficients\n", + "# The identity contributes `identity_coef * S` to the Hamiltonian matrix.\n", + "identity_coef = sum(\n", + " term.evaluate_coefficient().real\n", + " for term in hamiltonian\n", + " if term.is_identity())\n", + "\n", "print(coefficient)\n", "print(pauli_string)" ] @@ -365,7 +376,6 @@ "metadata": {}, "outputs": [], "source": [ - "\n", "# Create the identity operator\n", "identity_op = cudaq.SpinOperator.from_word('I' * qubits_num)\n", "# Get the Pauli word and convert it to a list of integers\n", @@ -423,7 +433,7 @@ " # 2 entry array that stores real and imaginary part of matrix element\n", " tot_e = np.zeros(2)\n", "\n", - " # Loops over the terms in the Hamiltonian, computing expectation values\n", + " # Loops over the (non-identity) terms in the Hamiltonian, computing expectation values\n", " for coef, word in zip(coefficient, pauli_string):\n", " pauli_list = pauli_str(word, qubits_num)\n", " \n", @@ -441,8 +451,8 @@ " tot_e[0] += temp[0]\n", " tot_e[1] += temp[1]\n", "\n", - " # Sums real and imaginary totals to specify Hamiltonian entry\n", - " ham_matrx[m, n] = tot_e[0] + tot_e[1] * 1j\n", + " # Adds back the identity-term contribution.\n", + " ham_matrx[m, n] = tot_e[0] + tot_e[1] * 1j + identity_coef * wf_overlap[m, n]\n", " if n != m:\n", " ham_matrx[n, m] = np.conj(ham_matrx[m, n])" ] @@ -512,7 +522,7 @@ "output_type": "stream", "text": [ "Energy from QFD:\n", - "(-1.137176660753775-1.6945689273261445e-07j)\n" + "(-1.1359686811350462-4.497484607599205e-09j)\n" ] } ], diff --git a/docs/sphinx/examples/cpp/basics/mid_circuit_measurement.cpp b/docs/sphinx/examples/cpp/basics/mid_circuit_measurement.cpp index 36c548b6b5a..ce987a92c39 100644 --- a/docs/sphinx/examples/cpp/basics/mid_circuit_measurement.cpp +++ b/docs/sphinx/examples/cpp/basics/mid_circuit_measurement.cpp @@ -6,7 +6,7 @@ #include struct kernel { - bool operator()() __qpu__ { + auto operator()() __qpu__ { cudaq::qarray<3> q; // Initial state preparation x(q[0]); diff --git a/docs/sphinx/examples/cpp/measuring_kernels.cpp b/docs/sphinx/examples/cpp/measuring_kernels.cpp index 53ee6361326..0c4664c042b 100644 --- a/docs/sphinx/examples/cpp/measuring_kernels.cpp +++ b/docs/sphinx/examples/cpp/measuring_kernels.cpp @@ -27,7 +27,7 @@ __qpu__ void kernel1() { // [End Sample2] // [Begin Run0] -__qpu__ std::vector kernel2() { +__qpu__ auto kernel2() { cudaq::qvector q(2); h(q[0]); auto b0 = mz(q[0]); @@ -37,7 +37,7 @@ __qpu__ std::vector kernel2() { if (b0) { h(q[1]); } - return cudaq::to_bool_vector(mz(q)); + return mz(q); } int main() { diff --git a/docs/sphinx/examples/cpp/sample_to_run_migration.cpp b/docs/sphinx/examples/cpp/sample_to_run_migration.cpp index fe557aa25c9..7ce52822b91 100644 --- a/docs/sphinx/examples/cpp/sample_to_run_migration.cpp +++ b/docs/sphinx/examples/cpp/sample_to_run_migration.cpp @@ -31,7 +31,7 @@ __qpu__ void reset_pattern() { // [Begin Example1] struct simple_conditional { - bool operator()() __qpu__ { + auto operator()() __qpu__ { cudaq::qvector q(2); h(q[0]); auto r = mz(q[0]); diff --git a/docs/sphinx/targets/cpp/qbraid.cpp b/docs/sphinx/targets/cpp/qbraid.cpp new file mode 100644 index 00000000000..f7a15a0906e --- /dev/null +++ b/docs/sphinx/targets/cpp/qbraid.cpp @@ -0,0 +1,48 @@ +// Compile and run with: +// ``` +// nvq++ --target qbraid qbraid.cpp -o out.x && ./out.x +// ``` +// This will submit the job to the qBraid ideal simulator target (default). + +#include +#include + +// Define a simple quantum kernel to execute on qBraid. +struct ghz { + // Maximally entangled state between 5 qubits. + auto operator()() __qpu__ { + cudaq::qvector q(5); + h(q[0]); + for (int i = 0; i < 4; i++) { + x(q[i], q[i + 1]); + } + auto result = mz(q); + } +}; + +int main() { + // Submit to qBraid asynchronously (e.g., continue executing + // code in the file until the job has been returned). + auto future = cudaq::sample_async(ghz{}); + // ... classical code to execute in the meantime ... + + // Can write the future to file: + { + std::ofstream out("saveMe.json"); + out << future; + } + + // Then come back and read it in later. + cudaq::async_result readIn; + std::ifstream in("saveMe.json"); + in >> readIn; + + // Get the results of the read in future. + auto async_counts = readIn.get(); + async_counts.dump(); + + // OR: Submit to qBraid synchronously (e.g., wait for the job + // result to be returned before proceeding). + auto counts = cudaq::sample(ghz{}); + counts.dump(); +} diff --git a/docs/sphinx/targets/python/qbraid.py b/docs/sphinx/targets/python/qbraid.py new file mode 100644 index 00000000000..dc61d605709 --- /dev/null +++ b/docs/sphinx/targets/python/qbraid.py @@ -0,0 +1,51 @@ +import cudaq + +# You only have to set the target once! No need to redefine it +# for every execution call on your kernel. +# To use different targets in the same file, you must update +# it via another call to `cudaq.set_target()` +cudaq.set_target("qbraid") + + +# Create the kernel we'd like to execute on qBraid. +@cudaq.kernel +def kernel(): + qvector = cudaq.qvector(2) + h(qvector[0]) + x.ctrl(qvector[0], qvector[1]) + + +# Execute on qBraid and print out the results. + +# Option A: +# By using the asynchronous `cudaq.sample_async`, the remaining +# classical code will be executed while the job is being handled +# by qBraid. This is ideal when submitting via a queue over +# the cloud. +async_results = cudaq.sample_async(kernel) +# ... more classical code to run ... + +# We can either retrieve the results later in the program with +# ``` +# async_counts = async_results.get() +# ``` +# or we can also write the job reference (`async_results`) to +# a file and load it later or from a different process. +file = open("future.txt", "w") +file.write(str(async_results)) +file.close() + +# We can later read the file content and retrieve the job +# information and results. +same_file = open("future.txt", "r") +retrieved_async_results = cudaq.AsyncSampleResult(str(same_file.read())) + +counts = retrieved_async_results.get() +print(counts) + +# Option B: +# By using the synchronous `cudaq.sample`, the execution of +# any remaining classical code in the file will occur only +# after the job has been returned from qBraid. +counts = cudaq.sample(kernel) +print(counts) diff --git a/docs/sphinx/using/backends/cloud.rst b/docs/sphinx/using/backends/cloud.rst index 8c03a4398cc..ebd02e033e8 100644 --- a/docs/sphinx/using/backends/cloud.rst +++ b/docs/sphinx/using/backends/cloud.rst @@ -5,6 +5,7 @@ CUDA-Q provides a number of options to access hardware resources (GPUs and QPUs) .. toctree:: :maxdepth: 1 - + Amazon Braket (braket) Scaleway QaaS (scaleway) + qBraid diff --git a/docs/sphinx/using/backends/cloud/qbraid.rst b/docs/sphinx/using/backends/cloud/qbraid.rst new file mode 100644 index 00000000000..dfa72e53913 --- /dev/null +++ b/docs/sphinx/using/backends/cloud/qbraid.rst @@ -0,0 +1,101 @@ +qBraid +++++++ + +.. _qbraid-backend: + +`qBraid `__ is a cloud platform that brokers access to +quantum simulators and hardware from multiple vendors through a single API. +CUDA-Q can submit OpenQASM 2 jobs to any device exposed by the qBraid service. +See the `qBraid device catalog `__ for the +set of simulators and QPUs currently available. + +Setting Credentials +``````````````````` + +Generate an API key from your `qBraid account `__ +and export it as an environment variable: + +.. code:: bash + + export QBRAID_API_KEY="qbraid_generated_api_key" + +Alternatively, the API key can be passed directly to ``cudaq.set_target`` via +the ``api_key`` argument (see below). + +Submitting +`````````` + +.. tab:: Python + + The target to which quantum kernels are submitted can be controlled with + the ``cudaq.set_target()`` function. + + .. code:: python + + cudaq.set_target("qbraid") + + By default, jobs are submitted to the qBraid state vector simulator + (``qbraid:qbraid:sim:qir-sv``). + + To specify a different qBraid device, set the ``machine`` parameter to its + qBraid device ID. + + .. code:: python + + cudaq.set_target("qbraid", machine="qbraid:qbraid:sim:qir-sv") + + The API key can also be supplied inline instead of through the + ``QBRAID_API_KEY`` environment variable. + + .. code:: python + + cudaq.set_target("qbraid", api_key="qbraid_generated_api_key") + + qBraid devices are cloud-hosted, so local emulation via the ``emulate`` + flag is not supported — all jobs are executed on the qBraid service. + To run without submitting to real hardware, select one of the qBraid + simulator devices (for example, ``qbraid:qbraid:sim:qir-sv``) via the + ``machine`` argument. + + The number of shots for a kernel execution can be set through the + ``shots_count`` argument to ``cudaq.sample`` or ``cudaq.observe``. The + default is 1000. + + .. code:: python + + cudaq.sample(kernel, shots_count=10000) + +.. tab:: C++ + + To target quantum kernel code for execution on qBraid, pass the flag + ``--target qbraid`` to the ``nvq++`` compiler. By default jobs are + submitted to the qBraid state vector simulator + (``qbraid:qbraid:sim:qir-sv``). + + .. code:: bash + + nvq++ --target qbraid src.cpp + + To execute kernels on a different device, pass ``--qbraid-machine`` with + the qBraid device ID: + + .. code:: bash + + nvq++ --target qbraid --qbraid-machine "qbraid:qbraid:sim:qir-sv" src.cpp + + The API key can be passed explicitly with ``--qbraid-api_key`` instead of + being read from ``QBRAID_API_KEY``: + + .. code:: bash + + nvq++ --target qbraid --qbraid-api_key "qbraid_generated_api_key" src.cpp + + qBraid devices are cloud-hosted, so the ``--emulate`` flag is not + supported for this target — all jobs are executed on the qBraid + service. To run without submitting to real hardware, pass + ``--qbraid-machine`` with a qBraid simulator device ID (for example, + ``qbraid:qbraid:sim:qir-sv``). + +To see a complete example for using qBraid's backends, take a look at our +:doc:`Python examples <../../examples/examples>` and +:doc:`C++ examples <../../examples/examples>`. diff --git a/docs/sphinx/using/basics/run_kernel.rst b/docs/sphinx/using/basics/run_kernel.rst index 3999fb4e673..371fdd4685d 100644 --- a/docs/sphinx/using/basics/run_kernel.rst +++ b/docs/sphinx/using/basics/run_kernel.rst @@ -218,7 +218,7 @@ The observe function allows us to calculate expectation values for a defined qua The :func:`cudaq.observe` method takes a kernel and its arguments as inputs, along with a :class:`cudaq.operators.spin.SpinOperator`. Using the `cudaq.spin` module, operators may be defined as a linear combination of Pauli strings. Functions, such - as :func:`cudaq.spin.i`, :func:`cudaq.spin.x`, :func:`cudaq.spin.y`, :func:`cudaq.spin.z` may be used to construct more + as `cudaq.spin.i`, `cudaq.spin.x`, `cudaq.spin.y`, `cudaq.spin.z` may be used to construct more complex spin Hamiltonians on multiple qubits. .. tab:: C++ diff --git a/docs/sphinx/using/basics/troubleshooting.rst b/docs/sphinx/using/basics/troubleshooting.rst index 608e7086f60..5ca7bbcf2a2 100644 --- a/docs/sphinx/using/basics/troubleshooting.rst +++ b/docs/sphinx/using/basics/troubleshooting.rst @@ -38,4 +38,27 @@ variable. For any CUDA-Q executable, just prepend as follows: CUDAQ_DUMP_JIT_IR=1 ./a.out # or - CUDAQ_DUMP_JIT_IR= ./a.out \ No newline at end of file + CUDAQ_DUMP_JIT_IR= ./a.out + +Python Stack-Traces +++++++++++++++++++++++++ + +When CUDA-Q parses Python command-line options via :func:`cudaq.parse_args`, +Python stack-traces are suppressed by default to keep runtime errors concise. +To show the full stack-trace for debugging, pass +:code:`--cudaq-full-stack-trace` when invoking your script. + +.. code-block:: bash + + python3 program.py --cudaq-full-stack-trace + +This flag can be combined with other CUDA-Q Python runtime options such as +:code:`--target`, :code:`--target-option`, and :code:`--emulate`. + +.. code-block:: bash + + python3 program.py --target nvidia --target-option fp64 --cudaq-full-stack-trace + +If your application parses CUDA-Q command-line arguments explicitly, call +:func:`cudaq.parse_args` before running the rest of the program so the flag is +recognized. diff --git a/include/cudaq/Optimizer/CodeGen/Passes.td b/include/cudaq/Optimizer/CodeGen/Passes.td index 0424599a46a..598e9d2c7c0 100644 --- a/include/cudaq/Optimizer/CodeGen/Passes.td +++ b/include/cudaq/Optimizer/CodeGen/Passes.td @@ -278,5 +278,33 @@ def ReturnToOutputLog : Pass<"return-to-output-log", "mlir::ModuleOp"> { ]; } +def EliminateDeadHeapCopy + : Pass<"eliminate-dead-heap-copy", "mlir::func::FuncOp"> { + let summary = "Eliminate dead heap copies from return value logging."; + let description = [{ + When a kernel returns a vector (e.g., measurement results), the frontend + wraps the return value with `__nvqpp_vectorCopyCtor`, which performs a + malloc+memcpy to copy the data from the callee's stack to the heap. This + is necessary because the returned data must outlive the callee's stack + frame when one kernel calls another. After AggressiveInlining, this + intrinsic is expanded into raw malloc and memcpy operations in the caller. + + After ReturnToOutputLog converts return values to QIR output logging + calls (e.g., `__quantum__rt__*_record_output`), it reads from the + cc.stdvec_init's buffer (the malloc'd pointer) and creates new load ops + from it, leaving the cc.stdvec_init with no users. The malloc+memcpy + are then only needed to populate the heap buffer that the output logging + reads from. This pass redirects those reads to the memcpy source (the + original stack data), making the malloc+memcpy dead, and erases them + along with the now-unused cc.stdvec_init. + + Note: this pass is only needed on code paths that do not run LLVM's + optimization passes (e.g., when emitting MLIR rather than LLVM IR for + a remote backend). When the full LLVM opt pipeline runs, it would eliminate + these dead allocations on its own. + }]; + let dependentDialects = ["cudaq::cc::CCDialect", "mlir::func::FuncDialect"]; +} + #endif // CUDAQ_OPT_OPTIMIZER_CODEGEN_PASSES diff --git a/include/cudaq/Optimizer/CodeGen/QIRFunctionNames.h b/include/cudaq/Optimizer/CodeGen/QIRFunctionNames.h index b471ceebb23..56d6006b407 100644 --- a/include/cudaq/Optimizer/CodeGen/QIRFunctionNames.h +++ b/include/cudaq/Optimizer/CodeGen/QIRFunctionNames.h @@ -81,10 +81,6 @@ static constexpr const char QIRArrayConcatArray[] = "__quantum__rt__array_concatenate"; static constexpr const char QIRArrayCreateArray[] = "__quantum__rt__array_create_1d"; -static constexpr const char QIRResultArrayCreate[] = - "__quantum__rt__result_array_create_1d"; -static constexpr const char QIRResultArrayGetElementPtr1d[] = - "__quantum__rt__result_array_get_element_ptr_1d"; /// Dynamic qubit management helper functions. These are currently only used by /// the NVQIR simulator. diff --git a/include/cudaq/Optimizer/Dialect/Quake/QuakeOps.td b/include/cudaq/Optimizer/Dialect/Quake/QuakeOps.td index 4edc20ca348..a1f1ec77d5c 100644 --- a/include/cudaq/Optimizer/Dialect/Quake/QuakeOps.td +++ b/include/cudaq/Optimizer/Dialect/Quake/QuakeOps.td @@ -261,21 +261,21 @@ def quake_ExtractRefOp : QuakeOp<"extract_ref", [Pure]> { } def quake_RelaxSizeOp : QuakeOp<"relax_size", [Pure]> { - let summary = "Relax the constant size on a sized type to be unknown."; + let summary = "Relax the constant size on a !veq to be unknown."; let description = [{ - Demotes a sized `!quake.veq` to `!quake.veq`, or a sized - `!quake.measurements` to `!quake.measurements`. Required to preserve - strongly-typed IR at function call/return boundaries. + At times, the IR needs to forget the length of an SSA-value of type + `!quake.veq` and demote it to type `!quake.veq` where the size is + said to be unknown. This demotion is required to preserve a valid, + strongly-typed IR. - Examples: + Example: ```mlir %uqv = quake.relax_size %qv : (!quake.veq<4>) -> !quake.veq - %ums = quake.relax_size %ms : (!quake.measurements<4>) -> !quake.measurements ``` }]; - let arguments = (ins AnyTypeOf<[VeqType, MeasurementsType]>:$inputVec); - let results = (outs AnyTypeOf<[VeqType, MeasurementsType]>); + let arguments = (ins VeqType:$inputVec); + let results = (outs VeqType); let assemblyFormat = [{ $inputVec `:` functional-type(operands, results) attr-dict @@ -377,34 +377,6 @@ def quake_VeqSizeOp : QuakeOp<"veq_size", [Pure]> { let hasCanonicalizer = 1; } -def quake_MeasurementsSizeOp : QuakeOp<"measurements_size", [Pure]> { - let summary = "Return the size of a measurements collection."; - let description = [{ - Returns the number of individual measurements in a `!quake.measurements` - collection. If the collection has a static size, the static size is returned - (effectively as a constant). If the size is dynamic, the value will be an - SSA-value. - - Examples: - ```mlir - %ms = quake.mz %qubits : (!quake.veq<4>) -> !quake.measurements<4> - %n = quake.measurements_size %ms : (!quake.measurements<4>) -> i64 - - %ms2 = quake.mz %dyn_veq : (!quake.veq) -> !quake.measurements - %n2 = quake.measurements_size %ms2 : (!quake.measurements) -> i64 - ``` - }]; - - let arguments = (ins MeasurementsType:$measurements); - let results = (outs AnySignlessIntegerOrIndex:$size); - - let assemblyFormat = [{ - $measurements `:` functional-type(operands, results) attr-dict - }]; - - let hasCanonicalizer = 1; -} - //===----------------------------------------------------------------------===// // Application, ComputeAction(Uncompute) //===----------------------------------------------------------------------===// @@ -1094,7 +1066,7 @@ class Measurement : QuakeOp:$registerName ); let results = (outs - AnyTypeOf<[MeasureType, MeasurementsType]>:$measOut, + AnyTypeOf<[MeasureType, StdvecOf<[MeasureType]>]>:$measOut, Variadic:$wires ); @@ -1111,15 +1083,13 @@ class Measurement : QuakeOp { let summary = "Measurement along the x-axis"; let description = [{ - The `mx` operation measures the state of qubits along the x-axis. For a - single qubit the result is `!quake.measure`; for multiple qubits the result - is `!quake.measurements` (or `!quake.measurements` when unsized). + The `mx` operation measures the state of qubits into classical bits + represented by a `i1` (or a vector of `i1`), along the x-axis. The state of the qubits is collapsed into one of the computational basis states, i.e., either |0> or |1>. A `reset` operation can guarantee that the @@ -1132,9 +1102,8 @@ def MxOp : Measurement<"mx"> { def MyOp : Measurement<"my"> { let summary = "Measurement along the y-axis"; let description = [{ - The `my` operation measures the state of qubits along the y-axis. For a - single qubit the result is `!quake.measure`; for multiple qubits the result - is `!quake.measurements` (or `!quake.measurements` when unsized). + The `my` operation measures the state of qubits into classical bits + represented by a `i1` (or a vector of `i1`), along the y-axis. The state of the qubit is collapsed into one of the computational basis states, i.e., either |0> or |1>. A `reset` operation can guarantee that the @@ -1147,10 +1116,9 @@ def MyOp : Measurement<"my"> { def MzOp : Measurement<"mz"> { let summary = "Measurement along the z-axis"; let description = [{ - The `mz` operation measures the state of qubits along the z-axis---the - so-called computational basis. For a single qubit the result is - `!quake.measure`; for multiple qubits the result is - `!quake.measurements` (or `!quake.measurements` when unsized). + The `mz` operation measures the state of qubits into a classical bits + represented by a `i1` (or a vector of `i1`), along the z-axis---the + so-called computational basis. The state of the qubit is collapsed into one of the computational basis states, i.e., either |0> or |1>. A `reset` operation can guarantee that the @@ -1175,7 +1143,7 @@ def quake_DiscriminateOp : QuakeOp<"discriminate", [Pure]> { }]; let arguments = (ins - AnyTypeOf<[MeasureType, MeasurementsType]>:$measurement + AnyTypeOf<[MeasureType, StdvecOf<[MeasureType]>]>:$measurement ); let results = (outs AnyTypeOf<[AnySignlessInteger, StdvecOf<[AnySignlessInteger]>]> @@ -1188,64 +1156,6 @@ def quake_DiscriminateOp : QuakeOp<"discriminate", [Pure]> { let hasVerifier = 1; } -//===----------------------------------------------------------------------===// -// GetMeasureOp -//===----------------------------------------------------------------------===// - -def quake_GetMeasureOp : QuakeOp<"get_measure", [Pure]> { - let summary = - "Extract a single measurement from a measurements collection."; - let description = [{ - Extracts a single `!quake.measure` value from a `!quake.measurements` - collection by index. This is analogous to `quake.extract_ref` for qubits. - - Example: - ```mlir - %m = quake.get_measure %ms[0] : (!quake.measurements<4>) -> !quake.measure - ``` - }]; - - let arguments = (ins - MeasurementsType:$measurements, - Optional:$index, - I64Attr:$rawIndex - ); - let results = (outs MeasureType:$measure); - - let builders = [ - OpBuilder<(ins "mlir::Value":$measurements, "mlir::Value":$index, - "mlir::IntegerAttr":$rawIndex), [{ - return build($_builder, $_state, $_builder.getType(), - measurements, index, rawIndex); - }]>, - OpBuilder<(ins "mlir::Value":$measurements, "mlir::Value":$index), [{ - return build($_builder, $_state, $_builder.getType(), - measurements, index, GetMeasureOp::kDynamicIndex); - }]>, - OpBuilder<(ins "mlir::Value":$measurements, "std::size_t":$rawIndex), [{ - auto i64Ty = $_builder.getI64Type(); - return build($_builder, $_state, $_builder.getType(), - measurements, mlir::Value{}, - mlir::IntegerAttr::get(i64Ty, rawIndex)); - }]> - ]; - - let assemblyFormat = [{ - $measurements `[` custom($index, $rawIndex) `]` `:` - functional-type(operands, results) attr-dict - }]; - - let hasVerifier = 1; - - let extraClassDeclaration = [{ - static constexpr std::size_t kDynamicIndex = - std::numeric_limits::max(); - - bool hasConstantIndex() { return !getIndex(); } - std::size_t getConstantIndex() { return getRawIndex(); } - }]; -} - //===----------------------------------------------------------------------===// // Quantum gates //===----------------------------------------------------------------------===// diff --git a/include/cudaq/Optimizer/Dialect/Quake/QuakeTypes.h b/include/cudaq/Optimizer/Dialect/Quake/QuakeTypes.h index 3bc91c21479..6c0d3ff51ed 100644 --- a/include/cudaq/Optimizer/Dialect/Quake/QuakeTypes.h +++ b/include/cudaq/Optimizer/Dialect/Quake/QuakeTypes.h @@ -31,8 +31,7 @@ inline bool isQuantumType(mlir::Type ty) { /// \returns true if \p `ty` is a Quake type. inline bool isQuakeType(mlir::Type ty) { // This should correspond to the registered types in QuakeTypes.cpp. - return isQuantumType(ty) || - mlir::isa(ty); + return isQuantumType(ty) || mlir::isa(ty); } /// \returns true if \p ty is a quantum reference type, excluding `struq`. diff --git a/include/cudaq/Optimizer/Dialect/Quake/QuakeTypes.td b/include/cudaq/Optimizer/Dialect/Quake/QuakeTypes.td index 542f4861069..2f4f98cf0fd 100644 --- a/include/cudaq/Optimizer/Dialect/Quake/QuakeTypes.td +++ b/include/cudaq/Optimizer/Dialect/Quake/QuakeTypes.td @@ -251,41 +251,6 @@ def MeasureType : QuakeType<"Measure", "measure"> { let genStorageClass = 0; } -//===----------------------------------------------------------------------===// -// MeasurementsType: classical data type for a collection of measurements -//===----------------------------------------------------------------------===// - -def MeasurementsType : QuakeType<"Measurements", "measurements"> { - let summary = "a sequence of measurement results"; - let description = [{ - A value of type `measurements` is a collection of values of type `measure`. - This is the natural result type of measuring multiple qubits. Like `veq` is - to `ref`, `measurements` is to `measure`. - - ```mlir - %ms = quake.mz %qubits : (!quake.veq<4>) -> !quake.measurements<4> - %m0 = quake.get_measure %ms[0] : (!quake.measurements<4>) -> !quake.measure - ``` - }]; - - let parameters = (ins "std::size_t":$size); - - let hasCustomAssemblyFormat = 1; - - let extraClassDeclaration = [{ - static constexpr std::size_t kDynamicSize = - std::numeric_limits::max(); - - bool hasSpecifiedSize() const { return getSize() != kDynamicSize; } - bool hasNonZeroSpecifiedSize() const { - return hasSpecifiedSize() && getSize(); - } - static MeasurementsType getUnsized(mlir::MLIRContext *ctx) { - return MeasurementsType::get(ctx, kDynamicSize); - } - }]; -} - //===----------------------------------------------------------------------===// // StateType //===----------------------------------------------------------------------===// diff --git a/include/cudaq/Optimizer/Transforms/Passes.td b/include/cudaq/Optimizer/Transforms/Passes.td index 98a3a0ec3ff..df9890e47b3 100644 --- a/include/cudaq/Optimizer/Transforms/Passes.td +++ b/include/cudaq/Optimizer/Transforms/Passes.td @@ -219,7 +219,7 @@ def CombineMeasurements : %1 = ... : !quake.veq<4> %2 = quake.subveq %1, %c2, %c3 : (!quake.veq<4>, i32, i32) -> !quake.veq<2> - %measOut = quake.mz %2 : (!quake.veq<2>) -> !quake.measurements<2> + %measOut = quake.mz %2 : (!quake.veq<2>) -> !cc.stdvec } ``` with: @@ -227,7 +227,7 @@ def CombineMeasurements : func.func @kernel() attributes {"cudaq-entrypoint", ["output_names", "[[[0,[1,\22q0\22]],[1,[2,\22q1\22]]]]"]} { %1 = ... : !quake.veq<4> - %measOut = quake.mz %1 : (!quake.veq<4>) -> !quake.measurements<4> + %measOut = quake.mz %1 : (!quake.veq<4>) -> !cc.stdvec } ``` }]; @@ -478,30 +478,15 @@ def ExpandControlVeqs: Pass<"expand-control-veqs", "mlir::func::FuncOp"> { } def ExpandMeasurements : Pass<"expand-measurements"> { - let summary = "Expand multi-qubit measurements to individual qubit ops."; + let summary = "Expand multi-ref measurements to series on single refs."; let description = [{ - The `mx`, `my`, `mz` ops can take a list of qubits and/or veq arguments - and return a `!quake.measurements` collection. The target may only - support measuring a single qubit however. This pass expands these - multi-qubit measurements into individual single-qubit measurements in two - steps. - - Step 1: Any `quake.discriminate` on a sized `!quake.measurements` - value is expanded into N individual `quake.get_measure` + - `quake.discriminate` operations on single `!quake.measure` values, with - results collected into a `!cc.stdvec`. - - Step 2: Multi-qubit `mx`, `my`, `mz` ops are replaced with individual - per-qubit measurements. For `veq` targets with known size, the qubits - are extracted via `quake.extract_ref` and measured individually. For - `veq` targets with dynamically-sized targets, a loop is generated - using `quake.veq_size` to compute the iteration count and individual - qubits are extracted via `quake.extract_ref` within the loop body. - - Multi-qubit measurements without local discriminate users are left intact. + The `mx`, `my`, `mz` ops can take a list of qubits and/or veq arguments. + The target may only support measuring a single qubit however. This pass + expands these ops in list format into a series of measurements (including + loops) on individual qubits and into a single `std::vector` result. - The `reset` op can also take a veq argument and this pass will expand that - to a loop of `reset` operations on individual qubits. + The `reset` op can also take a veq argument and this pass will also expand + that to a series of `reset` operations on single qubits. }]; let dependentDialects = ["cudaq::cc::CCDialect", "mlir::LLVM::LLVMDialect"]; diff --git a/lib/Frontend/nvqpp/ASTBridge.cpp b/lib/Frontend/nvqpp/ASTBridge.cpp index c3ac466c2e9..724b13e16e7 100644 --- a/lib/Frontend/nvqpp/ASTBridge.cpp +++ b/lib/Frontend/nvqpp/ASTBridge.cpp @@ -62,8 +62,8 @@ static bool isQubitType(Type ty) { } // Check the builtin type FunctionType to see if it has any references to Quake -// types (including measurement) in its arguments and/or results. -static bool hasAnyQuakeTypes(FunctionType funcTy) { +// qubit types in its arguments and/or results. +static bool hasAnyQubitTypes(FunctionType funcTy) { for (auto ty : funcTy.getInputs()) if (isQubitType(ty)) return true; @@ -639,7 +639,7 @@ void ASTBridgeAction::ASTBridgeConsumer::HandleTranslationUnit( auto unitAttr = UnitAttr::get(ctx); // Flag func as a quantum kernel. func->setAttr(kernelAttrName, unitAttr); - if ((!hasAnyQuakeTypes(func.getFunctionType())) && + if ((!hasAnyQubitTypes(func.getFunctionType())) && (!cudaq::ASTBridgeAction::ASTBridgeConsumer::isCustomOpGenerator( fdPair.second))) { // Flag func as an entry point to a quantum kernel. diff --git a/lib/Frontend/nvqpp/ConvertDecl.cpp b/lib/Frontend/nvqpp/ConvertDecl.cpp index 26ab9af5a25..fd01e4b5ec4 100644 --- a/lib/Frontend/nvqpp/ConvertDecl.cpp +++ b/lib/Frontend/nvqpp/ConvertDecl.cpp @@ -93,9 +93,8 @@ void QuakeBridgeVisitor::addArgumentSymbols( auto parmTy = entryBlock->getArgument(index).getType(); if (isa(parmTy)) { + quake::ControlType, quake::RefType, quake::StruqType, + quake::VeqType, quake::WireType>(parmTy)) { symbolTable.insert(name, entryBlock->getArgument(index)); } else { auto stackSlot = builder.create(loc, parmTy); @@ -170,9 +169,6 @@ bool QuakeBridgeVisitor::interceptRecordDecl(clang::RecordDecl *x) { auto fnTy = cast(popType()); return pushType(cc::IndirectCallableType::get(fnTy)); } - // Measurement result type. - if (name == "measure_result") - return pushType(quake::MeasureType::get(ctx)); if (!isInNamespace(x, "solvers") && !isInNamespace(x, "qec")) { auto loc = toLocation(x); TODO_loc(loc, "unhandled type, " + name + ", in cudaq namespace"); @@ -192,10 +188,6 @@ bool QuakeBridgeVisitor::interceptRecordDecl(clang::RecordDecl *x) { "std::vector element type is not supported"); return false; } - // TODO: std::vector will be replaced by - // cudaq::measure_vector, recognized directly by class name (see spec). - if (isa(ty)) - return pushType(quake::MeasurementsType::getUnsized(ctx)); return pushType(cc::StdvecType::get(ctx, ty)); } // std::vector => cc.stdvec @@ -740,14 +732,7 @@ bool QuakeBridgeVisitor::VisitVarDecl(clang::VarDecl *x) { return true; } - if (isa(type)) { - assert(x->getInit() && "`measure_result` has no default constructor"); - auto initVal = popValue(); - symbolTable.insert(x->getName(), initVal); - if (auto meas = initVal.getDefiningOp()) - meas.setRegisterName(builder.getStringAttr(x->getName())); - return true; - } + // Here we maybe have something like auto var = mz(qreg) if (auto vecType = dyn_cast(type)) { // Variable is of !cc.stdvec type. if (x->getInit()) { @@ -759,11 +744,6 @@ bool QuakeBridgeVisitor::VisitVarDecl(clang::VarDecl *x) { // and if so, find the mz and tag it with the variable name auto elementType = vecType.getElementType(); - if (auto meas = initVec.getDefiningOp()) { - meas.setRegisterName(builder.getStringAttr(x->getName())); - return true; - } - // Drop out if this is not an i1 if (!elementType.isIntOrFloat() || elementType.getIntOrFloatBitWidth() != 1) @@ -801,11 +781,6 @@ bool QuakeBridgeVisitor::VisitVarDecl(clang::VarDecl *x) { auto firstGepUser = *gepOp->getResult(0).getUsers().begin(); if (auto storeOp = dyn_cast(firstGepUser)) { auto result = storeOp->getOperand(0); - if (auto measureOp = - result.getDefiningOp()) { - measureOp.setRegisterName(builder.getStringAttr(x->getName())); - break; - } if (auto discr = result.getDefiningOp()) if (auto mzOp = discr.getMeasurement().getDefiningOp()) { @@ -842,8 +817,9 @@ bool QuakeBridgeVisitor::VisitVarDecl(clang::VarDecl *x) { // If this was an auto var = mz(q), then we want to know the // var name, as it will serve as the classical bit register name - if (auto meas = initValue.getDefiningOp()) - meas.setRegisterName(builder.getStringAttr(x->getName())); + if (auto discr = initValue.getDefiningOp()) + if (auto mz = discr.getMeasurement().getDefiningOp()) + mz.setRegisterName(builder.getStringAttr(x->getName())); assert(initValue && "initializer value must be lowered"); if (isa(initValue.getType()) && isa(type)) { diff --git a/lib/Frontend/nvqpp/ConvertExpr.cpp b/lib/Frontend/nvqpp/ConvertExpr.cpp index a2ee180f508..70aaf25f990 100644 --- a/lib/Frontend/nvqpp/ConvertExpr.cpp +++ b/lib/Frontend/nvqpp/ConvertExpr.cpp @@ -555,13 +555,6 @@ SmallVector QuakeBridgeVisitor::convertKernelArgs( continue; } } - if (auto vMeasTy = dyn_cast(vTy)) - if (auto kMeasTy = dyn_cast(kTy)) - if (vMeasTy.hasSpecifiedSize() && !kMeasTy.hasSpecifiedSize()) { - auto relax = builder.create(loc, kMeasTy, v); - result.push_back(relax); - continue; - } LLVM_DEBUG(llvm::dbgs() << "convert: " << v << "\nto:" << kTy << '\n'); TODO_loc(loc, "argument type conversion"); @@ -665,7 +658,7 @@ bool QuakeBridgeVisitor::VisitCastExpr(clang::CastExpr *x) { } case clang::CastKind::CK_UserDefinedConversion: { auto sub = popValue(); - // castToTy is the conversion function signature. + // castToTy is the converion function signature. castToTy = popType(); if (isa(castToTy) && isa(sub.getType())) { auto locSub = toLocation(x->getSubExpr()); @@ -673,29 +666,6 @@ bool QuakeBridgeVisitor::VisitCastExpr(clang::CastExpr *x) { assert(result && "integer conversion failed"); return result; } - auto i1Type = builder.getI1Type(); - // Handle conversion of `measure_result` - auto measTy = quake::MeasureType::get(builder.getContext()); - if (sub.getType() == measTy) { - auto i1Val = builder.create(loc, i1Type, sub); - // Convert to `int` - if (isa(castToTy)) - return pushValue(builder.create( - loc, castToTy, i1Val, cudaq::cc::CastOpMode::Unsigned)); - // Convert to `float` - if (isa(castToTy)) - return pushValue(builder.create( - loc, castToTy, i1Val, cudaq::cc::CastOpMode::Unsigned)); - // Otherwise, just return the `i1` value - return pushValue(i1Val); - } - - // Handle conversion of measurement collection to std::vector. - // TODO: will become measure_vector::operator std::vector(). - if (isa(sub.getType())) - return pushValue(builder.create( - loc, cc::StdvecType::get(i1Type), sub)); - TODO_loc(loc, "unhandled user-defined implicit conversion"); } case clang::CastKind::CK_ConstructorConversion: { @@ -1045,7 +1015,7 @@ bool QuakeBridgeVisitor::VisitMaterializeTemporaryExpr( // In those cases, there is nothing to materialize, so we can just pass the // Value on the top of the stack. if (isa(ty)) + quake::StateType>(ty)) return true; // If not one of the above special cases, then materialize the value to a @@ -1311,14 +1281,6 @@ bool QuakeBridgeVisitor::VisitCallExpr(clang::CallExpr *x) { auto svec = popValue(); if (isa(svec.getType())) svec = builder.create(loc, svec); - if (isa(svec.getType()) && funcName == "size") - if (auto memberCall = dyn_cast(x)) - if (memberCall->getImplicitObjectArgument()) { - [[maybe_unused]] auto calleeTy = popType(); - assert(isa(calleeTy)); - return pushValue(builder.create( - loc, builder.getI64Type(), svec)); - } auto ext = builder.create(loc, builder.getI64Type(), svec); if (funcName == "size") @@ -1558,38 +1520,10 @@ bool QuakeBridgeVisitor::VisitCallExpr(clang::CallExpr *x) { auto funcArity = func->getNumParams(); SmallVector args = lastValues(funcArity); if (isa(func)) { - auto thisPtrValue = popValue(); - - // For `measure_result`, the implicit "this" value is the `!quake.measure` - // SSA value; forward it unchanged - if (isa(func) && - isInClassInNamespace(func, "measure_result", "cudaq")) - return pushValue(thisPtrValue); + [[maybe_unused]] auto thisPtrValue = popValue(); } auto calleeOp = popValue(); - // Handle operator== and operator!= for measure_result (friend functions) - if (func->isOverloadedOperator() && isInNamespace(func, "cudaq")) { - auto opKind = func->getOverloadedOperator(); - if ((opKind == clang::OO_EqualEqual || opKind == clang::OO_ExclaimEqual) && - args.size() == 2) { - auto lhs = args[0]; - auto rhs = args[1]; - auto measTy = quake::MeasureType::get(builder.getContext()); - if (lhs.getType() == measTy || rhs.getType() == measTy) { - auto i1Type = builder.getI1Type(); - if (lhs.getType() == measTy) - lhs = builder.create(loc, i1Type, lhs); - if (rhs.getType() == measTy) - rhs = builder.create(loc, i1Type, rhs); - // Choose predicate based on operator - auto pred = (opKind == clang::OO_EqualEqual) ? arith::CmpIPredicate::eq - : arith::CmpIPredicate::ne; - return pushValue(builder.create(loc, pred, lhs, rhs)); - } - } - } - if (isInNamespace(func, "cudaq")) { // Check and see if this quantum operation is adjoint bool isAdjoint = false; @@ -1712,33 +1646,25 @@ bool QuakeBridgeVisitor::VisitCallExpr(clang::CallExpr *x) { } if (funcName == "mx" || funcName == "my" || funcName == "mz") { - bool useMeasurements = + // Measurements always return a bool or a std::vector. + bool useStdvec = (args.size() > 1) || (args.size() == 1 && isa(args[0].getType())); auto measure = [&]() -> Value { Type measTy = quake::MeasureType::get(builder.getContext()); - if (useMeasurements) { - std::size_t totalSize = 0; - bool allKnown = true; - for (auto a : args) { - if (quake::isConstantQuantumRefType(a.getType())) - totalSize += quake::getAllocationSize(a.getType()); - else - allKnown = false; - } - if (allKnown && totalSize > 0) - measTy = - quake::MeasurementsType::get(builder.getContext(), totalSize); - else - measTy = quake::MeasurementsType::getUnsized(builder.getContext()); - } + if (useStdvec) + measTy = cc::StdvecType::get(measTy); if (funcName == "mx") return builder.create(loc, measTy, args).getMeasOut(); if (funcName == "my") return builder.create(loc, measTy, args).getMeasOut(); return builder.create(loc, measTy, args).getMeasOut(); }(); - return pushValue(measure); + Type resTy = builder.getI1Type(); + if (useStdvec) + resTy = cc::StdvecType::get(resTy); + return pushValue( + builder.create(loc, resTy, measure)); } // Handle the quantum gate set. @@ -2198,39 +2124,16 @@ bool QuakeBridgeVisitor::VisitCallExpr(clang::CallExpr *x) { return true; } - // TODO: will be replaced by measure_vector::operator std::int64_t(). if (funcName == "toInteger" || funcName == "to_integer") { - auto arg = args[0]; - auto i1Ty = builder.getI1Type(); - auto boolVecTy = cc::StdvecType::get(i1Ty); - if (isa(arg.getType())) - arg = builder.create(loc, boolVecTy, arg); - else if (arg.getType() != boolVecTy) - reportClangError(x, mangler, - "`to_integer` requires measurements or " - "std::vector argument"); IRBuilder irBuilder(builder.getContext()); if (failed(irBuilder.loadIntrinsic(module, cudaqConvertToInteger))) { reportClangError(x, mangler, "cannot load cudaqConvertToInteger"); return false; } auto i64Ty = builder.getI64Type(); - return pushValue(builder - .create(loc, i64Ty, - cudaqConvertToInteger, - ValueRange{arg}) - .getResult(0)); - } - - // TODO: will be replaced by measure_vector::operator std::vector(). - if (funcName == "to_bool_vector") { - auto arg = args[0]; - assert(isa(arg.getType()) && - "to_bool_vector requires measurements type argument"); - auto i1Ty = builder.getI1Type(); - arg = builder.create( - loc, cc::StdvecType::get(i1Ty), arg); - return pushValue(arg); + return pushValue( + builder.create(loc, i64Ty, cudaqConvertToInteger, args) + .getResult(0)); } if (funcName == "slice_vector") { @@ -2627,10 +2530,6 @@ bool QuakeBridgeVisitor::VisitCXXOperatorCallExpr( auto svec = popValue(); if (isa(svec.getType())) svec = builder.create(loc, svec); - if (isa(svec.getType())) { - auto getMeas = builder.create(loc, svec, indexVar); - return replaceTOSValue(getMeas); - } if (!isa(svec.getType())) { TODO_x(loc, x, mangler, "vector dereference"); return false; @@ -3352,20 +3251,6 @@ bool QuakeBridgeVisitor::VisitCXXConstructExpr(clang::CXXConstructExpr *x) { return pushValue(builder.create(loc, copyObj)); } - // For `measure_result`, the implicit "this" value is the `!quake.measure` - // SSA value; forward it unchanged. - // Note: Copy support is a temporary concession while - // `std::vector` exists (its `operator[]` returns by - // reference, forcing copies). Once replaced, it becomes move-only. - if ((ctor->isCopyConstructor() || ctor->isMoveConstructor()) && - isInClassInNamespace(ctor, "measure_result", "cudaq")) { - assert(x->getNumArgs() == 1); - auto src = popValue(); - assert(isa(src.getType()) && - "`measure_result` copy/move source must be `!quake.measure`"); - return pushValue(src); - } - // TODO: remove this when we can handle ctors more generally. if (!ctor->isDefaultConstructor()) { LLVM_DEBUG(llvm::dbgs() << ctorName << " - unhandled ctor:\n"; x->dump()); diff --git a/lib/Frontend/nvqpp/ConvertStmt.cpp b/lib/Frontend/nvqpp/ConvertStmt.cpp index 23ee12901f5..54bd9ca50ec 100644 --- a/lib/Frontend/nvqpp/ConvertStmt.cpp +++ b/lib/Frontend/nvqpp/ConvertStmt.cpp @@ -263,46 +263,6 @@ bool QuakeBridgeVisitor::TraverseCXXForRangeStmt(clang::CXXForRangeStmt *x, auto idxIters = builder.create( loc, i64Ty, iters, cudaq::cc::CastOpMode::Unsigned); opt::factory::createInvariantLoop(builder, loc, idxIters, bodyBuilder); - } else if (auto measTy = - dyn_cast(buffer.getType())) { - Value iters; - if (measTy.hasSpecifiedSize()) { - iters = - builder.create(loc, measTy.getSize(), i64Ty); - } else if (auto measIface = dyn_cast_or_null( - buffer.getDefiningOp())) { - // Derive the iteration count from the measurement op's qubit targets. - for (auto target : measIface.getTargets()) { - Value count; - if (auto veqTy = dyn_cast(target.getType())) { - if (veqTy.hasSpecifiedSize()) - count = builder.create(loc, veqTy.getSize(), - i64Ty); - else - count = builder.create(loc, i64Ty, target); - } else { - count = builder.create(loc, 1, i64Ty); - } - iters = - iters ? builder.create(loc, iters, count).getResult() - : count; - } - } else { - iters = builder.create(loc, i64Ty, buffer); - } - auto bodyBuilder = [&](OpBuilder &builder, Location loc, Region ®ion, - Block &block) { - OpBuilder::InsertionGuard guard(builder); - builder.setInsertionPointToStart(&block); - Value index = block.getArgument(0); - Value measure = builder.create(loc, buffer, index); - symbolTable.insert(loopVar->getName(), measure); - if (!TraverseStmt(static_cast(body))) - result = false; - }; - auto idxIters = builder.create( - loc, i64Ty, iters, cudaq::cc::CastOpMode::Unsigned); - opt::factory::createInvariantLoop(builder, loc, idxIters, bodyBuilder); } else { TODO_x(toLocation(x), x, mangler, "ranged for statement"); } @@ -376,22 +336,6 @@ bool QuakeBridgeVisitor::VisitReturnStmt(clang::ReturnStmt *x) { result = builder.create(loc, i1Ty, result); } } - // Relax sized measurements to unsized when the function expects unsized. - if (auto measTy = dyn_cast(result.getType())) { - auto *parentOp = builder.getBlock()->getParentOp(); - auto funcOp = dyn_cast(parentOp); - if (!funcOp) - funcOp = parentOp->getParentOfType(); - if (funcOp) { - auto fnTy = funcOp.getFunctionType(); - if (fnTy.getNumResults() == 1) - if (auto fnResMeasTy = - dyn_cast(fnTy.getResult(0))) - if (measTy != fnResMeasTy) - result = - builder.create(loc, fnResMeasTy, result); - } - } if (auto vecTy = dyn_cast(resTy)) { // Returning vector data that was allocated on the stack is not valid. // Allocate space on the heap and make a copy of the vector instead. It diff --git a/lib/Frontend/nvqpp/ConvertType.cpp b/lib/Frontend/nvqpp/ConvertType.cpp index e151331aafd..c21ef8d56a9 100644 --- a/lib/Frontend/nvqpp/ConvertType.cpp +++ b/lib/Frontend/nvqpp/ConvertType.cpp @@ -124,21 +124,13 @@ static bool isFunctionCallable(Type t) { return false; } -static bool isMeasureType(Type t) { - if (isa(t)) - return true; - if (auto vec = dyn_cast(t)) - return isMeasureType(vec.getElementType()); - return false; -} - /// Return true if and only if \p t is a (simple) arithmetic type, an arithmetic /// sequence type (possibly dynamic in length), or a static product type of /// arithmetic types. Note that this means a product type with a dynamic /// sequence of arithmetic types is \em disallowed. static bool isKernelResultType(Type t) { return isArithmeticType(t) || isArithmeticSequenceType(t) || - isStaticArithmeticProductType(t) || isMeasureType(t); + isStaticArithmeticProductType(t); } /// Return true if and only if \p t is a (simple) arithmetic type, an possibly @@ -147,7 +139,7 @@ static bool isKernelResultType(Type t) { static bool isKernelArgumentType(Type t) { return isArithmeticType(t) || isComposedArithmeticType(t) || quake::isQuantumReferenceType(t) || isKernelCallable(t) || - isFunctionCallable(t) || isMeasureType(t) || + isFunctionCallable(t) || // TODO: move from pointers to a builtin string type. cudaq::isCharPointerType(t); } @@ -457,8 +449,7 @@ bool QuakeBridgeVisitor::VisitLValueReferenceType( return pushType(cc::PointerType::get(builder.getContext())); auto eleTy = popType(); if (isa(eleTy)) + quake::VeqType, quake::RefType, quake::StruqType>(eleTy)) return pushType(eleTy); return pushType(cc::PointerType::get(eleTy)); } @@ -471,8 +462,7 @@ bool QuakeBridgeVisitor::VisitRValueReferenceType( // FIXME: LLVMStructType is promoted as a temporary workaround. if (isa(eleTy)) + quake::StruqType, LLVM::LLVMStructType>(eleTy)) return pushType(eleTy); return pushType(cc::PointerType::get(eleTy)); } diff --git a/lib/Optimizer/Builder/Intrinsics.cpp b/lib/Optimizer/Builder/Intrinsics.cpp index 968035e37c0..c611b15a1f5 100644 --- a/lib/Optimizer/Builder/Intrinsics.cpp +++ b/lib/Optimizer/Builder/Intrinsics.cpp @@ -551,12 +551,10 @@ static constexpr IntrinsicCode intrinsicTable[] = { func.func private @__quantum__rt__qubit_release(!qir_qubit) func.func private @__quantum__rt__array_create_1d(i32, i64) -> !qir_array - func.func private @__quantum__rt__result_array_create_1d(i64) -> !qir_array func.func private @__quantum__rt__array_concatenate(!qir_array, !qir_array) -> !qir_array func.func private @__quantum__rt__array_get_size_1d(!qir_array) -> i64 func.func private @__quantum__rt__array_slice(!qir_array, i32, i64, i64, i64) -> !qir_array func.func private @__quantum__rt__array_get_element_ptr_1d(!qir_array, i64) -> !cc.ptr - func.func private @__quantum__rt__result_array_get_element_ptr_1d(!qir_array, i64) -> !cc.ptr func.func private @__quantum__qis__h__ctl(!qir_array, !qir_qubit) func.func private @__quantum__qis__x__ctl(!qir_array, !qir_qubit) diff --git a/lib/Optimizer/Builder/Marshal.cpp b/lib/Optimizer/Builder/Marshal.cpp index 03633b8d496..7c272eb3f12 100644 --- a/lib/Optimizer/Builder/Marshal.cpp +++ b/lib/Optimizer/Builder/Marshal.cpp @@ -761,10 +761,10 @@ void cudaq::opt::marshal::populateCallbackBuffer( bool cudaq::opt::marshal::hasLegalType(FunctionType funTy) { for (auto ty : funTy.getInputs()) - if (quake::isQuakeType(ty)) + if (quake::isQuantumType(ty)) return false; for (auto ty : funTy.getResults()) - if (quake::isQuakeType(ty)) + if (quake::isQuantumType(ty)) return false; return true; } @@ -790,10 +790,6 @@ std::pair cudaq::opt::marshal::lookupHostEntryPointFunc( // No host entry point needed. return {false, func::FuncOp{}}; } - // Device-only kernels (those with quantum types or `measure_result` in their - // signature) have no host-side entry point, so skip them. - if (!funcOp->hasAttr(cudaq::entryPointAttrName)) - return {false, func::FuncOp{}}; if (auto *decl = module.lookupSymbol(mangledEntryPointName)) if (auto func = dyn_cast(decl)) { func.eraseBody(); diff --git a/lib/Optimizer/CodeGen/CMakeLists.txt b/lib/Optimizer/CodeGen/CMakeLists.txt index d64b2b32fd9..d6036b56e60 100644 --- a/lib/Optimizer/CodeGen/CMakeLists.txt +++ b/lib/Optimizer/CodeGen/CMakeLists.txt @@ -21,6 +21,7 @@ add_cudaq_library(OptCodeGen ConvertToQIR.cpp ConvertToQIRAPI.cpp DelayMeasurements.cpp + EliminateDeadHeapCopy.cpp OptUtils.cpp Passes.cpp Pipelines.cpp diff --git a/lib/Optimizer/CodeGen/ConvertToExecMgr.cpp b/lib/Optimizer/CodeGen/ConvertToExecMgr.cpp index 78585b13502..9cb7869cd66 100644 --- a/lib/Optimizer/CodeGen/ConvertToExecMgr.cpp +++ b/lib/Optimizer/CodeGen/ConvertToExecMgr.cpp @@ -49,8 +49,7 @@ struct QuakeTypeConverter : public TypeConverter { return cudaq::cc::StructType::get(ty.getContext(), mems); }); addConversion([](quake::MeasureType ty) { - auto i64Ty = IntegerType::get(ty.getContext(), 64); - return cudaq::cc::StructType::get(ty.getContext(), {i64Ty, i64Ty}); + return IntegerType::get(ty.getContext(), 64); }); } }; diff --git a/lib/Optimizer/CodeGen/ConvertToQIR.cpp b/lib/Optimizer/CodeGen/ConvertToQIR.cpp index a9770100005..686eb82d806 100644 --- a/lib/Optimizer/CodeGen/ConvertToQIR.cpp +++ b/lib/Optimizer/CodeGen/ConvertToQIR.cpp @@ -206,10 +206,8 @@ void cudaq::opt::initializeTypeConversions(LLVMTypeConverter &typeConverter) { return LLVM::LLVMStructType::getLiteral(type.getContext(), mems, /*packed=*/false); }); - typeConverter.addConversion( - [](quake::MeasureType type) { return getResultType(type.getContext()); }); - typeConverter.addConversion([](quake::MeasurementsType type) { - return getArrayType(type.getContext()); + typeConverter.addConversion([](quake::MeasureType type) { + return IntegerType::get(type.getContext(), 1); }); cudaq::opt::populateCCTypeConversions(&typeConverter); } diff --git a/lib/Optimizer/CodeGen/ConvertToQIRAPI.cpp b/lib/Optimizer/CodeGen/ConvertToQIRAPI.cpp index 8467cacc73c..0d08c1416e3 100644 --- a/lib/Optimizer/CodeGen/ConvertToQIRAPI.cpp +++ b/lib/Optimizer/CodeGen/ConvertToQIRAPI.cpp @@ -7,7 +7,6 @@ ******************************************************************************/ #include "CodeGenOps.h" -#include "cudaq/Optimizer/Builder/Factory.h" #include "cudaq/Optimizer/Builder/Intrinsics.h" #include "cudaq/Optimizer/Builder/Runtime.h" #include "cudaq/Optimizer/CodeGen/CodeGenDialect.h" @@ -149,31 +148,7 @@ struct QIRAPITypeConverter : public TypeConverter { [&](quake::CableType ty) { return getArrayType(ty.getContext()); }); addConversion( [&](quake::MeasureType ty) { return getResultType(ty.getContext()); }); - addConversion([&](quake::MeasurementsType ty) { - return getArrayType(ty.getContext()); - }); addConversion([&](quake::StruqType ty) { return convertStruqType(ty); }); - addConversion([&](cudaq::cc::StdvecType ty) { - return cudaq::cc::StdvecType::get(ty.getContext(), - convertType(ty.getElementType())); - }); - addConversion([&](cudaq::cc::ArrayType ty) { - auto newEleTy = convertType(ty.getElementType()); - auto size = ty.getSize(); - if (size) - return cudaq::cc::ArrayType::get(ty.getContext(), newEleTy, size); - return cudaq::cc::ArrayType::get(newEleTy); - }); - addConversion([&](cudaq::cc::StructType ty) -> Type { - if (ty.getOpaque()) - return ty; - SmallVector members; - for (auto memTy : ty.getMembers()) - members.push_back(convertType(memTy)); - return cudaq::cc::StructType::get(ty.getContext(), ty.getName(), members, - /*opaque=*/false, ty.getPacked(), - ty.getBitSize(), ty.getAlignment()); - }); } Type convertFunctionType(FunctionType ty) { @@ -741,44 +716,6 @@ struct DeallocLikeErase : public OpConversionPattern { using DeallocOpErase = DeallocLikeErase; using SinkOpErase = DeallocLikeErase; - -// Lower `quake.get_measure` to `result_array_get_element_ptr_1d`. -struct GetMeasureOpRewrite : public OpConversionPattern { - using OpConversionPattern::OpConversionPattern; - - LogicalResult - matchAndRewrite(quake::GetMeasureOp getMeas, OpAdaptor adaptor, - ConversionPatternRewriter &rewriter) const override { - auto loc = getMeas.getLoc(); - auto i64Ty = rewriter.getI64Type(); - Value index; - if (!adaptor.getIndex()) { - index = - rewriter.create(loc, getMeas.getRawIndex(), 64); - } else { - index = adaptor.getIndex(); - if (isa(index.getType())) { - index = rewriter.create(loc, i64Ty, index); - } else if (isa(index.getType())) { - auto width = cast(index.getType()).getWidth(); - if (width < 64) - index = rewriter.create( - loc, i64Ty, index, cudaq::cc::CastOpMode::Unsigned); - else if (width > 64) - index = rewriter.create(loc, i64Ty, index); - } - } - auto resultTy = - getTypeConverter()->convertType(getMeas.getMeasure().getType()); - auto ptrResultTy = cudaq::cc::PointerType::get(resultTy); - auto call = rewriter.create( - loc, TypeRange{ptrResultTy}, cudaq::opt::QIRResultArrayGetElementPtr1d, - ArrayRef{adaptor.getMeasurements(), index}); - rewriter.replaceOpWithNewOp(getMeas, call.getResult(0)); - return success(); - } -}; - struct DiscriminateOpRewrite : public OpConversionPattern { using OpConversionPattern::OpConversionPattern; @@ -788,86 +725,9 @@ struct DiscriminateOpRewrite ConversionPatternRewriter &rewriter) const override { auto loc = disc.getLoc(); Value m = adaptor.getMeasurement(); - - // If the result is a stdvec (indicating a `MeasurementsType` input), loop - // over the result array and read each result. NB: we check the result type - // rather than the operand type because the type converter has already - // remapped the operand from MeasurementsType to Array*. - if (isa(disc.getResult().getType())) { - auto i1Ty = rewriter.getI1Type(); - auto i1PtrTy = cudaq::cc::PointerType::get(i1Ty); - auto i64Ty = rewriter.getI64Type(); - auto resultTy = cudaq::cg::getResultType(rewriter.getContext()); - auto ptrResultTy = cudaq::cc::PointerType::get(resultTy); - - auto stdvecResTy = cast( - getTypeConverter()->convertType(disc.getResult().getType())); - auto elemTy = stdvecResTy.getElementType(); - unsigned elemWidth = cast(elemTy).getWidth(); - Type bufElemTy = - elemWidth > 8 ? elemTy : static_cast(rewriter.getI8Type()); - - Value arraySize = - rewriter - .create(loc, i64Ty, cudaq::opt::QIRArrayGetSize, - ValueRange{m}) - .getResult(0); - Value buff = - rewriter.create(loc, bufElemTy, arraySize); - - cudaq::opt::factory::createInvariantLoop( - rewriter, loc, arraySize, - [&](OpBuilder &builder, Location loc, Region &, Block &block) { - Value iv = block.getArgument(0); - Value elemPtr = builder - .create( - loc, ptrResultTy, - cudaq::opt::QIRResultArrayGetElementPtr1d, - ValueRange{m, iv}) - .getResult(0); - Value resultVal = builder.create(loc, elemPtr); - Value bitPtr = - builder.create(loc, i1PtrTy, resultVal); - Value bit = builder.create(loc, bitPtr); - Value addr = builder.create( - loc, cudaq::cc::PointerType::get(bufElemTy), buff, iv); - Value stored = (i1Ty != bufElemTy) - ? builder - .create( - loc, bufElemTy, bit, - cudaq::cc::CastOpMode::Unsigned) - .getResult() - : static_cast(bit); - builder.create(loc, stored, addr); - }); - - auto ptrArrElemTy = - cudaq::cc::PointerType::get(cudaq::cc::ArrayType::get(elemTy)); - auto buffCast = - rewriter.create(loc, ptrArrElemTy, buff); - rewriter.replaceOpWithNewOp(disc, stdvecResTy, - buffCast, arraySize); - return success(); - } - - auto i1Ty = rewriter.getI1Type(); - auto i1PtrTy = cudaq::cc::PointerType::get(i1Ty); - auto origResTy = disc.getResult().getType(); - Value loaded; - if (auto intTy = dyn_cast(origResTy); - intTy && intTy.getWidth() > 1) { - // For wider-than-i1 types: use byte-addressable i8* load, then cc.cast - // to truncate to the target width. - auto i8Ty = rewriter.getI8Type(); - auto i8PtrTy = cudaq::cc::PointerType::get(i8Ty); - auto bytePtr = rewriter.create(loc, i8PtrTy, m); - Value byteVal = rewriter.create(loc, bytePtr); - loaded = rewriter.create(loc, origResTy, byteVal); - } else { - auto ptrCast = rewriter.create(loc, i1PtrTy, m); - loaded = rewriter.create(loc, ptrCast); - } - rewriter.replaceOp(disc, loaded); + auto i1PtrTy = cudaq::cc::PointerType::get(rewriter.getI1Type()); + auto cast = rewriter.create(loc, i1PtrTy, m); + rewriter.replaceOpWithNewOp(disc, cast); return success(); } }; @@ -883,45 +743,26 @@ struct DiscriminateOpToCallRewrite LogicalResult matchAndRewrite(quake::DiscriminateOp disc, OpAdaptor adaptor, ConversionPatternRewriter &rewriter) const override { - // This pattern handles single-qubit MeasureType only. - if (isa(disc.getResult().getType())) - return failure(); - auto loc = disc.getLoc(); - auto i1Ty = rewriter.getI1Type(); - Value loaded; if constexpr (M::discriminateToClassical) { - StringRef readFn = M::qirVersion == QirVersion::version_1_0 - ? cudaq::opt::qir1_0::ReadResult - : cudaq::opt::qir0_1::ReadResultBody; - auto call = rewriter.create(loc, i1Ty, readFn, - adaptor.getOperands()); - loaded = call.getResult(0); + if constexpr (M::qirVersion == QirVersion::version_1_0) { + rewriter.replaceOpWithNewOp( + disc, rewriter.getI1Type(), cudaq::opt::qir1_0::ReadResult, + adaptor.getOperands()); + } else { + rewriter.replaceOpWithNewOp( + disc, rewriter.getI1Type(), cudaq::opt::qir0_1::ReadResultBody, + adaptor.getOperands()); + } } else { + auto loc = disc.getLoc(); // NB: the double cast here is to avoid folding the pointer casts. auto i64Ty = rewriter.getI64Type(); auto unu = rewriter.create(loc, i64Ty, adaptor.getOperands()); - auto origResTy = disc.getResult().getType(); - if (auto intTy = dyn_cast(origResTy); - intTy && intTy.getWidth() > 1) { - auto i8Ty = rewriter.getI8Type(); - auto i8PtrTy = cudaq::cc::PointerType::get(i8Ty); - auto du = rewriter.create(loc, i8PtrTy, unu); - Value byteVal = rewriter.create(loc, du); - loaded = rewriter.create(loc, origResTy, byteVal); - } else { - auto ptrI1Ty = cudaq::cc::PointerType::get(i1Ty); - auto du = rewriter.create(loc, ptrI1Ty, unu); - loaded = rewriter.create(loc, du); - } + auto ptrI1Ty = cudaq::cc::PointerType::get(rewriter.getI1Type()); + auto du = rewriter.create(loc, ptrI1Ty, unu); + rewriter.replaceOpWithNewOp(disc, du); } - auto origResTy = disc.getResult().getType(); - if constexpr (M::discriminateToClassical) { - if (auto intTy = dyn_cast(origResTy); - intTy && intTy.getWidth() > 1) - loaded = rewriter.create(loc, origResTy, loaded); - } - rewriter.replaceOp(disc, loaded); return success(); } @@ -950,7 +791,7 @@ struct ExtractRefOpRewrite : public OpConversionPattern { loc, extract.getConstantIndex(), 64); } else { index = adaptor.getIndex(); - if (isa(index.getType())) { + if (index.getType().isIntOrFloat()) { if (cast(index.getType()).getWidth() < 64) index = rewriter.create( loc, i64Ty, index, cudaq::cc::CastOpMode::Unsigned); @@ -1005,20 +846,6 @@ struct VeqSizeOpRewrite : public OpConversionPattern { } }; -struct MeasurementsSizeOpRewrite - : public OpConversionPattern { - using OpConversionPattern::OpConversionPattern; - - LogicalResult - matchAndRewrite(quake::MeasurementsSizeOp msize, OpAdaptor adaptor, - ConversionPatternRewriter &rewriter) const override { - rewriter.replaceOpWithNewOp(msize, TypeRange{msize.getType()}, - cudaq::opt::QIRArrayGetSize, - adaptor.getOperands()); - return success(); - } -}; - struct MakeStruqOpRewrite : public OpConversionPattern { using OpConversionPattern::OpConversionPattern; @@ -1473,12 +1300,9 @@ struct MeasurementOpPattern : public OpConversionPattern { SmallVector args{adaptor.getTargets().begin(), adaptor.getTargets().end()}; auto functionName = M::getQIRMeasure(); - bool isMultiQubit = isa(mz.getMeasOut().getType()); + // Are we using the measurement that returns a result? if constexpr (M::mzReturnsResultType) { - if (isMultiQubit) - return rewriteMultiQubitMeasurement(mz, adaptor, rewriter, loc, - regNameAttr); // Yes, the measurement results the result, so we can use a // straightforward codegen pattern. Use either the mz or the // mz_to_register call (with the name as an extra argument) and forward @@ -1542,113 +1366,6 @@ struct MeasurementOpPattern : public OpConversionPattern { } return success(); } - -private: - LogicalResult - rewriteMultiQubitMeasurement(quake::MzOp mz, OpAdaptor adaptor, - ConversionPatternRewriter &rewriter, - Location loc, StringAttr regNameAttr) const { - auto *ctx = rewriter.getContext(); - auto i64Ty = rewriter.getI64Type(); - auto resultTy = M::getResultType(ctx); - auto arrayTy = M::getArrayType(ctx); - auto qubitTy = M::getQubitType(ctx); - auto ptrQubitTy = cudaq::cc::PointerType::get(qubitTy); - auto ptrResultTy = cudaq::cc::PointerType::get(resultTy); - - // Compute total number of qubits across all targets, caching veq sizes. - SmallVector veqSizes; - Value totalQubits = rewriter.create(loc, 0, 64); - for (auto [origTarget, convTarget] : - llvm::zip(mz.getTargets(), adaptor.getTargets())) { - if (isa(origTarget.getType())) { - Value one = rewriter.create(loc, 1, 64); - totalQubits = rewriter.create(loc, totalQubits, one); - veqSizes.push_back(Value{}); - } else { - Value sz = - rewriter - .create(loc, i64Ty, cudaq::opt::QIRArrayGetSize, - ValueRange{convTarget}) - .getResult(0); - totalQubits = rewriter.create(loc, totalQubits, sz); - veqSizes.push_back(sz); - } - } - - // Allocate the result array. - Value resultArray = rewriter - .create( - loc, arrayTy, cudaq::opt::QIRResultArrayCreate, - ValueRange{totalQubits}) - .getResult(0); - - auto functionName = M::getQIRMeasure(); - Value cstringGlobal; - if (mz->getAttr(cudaq::opt::MzAssignedNameAttrName)) { - functionName = cudaq::opt::QIRMeasureToRegister; - cstringGlobal = - createGlobalCString(mz, loc, rewriter, regNameAttr.getValue()); - } - - auto getResultSlot = [&](OpBuilder &builder, Location loc, Value array, - Value index) -> Value { - return builder - .create(loc, ptrResultTy, - cudaq::opt::QIRResultArrayGetElementPtr1d, - ValueRange{array, index}) - .getResult(0); - }; - - // Iterate over targets, measure each qubit, store Result* in the array. - Value offset = rewriter.create(loc, 0, 64); - Value one = rewriter.create(loc, 1, 64); - unsigned sizeIdx = 0; - for (auto [origTarget, convTarget] : - llvm::zip(mz.getTargets(), adaptor.getTargets())) { - if (isa(origTarget.getType())) { - SmallVector mzArgs{convTarget}; - if (cstringGlobal) - mzArgs.push_back(cstringGlobal); - Value result = - rewriter.create(loc, resultTy, functionName, mzArgs) - .getResult(0); - Value slot = getResultSlot(rewriter, loc, resultArray, offset); - rewriter.create(loc, result, slot); - offset = rewriter.create(loc, offset, one); - ++sizeIdx; - } else { - Value veqSize = veqSizes[sizeIdx++]; - auto savedOffset = offset; - cudaq::opt::factory::createInvariantLoop( - rewriter, loc, veqSize, - [&](OpBuilder &builder, Location loc, Region &, Block &block) { - Value iv = block.getArgument(0); - Value qubitPtr = - builder - .create(loc, ptrQubitTy, - cudaq::opt::QIRArrayGetElementPtr1d, - ValueRange{convTarget, iv}) - .getResult(0); - Value qubit = builder.create(loc, qubitPtr); - SmallVector mzArgs{qubit}; - if (cstringGlobal) - mzArgs.push_back(cstringGlobal); - Value result = - builder - .create(loc, resultTy, functionName, mzArgs) - .getResult(0); - Value idx = builder.create(loc, savedOffset, iv); - Value slot = getResultSlot(builder, loc, resultArray, idx); - builder.create(loc, result, slot); - }); - offset = rewriter.create(loc, offset, veqSize); - } - } - - rewriter.replaceOp(mz, resultArray); - return success(); - } }; template @@ -2154,23 +1871,20 @@ struct InstantiateCallablePattern } }; -template -struct ZeroResultOpPattern : public OpConversionPattern { - using Base = OpConversionPattern; +struct StoreOpPattern : public OpConversionPattern { + using Base = OpConversionPattern; using Base::Base; + using Base::getTypeConverter; LogicalResult - matchAndRewrite(OP op, typename Base::OpAdaptor adaptor, + matchAndRewrite(cudaq::cc::StoreOp op, OpAdaptor adaptor, ConversionPatternRewriter &rewriter) const override { - rewriter.replaceOpWithNewOp(op, TypeRange{}, adaptor.getOperands(), - op->getAttrs()); + rewriter.replaceOpWithNewOp( + op, TypeRange{}, adaptor.getOperands(), op->getAttrs()); return success(); } }; -using StoreOpPattern = ZeroResultOpPattern; -using LogOutputOpPattern = ZeroResultOpPattern; - template struct CallOpInterfacePattern : public OpConversionPattern { using Base = OpConversionPattern; @@ -2245,24 +1959,22 @@ struct CallableClosurePattern static void commonClassicalHandlingPatterns(RewritePatternSet &patterns, TypeConverter &typeConverter, MLIRContext *ctx) { - patterns - .insert( - typeConverter, ctx); + patterns.insert( + typeConverter, ctx); } static void commonQuakeHandlingPatterns(RewritePatternSet &patterns, TypeConverter &typeConverter, MLIRContext *ctx) { - patterns.insert(typeConverter, ctx); } @@ -2530,8 +2242,7 @@ struct QuakeToQIRAPIPass cudaq::cc::NoInlineCallOp, cudaq::cc::VarargCallOp, cudaq::cc::CallCallableOp, cudaq::cc::CallIndirectCallableOp, cudaq::cc::CastOp, cudaq::cc::FuncToPtrOp, cudaq::cc::StoreOp, - cudaq::cc::LoadOp, cudaq::cc::ComputePtrOp, cudaq::cc::StdvecInitOp, - cudaq::cc::StdvecDataOp, cudaq::cc::LogOutputOp>([&](Operation *op) { + cudaq::cc::LoadOp>([&](Operation *op) { for (auto opnd : op->getOperands()) if (hasQuakeType(opnd.getType())) return false; @@ -2549,16 +2260,6 @@ struct QuakeToQIRAPIPass static bool hasQuakeType(Type ty) { if (auto pty = dyn_cast(ty)) return hasQuakeType(pty.getElementType()); - if (auto aty = dyn_cast(ty)) - return hasQuakeType(aty.getElementType()); - if (auto sty = dyn_cast(ty)) - return hasQuakeType(sty.getElementType()); - if (auto sty = dyn_cast(ty)) { - for (auto memTy : sty.getMembers()) - if (hasQuakeType(memTy)) - return true; - return false; - } if (auto cty = dyn_cast(ty)) return hasQuakeType(cty.getSignature()); if (auto cty = dyn_cast(ty)) diff --git a/lib/Optimizer/CodeGen/EliminateDeadHeapCopy.cpp b/lib/Optimizer/CodeGen/EliminateDeadHeapCopy.cpp new file mode 100644 index 00000000000..981a45b0f48 --- /dev/null +++ b/lib/Optimizer/CodeGen/EliminateDeadHeapCopy.cpp @@ -0,0 +1,109 @@ +/******************************************************************************* + * Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. * + * All rights reserved. * + * * + * This source code and the accompanying materials are made available under * + * the terms of the Apache License 2.0 which accompanies this distribution. * + ******************************************************************************/ + +#include "PassDetails.h" +#include "cudaq/Optimizer/CodeGen/Passes.h" +#include "cudaq/Optimizer/Dialect/CC/CCOps.h" + +#define DEBUG_TYPE "eliminate-dead-heap-copy" + +namespace cudaq::opt { +#define GEN_PASS_DEF_ELIMINATEDEADHEAPCOPY +#include "cudaq/Optimizer/CodeGen/Passes.h.inc" +} // namespace cudaq::opt + +using namespace mlir; + +namespace { + +/// When a kernel returns a vector, the frontend copies the stack data to the +/// heap via malloc+memcpy (from __nvqpp_vectorCopyCtor) so the data outlives +/// the callee's stack frame. After inlining and ReturnToOutputLog, the output +/// logging reads from the heap buffer through cc.cast ops, and the +/// cc.stdvec_init that wrapped the malloc becomes dead. This pass redirects +/// those cc.cast reads to the memcpy source (the original stack buffer) and +/// erases the now-dead malloc, memcpy, and cc.stdvec_init. +struct EliminateDeadHeapCopyPass + : public cudaq::opt::impl::EliminateDeadHeapCopyBase< + EliminateDeadHeapCopyPass> { + using EliminateDeadHeapCopyBase::EliminateDeadHeapCopyBase; + + void runOnOperation() override { + auto func = getOperation(); + SmallVector mallocCalls; + func.walk([&](func::CallOp callOp) { + if (callOp.getCallee() == "malloc") + mallocCalls.push_back(callOp); + }); + + for (auto mallocCall : mallocCalls) { + // malloc should return exactly one result (the allocated pointer). + if (mallocCall->getNumResults() != 1) + continue; + Value mallocResult = mallocCall.getResult(0); + + // Classify users of the malloc result. + func::CallOp memcpyCall; + SmallVector deadVecInits; + SmallVector castUsers; + bool hasUnsafeUser = false; + + for (auto *user : mallocResult.getUsers()) { + if (auto userCall = dyn_cast(user)) { + if (userCall.getCallee().starts_with("llvm.memcpy") && + userCall.getOperand(0) == mallocResult) { + if (memcpyCall) { + // Multiple memcpys to the same malloc dest — bail out. + hasUnsafeUser = true; + break; + } + memcpyCall = userCall; + continue; + } + } + // A dead stdvec_init (no remaining users) can be safely erased. + // One with live users is treated as unsafe. + if (auto vecInit = dyn_cast(user)) { + if (vecInit->use_empty()) { + deadVecInits.push_back(vecInit); + continue; + } + } + // A cc.cast is safe to redirect: since the memcpy copies from + // source to the malloc buffer, reading through either pointer + // yields the same data. + if (auto castOp = dyn_cast(user)) { + castUsers.push_back(castOp); + continue; + } + // Any other user prevents elimination. + hasUnsafeUser = true; + break; + } + + if (!memcpyCall || hasUnsafeUser) + continue; + + Value memcpySrc = memcpyCall.getOperand(1); + + // Redirect cc.cast users from the malloc result to the memcpy source. + for (auto castOp : castUsers) + castOp->replaceUsesOfWith(mallocResult, memcpySrc); + + // Erase dead stdvec_inits. + for (auto vecInit : deadVecInits) + vecInit->erase(); + + // Erase memcpy and malloc. + memcpyCall->erase(); + mallocCall->erase(); + } + } +}; + +} // namespace diff --git a/lib/Optimizer/CodeGen/Pipelines.cpp b/lib/Optimizer/CodeGen/Pipelines.cpp index 43b26f15f2b..377b52b7797 100644 --- a/lib/Optimizer/CodeGen/Pipelines.cpp +++ b/lib/Optimizer/CodeGen/Pipelines.cpp @@ -171,7 +171,6 @@ void cudaq::opt::createPipelineTransformsForPythonToOpenQASM( } void cudaq::opt::addPipelineTranslateToOpenQASM(PassManager &pm) { - createCommonTargetCodegenPipeline(pm, {}); pm.addNestedPass(createClassicalMemToReg()); pm.addNestedPass(createCanonicalizerPass()); pm.addNestedPass(createDeadStoreRemoval()); diff --git a/lib/Optimizer/CodeGen/QuakeToExecMgr.cpp b/lib/Optimizer/CodeGen/QuakeToExecMgr.cpp index 35f4380c4e0..8dbeac4659c 100644 --- a/lib/Optimizer/CodeGen/QuakeToExecMgr.cpp +++ b/lib/Optimizer/CodeGen/QuakeToExecMgr.cpp @@ -450,25 +450,6 @@ class VeqSizeOpRewrite : public OpConversionPattern { } }; -class MeasurementsSizeOpRewrite - : public OpConversionPattern { -public: - using OpConversionPattern::OpConversionPattern; - - LogicalResult - matchAndRewrite(quake::MeasurementsSizeOp msize, OpAdaptor adaptor, - ConversionPatternRewriter &rewriter) const override { - auto loc = msize->getLoc(); - auto i64Ty = rewriter.getI64Type(); - auto ptrI64Ty = cudaq::cc::PointerType::get(i64Ty); - auto sizeptr = rewriter.create( - loc, ptrI64Ty, adaptor.getMeasurements(), - ArrayRef{1}); - rewriter.replaceOpWithNewOp(msize, sizeptr); - return success(); - } -}; - } // namespace void cudaq::opt::populateQuakeToCCPatterns(TypeConverter &converter, @@ -476,9 +457,8 @@ void cudaq::opt::populateQuakeToCCPatterns(TypeConverter &converter, auto *context = patterns.getContext(); patterns.insert, - GenericRewrite, + MzOpRewrite, ResetRewrite, SubveqOpRewrite, + GenericRewrite, GenericRewrite, GenericRewrite, GenericRewrite, GenericRewrite, GenericRewrite, GenericRewrite, GenericRewrite, diff --git a/lib/Optimizer/CodeGen/QuakeToLLVM.cpp b/lib/Optimizer/CodeGen/QuakeToLLVM.cpp index f6dbd0206c7..32d845d2b6a 100644 --- a/lib/Optimizer/CodeGen/QuakeToLLVM.cpp +++ b/lib/Optimizer/CodeGen/QuakeToLLVM.cpp @@ -1144,7 +1144,11 @@ class MeasureRewrite : public ConvertOpToLLVMPattern { loc, cudaq::opt::getResultType(context), symbolRef, ValueRange{args}); if (regName) callOp->setAttr("registerName", regName); - rewriter.replaceOp(measure, callOp.getResult()); + auto i1Ty = rewriter.getI1Type(); + auto i1PtrTy = LLVM::LLVMPointerType::get(i1Ty); + auto cast = + rewriter.create(loc, i1PtrTy, callOp.getResult()); + rewriter.replaceOpWithNewOp(measure, i1Ty, cast); return success(); } @@ -1174,28 +1178,6 @@ class GetVeqSizeOpRewrite : public OpConversionPattern { } }; -class GetMeasurementsSizeOpRewrite - : public OpConversionPattern { -public: - using OpConversionPattern::OpConversionPattern; - - LogicalResult - matchAndRewrite(quake::MeasurementsSizeOp msize, OpAdaptor adaptor, - ConversionPatternRewriter &rewriter) const override { - auto parentModule = msize->getParentOfType(); - auto context = parentModule->getContext(); - auto qFunctionName = cudaq::opt::QIRArrayGetSize; - - auto symbolRef = cudaq::opt::factory::createLLVMFunctionSymbol( - qFunctionName, rewriter.getI64Type(), - {cudaq::opt::getArrayType(context)}, parentModule); - - rewriter.replaceOpWithNewOp(msize, rewriter.getI64Type(), - symbolRef, adaptor.getOperands()); - return success(); - } -}; - //===----------------------------------------------------------------------===// // Other conversion patterns. //===----------------------------------------------------------------------===// @@ -1429,8 +1411,9 @@ void cudaq::opt::populateQuakeToLLVMPatterns(LLVMTypeConverter &typeConverter, unsigned &measureCounter) { auto *context = patterns.getContext(); cudaq::opt::populateQuakeToCCPrepPatterns(patterns); - patterns.insert(context); + patterns + .insert( + context); patterns .insert std::optional { - // Size of `measure_result` {value, unique_id} = 16 bytes - return {16}; - }) .Default({}); if (rawSize) diff --git a/lib/Optimizer/Dialect/Quake/CanonicalPatterns.inc b/lib/Optimizer/Dialect/Quake/CanonicalPatterns.inc index e6d4bddb291..a2d45bc0d21 100644 --- a/lib/Optimizer/Dialect/Quake/CanonicalPatterns.inc +++ b/lib/Optimizer/Dialect/Quake/CanonicalPatterns.inc @@ -100,28 +100,6 @@ struct ForwardConstantVeqSizePattern } }; -// %4 = quake.measurements_size %3 : (!quake.measurements<10>) -> i64 -// ───────────────────────────────────────────────────────────────── -// %4 = constant 10 : i64 -struct ForwardConstantMeasurementsSizePattern - : public OpRewritePattern { - using OpRewritePattern::OpRewritePattern; - - LogicalResult matchAndRewrite(quake::MeasurementsSizeOp msSize, - PatternRewriter &rewriter) const override { - auto msTy = - dyn_cast(msSize.getMeasurements().getType()); - if (!msTy) - return failure(); - if (!msTy.hasSpecifiedSize()) - return failure(); - auto resTy = msSize.getType(); - rewriter.replaceOpWithNewOp(msSize, msTy.getSize(), - resTy); - return success(); - } -}; - // %2 = constant 10 : i32 // %3 = quake.alloca !quake.veq[%2 : i32] // ───────────────────────────────────────── @@ -737,98 +715,6 @@ struct MergeRotationPattern : public OpRewritePattern { } }; -// %0 = quake.alloca !quake.veq<2> -// %1 = quake.mz %0 : (!quake.veq<2>) -> !quake.measurements -// ──────────────────────────────────────────────────────────── -// %0 = quake.alloca !quake.veq<2> -// %1 = quake.mz %0 : (!quake.veq<2>) -> !quake.measurements<2> -template -struct FuseSizeToMeasurementPattern : public OpRewritePattern { - using OpRewritePattern::OpRewritePattern; - - LogicalResult matchAndRewrite(MeasOp measOp, - PatternRewriter &rewriter) const override { - auto measTy = - dyn_cast(measOp.getMeasOut().getType()); - if (!measTy || measTy.hasSpecifiedSize()) - return failure(); - - std::size_t totalSize = 0; - for (auto target : measOp.getTargets()) { - if (quake::isConstantQuantumRefType(target.getType())) { - totalSize += quake::getAllocationSize(target.getType()); - continue; - } - return failure(); - } - if (totalSize == 0) - return failure(); - - auto newMeasTy = - quake::MeasurementsType::get(rewriter.getContext(), totalSize); - - // If any user expects unsized measurements (return-like ops where the - // function returns unsized, or call ops where the callee parameter is - // unsized), insert a quake.relax_size to bridge the sized-to-unsized gap. - auto needsCastForUser = [&](OpOperand &use) -> bool { - auto *user = use.getOwner(); - if (isa(user)) { - if (auto funcOp = user->getParentOfType()) - if (funcOp.getFunctionType().getNumResults() == 1) - if (auto fnResMeasTy = dyn_cast( - funcOp.getFunctionType().getResult(0))) - return !fnResMeasTy.hasSpecifiedSize(); - return false; - } - auto checkCalleeArgType = [&](mlir::FunctionType calleeType, - unsigned argIdx) -> bool { - if (argIdx < calleeType.getNumInputs()) - if (auto paramMeasTy = dyn_cast( - calleeType.getInput(argIdx))) - return !paramMeasTy.hasSpecifiedSize(); - return false; - }; - if (auto callOp = dyn_cast(user)) - return checkCalleeArgType(callOp.getCalleeType(), - use.getOperandNumber()); - if (auto callOp = dyn_cast(user)) { - unsigned opIdx = use.getOperandNumber(); - if (opIdx == 0) - return false; // operand 0 is the callee value itself - auto calleeFnTy = cast( - callOp.getCallee().getType()); - return checkCalleeArgType(calleeFnTy, opIdx - 1); - } - return false; - }; - - SmallVector resultTypes; - resultTypes.push_back(newMeasTy); - for (unsigned i = 1; i < measOp->getNumResults(); ++i) - resultTypes.push_back(measOp->getResult(i).getType()); - - auto oldAttrs = measOp->getAttrs(); - auto newOp = rewriter.replaceOpWithNewOp(measOp, - TypeRange{resultTypes}, - measOp.getTargets(), - measOp.getRegisterNameAttr()); - for (auto &attr : oldAttrs) - if (!newOp->getAttr(attr.getName())) - newOp->setAttr(attr.getName(), attr.getValue()); - - for (auto &use : - llvm::make_early_inc_range(newOp.getMeasOut().getUses())) { - if (needsCastForUser(use)) { - rewriter.setInsertionPoint(use.getOwner()); - auto relax = rewriter.create( - use.getOwner()->getLoc(), measTy, newOp.getMeasOut()); - use.set(relax); - } - } - return success(); - } -}; - // Forward the argument to a relax_size to the users for all users that are // quake operations. All quake ops that take a sized veq argument are // polymorphic on all veq types. If the op is not a quake op, then maintain diff --git a/lib/Optimizer/Dialect/Quake/QuakeOps.cpp b/lib/Optimizer/Dialect/Quake/QuakeOps.cpp index e8c8228f6fa..ac459be3e4e 100644 --- a/lib/Optimizer/Dialect/Quake/QuakeOps.cpp +++ b/lib/Optimizer/Dialect/Quake/QuakeOps.cpp @@ -625,30 +625,6 @@ void quake::GetMemberOp::getCanonicalizationPatterns( patterns.add(context); } -//===----------------------------------------------------------------------===// -// GetMeasureOp -//===----------------------------------------------------------------------===// - -LogicalResult quake::GetMeasureOp::verify() { - if (getIndex()) { - if (getRawIndex() != kDynamicIndex) - return emitOpError( - "must not have both a constant index and an index argument."); - } else { - if (getRawIndex() == kDynamicIndex) { - return emitOpError("invalid constant index value"); - } else { - auto msSize = getMeasurements().getType().getSize(); - if (getMeasurements().getType().hasSpecifiedSize() && - getRawIndex() >= msSize) - return emitOpError("invalid index [" + std::to_string(getRawIndex()) + - "] because >= size [" + std::to_string(msSize) + - "]"); - } - } - return success(); -} - //===----------------------------------------------------------------------===// // InitializeStateOp //===----------------------------------------------------------------------===// @@ -702,19 +678,8 @@ LogicalResult quake::MakeStruqOp::verify() { //===----------------------------------------------------------------------===// LogicalResult quake::RelaxSizeOp::verify() { - auto inTy = getInputVec().getType(); - auto resTy = getType(); - if (auto veqTy = dyn_cast(resTy)) { - if (veqTy.hasSpecifiedSize()) - return emitOpError("result veq type must not specify a size"); - if (!isa(inTy)) - return emitOpError("input and result must both be veq types"); - } else if (auto measTy = dyn_cast(resTy)) { - if (measTy.hasSpecifiedSize()) - return emitOpError("result measurements type must not specify a size"); - if (!isa(inTy)) - return emitOpError("input and result must both be measurements types"); - } + if (cast(getType()).hasSpecifiedSize()) + emitOpError("return veq type must not specify a size"); return success(); } @@ -767,15 +732,6 @@ void quake::VeqSizeOp::getCanonicalizationPatterns(RewritePatternSet &patterns, context); } -//===----------------------------------------------------------------------===// -// MeasurementsSizeOp -//===----------------------------------------------------------------------===// - -void quake::MeasurementsSizeOp::getCanonicalizationPatterns( - RewritePatternSet &patterns, MLIRContext *context) { - patterns.add(context); -} - //===----------------------------------------------------------------------===// // WrapOp //===----------------------------------------------------------------------===// @@ -868,12 +824,12 @@ LogicalResult verifyMeasurements(MEAS op, TypeRange targetsType, const Type bitsType) { if (failed(verifyWireResultsAreLinear(op))) return failure(); - bool mustBeCollection = + bool mustBeStdvec = targetsType.size() > 1 || (targetsType.size() == 1 && isa(targetsType[0])); - if (mustBeCollection) { - if (!isa(op.getMeasOut().getType())) - return op.emitOpError("must return `!quake.measurements`, when " + if (mustBeStdvec) { + if (!isa(op.getMeasOut().getType())) + return op.emitOpError("must return `!cc.stdvec`, when " "measuring a qreg, a series of qubits, or both"); } else { if (!isa(op.getMeasOut().getType())) @@ -901,34 +857,19 @@ LogicalResult quake::MzOp::verify() { getMeasOut().getType()); } -void quake::MxOp::getCanonicalizationPatterns(RewritePatternSet &patterns, - MLIRContext *context) { - patterns.add>(context); -} - -void quake::MyOp::getCanonicalizationPatterns(RewritePatternSet &patterns, - MLIRContext *context) { - patterns.add>(context); -} - -void quake::MzOp::getCanonicalizationPatterns(RewritePatternSet &patterns, - MLIRContext *context) { - patterns.add>(context); -} - //===----------------------------------------------------------------------===// // Discriminate //===----------------------------------------------------------------------===// LogicalResult quake::DiscriminateOp::verify() { - if (isa(getMeasurement().getType())) { + if (isa(getMeasurement().getType())) { auto stdvecTy = dyn_cast(getResult().getType()); if (!stdvecTy || !isa(stdvecTy.getElementType())) return emitOpError("must return a !cc.stdvec type, when " - "discriminating a measurements collection"); + "discriminating a qreg, a series of qubits, or both"); } else { - if (!isa(getMeasurement().getType()) || - !isa(getResult().getType())) + auto measTy = isa(getMeasurement().getType()); + if (!measTy || !isa(getResult().getType())) return emitOpError( "must return integral type when discriminating exactly one qubit"); } diff --git a/lib/Optimizer/Dialect/Quake/QuakeTypes.cpp b/lib/Optimizer/Dialect/Quake/QuakeTypes.cpp index d61381860ee..b127ea60e88 100644 --- a/lib/Optimizer/Dialect/Quake/QuakeTypes.cpp +++ b/lib/Optimizer/Dialect/Quake/QuakeTypes.cpp @@ -49,34 +49,6 @@ Type quake::VeqType::parse(AsmParser &parser) { return get(parser.getContext(), size); } -//===----------------------------------------------------------------------===// -// Measurements' custom parser and pretty printing. -// -// measurements `<` (`?` | int) `>` -//===----------------------------------------------------------------------===// - -void quake::MeasurementsType::print(AsmPrinter &os) const { - os << '<'; - if (hasSpecifiedSize()) - os << getSize(); - else - os << '?'; - os << '>'; -} - -Type quake::MeasurementsType::parse(AsmParser &parser) { - if (parser.parseLess()) - return {}; - std::size_t size = kDynamicSize; - if (succeeded(parser.parseOptionalQuestion())) - size = kDynamicSize; - else if (parser.parseInteger(size)) - return {}; - if (parser.parseGreater()) - return {}; - return get(parser.getContext(), size); -} - //===----------------------------------------------------------------------===// Type quake::StruqType::parse(AsmParser &parser) { @@ -185,6 +157,6 @@ std::size_t quake::getAllocationSize(Type ty) { //===----------------------------------------------------------------------===// void quake::QuakeDialect::registerTypes() { - addTypes(); + addTypes(); } diff --git a/lib/Optimizer/Transforms/AddMeasurements.cpp b/lib/Optimizer/Transforms/AddMeasurements.cpp index b3776062286..1b71702ae1a 100644 --- a/lib/Optimizer/Transforms/AddMeasurements.cpp +++ b/lib/Optimizer/Transforms/AddMeasurements.cpp @@ -92,14 +92,10 @@ addMeasurements(func::FuncOp funcOp, SmallVector &allocations, builder.setInsertionPointToEnd(newBlock); auto measTy = quake::MeasureType::get(builder.getContext()); for (auto &[index, alloca] : llvm::enumerate(allocations)) { - if (auto veqTy = dyn_cast(alloca->getResult(0).getType())) { - Type measurementsTy = [&]() { - auto *ctx = builder.getContext(); - if (veqTy.hasSpecifiedSize()) - return quake::MeasurementsType::get(ctx, veqTy.getSize()); - return quake::MeasurementsType::getUnsized(ctx); - }(); - builder.create(loc, measurementsTy, alloca->getResult(0)); + if (isa(alloca->getResult(0).getType())) { + auto stdvecTy = cudaq::cc::StdvecType::get(measTy); + builder.create(loc, stdvecTy, + ValueRange{alloca->getResult(0)}); } else { builder.create(loc, measTy, alloca->getResult(0)); } diff --git a/lib/Optimizer/Transforms/ApplyOpSpecialization.cpp b/lib/Optimizer/Transforms/ApplyOpSpecialization.cpp index ad45ca10c05..c428bfdec2a 100644 --- a/lib/Optimizer/Transforms/ApplyOpSpecialization.cpp +++ b/lib/Optimizer/Transforms/ApplyOpSpecialization.cpp @@ -548,7 +548,13 @@ class ApplySpecializationPass << "cannot make adjoint of kernel: unstructured control flow\n"); return failure(); } - if (cudaq::opt::hasCallOp(func)) { + // quake::ApplyOp implements CallOpInterface but can be handled below by + // toggling isAdj. Reject any other call-like op that we cannot invert. + if (cudaq::opt::internal::hasCharacteristic( + [](Operation &op) { + return isa(op) && !isa(op); + }, + *func.getOperation())) { LLVM_DEBUG(llvm::dbgs() << "cannot make adjoint of kernel with calls\n"); return failure(); } @@ -583,7 +589,7 @@ class ApplySpecializationPass static SmallVector getOpsToInvert(Block &block) { SmallVector ops; for (auto &op : block) - if (cudaq::opt::hasQuantum(op)) + if (cudaq::opt::hasQuantum(op) || isa(op)) ops.push_back(&op); return ops; } @@ -775,6 +781,19 @@ class ApplySpecializationPass continue; } + if (auto applyOp = dyn_cast(op)) { + LLVM_DEBUG(llvm::dbgs() << "moving apply op: " << *op << ".\n"); + // Adjoint of an ApplyOp: toggles the isAdj flag. + mlir::UnitAttr newIsAdj = + applyOp.getIsAdj() ? mlir::UnitAttr{} + : mlir::UnitAttr::get(builder.getContext()); + builder.create( + applyOp.getLoc(), applyOp.getResultTypes(), applyOp.getCalleeAttr(), + newIsAdj, applyOp.getControls(), applyOp.getActuals()); + applyOp->erase(); + continue; + } + bool opWasNegated = false; IRMapping mapper; LLVM_DEBUG(llvm::dbgs() << "moving quantum op: " << *op << ".\n"); diff --git a/lib/Optimizer/Transforms/CombineMeasurements.cpp b/lib/Optimizer/Transforms/CombineMeasurements.cpp index f3d422e65d7..5065c8aa6b6 100644 --- a/lib/Optimizer/Transforms/CombineMeasurements.cpp +++ b/lib/Optimizer/Transforms/CombineMeasurements.cpp @@ -109,7 +109,7 @@ class ExtendQubitMeasurePattern : public OpRewritePattern { // with: // ``` // %1 = ... : !quake.veq<4> - // %measOut = quake.mz %1 : (!quake.veq<4>) -> !quake.measurements<4> + // %measOut = quake.mz %1 : (!quake.veq<4>) -> !cc.stdvec // ``` // And collect output names information: `"[[[0,[1,"q0"]],[1,[2,"q1"]]]]"` LogicalResult matchAndRewrite(quake::MzOp measure, @@ -132,12 +132,7 @@ class ExtendQubitMeasurePattern : public OpRewritePattern { analysis.resultQubitVals[offset] = std::make_pair(idx, std::to_string(idx)); - Type resultType; - if (quake::isConstantQuantumRefType(veq.getType())) - resultType = quake::MeasurementsType::get( - measure->getContext(), quake::getAllocationSize(veq.getType())); - else - resultType = quake::MeasurementsType::getUnsized(measure->getContext()); + auto resultType = cudaq::cc::StdvecType::get(measure.getType(0)); if (measure == analysis.lastMeasurement) { rewriter.replaceOpWithNewOp(measure, TypeRange{resultType}, ValueRange{veq}, @@ -170,12 +165,12 @@ class ExtendVeqMeasurePattern : public OpRewritePattern { // %1 = ... : !quake.veq<4> // %2 = quake.subveq %1, %c1, %c2 : (!quake.veq<4>, i32, i32) -> // !quake.veq<2> - // %measOut = quake.mz %2 : (!quake.veq<2>) -> !quake.measurements<2> + // %measOut = quake.mz %2 : (!quake.veq<2>) -> !cc.stdvec // ``` // with: // ``` // %1 = ... : !quake.veq<4> - // %measOut = quake.mz %1 : (!quake.veq<4>) -> !quake.measurements<4> + // %measOut = quake.mz %1 : (!quake.veq<4>) -> !cc.stdvec // ``` // And collect output names information: `"[[[0,[1,"q0"]],[1,[2,"q1"]]]]"` LogicalResult matchAndRewrite(quake::MzOp measure, @@ -208,21 +203,12 @@ class ExtendVeqMeasurePattern : public OpRewritePattern { analysis.resultQubitVals[offset] = std::make_pair(i, std::to_string(i)); } - if (measure == analysis.lastMeasurement) { - auto veq = subveq.getVeq(); - Type resultType; - if (quake::isConstantQuantumRefType(veq.getType())) - resultType = quake::MeasurementsType::get( - measure->getContext(), quake::getAllocationSize(veq.getType())); - else - resultType = - quake::MeasurementsType::getUnsized(measure->getContext()); - rewriter.replaceOpWithNewOp(measure, TypeRange{resultType}, - ValueRange{veq}, - measure.getRegisterNameAttr()); - } else if (measure.use_empty()) { + if (measure == analysis.lastMeasurement) + rewriter.replaceOpWithNewOp( + measure, measure.getResultTypes(), ValueRange{subveq.getVeq()}, + measure.getRegisterNameAttr()); + else if (measure.use_empty()) rewriter.eraseOp(measure); - } return success(); } diff --git a/lib/Optimizer/Transforms/DecompositionPatternSelection.cpp b/lib/Optimizer/Transforms/DecompositionPatternSelection.cpp index fda09bc8db2..9fac90636bf 100644 --- a/lib/Optimizer/Transforms/DecompositionPatternSelection.cpp +++ b/lib/Optimizer/Transforms/DecompositionPatternSelection.cpp @@ -63,6 +63,21 @@ struct OperatorInfo { return name == other.name && numControls == other.numControls && isAdj == other.isAdj; } + + bool isUnbounded() const { + return numControls == std::numeric_limits::max(); + } + + /// Check if this gate matches another, treating unbounded (n) control + /// count as a wildcard that matches any concrete count. + bool matches(const OperatorInfo &other) const { + if (name != other.name || isAdj != other.isAdj) + return false; + constexpr auto unbounded = std::numeric_limits::max(); + if (numControls == unbounded || other.numControls == unbounded) + return true; + return numControls == other.numControls; + } }; struct BasisTarget : public ConversionTarget { @@ -175,14 +190,15 @@ class DecompositionGraph { } /// Return all patterns that have the given gate as one of their targets. - /// - /// @param gate The gate to find incoming patterns for - /// @return A vector of pattern names (StringRef) whose targets include the - /// given gate - llvm::ArrayRef incomingPatterns(const OperatorInfo &gate) const { - static const llvm::SmallVector empty; - auto it = targetToPatterns.find(gate); - return it == targetToPatterns.end() ? empty : it->second; + /// Uses OperatorInfo::matches() to handle unbounded (n) control counts. + llvm::SmallVector + incomingPatterns(const OperatorInfo &gate) const { + llvm::SmallVector result; + for (const auto &[key, patterns] : targetToPatterns) { + if (key.matches(gate)) + result.append(patterns.begin(), patterns.end()); + } + return result; } /// Select subset of patterns relevant to decomposing to the given basis @@ -207,7 +223,12 @@ class DecompositionGraph { for (const auto &patternName : patternSelectionCache[hashVal]) { const auto &pattern = getPatternType(patternName); - patterns.add(pattern->create(patterns.getContext())); + // Patterns with unbounded (n) control counts get lower benefit so + // that specific patterns (e.g., CR1ToCX for r1(1)) are preferred + // when both match the same op. + OperatorInfo sourceInfo(pattern->getSourceOp()); + PatternBenefit benefit = sourceInfo.isUnbounded() ? 1 : 2; + patterns.add(pattern->create(patterns.getContext(), benefit)); } } @@ -260,18 +281,29 @@ class DecompositionGraph { gatesToVisit.push({gate, 0, std::nullopt}); } + /// Find the distance for a gate, handling unbounded (n) control counts. + /// Exact hash lookup first for the common case, then a scan when the + /// query or any visited entry uses unbounded controls. + auto findGateDist = [&](const OperatorInfo &gate) -> std::size_t { + auto it = visitedGates.find(gate); + if (it != visitedGates.end()) + return it->second; + // Scan for wildcard matches (either side could be unbounded). + std::size_t best = std::numeric_limits::max(); + for (const auto &[visited, dist] : visitedGates) { + if (visited.matches(gate)) + best = std::min(best, dist); + } + return best; + }; + /// Compute the maximum distance from a pattern's targets to the basis /// gates. auto getPatternDist = [&](const auto &pattern) { auto targetGates = pattern->getTargetOps(); std::vector targetDistances; - for (const auto &targetGate : targetGates) { - if (visitedGates.count(targetGate)) { - targetDistances.push_back(visitedGates.at(targetGate)); - } else { - targetDistances.push_back(std::numeric_limits::max()); - } - } + for (const auto &targetGate : targetGates) + targetDistances.push_back(findGateDist(targetGate)); return *std::max_element(targetDistances.begin(), targetDistances.end()); }; diff --git a/lib/Optimizer/Transforms/DecompositionPatterns.cpp b/lib/Optimizer/Transforms/DecompositionPatterns.cpp index 1add53a2f85..755ab75af38 100644 --- a/lib/Optimizer/Transforms/DecompositionPatterns.cpp +++ b/lib/Optimizer/Transforms/DecompositionPatterns.cpp @@ -334,10 +334,9 @@ LogicalResult checkAndExtractControls(quake::OperatorInterface op, }; \ CUDAQ_REGISTER_TYPE(cudaq::DecompositionPatternType, PATTERN##Type, PATTERN) -// TODO: The decomposition patterns "SToR1", "TToR1", "R1ToU3", "U3ToRotations" -// can handle arbitrary number of controls, but currently metadata cannot -// capture this. The pattern types therefore only advertise them for a fixed -// number of controls (1 for "SToR1" and "TToR1", 0 for the rest). +// NOTE: The patterns SToR1, TToR1, R1ToU3, and U3ToRotations handle arbitrary +// control counts and are registered with (n) metadata. R1ToRz explicitly +// rejects controlled ops and uses bare metadata. //===----------------------------------------------------------------------===// // HOp decompositions @@ -608,7 +607,7 @@ struct R1ToU3 : public cudaq::DecompositionPattern { return success(); } }; -REGISTER_DECOMPOSITION_PATTERN(R1ToU3, "r1", "u3"); +REGISTER_DECOMPOSITION_PATTERN(R1ToU3, "r1(n)", "u3(n)"); // quake.r1 (θ) target // ───────────────────────────────── @@ -800,7 +799,7 @@ struct SToR1 : public cudaq::DecompositionPattern { return success(); } }; -REGISTER_DECOMPOSITION_PATTERN(SToR1, "s(1)", "r1(1)"); +REGISTER_DECOMPOSITION_PATTERN(SToR1, "s(n)", "r1(n)"); //===----------------------------------------------------------------------===// // TOp decompositions @@ -881,7 +880,7 @@ struct TToR1 : public cudaq::DecompositionPattern { return success(); } }; -REGISTER_DECOMPOSITION_PATTERN(TToR1, "t(1)", "r1(1)"); +REGISTER_DECOMPOSITION_PATTERN(TToR1, "t(n)", "r1(n)"); //===----------------------------------------------------------------------===// // XOp decompositions @@ -1818,7 +1817,7 @@ struct U3ToRotations return success(); } }; -REGISTER_DECOMPOSITION_PATTERN(U3ToRotations, "u3", "rz", "rx"); +REGISTER_DECOMPOSITION_PATTERN(U3ToRotations, "u3(n)", "rz(n)", "rx(n)"); } // namespace diff --git a/lib/Optimizer/Transforms/ExpandMeasurements.cpp b/lib/Optimizer/Transforms/ExpandMeasurements.cpp index e0f4fc299f7..1527608dca0 100644 --- a/lib/Optimizer/Transforms/ExpandMeasurements.cpp +++ b/lib/Optimizer/Transforms/ExpandMeasurements.cpp @@ -11,113 +11,68 @@ #include "cudaq/Optimizer/Dialect/CC/CCOps.h" #include "cudaq/Optimizer/Dialect/Quake/QuakeOps.h" #include "cudaq/Optimizer/Transforms/Passes.h" +#include "cudaq/Todo.h" #include "mlir/IR/PatternMatch.h" #include "mlir/Transforms/DialectConversion.h" -#include "mlir/Transforms/GreedyPatternRewriteDriver.h" #include "mlir/Transforms/Passes.h" using namespace mlir; -namespace { // Only an individual qubit measurement returns a bool. template bool usesIndividualQubit(A x) { return x.getType() == quake::MeasureType::get(x.getContext()); } -// Pattern for expanding a multi-qubit measurement on unsized veq targets -// into a dynamic loop of individual measurements. +// Generalized pattern for expanding a multiple qubit measurement (whether it is +// mx, my, or mz) to a series of individual measurements. template -class ExpandUnsizedMeasurePattern : public OpRewritePattern { +class ExpandRewritePattern : public OpRewritePattern { public: using OpRewritePattern::OpRewritePattern; LogicalResult matchAndRewrite(A measureOp, PatternRewriter &rewriter) const override { - if (usesIndividualQubit(measureOp.getMeasOut())) - return failure(); - - // Only handle the unsized case here. - bool hasUnsizedTarget = false; - for (auto v : measureOp.getTargets()) - if (auto veqTy = dyn_cast(v.getType())) - if (!veqTy.hasSpecifiedSize()) - hasUnsizedTarget = true; - if (!hasUnsizedTarget) - return failure(); - - // Only expand if every user of the measurement result is a DiscriminateOp. - for (auto *user : measureOp.getMeasOut().getUsers()) - if (!isa(user)) - return failure(); - - // Even without discriminate users we must expand, because downstream QIR - // lowering cannot handle mz on veq. When discriminate users exist we - // additionally allocate a buffer to collect per-qubit results and build the - // stdvec that replaces each discriminate. - bool hasDiscriminateUsers = !measureOp.getMeasOut().use_empty(); auto loc = measureOp.getLoc(); - auto i64Ty = rewriter.getI64Type(); - auto measTy = quake::MeasureType::get(rewriter.getContext()); - // 1. Determine the total number of qubits we need to measure. This // determines the size of the buffer of bools to create to store the results // in. - Value buff, totalToRead, buffOff, one; - Type elemTy, bufElemTy; - if (hasDiscriminateUsers) { - auto firstDisc = cast( - *measureOp.getMeasOut().getUsers().begin()); - auto stdvecTy = - cast(firstDisc.getResult().getType()); - elemTy = stdvecTy.getElementType(); - unsigned elemWidth = cast(elemTy).getWidth(); - bufElemTy = - elemWidth > 8 ? elemTy : static_cast(rewriter.getI8Type()); - - unsigned numQubits = 0u; - for (auto v : measureOp.getTargets()) - if (v.getType().template isa()) - ++numQubits; - totalToRead = - rewriter.template create(loc, numQubits, 64); - for (auto v : measureOp.getTargets()) - if (v.getType().template isa()) { - Value vecSz = - rewriter.template create(loc, i64Ty, v); - totalToRead = - rewriter.template create(loc, totalToRead, vecSz); - } + unsigned numQubits = 0u; + for (auto v : measureOp.getTargets()) + if (v.getType().template isa()) + ++numQubits; + Value totalToRead = + rewriter.template create(loc, numQubits, 64); + auto i64Ty = rewriter.getI64Type(); + for (auto v : measureOp.getTargets()) + if (v.getType().template isa()) { + Value vecSz = rewriter.template create(loc, i64Ty, v); + totalToRead = + rewriter.template create(loc, totalToRead, vecSz); + } - // 2. Create the buffer. - buff = rewriter.template create(loc, bufElemTy, - totalToRead); - buffOff = rewriter.template create(loc, 0, 64); - one = rewriter.template create(loc, 1, 64); - } + // 2. Create the buffer. + auto i1Ty = rewriter.getI1Type(); + auto i8Ty = rewriter.getI8Type(); + Value buff = + rewriter.template create(loc, i8Ty, totalToRead); // 3. Measure each individual qubit and insert the result, in order, into // the buffer. For registers/vectors, loop over the entire set of qubits. + Value buffOff = rewriter.template create(loc, 0, 64); + Value one = rewriter.template create(loc, 1, 64); + auto measTy = quake::MeasureType::get(rewriter.getContext()); for (auto v : measureOp.getTargets()) { if (isa(v.getType())) { - auto meas = rewriter.template create(loc, measTy, v); - if (auto registerName = measureOp.getRegisterNameAttr()) - meas.setRegisterName(registerName); - if (hasDiscriminateUsers) { - auto bit = rewriter.template create( - loc, elemTy, meas.getMeasOut()); - Value addr = rewriter.template create( - loc, cudaq::cc::PointerType::get(bufElemTy), buff, buffOff); - Value stored = (elemTy != bufElemTy) - ? rewriter - .template create( - loc, bufElemTy, bit, - cudaq::cc::CastOpMode::Unsigned) - .getResult() - : static_cast(bit); - rewriter.template create(loc, stored, addr); - buffOff = rewriter.template create(loc, buffOff, one); - } + auto meas = rewriter.template create(loc, measTy, v).getMeasOut(); + auto bit = + rewriter.template create(loc, i1Ty, meas); + Value addr = rewriter.template create( + loc, cudaq::cc::PointerType::get(i8Ty), buff, buffOff); + auto bitByte = rewriter.template create( + loc, i8Ty, bit, cudaq::cc::CastOpMode::Unsigned); + rewriter.template create(loc, bitByte, addr); + buffOff = rewriter.template create(loc, buffOff, one); } else { assert(isa(v.getType())); Value vecSz = rewriter.template create(loc, i64Ty, v); @@ -128,138 +83,41 @@ class ExpandUnsizedMeasurePattern : public OpRewritePattern { Value qv = builder.template create(loc, v, iv); auto meas = builder.template create(loc, measTy, qv); + auto bit = builder.template create( + loc, i1Ty, meas.getMeasOut()); if (auto registerName = measureOp.getRegisterNameAttr()) meas.setRegisterName(registerName); - if (hasDiscriminateUsers) { - auto bit = builder.template create( - loc, elemTy, meas.getMeasOut()); - Value offset = - builder.template create(loc, iv, buffOff); - auto addr = builder.template create( - loc, cudaq::cc::PointerType::get(bufElemTy), buff, offset); - Value stored = (elemTy != bufElemTy) - ? builder - .template create( - loc, bufElemTy, bit, - cudaq::cc::CastOpMode::Unsigned) - .getResult() - : static_cast(bit); - builder.template create(loc, stored, addr); - } + Value offset = + builder.template create(loc, iv, buffOff); + auto addr = builder.template create( + loc, cudaq::cc::PointerType::get(i8Ty), buff, offset); + auto bitByte = rewriter.template create( + loc, i8Ty, bit, cudaq::cc::CastOpMode::Unsigned); + builder.template create(loc, bitByte, addr); }); - if (hasDiscriminateUsers) - buffOff = - rewriter.template create(loc, buffOff, vecSz); + buffOff = rewriter.template create(loc, buffOff, vecSz); } } // 4. Use the buffer as an initialization expression and create the // std::vec value. - if (hasDiscriminateUsers) { - auto stdvecTy = cudaq::cc::StdvecType::get(rewriter.getContext(), elemTy); - SmallVector discs; - for (auto *out : measureOp.getMeasOut().getUsers()) - if (auto disc = dyn_cast_if_present(out)) - discs.push_back(disc); - for (auto disc : discs) { - auto ptrArrTy = - cudaq::cc::PointerType::get(cudaq::cc::ArrayType::get(elemTy)); + auto stdvecTy = cudaq::cc::StdvecType::get(rewriter.getContext(), i1Ty); + for (auto *out : measureOp.getMeasOut().getUsers()) + if (auto disc = dyn_cast_if_present(out)) { + auto ptrArrI1Ty = + cudaq::cc::PointerType::get(cudaq::cc::ArrayType::get(i1Ty)); auto buffCast = - rewriter.template create(loc, ptrArrTy, buff); + rewriter.template create(loc, ptrArrI1Ty, buff); rewriter.template replaceOpWithNewOp( disc, stdvecTy, buffCast, totalToRead); } - } rewriter.eraseOp(measureOp); return success(); } }; -using MxUnsizedRewrite = ExpandUnsizedMeasurePattern; -using MyUnsizedRewrite = ExpandUnsizedMeasurePattern; -using MzUnsizedRewrite = ExpandUnsizedMeasurePattern; - -// Generalized pattern for expanding a multiple qubit measurement (whether it is -// mx, my, or mz) to a series of individual measurements. -template -class ExpandRewritePattern : public OpRewritePattern { -public: - using OpRewritePattern::OpRewritePattern; - - LogicalResult matchAndRewrite(A measureOp, - PatternRewriter &rewriter) const override { - if (usesIndividualQubit(measureOp.getMeasOut())) - return failure(); - - // Collect all the `get_measure` ops for this measurement operation. - SmallVector getMeasureOps; - for (auto *user : measureOp.getMeasOut().getUsers()) - if (auto gm = dyn_cast(user)) - getMeasureOps.push_back(gm); - - // Can only replace `get_measure %m[i]` with per-qubit measurements, else - // bail out. - if (getMeasureOps.empty() && !measureOp.getMeasOut().use_empty()) - return failure(); - - // Validate that all `get_measure` ops have constant indices and all the veq - // targets have known sizes. - for (auto gm : getMeasureOps) - if (!gm.hasConstantIndex()) - return failure(); - std::size_t totalMeasures = 0; - for (auto v : measureOp.getTargets()) { - if (isa(v.getType())) { - ++totalMeasures; - } else { - auto veqTy = cast(v.getType()); - if (!veqTy.hasSpecifiedSize()) - return failure(); - totalMeasures += veqTy.getSize(); - } - } - // Bounds check - for (auto gm : getMeasureOps) - if (gm.getConstantIndex() >= totalMeasures) - return failure(); - - auto loc = measureOp.getLoc(); - auto measTy = quake::MeasureType::get(rewriter.getContext()); - - // Create individual per-qubit measurements for each target. - SmallVector individualMeasures; - for (auto v : measureOp.getTargets()) { - if (isa(v.getType())) { - auto meas = rewriter.template create(loc, measTy, v); - if (auto registerName = measureOp.getRegisterNameAttr()) - meas.setRegisterName(registerName); - individualMeasures.push_back(meas.getMeasOut()); - } else { - auto veqTy = cast(v.getType()); - for (std::size_t i = 0; i < veqTy.getSize(); ++i) { - Value idx = - rewriter.template create(loc, i, 64); - Value qv = rewriter.template create(loc, v, idx); - auto meas = rewriter.template create(loc, measTy, qv); - if (auto registerName = measureOp.getRegisterNameAttr()) - meas.setRegisterName(registerName); - individualMeasures.push_back(meas.getMeasOut()); - } - } - } - - // Replace each get_measure op with the corresponding individual result. - for (auto gm : getMeasureOps) - rewriter.replaceOp(gm, individualMeasures[gm.getConstantIndex()]); - - if (measureOp.getMeasOut().use_empty()) - rewriter.eraseOp(measureOp); - - return success(); - } -}; - +namespace { using MxRewrite = ExpandRewritePattern; using MyRewrite = ExpandRewritePattern; using MzRewrite = ExpandRewritePattern; @@ -272,10 +130,8 @@ class ResetRewrite : public OpRewritePattern { LogicalResult matchAndRewrite(quake::ResetOp resetOp, PatternRewriter &rewriter) const override { - auto veqArg = resetOp.getTargets(); - if (!isa(veqArg.getType())) - return failure(); auto loc = resetOp.getLoc(); + auto veqArg = resetOp.getTargets(); auto i64Ty = rewriter.getI64Type(); Value vecSz = rewriter.create(loc, i64Ty, veqArg); cudaq::opt::factory::createInvariantLoop( @@ -290,106 +146,29 @@ class ResetRewrite : public OpRewritePattern { } }; -// Pattern for expanding a `quake.discriminate` op on a `quake.measurements` -// with a known size into a series of `quake.discriminate` ops on individual -// `quake.measure` results via `quake.get_measure`. -class ExpandDiscriminatePattern - : public OpRewritePattern { -public: - using OpRewritePattern::OpRewritePattern; - - LogicalResult matchAndRewrite(quake::DiscriminateOp discOp, - PatternRewriter &rewriter) const override { - auto measVal = discOp.getMeasurement(); - auto measTy = dyn_cast(measVal.getType()); - if (!measTy) - return failure(); - if (!measTy.hasSpecifiedSize()) - return failure(); - - auto loc = discOp.getLoc(); - auto stdvecResTy = - cast(discOp.getResult().getType()); - auto elemTy = stdvecResTy.getElementType(); - unsigned elemWidth = cast(elemTy).getWidth(); - Type bufElemTy = elemWidth > 8 ? elemTy : rewriter.getI8Type(); - - Value totalToRead = - rewriter.create(loc, measTy.getSize(), 64); - Value buff = - rewriter.create(loc, bufElemTy, totalToRead); - - // TODO: For large N, consider emitting a loop to avoid IR bloat. - std::size_t n = measTy.getSize(); - for (std::size_t i = 0; i < n; ++i) { - Value getMeas = rewriter.create(loc, measVal, i); - Value bit = rewriter.create(loc, elemTy, getMeas); - Value idx = rewriter.create(loc, i, 64); - Value addr = rewriter.create( - loc, cudaq::cc::PointerType::get(bufElemTy), buff, idx); - Value stored = - (elemTy != bufElemTy) - ? rewriter - .create(loc, bufElemTy, bit, - cudaq::cc::CastOpMode::Unsigned) - .getResult() - : bit; - rewriter.create(loc, stored, addr); - } - - auto ptrArrElemTy = - cudaq::cc::PointerType::get(cudaq::cc::ArrayType::get(elemTy)); - auto buffCast = rewriter.create(loc, ptrArrElemTy, buff); - rewriter.replaceOpWithNewOp(discOp, stdvecResTy, - buffCast, totalToRead); - return success(); - } -}; - class ExpandMeasurementsPass : public cudaq::opt::ExpandMeasurementsBase { public: void runOnOperation() override { auto *op = getOperation(); auto *ctx = &getContext(); - - // Step 1: Expand discriminate(measurements) into individual - // get_measure + discriminate ops. This must run first so that step 2's - // ExpandRewritePattern can see the resulting get_measure users. - { - RewritePatternSet patterns(ctx); - patterns.insert(ctx); - ConversionTarget target(*ctx); - target.addLegalDialect(); - target.addDynamicallyLegalOp( - [](quake::DiscriminateOp d) { - auto measTy = - dyn_cast(d.getMeasurement().getType()); - if (!measTy) - return true; - return !measTy.hasSpecifiedSize(); - }); - if (failed(applyPartialConversion(op, target, std::move(patterns)))) { - op->emitOpError("could not expand discriminate ops"); - signalPassFailure(); - return; - } - } - - // Step 2: Expand multi-qubit m[xyz] and reset ops. - // ExpandRewritePattern handles sized targets (veq) via unrolling. - // ExpandUnsizedMeasurePattern handles unsized targets (veq) via - // dynamic loops using VeqSizeOp + createInvariantLoop. - { - RewritePatternSet patterns(ctx); - patterns.insert(ctx); - patterns.insert( - ctx); - if (failed(applyPatternsAndFoldGreedily(op, std::move(patterns)))) { - op->emitOpError("could not expand measurements"); - signalPassFailure(); - } + RewritePatternSet patterns(ctx); + patterns.insert(ctx); + ConversionTarget target(*ctx); + target.addLegalDialect(); + target.addDynamicallyLegalOp( + [](quake::MxOp x) { return usesIndividualQubit(x.getMeasOut()); }); + target.addDynamicallyLegalOp( + [](quake::MyOp x) { return usesIndividualQubit(x.getMeasOut()); }); + target.addDynamicallyLegalOp( + [](quake::MzOp x) { return usesIndividualQubit(x.getMeasOut()); }); + target.addDynamicallyLegalOp([](quake::ResetOp r) { + return !isa(r.getTargets().getType()); + }); + if (failed(applyPartialConversion(op, target, std::move(patterns)))) { + op->emitOpError("could not expand measurements"); + signalPassFailure(); } } }; diff --git a/lib/Optimizer/Transforms/Mapping.cpp b/lib/Optimizer/Transforms/Mapping.cpp index f6a09f1bf16..c7b1f33d910 100644 --- a/lib/Optimizer/Transforms/Mapping.cpp +++ b/lib/Optimizer/Transforms/Mapping.cpp @@ -823,8 +823,6 @@ struct MappingFunc : public cudaq::opt::impl::MappingFuncBase { auto measureOp = builder.create( finalQubitWire[i].getLoc(), TypeRange{measTy, wireTy}, finalQubitWire[i]); - /// NOTE: Eagerly discriminate here since these are terminal - /// measurements and would need classical readout. builder.create(finalQubitWire[i].getLoc(), resTy, measureOp.getMeasOut()); diff --git a/lib/Optimizer/Transforms/ResourceCount.cpp b/lib/Optimizer/Transforms/ResourceCount.cpp index 918667f906a..ed6ce573c67 100644 --- a/lib/Optimizer/Transforms/ResourceCount.cpp +++ b/lib/Optimizer/Transforms/ResourceCount.cpp @@ -16,6 +16,25 @@ using namespace mlir; mlir::FailureOr cudaq::opt::countResourcesFromIR(ModuleOp module) { + // Check upfront whether all qubit allocations have statically known sizes. + // If any veq has a dynamic size we cannot count qubits statically, so bail + // out before running the gate-erasing pass manager. + std::size_t allocated = 0; + bool unresolvedVeq = false; + module.walk([&](quake::AllocaOp alloc) { + if (isa(alloc.getType())) { + allocated++; + } else if (auto size = quake::getVeqSize(alloc.getResult())) { + allocated += *size; + } else { + unresolvedVeq = true; + } + }); + if (unresolvedVeq) + return failure(); + + // All qubit sizes are statically known — proceed to count gates and erase + // them from the IR so the subsequent JIT compiles a near-empty module. cudaq::Resources counts; auto countGate = [&counts](std::string gate, std::vector controls, @@ -40,21 +59,6 @@ cudaq::opt::countResourcesFromIR(ModuleOp module) { if (failed(pmResult)) return failure(); - // Count allocated qubits from the IR. - std::size_t allocated = 0; - bool unresolvedVeq = false; - module.walk([&](quake::AllocaOp alloc) { - if (isa(alloc.getType())) { - allocated++; - } else if (auto size = quake::getVeqSize(alloc.getResult())) { - allocated += *size; - } else { - unresolvedVeq = true; - } - }); - if (unresolvedVeq) - return failure(); counts.setNumQubits(allocated); - return counts; } diff --git a/pyproject.toml.cu12 b/pyproject.toml.cu12 index b86d7743d64..b07ec657757 100644 --- a/pyproject.toml.cu12 +++ b/pyproject.toml.cu12 @@ -19,9 +19,9 @@ license = "Apache-2.0" license-files = ["LICENSE", "NOTICE", "CITATION.cff"] dependencies = [ 'astpretty ~= 3.0', - 'custatevec-cu12 ~= 1.12', - 'cutensornet-cu12 ~= 2.11', - 'cudensitymat-cu12 ~= 0.4', + 'custatevec-cu12 ~= 1.13.1', + 'cutensornet-cu12 ~= 2.12.1', + 'cudensitymat-cu12 ~= 0.5.1', 'numpy >= 1.24', 'scipy >= 1.10.1', 'requests >= 2.32.4', diff --git a/pyproject.toml.cu13 b/pyproject.toml.cu13 index bae9dbbb929..7e9a6dd6926 100644 --- a/pyproject.toml.cu13 +++ b/pyproject.toml.cu13 @@ -23,9 +23,9 @@ dependencies = [ 'scipy >= 1.10.1', 'requests >= 2.32.4', # CUDA dependencies - excluded on macOS (CPU-only support) - 'custatevec-cu13 ~= 1.12; sys_platform != "darwin"', - 'cutensornet-cu13 ~= 2.11; sys_platform != "darwin"', - 'cudensitymat-cu13 ~= 0.4; sys_platform != "darwin"', + 'custatevec-cu13 ~= 1.13.1; sys_platform != "darwin"', + 'cutensornet-cu13 ~= 2.12.1; sys_platform != "darwin"', + 'cudensitymat-cu13 ~= 0.5.1; sys_platform != "darwin"', 'nvidia-cublas ~= 13.0; sys_platform != "darwin"', 'nvidia-curand ~= 10.4; sys_platform != "darwin"', 'nvidia-cusparse ~= 12.6; sys_platform != "darwin"', diff --git a/python/cudaq/dynamics/integrator.py b/python/cudaq/dynamics/integrator.py index 019beee38b1..134e40b18e2 100644 --- a/python/cudaq/dynamics/integrator.py +++ b/python/cudaq/dynamics/integrator.py @@ -35,7 +35,14 @@ def __init__(self, **kwargs): self.dimensions = None self.schedule = None self.hamiltonian = None + # The actual stepper used for integration. + # This may be set in the constructor with a user-provided stepper, + # or it may be auto-created by the integrator when `integrate()` is called based on the system dynamics. self.stepper = None + # User-provided stepper. + # This will be used for integration if provided. + # Note: it's user's responsibility to ensure that the provided stepper is compatible with the system dynamics. + self._user_provided_stepper = None self.collapse_operators = None self.super_op = None self.__post_init__() @@ -69,7 +76,10 @@ def set_system(self, self.hamiltonian = hamiltonian self.collapse_operators = collapse_operators - self.stepper = None + # Restore the user-provided stepper if one was given at construction, + # otherwise reset to None so `integrate()` builds a fresh stepper from + # the new system dynamics. + self.stepper = self._user_provided_stepper @abstractmethod def integrate(self, t): diff --git a/python/cudaq/dynamics/integrators/cuda_torchdiffeq_integrator.py b/python/cudaq/dynamics/integrators/cuda_torchdiffeq_integrator.py index 50e4dd90a0b..52863a2cf48 100644 --- a/python/cudaq/dynamics/integrators/cuda_torchdiffeq_integrator.py +++ b/python/cudaq/dynamics/integrators/cuda_torchdiffeq_integrator.py @@ -9,6 +9,7 @@ from ..integrator import BaseTimeStepper, BaseIntegrator from .builtin_integrators import cuDensityMatTimeStepper, cuDensityMatSuperOpTimeStepper from ...mlir._mlir_libs._quakeDialects import cudaq_runtime +from typing import Optional import math has_cupy = True @@ -71,7 +72,7 @@ class CUDATorchDiffEqIntegrator(BaseIntegrator[cudaq_runtime.State]): rtol = 1e-7 def __init__(self, - stepper: BaseTimeStepper[cudaq_runtime.State], + stepper: Optional[BaseTimeStepper[cudaq_runtime.State]] = None, solver: str = 'rk4', **kwargs): if not has_dynamics: @@ -96,7 +97,8 @@ def __init__(self, 'CuPy is required to use Torch-based integrators.') super().__init__(**kwargs) - self.stepper = stepper + # Store the user-provided stepper so it survives `set_system()` calls. + self._user_provided_stepper = stepper self.solver = solver self.dm_shape = None self.n_steps = 10 @@ -105,6 +107,7 @@ def __init__(self, self.batchSize = None self._dimensions_list = None self._solver_instance = None + self._use_compute_inplace = None def compute_rhs(self, t, vec): if torch.is_tensor(t): @@ -121,21 +124,25 @@ def compute_rhs(self, t, vec): device_ptr = vec.data_ptr() size = vec.numel() - if self._dimensions_list is None: - self._dimensions_list = list(self.dimensions) - # Wrap the device pointer as a `cudaq::state` (no copy) temp_state = bindings.initializeState(device_ptr, size, self._dimensions_list, self.batchSize) - # Pre-allocate output tensor (torch tensor) - result_vec = torch.zeros_like(vec) - # Wrap the output tensor device pointer as a `cudaq::state` (no copy) - result_state = bindings.initializeState(result_vec.data_ptr(), size, - self._dimensions_list, - self.batchSize) - # Compute the RHS into the output state - self.stepper.compute_inplace(temp_state, t_scalar, result_state) + if self._use_compute_inplace: + # If `compute_inplace` is available, use it to avoid extra data conversion (`dlpack` conversion between `torch` and `cupy`). + # Pre-allocate output tensor (torch tensor) + result_vec = torch.zeros_like(vec) + # Wrap the output tensor device pointer as a `cudaq::state` (no copy) + result_state = bindings.initializeState(result_vec.data_ptr(), size, + self._dimensions_list, + self.batchSize) + self.stepper.compute_inplace(temp_state, t_scalar, result_state) + else: + # Stepper only provides compute(); call it and convert the returned + # state back to a torch tensor via `dlpack` (no extra copy). + result_state_obj = self.stepper.compute(temp_state, t_scalar) + result_cupy = to_cupy_array(result_state_obj) + result_vec = torch.from_dlpack(result_cupy) return result_vec def _create_wrapped_rhs_func(self): @@ -176,6 +183,11 @@ def _get_solver_class(self): return solver_map.get(self.solver) def integrate(self, t): + if self.is_density_state is None: + self.is_density_state = ( + (math.prod(self.dimensions)**2 * + self.batchSize) == self.state.getTensor().get_num_elements()) + if self.stepper is None: if self.dimensions is None: raise ValueError( @@ -188,10 +200,6 @@ def integrate(self, t): ) self.schedule_ = bindings.Schedule(self.schedule._steps, list(self.schedule._parameters)) - if self.is_density_state is None: - self.is_density_state = ( - (math.prod(self.dimensions)**2 * self.batchSize - ) == self.state.getTensor().get_num_elements()) if self.super_op is None: # Create a stepper based on the provided Hamiltonian and collapse operators @@ -205,6 +213,11 @@ def integrate(self, t): self.stepper = cuDensityMatSuperOpTimeStepper( self.super_op, self.schedule_, list(self.dimensions)) + # Cache whether the stepper provides `compute_inplace` to dispatch proper call in `compute_rhs`. + self._use_compute_inplace = hasattr(self.stepper, 'compute_inplace') + if self._dimensions_list is None: + self._dimensions_list = list(self.dimensions) + if t <= self.t: raise ValueError( "Integration time must be greater than current time") @@ -256,9 +269,6 @@ def integrate(self, t): # convert the solution back to CuPy array y_t_cupy = cp.from_dlpack(y_t) - if self._dimensions_list is None: - self._dimensions_list = list(self.dimensions) - # Keep results in GPU memory self.state = cudaq_runtime.State.from_data(y_t_cupy) self.state = bindings.initializeState(self.state, self._dimensions_list, @@ -275,7 +285,7 @@ def set_state(self, state: cudaq_runtime.State, t: float = 0.0): class CUDATorchDiffEqRK4Integrator(CUDATorchDiffEqIntegrator): def __init__(self, - stepper: BaseTimeStepper[cudaq_runtime.State] = None, + stepper: Optional[BaseTimeStepper[cudaq_runtime.State]] = None, **kwargs): super().__init__(stepper, solver='rk4', **kwargs) @@ -283,7 +293,7 @@ def __init__(self, class CUDATorchDiffEqEulerIntegrator(CUDATorchDiffEqIntegrator): def __init__(self, - stepper: BaseTimeStepper[cudaq_runtime.State] = None, + stepper: Optional[BaseTimeStepper[cudaq_runtime.State]] = None, **kwargs): super().__init__(stepper, solver='euler', **kwargs) @@ -291,7 +301,7 @@ def __init__(self, class CUDATorchDiffEqMidpointIntegrator(CUDATorchDiffEqIntegrator): def __init__(self, - stepper: BaseTimeStepper[cudaq_runtime.State] = None, + stepper: Optional[BaseTimeStepper[cudaq_runtime.State]] = None, **kwargs): super().__init__(stepper, solver='midpoint', **kwargs) @@ -299,7 +309,7 @@ def __init__(self, class CUDATorchDiffEqDopri5Integrator(CUDATorchDiffEqIntegrator): def __init__(self, - stepper: BaseTimeStepper[cudaq_runtime.State] = None, + stepper: Optional[BaseTimeStepper[cudaq_runtime.State]] = None, **kwargs): super().__init__(stepper, solver='dopri5', **kwargs) @@ -307,7 +317,7 @@ def __init__(self, class CUDATorchDiffEqDopri8Integrator(CUDATorchDiffEqIntegrator): def __init__(self, - stepper: BaseTimeStepper[cudaq_runtime.State] = None, + stepper: Optional[BaseTimeStepper[cudaq_runtime.State]] = None, **kwargs): super().__init__(stepper, solver='dopri8', **kwargs) @@ -315,7 +325,7 @@ def __init__(self, class CUDATorchDiffEqBosh3Integrator(CUDATorchDiffEqIntegrator): def __init__(self, - stepper: BaseTimeStepper[cudaq_runtime.State] = None, + stepper: Optional[BaseTimeStepper[cudaq_runtime.State]] = None, **kwargs): super().__init__(stepper, solver='bosh3', **kwargs) @@ -323,7 +333,7 @@ def __init__(self, class CUDATorchDiffEqAdaptiveHeunIntegrator(CUDATorchDiffEqIntegrator): def __init__(self, - stepper: BaseTimeStepper[cudaq_runtime.State] = None, + stepper: Optional[BaseTimeStepper[cudaq_runtime.State]] = None, **kwargs): super().__init__(stepper, solver='adaptive_heun', **kwargs) @@ -331,7 +341,7 @@ def __init__(self, class CUDATorchDiffEqExplicitAdamsIntegrator(CUDATorchDiffEqIntegrator): def __init__(self, - stepper: BaseTimeStepper[cudaq_runtime.State] = None, + stepper: Optional[BaseTimeStepper[cudaq_runtime.State]] = None, **kwargs): super().__init__(stepper, solver='explicit_adams', **kwargs) @@ -339,7 +349,7 @@ def __init__(self, class CUDATorchDiffEqFehlberg2Integrator(CUDATorchDiffEqIntegrator): def __init__(self, - stepper: BaseTimeStepper[cudaq_runtime.State] = None, + stepper: Optional[BaseTimeStepper[cudaq_runtime.State]] = None, **kwargs): super().__init__(stepper, solver='fehlberg2', **kwargs) @@ -347,7 +357,7 @@ def __init__(self, class CUDATorchDiffEqHeun3Integrator(CUDATorchDiffEqIntegrator): def __init__(self, - stepper: BaseTimeStepper[cudaq_runtime.State] = None, + stepper: Optional[BaseTimeStepper[cudaq_runtime.State]] = None, **kwargs): super().__init__(stepper, solver='heun3', **kwargs) @@ -355,7 +365,7 @@ def __init__(self, class CUDATorchDiffEqImplicitAdamsIntegrator(CUDATorchDiffEqIntegrator): def __init__(self, - stepper: BaseTimeStepper[cudaq_runtime.State] = None, + stepper: Optional[BaseTimeStepper[cudaq_runtime.State]] = None, **kwargs): super().__init__(stepper, solver='implicit_adams', **kwargs) @@ -363,6 +373,6 @@ def __init__(self, class CUDATorchDiffEqFixedAdamsIntegrator(CUDATorchDiffEqIntegrator): def __init__(self, - stepper: BaseTimeStepper[cudaq_runtime.State] = None, + stepper: Optional[BaseTimeStepper[cudaq_runtime.State]] = None, **kwargs): super().__init__(stepper, solver='fixed_adams', **kwargs) diff --git a/python/cudaq/dynamics/integrators/scipy_integrators.py b/python/cudaq/dynamics/integrators/scipy_integrators.py index 3cc434ff320..48799fa4fa3 100644 --- a/python/cudaq/dynamics/integrators/scipy_integrators.py +++ b/python/cudaq/dynamics/integrators/scipy_integrators.py @@ -9,6 +9,7 @@ from ..integrator import BaseTimeStepper, BaseIntegrator from .builtin_integrators import cuDensityMatTimeStepper, cuDensityMatSuperOpTimeStepper from ...mlir._mlir_libs._quakeDialects import cudaq_runtime +from typing import Optional import numpy, math has_dynamics = True @@ -31,7 +32,9 @@ class ScipyZvodeIntegrator(BaseIntegrator[cudaq_runtime.State]): rtol = 1e-6 order = 12 - def __init__(self, stepper: BaseTimeStepper[cudaq_runtime.State], **kwargs): + def __init__(self, + stepper: Optional[BaseTimeStepper[cudaq_runtime.State]] = None, + **kwargs): if not has_dynamics: raise ImportError( 'CUDA-Q is missing dynamics support. Please check your installation' @@ -39,15 +42,11 @@ def __init__(self, stepper: BaseTimeStepper[cudaq_runtime.State], **kwargs): if not has_scipy: raise ImportError("scipy is required to use this integrator.") super().__init__(**kwargs) - self.stepper = stepper + # Store the user-provided stepper so it survives `set_system()` calls. + self._user_provided_stepper = stepper self.is_density_state = None self.batchSize = None - def __init__(self, **kwargs): - if not has_scipy: - raise ImportError("scipy is required to use this integrator.") - super().__init__(**kwargs) - def compute_rhs(self, t, vec): state = cudaq_runtime.State.from_data(vec) state = bindings.initializeState(state, list(self.dimensions), diff --git a/python/cudaq/kernel/analysis.py b/python/cudaq/kernel/analysis.py index 09e0c962b88..172cf27f681 100644 --- a/python/cudaq/kernel/analysis.py +++ b/python/cudaq/kernel/analysis.py @@ -11,6 +11,8 @@ import textwrap from typing import Optional, Type +from .utils import get_function_source_or_raise + class FunctionDefVisitor(ast.NodeVisitor): """ @@ -108,7 +110,8 @@ def _getChildFuncNames(func_obj: object, if name is None: name = func_obj.__name__ - tree = ast.parse(textwrap.dedent(inspect.getsource(func_obj))) + src, _ = get_function_source_or_raise(func_obj) + tree = ast.parse(src) vis = FindDepFuncsVisitor() visit_set.add(name) vis.visit(tree) @@ -141,7 +144,9 @@ def fetch(func_obj: object): else: this_func_obj = FetchDepFuncsSourceCode._getFuncObj( funcName, callingFrame) - src = textwrap.dedent(inspect.getsource(this_func_obj)) + if this_func_obj is None: + continue + src, _ = get_function_source_or_raise(this_func_obj) code += src + '\n' diff --git a/python/cudaq/kernel/ast_bridge.py b/python/cudaq/kernel/ast_bridge.py index efb7bb78033..f568867381a 100644 --- a/python/cudaq/kernel/ast_bridge.py +++ b/python/cudaq/kernel/ast_bridge.py @@ -2672,11 +2672,33 @@ def checkModule(obj, moduleNames): node.func.value.id) and node.func.attr == 'kernel': return + def isExactCudaqDbgAstCall(func_node: ast.AST) -> bool: + """Return True iff `func_node` is the exact AST shape for + ``.dbg.ast.``. + + Runtime attribute lookup follows lazy aliases (e.g. ``cudaq.ast`` + resolves to ``cudaq.dbg.ast`` via ``_LAZY_SUBMODULES``), so + `devKey` is not a sufficient check. Walk the literal node + structure instead.""" + if not isinstance(func_node, ast.Attribute): + return False + if not isinstance( + func_node.value, + ast.Attribute) or func_node.value.attr != 'ast': + return False + if not isinstance( + func_node.value.value, + ast.Attribute) or func_node.value.value.attr != 'dbg': + return False + root = func_node.value.value.value + return isinstance(root, ast.Name) and self.isCudaqName(root.id) + devKey, name = resolveQualifiedName(node.func) if devKey: # Handle debug functions - if devKey == 'cudaq.dbg.ast': + if devKey == 'cudaq.dbg.ast' and isExactCudaqDbgAstCall( + node.func): # Handle a debug print statement arg = self.__groupValues(node.args, [1]) self.__insertDbgStmt(arg, name) @@ -2955,17 +2977,7 @@ def bodyBuilder(iterVar): measTy = quake.MeasureType.get() resTy = self.getIntegerType(1) else: - total_size = 0 - all_known = True - for q in qubits: - if quake.isConstantQuantumRefType(q.type): - total_size += quake.getAllocationSize(q.type) - else: - all_known = False - if all_known and total_size > 0: - measTy = quake.MeasurementsType.get(total_size) - else: - measTy = quake.MeasurementsType.get() + measTy = cc.StdvecType.get(quake.MeasureType.get()) resTy = cc.StdvecType.get(self.getIntegerType(1)) measureResult = processQuantumOperation( node.func.id.title(), [], @@ -3807,13 +3819,17 @@ def visit_ListComp(self, node): the MLIR. By simple, we mean expressions like `[expr(iter) for iter in iterable]` - or `myList = [exprThatReturns(iter) for iter in iterable]`. + or `myList = [exprThatReturns(iter) for iter in iterable]`, optionally + with `if` filter clause. """ if len(node.generators) > 1: self.emitFatalError( "CUDA-Q only supports single generators for list comprehension.", node) + if_clauses = node.generators[0].ifs + hasFilter = len(if_clauses) > 0 + self.visit(node.generators[0].iter) iterable = self.popValue() orig_iterable_type = iterable.type @@ -3852,6 +3868,15 @@ def process_void_list(): # `visit_For`, but that would be premature optimization. self.visit_For(forNode) + def evalFilter(): + cond = None + for if_node in if_clauses: + self.visit(if_node) + this_cond = self.__arithmetic_to_bool(self.popValue()) + cond = this_cond if cond is None else arith.AndIOp( + cond, this_cond).result + return cond + target_types = {} def get_target_type(target, targetType): @@ -4050,10 +4075,11 @@ def get_item_type(pyval): return if quake.RefType.isinstance(listElemTy): - if quake.VeqType.isinstance(orig_iterable_type): + if quake.VeqType.isinstance(orig_iterable_type) and not hasFilter: self.pushValue(iterable) return - if cc.StdvecType.isinstance(orig_iterable_type): + if (cc.StdvecType.isinstance(orig_iterable_type) or + quake.VeqType.isinstance(orig_iterable_type)): i64Ty = self.getIntegerType() veqTy = self.getVeqType() c0 = self.getConstantInt(0) @@ -4066,18 +4092,39 @@ def get_item_type(pyval): def bodyBuilder(args): i, curr_veq = args[0], args[1] - elem_addr = cc.ComputePtrOp( - cc.PointerType.get(iterTy), iterable, [i], - DenseI32ArrayAttr.get([kDynamicPtrIndex], - context=self.ctx)) - idx_val = cc.LoadOp(elem_addr).result + if quake.VeqType.isinstance(iterable.type): + idx_val = quake.ExtractRefOp(iterTy, + iterable, + -1, + index=i).result + else: + elem_addr = cc.ComputePtrOp( + cc.PointerType.get(iterTy), iterable, [i], + DenseI32ArrayAttr.get([kDynamicPtrIndex], + context=self.ctx)) + idx_val = cc.LoadOp(elem_addr).result self.symbolTable.beginBlock() self.__deconstructAssignment(node.generators[0].target, idx_val) - self.visit(node.elt) - ref = self.popValue() + if hasFilter: + cond = evalFilter() + ifOp = cc.IfOp([veqTy], cond, []) + thenBlock = Block.create_at_start(ifOp.thenRegion, []) + with InsertionPoint(thenBlock): + self.visit(node.elt) + ref = self.popValue() + appended = quake.ConcatOp(veqTy, + [curr_veq, ref]).result + cc.ContinueOp([appended]) + elseBlock = Block.create_at_start(ifOp.elseRegion, []) + with InsertionPoint(elseBlock): + cc.ContinueOp([curr_veq]) + new_veq = ifOp.result + else: + self.visit(node.elt) + ref = self.popValue() + new_veq = quake.ConcatOp(veqTy, [curr_veq, ref]).result self.symbolTable.endBlock() - new_veq = quake.ConcatOp(veqTy, [curr_veq, ref]).result cc.ContinueOp([i, new_veq]) loop = self.createForLoop( @@ -4099,47 +4146,72 @@ def bodyBuilder(args): TypeAttr.get(listElemTy), seqSize=iterableSize).result - # General case of - # `listVar = [expr(i) for i in iterable]` - # Need to think of this as - # `listVar = stdvec(iterable.size)` - # `for i, r in enumerate(listVar):` - # ` listVar[i] = expr(r)` - def bodyBuilder(iterVar): - self.symbolTable.beginBlock() + def extractIterVal(iterVar): if quake.VeqType.isinstance(iterable.type): - iterVal = quake.ExtractRefOp(iterTy, - iterable, - -1, - index=iterVar).result - else: - eleAddr = cc.ComputePtrOp( - cc.PointerType.get(iterTy), iterable, [iterVar], - DenseI32ArrayAttr.get([kDynamicPtrIndex], context=self.ctx)) - iterVal = cc.LoadOp(eleAddr).result - - # We don't do support anything within list comprehensions that would - # require being careful about assigning references, so simply - # adding them to the symbol table is enough for list comprehension. - self.__deconstructAssignment(node.generators[0].target, iterVal) + return quake.ExtractRefOp(iterTy, iterable, -1, + index=iterVar).result + eleAddr = cc.ComputePtrOp( + cc.PointerType.get(iterTy), iterable, [iterVar], + DenseI32ArrayAttr.get([kDynamicPtrIndex], context=self.ctx)) + return cc.LoadOp(eleAddr).result + + def storeElementAt(storeIdx): self.visit(node.elt) element = self.popValue() - # We do need to be careful, however, about validating the list - # elements. + # We do need to be careful about validating the list elements. self.__validate_container_entry(element, node.elt) - listValueAddr = cc.ComputePtrOp( - cc.PointerType.get(listElemTy), listValue, [iterVar], + cc.PointerType.get(listElemTy), listValue, [storeIdx], DenseI32ArrayAttr.get([kDynamicPtrIndex], context=self.ctx)) element = self.changeOperandToType(listElemTy, element, allowDemotion=False) cc.StoreOp(element, listValueAddr) - self.symbolTable.endBlock() - self.createInvariantForLoop(bodyBuilder, iterableSize) - res = cc.StdvecInitOp(resultVecTy, listValue, - length=iterableSize).result + if not hasFilter: + + def bodyBuilder(iterVar): + self.symbolTable.beginBlock() + iterVal = extractIterVal(iterVar) + self.__deconstructAssignment(node.generators[0].target, iterVal) + storeElementAt(iterVar) + self.symbolTable.endBlock() + + self.createInvariantForLoop(bodyBuilder, iterableSize) + res = cc.StdvecInitOp(resultVecTy, listValue, + length=iterableSize).result + self.pushValue(res) + return + + i64Ty = self.getIntegerType() + c0 = self.getConstantInt(0) + c1 = self.getConstantInt(1) + + def filteredBodyBuilder(args): + i, count = args[0], args[1] + self.symbolTable.beginBlock() + iterVal = extractIterVal(i) + self.__deconstructAssignment(node.generators[0].target, iterVal) + cond = evalFilter() + ifOp = cc.IfOp([i64Ty], cond, []) + thenBlock = Block.create_at_start(ifOp.thenRegion, []) + with InsertionPoint(thenBlock): + storeElementAt(count) + cc.ContinueOp([arith.AddIOp(count, c1).result]) + elseBlock = Block.create_at_start(ifOp.elseRegion, []) + with InsertionPoint(elseBlock): + cc.ContinueOp([count]) + nextCount = ifOp.result + self.symbolTable.endBlock() + cc.ContinueOp([i, nextCount]) + + loop = self.createForLoop( + [i64Ty, i64Ty], + filteredBodyBuilder, [c0, c0], lambda args: arith.CmpIOp( + IntegerAttr.get(i64Ty, 2), args[0], iterableSize).result, + lambda args: [arith.AddIOp(args[0], c1).result, args[1]]) + finalCount = loop.results[1] + res = cc.StdvecInitOp(resultVecTy, listValue, length=finalCount).result self.pushValue(res) return diff --git a/python/cudaq/kernel/kernel_builder.py b/python/cudaq/kernel/kernel_builder.py index 82b78fd7381..4abc9fb8733 100644 --- a/python/cudaq/kernel/kernel_builder.py +++ b/python/cudaq/kernel/kernel_builder.py @@ -653,7 +653,6 @@ def functor(op): cloned = otherST[calleeName].operation.clone() if 'cudaq-entrypoint' in cloned.operation.attributes: cloned.operation.attributes.__delitem__('cudaq-entrypoint') - print("adding", cloned) currentModule.body.append(cloned) visitAllCallOps(cloned) @@ -682,6 +681,12 @@ def __applyControlOrAdjoint(self, target, isAdjoint, controls, *args): otherFuncCloned, otherModule = self.__cloneOrGetFunction( target.name, self.module, target) assert isinstance(otherFuncCloned, func.FuncOp) + # Same as __addAllCalledFunctionsRecursively does for + # transitively called functions: a sub-kernel merged into this + # module is no longer an `entrypoint`. + if 'cudaq-entrypoint' in otherFuncCloned.operation.attributes: + otherFuncCloned.operation.attributes.__delitem__( + 'cudaq-entrypoint') self.__addAllCalledFunctionsRecursively(otherFuncCloned, self.module, otherModule) @@ -1113,24 +1118,6 @@ def reset(self, target): 'reset operation broadcasting on qvector not supported yet.' ) - @staticmethod - def _get_measurement_type(targets): - """ - Compute the appropriate measurement type for the given targets. - """ - if len(targets) == 1 and quake.RefType.isinstance(targets[0].type): - return quake.MeasureType.get() - total_size = 0 - all_known = True - for t in targets: - if quake.isConstantQuantumRefType(t.type): - total_size += quake.getAllocationSize(t.type) - else: - all_known = False - if all_known and total_size > 0: - return quake.MeasurementsType.get(total_size) - return quake.MeasurementsType.get() - def mz(self, target, regName=None): """ Measure the given qubit or qubits in the Z-basis. The optional @@ -1163,10 +1150,13 @@ def mz(self, target, regName=None): """ with self.ctx, self.insertPoint, self.loc: i1Ty = IntegerType.get_signless(1) - measTy = PyKernel._get_measurement_type([target.mlirValue]) + qubitTy = target.mlirValue.type retTy = i1Ty - if quake.MeasurementsType.isinstance(measTy): - retTy = cc.StdvecType.get(i1Ty) + measTy = quake.MeasureType.get() + stdvecTy = cc.StdvecType.get(i1Ty) + if quake.VeqType.isinstance(target.mlirValue.type): + retTy = stdvecTy + measTy = cc.StdvecType.get(measTy) if regName is not None: res = quake.MzOp(measTy, [], [target.mlirValue], registerName=StringAttr.get(regName, @@ -1207,10 +1197,13 @@ def mx(self, target, regName=None): """ with self.ctx, self.insertPoint, self.loc: i1Ty = IntegerType.get_signless(1) - measTy = PyKernel._get_measurement_type([target.mlirValue]) + qubitTy = target.mlirValue.type retTy = i1Ty - if quake.MeasurementsType.isinstance(measTy): - retTy = cc.StdvecType.get(i1Ty) + measTy = quake.MeasureType.get() + stdvecTy = cc.StdvecType.get(i1Ty) + if quake.VeqType.isinstance(target.mlirValue.type): + retTy = stdvecTy + measTy = cc.StdvecType.get(measTy) if regName is not None: res = quake.MxOp(measTy, [], [target.mlirValue], registerName=StringAttr.get(regName, @@ -1252,10 +1245,13 @@ def my(self, target, regName=None): """ with self.ctx, self.insertPoint, self.loc: i1Ty = IntegerType.get_signless(1) - measTy = PyKernel._get_measurement_type([target.mlirValue]) + qubitTy = target.mlirValue.type retTy = i1Ty - if quake.MeasurementsType.isinstance(measTy): - retTy = cc.StdvecType.get(i1Ty) + measTy = quake.MeasureType.get() + stdvecTy = cc.StdvecType.get(i1Ty) + if quake.VeqType.isinstance(target.mlirValue.type): + retTy = stdvecTy + measTy = cc.StdvecType.get(measTy) if regName is not None: res = quake.MyOp(measTy, [], [target.mlirValue], registerName=StringAttr.get(regName, diff --git a/python/cudaq/kernel/kernel_decorator.py b/python/cudaq/kernel/kernel_decorator.py index 11a03e5d6fd..aea0b992974 100644 --- a/python/cudaq/kernel/kernel_decorator.py +++ b/python/cudaq/kernel/kernel_decorator.py @@ -24,7 +24,8 @@ from .analysis import FunctionDefVisitor from .kernel_signature import CapturedLinkedKernel, CapturedVariable, KernelSignature from .ast_bridge import compile_to_mlir -from .utils import (emitFatalError, emitErrorIfInvalidPauli, get_module_name, +from .utils import (emitFatalError, emitErrorIfInvalidPauli, + get_function_source_or_raise, get_module_name, globalRegisteredTypes, mlirTypeFromPyType, mlirTypeToPyType, nvqppPrefix, getMLIRContext, recover_func_op, recover_value_of) @@ -736,14 +737,7 @@ def isa_kernel_decorator(object): def _get_source(function): if function is None: return None, None - # Get the function source location - location = (inspect.getfile(function), inspect.getsourcelines(function)[1]) - # Get the function source - src = inspect.getsource(function) - # Strip off the extra tabs - leadingSpaces = len(src) - len(src.lstrip()) - src = '\n'.join([line[leadingSpaces:] for line in src.split('\n')]) - return src, location + return get_function_source_or_raise(function) def _recover_defining_frame(): diff --git a/python/cudaq/kernel/utils.py b/python/cudaq/kernel/utils.py index 2e1f4d0b0fb..47c1eb3dfb5 100644 --- a/python/cudaq/kernel/utils.py +++ b/python/cudaq/kernel/utils.py @@ -265,6 +265,64 @@ def emitWarning(msg): Color.END + '\n\nOffending code:\n' + offendingSrc[0]) +def _format_missing_source_error(function, filename): + """ + Build a user-facing diagnostic explaining why source for `function` could + not be retrieved. Distinguishes between three buckets: + - Interactive interpreter-defined (`` or ``). + - Other synthetic filenames (code compiled with a non-file name). + - Real paths that failed to read (missing file, frozen module, + compiled extension). + """ + qualname = getattr(function, '__qualname__', + getattr(function, '__name__', '')) + if filename is None: + return (f"@cudaq.kernel could not determine a source location for " + f"function `{qualname}`. `@cudaq.kernel` requires source that " + f"Python's `inspect` module can recover. Move the kernel into " + f"a `.py` module.") + is_repl = filename == '' or filename.startswith('') + if is_repl: + return (f"@cudaq.kernel could not retrieve source for function " + f"`{qualname}` because it is defined in the Python REPL, " + f"which does not preserve source code that `inspect` can " + f"recover. To use `@cudaq.kernel`, either run from a " + f"Jupyter/IPython session (which preserves source via " + f"`linecache`) or move the kernel into a `.py` module.") + if is_synthetic: + return (f"@cudaq.kernel could not retrieve source for function " + f"`{qualname}`: it is defined in a non-file context " + f"(`{filename}`). `@cudaq.kernel` requires source that " + f"`inspect` can recover. Move the kernel into a `.py` " + f"module.") + return (f"@cudaq.kernel could not read source for function " + f"`{qualname}` at `{filename}` (the file may be missing, " + f"frozen, or a compiled extension).") + + +def get_function_source_or_raise(function): + """ + Return `(dedented_source, (filename, first_lineno))` for `function`. + Wraps `inspect.getfile`, `inspect.getsourcelines`, and + `inspect.getsource`. If any fail (most commonly because `function` was + defined in the interactive Python interpreter), raise `RuntimeError` + with a diagnostic + tailored to the failure mode, chained from the underlying exception. + """ + filename = None + try: + filename = inspect.getfile(function) + first_line = inspect.getsourcelines(function)[1] + src = inspect.getsource(function) + except OSError as e: + raise RuntimeError(_format_missing_source_error(function, + filename)) from e + leadingSpaces = len(src) - len(src.lstrip()) + src = '\n'.join([line[leadingSpaces:] for line in src.split('\n')]) + return src, (filename, first_line) + + def mlirTryCreateStructType(mlirEleTypes, name=None, context=None): """ Creates either a `quake.StruqType` or a `cc.StructType` used to represent diff --git a/python/cudaq/operators/scalar/scalar_op.py b/python/cudaq/operators/scalar/scalar_op.py index c95927e1900..2c0245badb2 100644 --- a/python/cudaq/operators/scalar/scalar_op.py +++ b/python/cudaq/operators/scalar/scalar_op.py @@ -7,11 +7,11 @@ # ============================================================================ # from __future__ import annotations -import inspect, numpy # type: ignore -from typing import Any, Callable, Mapping, Optional +import numpy # type: ignore +from typing import Any, Callable, Mapping from numpy.typing import NDArray -from ..helpers import NumericType, _aggregate_parameters, _args_from_kwargs, _parameter_docs +from ..helpers import NumericType, _aggregate_parameters from cudaq.mlir._mlir_libs._quakeDialects.cudaq_runtime import ScalarOperator @@ -21,7 +21,7 @@ def _const_init(cls, constant_value: NumericType) -> ScalarOperator: """ if not isinstance(constant_value, NumericType): raise ValueError("argument must be a numeric constant") - return cls(constant_value) + return cls(complex(constant_value)) ScalarOperator.const = classmethod(_const_init) @@ -65,7 +65,7 @@ def _compose( if self.is_constant(): return ScalarOperator.const(fct(self.evaluate(), other)) generator = lambda **kwargs: fct(self.evaluate(**kwargs), other) - return ScalarOperator(generator, self.parameters) + return ScalarOperator(generator, **self.parameters) elif type(other) == ScalarOperator: if self.is_constant() and other.is_constant(): return ScalarOperator.const(fct(self.evaluate(), other.evaluate())) @@ -73,7 +73,7 @@ def _compose( other.evaluate(**kwargs)) parameter_info = _aggregate_parameters( [self.parameters, other.parameters]) - return ScalarOperator(generator, parameter_info) + return ScalarOperator(generator, **parameter_info) return NotImplemented @@ -97,44 +97,3 @@ def _compose( v2: v2 + v1) ScalarOperator.__rsub__ = lambda self, other: _compose(self, other, lambda v1, v2: v2 - v1) - - -def _instantiate(cls, - generator: NumericType | Callable[..., NumericType], - parameter_info: Optional[Mapping[str, str]] = None) -> None: - """ - Instantiates a scalar operator. - - Arguments: - generator: The value of the scalar operator as a function of its - parameters. The generator may take any number of complex-valued - arguments and must return a number. Each parameter must be passed - as a keyword argument when evaluating the operator. - """ - instance = super(ScalarOperator, cls).__new__(cls) - if isinstance(generator, NumericType): - instance.__init__(numpy.complex128(generator)) - else: - # A variable number of arguments (i.e. `*args`) cannot be supported - # for generators; it would prevent proper argument handling while - # supporting additions and multiplication of all kinds of operators. - arg_spec = inspect.getfullargspec(generator) - if arg_spec.varargs is not None: - raise ValueError( - f"the function defining a scalar operator must not take *args") - if parameter_info is None: - parameter_info = {} - for arg_name in arg_spec.args + arg_spec.kwonlyargs: - parameter_info[arg_name] = _parameter_docs( - arg_name, generator.__doc__) - - def generator_wrapper(kwargs: dict[str, NumericType]): - generator_args, remaining_kwargs = _args_from_kwargs( - generator, **kwargs) - return generator(*generator_args, **remaining_kwargs) - - instance.__init__(generator_wrapper, **parameter_info) - return instance - - -ScalarOperator.__new__ = staticmethod(_instantiate) diff --git a/python/cudaq/runtime/sample.py b/python/cudaq/runtime/sample.py index 0e44761557f..b2c97d97800 100644 --- a/python/cudaq/runtime/sample.py +++ b/python/cudaq/runtime/sample.py @@ -224,6 +224,9 @@ def sample_async(decorator, explicit_measurements (Optional[bool]): A flag to indicate whether or not to concatenate measurements in execution order for the returned sample result. + noise_model (Optional[`NoiseModel`]): The optional :class:`NoiseModel` + to add noise to the kernel execution on the simulator. Defaults to + an empty noise model. `qpu_id` (Optional[int]): The optional identification for which QPU on the platform to target. Defaults to zero. Key-word only. diff --git a/python/extension/CMakeLists.txt b/python/extension/CMakeLists.txt index 52bb22a14db..d5bad6f9ac1 100644 --- a/python/extension/CMakeLists.txt +++ b/python/extension/CMakeLists.txt @@ -13,6 +13,43 @@ endif() include(HandleLLVMOptions) include(AddMLIRPython) +function(add_mlir_python_extension libname extname) + cmake_parse_arguments(ARG + "" + "INSTALL_COMPONENT;INSTALL_DIR;OUTPUT_DIRECTORY" + "SOURCES;LINK_LIBS" + ${ARGN}) + + # Use nanobind for CUDA-Q's own extension (_quakeDialects) and pybind11 + # for upstream MLIR extensions (AsyncPasses, RegisterEverything, etc.). + if(libname MATCHES "_quakeDialects") + nanobind_add_module(${libname} NB_STATIC ${ARG_SOURCES}) + target_compile_options(${libname} PRIVATE -frtti -fexceptions -Wno-cast-qual) + else() + pybind11_add_module(${libname} MODULE ${ARG_SOURCES}) + target_compile_options(${libname} PRIVATE -frtti -fexceptions) + endif() + + set_target_properties(${libname} PROPERTIES + LIBRARY_OUTPUT_DIRECTORY ${ARG_OUTPUT_DIRECTORY} + OUTPUT_NAME "${extname}" + NO_SONAME ON + ) + + target_link_libraries(${libname} PRIVATE ${ARG_LINK_LIBS}) + target_link_options(${libname} PRIVATE + $<$:LINKER:--exclude-libs,ALL> + ) + + if(ARG_INSTALL_DIR) + install(TARGETS ${libname} + COMPONENT ${ARG_INSTALL_COMPONENT} + LIBRARY DESTINATION "${ARG_INSTALL_DIR}" + RUNTIME DESTINATION "${ARG_INSTALL_DIR}" + ) + endif() +endfunction() + # Specifies that all MLIR packages are co-located under the cudaq # top level package (the API has been embedded in a relocatable way). add_compile_definitions("MLIR_PYTHON_PACKAGE_PREFIX=cudaq.mlir.") @@ -92,6 +129,7 @@ declare_mlir_python_extension(CUDAQuantumPythonSources.Extension ../../runtime/cudaq/platform/default/rest/RemoteRESTQPU.cpp ../../runtime/cudaq/platform/default/python/QPU.cpp ../../runtime/internal/compiler/ArgumentConversion.cpp + ../../runtime/internal/compiler/CompiledModuleHelper.cpp ../../runtime/internal/compiler/LayoutInfo.cpp ../../runtime/internal/compiler/RuntimeMLIR.cpp ../../runtime/internal/compiler/RuntimePyMLIR.cpp @@ -112,8 +150,8 @@ declare_mlir_python_extension(CUDAQuantumPythonSources.Extension cudaq-mlir-runtime-headers ) -target_include_directories(CUDAQuantumPythonSources.Extension INTERFACE - ${CMAKE_SOURCE_DIR}/python +target_include_directories(CUDAQuantumPythonSources.Extension INTERFACE + ${CMAKE_SOURCE_DIR}/python ${CMAKE_SOURCE_DIR}/python/utils ${CMAKE_SOURCE_DIR}/runtime ) @@ -161,6 +199,10 @@ add_mlir_python_modules(CUDAQuantumPythonModules CUDAQuantumPythonCAPI ) +if(TARGET nanobind-static) + target_compile_options(nanobind-static PRIVATE -Wno-cast-qual -Wno-covered-switch-default) +endif() + ## The Python bindings module for Quake dialect depends on CUDAQ libraries ## which it can't locate since they are in "../../lib" and the 'rpath' is set ## to '$ORIGIN' by default. diff --git a/python/extension/CUDAQuantumExtension.cpp b/python/extension/CUDAQuantumExtension.cpp index 679c191a7a6..ac1ca729446 100644 --- a/python/extension/CUDAQuantumExtension.cpp +++ b/python/extension/CUDAQuantumExtension.cpp @@ -43,24 +43,26 @@ #include "runtime/cudaq/qis/py_pauli_word.h" #include "runtime/cudaq/target/py_runtime_target.h" #include "runtime/cudaq/target/py_testing_utils.h" -#include "runtime/interop/PythonCppInterop.h" +#include "runtime/interop/PythonCppInteropDecls.h" #include "runtime/mlir/py_register_dialects.h" #include "utils/LinkedLibraryHolder.h" +#include "utils/NanobindAdaptors.h" #include "utils/OpaqueArguments.h" -#include "mlir/Bindings/Python/PybindAdaptors.h" #include "mlir/Parser/Parser.h" #include "mlir/Target/LLVMIR/Dialect/LLVMIR/LLVMToLLVMIRTranslation.h" -#include -#include -#include - -namespace py = pybind11; +#include +#include +#include +#include +#include +#include +#include using namespace cudaq; static std::unique_ptr holder; -PYBIND11_MODULE(_quakeDialects, m) { +NB_MODULE(_quakeDialects, m) { holder = std::make_unique(); bindRegisterDialects(m); @@ -92,8 +94,10 @@ PYBIND11_MODULE(_quakeDialects, m) { holder->setTarget(*target, extraConfig); } }, - py::arg("option") = py::none(), py::arg("emulate") = py::none(), - py::arg("target") = py::none(), "Initialize the CUDA-Q environment."); + nanobind::arg("option") = nanobind::none(), + nanobind::arg("emulate") = nanobind::none(), + nanobind::arg("target") = nanobind::none(), + "Initialize the CUDA-Q environment."); bindRuntimeTarget(cudaqRuntime, *holder.get()); bindMeasureCounts(cudaqRuntime); @@ -197,41 +201,46 @@ PYBIND11_MODULE(_quakeDialects, m) { auto orcaSubmodule = cudaqRuntime.def_submodule("orca"); orcaSubmodule.def( "sample", - py::overload_cast &, std::vector &, - std::vector &, std::vector &, int, - std::size_t>(&orca::sample), + nanobind::overload_cast &, + std::vector &, std::vector &, + std::vector &, int, std::size_t>( + &orca::sample), "Performs Time Bin Interferometer (TBI) boson sampling experiments on " "ORCA's backends", - py::arg("input_state"), py::arg("loop_lengths"), py::arg("bs_angles"), - py::arg("ps_angles"), py::arg("n_samples") = 10000, - py::arg("qpu_id") = 0); + nanobind::arg("input_state"), nanobind::arg("loop_lengths"), + nanobind::arg("bs_angles"), nanobind::arg("ps_angles"), + nanobind::arg("n_samples") = 10000, nanobind::arg("qpu_id") = 0); orcaSubmodule.def( "sample", - py::overload_cast &, std::vector &, - std::vector &, int, std::size_t>(&orca::sample), + nanobind::overload_cast &, + std::vector &, std::vector &, + int, std::size_t>(&orca::sample), "Performs Time Bin Interferometer (TBI) boson sampling experiments on " "ORCA's backends", - py::arg("input_state"), py::arg("loop_lengths"), py::arg("bs_angles"), - py::arg("n_samples") = 10000, py::arg("qpu_id") = 0); + nanobind::arg("input_state"), nanobind::arg("loop_lengths"), + nanobind::arg("bs_angles"), nanobind::arg("n_samples") = 10000, + nanobind::arg("qpu_id") = 0); orcaSubmodule.def( "sample_async", - py::overload_cast &, std::vector &, - std::vector &, std::vector &, int, - std::size_t>(&orca::sample_async), + nanobind::overload_cast &, + std::vector &, std::vector &, + std::vector &, int, std::size_t>( + &orca::sample_async), "Performs Time Bin Interferometer (TBI) boson sampling experiments on " "ORCA's backends", - py::arg("input_state"), py::arg("loop_lengths"), py::arg("bs_angles"), - py::arg("ps_angles"), py::arg("n_samples") = 10000, - py::arg("qpu_id") = 0); + nanobind::arg("input_state"), nanobind::arg("loop_lengths"), + nanobind::arg("bs_angles"), nanobind::arg("ps_angles"), + nanobind::arg("n_samples") = 10000, nanobind::arg("qpu_id") = 0); orcaSubmodule.def( "sample_async", - py::overload_cast &, std::vector &, - std::vector &, int, std::size_t>( - &orca::sample_async), + nanobind::overload_cast &, + std::vector &, std::vector &, + int, std::size_t>(&orca::sample_async), "Performs Time Bin Interferometer (TBI) boson sampling experiments on " "ORCA's backends", - py::arg("input_state"), py::arg("loop_lengths"), py::arg("bs_angles"), - py::arg("n_samples") = 10000, py::arg("qpu_id") = 0); + nanobind::arg("input_state"), nanobind::arg("loop_lengths"), + nanobind::arg("bs_angles"), nanobind::arg("n_samples") = 10000, + nanobind::arg("qpu_id") = 0); auto photonicsSubmodule = cudaqRuntime.def_submodule("photonics"); photonicsSubmodule.def( @@ -239,7 +248,7 @@ PYBIND11_MODULE(_quakeDialects, m) { [](std::size_t &level) { return getExecutionManager()->allocateQudit(level); }, - "Allocate a qudit of given level.", py::arg("level")); + "Allocate a qudit of given level.", nanobind::arg("level")); photonicsSubmodule.def( "apply_operation", [](const std::string &name, std::vector ¶ms, @@ -254,20 +263,21 @@ PYBIND11_MODULE(_quakeDialects, m) { spin_op::identity()); }, "Apply the input photonics operation on the target qudits.", - py::arg("name"), py::arg("params"), py::arg("targets")); + nanobind::arg("name"), nanobind::arg("params"), nanobind::arg("targets")); photonicsSubmodule.def( "measure", [](std::size_t level, std::size_t id, const std::string ®Name) { return getExecutionManager()->measure(QuditInfo(level, id), regName); }, - "Measure the input qudit(s).", py::arg("level"), py::arg("qudit"), - py::arg("register_name") = ""); + "Measure the input qudit(s).", nanobind::arg("level"), + nanobind::arg("qudit"), nanobind::arg("register_name") = ""); photonicsSubmodule.def( "release_qudit", [](std::size_t level, std::size_t id) { getExecutionManager()->returnQudit(QuditInfo(level, id)); }, - "Release a qudit of given id.", py::arg("level"), py::arg("id")); + "Release a qudit of given id.", nanobind::arg("level"), + nanobind::arg("id")); cudaqRuntime.def("cloneModule", [](MlirModule mod) { return wrap(unwrap(mod).clone()); }); cudaqRuntime.def("isTerminator", [](MlirOperation op) { diff --git a/python/runtime/common/py_AnalogHamiltonian.cpp b/python/runtime/common/py_AnalogHamiltonian.cpp index b9049f6e961..ec182338e03 100644 --- a/python/runtime/common/py_AnalogHamiltonian.cpp +++ b/python/runtime/common/py_AnalogHamiltonian.cpp @@ -9,78 +9,78 @@ #include "py_AnalogHamiltonian.h" #include "common/AnalogHamiltonian.h" #include "common/JsonConvert.h" -#include - -namespace py = pybind11; +#include +#include +#include namespace cudaq { /// @brief Binds the `cudaq::ahs` classes. -void bindAnalogHamiltonian(py::module &mod) { +void bindAnalogHamiltonian(nanobind::module_ &mod) { - py::class_(mod, "AtomArrangement") - .def(py::init<>()) - .def_readwrite("sites", &cudaq::ahs::AtomArrangement::sites) - .def_readwrite("filling", &cudaq::ahs::AtomArrangement::filling); + nanobind::class_(mod, "AtomArrangement") + .def(nanobind::init<>()) + .def_rw("sites", &cudaq::ahs::AtomArrangement::sites) + .def_rw("filling", &cudaq::ahs::AtomArrangement::filling); - py::class_(mod, "SetUp") - .def(py::init<>()) - .def_readwrite("ahs_register", &cudaq::ahs::Setup::ahs_register); + nanobind::class_(mod, "SetUp") + .def(nanobind::init<>()) + .def_rw("ahs_register", &cudaq::ahs::Setup::ahs_register); - py::class_(mod, "TimeSeries") - .def(py::init<>()) - .def(py::init>>()) - .def_readwrite("values", &cudaq::ahs::TimeSeries::values) - .def_readwrite("times", &cudaq::ahs::TimeSeries::times); + nanobind::class_(mod, "TimeSeries") + .def(nanobind::init<>()) + .def(nanobind::init>>()) + .def_rw("values", &cudaq::ahs::TimeSeries::values) + .def_rw("times", &cudaq::ahs::TimeSeries::times); - py::class_(mod, "FieldPattern") + nanobind::class_(mod, "FieldPattern") /// NOTE: Other constructors not required from Python interface - .def(py::init<>()) - .def_readwrite("patternStr", &cudaq::ahs::FieldPattern::patternStr) - .def_readwrite("patternVals", &cudaq::ahs::FieldPattern::patternVals); - - py::class_(mod, "PhysicalField") - .def(py::init<>()) - .def_readwrite("time_series", &cudaq::ahs::PhysicalField::time_series) - .def_readwrite("pattern", &cudaq::ahs::PhysicalField::pattern); - - py::class_(mod, "DrivingField") - .def(py::init<>()) - .def_readwrite("amplitude", &cudaq::ahs::DrivingField::amplitude) - .def_readwrite("phase", &cudaq::ahs::DrivingField::phase) - .def_readwrite("detuning", &cudaq::ahs::DrivingField::detuning); - - py::class_(mod, "LocalDetuning") - .def(py::init<>()) - .def_readwrite("magnitude", &cudaq::ahs::LocalDetuning::magnitude); - - py::class_(mod, "Hamiltonian") - .def(py::init<>()) - .def_readwrite("drivingFields", &cudaq::ahs::Hamiltonian::drivingFields) - .def_readwrite("localDetuning", &cudaq::ahs::Hamiltonian::localDetuning); - - py::class_(mod, "Program") - .def(py::init<>()) - .def_readwrite("setup", &cudaq::ahs::Program::setup) - .def_readwrite("hamiltonian", &cudaq::ahs::Program::hamiltonian) + .def(nanobind::init<>()) + .def_rw("patternStr", &cudaq::ahs::FieldPattern::patternStr) + .def_rw("patternVals", &cudaq::ahs::FieldPattern::patternVals); + + nanobind::class_(mod, "PhysicalField") + .def(nanobind::init<>()) + .def_rw("time_series", &cudaq::ahs::PhysicalField::time_series) + .def_rw("pattern", &cudaq::ahs::PhysicalField::pattern); + + nanobind::class_(mod, "DrivingField") + .def(nanobind::init<>()) + .def_rw("amplitude", &cudaq::ahs::DrivingField::amplitude) + .def_rw("phase", &cudaq::ahs::DrivingField::phase) + .def_rw("detuning", &cudaq::ahs::DrivingField::detuning); + + nanobind::class_(mod, "LocalDetuning") + .def(nanobind::init<>()) + .def_rw("magnitude", &cudaq::ahs::LocalDetuning::magnitude); + + nanobind::class_(mod, "Hamiltonian") + .def(nanobind::init<>()) + .def_rw("drivingFields", &cudaq::ahs::Hamiltonian::drivingFields) + .def_rw("localDetuning", &cudaq::ahs::Hamiltonian::localDetuning); + + nanobind::class_(mod, "Program") + .def(nanobind::init<>()) + .def_rw("setup", &cudaq::ahs::Program::setup) + .def_rw("hamiltonian", &cudaq::ahs::Program::hamiltonian) .def( "to_json", [](const cudaq::ahs::Program &p) { return json(p).dump(); }, "Convert Program to JSON"); - py::class_(mod, "ShotMetadata") - .def(py::init<>()) - .def_readwrite("shotStatus", &cudaq::ahs::ShotMetadata::shotStatus); + nanobind::class_(mod, "ShotMetadata") + .def(nanobind::init<>()) + .def_rw("shotStatus", &cudaq::ahs::ShotMetadata::shotStatus); - py::class_(mod, "ShotResult") - .def(py::init<>()) - .def_readwrite("preSequence", &cudaq::ahs::ShotResult::preSequence) - .def_readwrite("postSequence", &cudaq::ahs::ShotResult::postSequence); + nanobind::class_(mod, "ShotResult") + .def(nanobind::init<>()) + .def_rw("preSequence", &cudaq::ahs::ShotResult::preSequence) + .def_rw("postSequence", &cudaq::ahs::ShotResult::postSequence); - py::class_(mod, "ShotMeasurement") - .def(py::init<>()) - .def_readwrite("shotMetadata", &cudaq::ahs::ShotMeasurement::shotMetadata) - .def_readwrite("shotResult", &cudaq::ahs::ShotMeasurement::shotResult); + nanobind::class_(mod, "ShotMeasurement") + .def(nanobind::init<>()) + .def_rw("shotMetadata", &cudaq::ahs::ShotMeasurement::shotMetadata) + .def_rw("shotResult", &cudaq::ahs::ShotMeasurement::shotResult); /// TODO: Add other classes if needed } diff --git a/python/runtime/common/py_AnalogHamiltonian.h b/python/runtime/common/py_AnalogHamiltonian.h index bfc098955a7..a1e039a8fa5 100644 --- a/python/runtime/common/py_AnalogHamiltonian.h +++ b/python/runtime/common/py_AnalogHamiltonian.h @@ -6,13 +6,11 @@ * the terms of the Apache License 2.0 which accompanies this distribution. * ******************************************************************************/ -#include - -namespace py = pybind11; +#include namespace cudaq { /// @brief Binds the `cudaq::ahs` classes. -void bindAnalogHamiltonian(py::module &mod); +void bindAnalogHamiltonian(nanobind::module_ &mod); } // namespace cudaq diff --git a/python/runtime/common/py_CustomOpRegistry.cpp b/python/runtime/common/py_CustomOpRegistry.cpp index 35c5132695b..6d09cd8d69b 100644 --- a/python/runtime/common/py_CustomOpRegistry.cpp +++ b/python/runtime/common/py_CustomOpRegistry.cpp @@ -7,9 +7,10 @@ ******************************************************************************/ #include "py_CustomOpRegistry.h" #include "common/CustomOp.h" -#include -#include -#include +#include +#include +#include +#include namespace cudaq { struct py_unitary_operation : public unitary_operation { @@ -22,7 +23,7 @@ struct py_unitary_operation : public unitary_operation { } }; -void bindCustomOpRegistry(py::module &mod) { +void bindCustomOpRegistry(nanobind::module_ &mod) { mod.def( "register_custom_operation", [&](const std::string &opName) { diff --git a/python/runtime/common/py_CustomOpRegistry.h b/python/runtime/common/py_CustomOpRegistry.h index dcd4f2c2b2e..f9b6d2003eb 100644 --- a/python/runtime/common/py_CustomOpRegistry.h +++ b/python/runtime/common/py_CustomOpRegistry.h @@ -6,11 +6,9 @@ * the terms of the Apache License 2.0 which accompanies this distribution. * ******************************************************************************/ -#include - -namespace py = pybind11; +#include namespace cudaq { /// @brief Bind the custom operation registry to Python. -void bindCustomOpRegistry(py::module &mod); +void bindCustomOpRegistry(nanobind::module_ &mod); } // namespace cudaq diff --git a/python/runtime/common/py_EvolveResult.cpp b/python/runtime/common/py_EvolveResult.cpp index 007acf6577e..6a57cebaa92 100644 --- a/python/runtime/common/py_EvolveResult.cpp +++ b/python/runtime/common/py_EvolveResult.cpp @@ -9,36 +9,36 @@ #include "py_EvolveResult.h" #include "common/EvolveResult.h" #include "cudaq/algorithms/evolve_internal.h" -#include -#include - -namespace py = pybind11; +#include +#include +#include namespace cudaq { /// @brief Bind the `cudaq::evolve_result` and `cudaq::async_evolve_result` /// data classes to python as `cudaq.EvolveResult` and /// `cudaq.AsyncEvolveResult`. -void bindEvolveResult(py::module &mod) { - py::class_( +void bindEvolveResult(nanobind::module_ &mod) { + nanobind::class_( mod, "EvolveResult", "Stores the execution data from an invocation of :func:`evolve`.\n") // IMPORTANT: state overloads must be provided before vector // overloads. Otherwise, Python might try to access the __len__ of state // during overload resolution. __len__ is not always well-defined for all // state types and may raise an exception. - .def(py::init()) - .def(py::init>()) - .def(py::init>()) - .def(py::init>()) - .def(py::init, - std::vector>>()) - .def(py::init, std::vector>>()) + .def(nanobind::init()) + .def(nanobind::init>()) + .def(nanobind::init>()) + .def(nanobind::init>()) + .def(nanobind::init, + std::vector>>()) + .def(nanobind::init, + std::vector>>()) .def( "final_state", - [](evolve_result &self) -> py::object { + [](evolve_result &self) -> nanobind::object { if (!self.states.has_value() || self.states->empty()) - return py::none(); - return py::cast(self.states->back()); + return nanobind::none(); + return nanobind::cast(self.states->back()); }, "Stores the final state produced by a call to :func:`evolve`. " "Represent the state of a quantum system after time evolution under " @@ -54,11 +54,11 @@ void bindEvolveResult(py::module &mod) { ":func:`evolve`.\n") .def( "final_expectation_values", - [](evolve_result &self) -> py::object { + [](evolve_result &self) -> nanobind::object { if (!self.expectation_values.has_value() || self.expectation_values->empty()) - return py::none(); - return py::cast(self.expectation_values->back()); + return nanobind::none(); + return nanobind::cast(self.expectation_values->back()); }, "Stores the final expectation values, that is the results produced " "by " @@ -81,12 +81,12 @@ void bindEvolveResult(py::module &mod) { "if no intermediate results were requested, or if no observables " "were specified in the call.\n"); - py::class_( + nanobind::class_( mod, "AsyncEvolveResult", "Stores the execution data from an invocation of :func:`evolve_async`.\n") .def( "get", [](async_evolve_result &self) { return self.get(); }, - py::call_guard(), + nanobind::call_guard(), "Retrieve the evolution result from the asynchronous evolve " "execution\n."); } diff --git a/python/runtime/common/py_EvolveResult.h b/python/runtime/common/py_EvolveResult.h index 48ddfb9950b..1bafe73cd2d 100644 --- a/python/runtime/common/py_EvolveResult.h +++ b/python/runtime/common/py_EvolveResult.h @@ -6,11 +6,9 @@ * the terms of the Apache License 2.0 which accompanies this distribution. * ******************************************************************************/ -#include - -namespace py = pybind11; +#include namespace cudaq { /// @brief Binds `cudaq.EvolveResult` and `cudaq.AsyncEvolveResult`. -void bindEvolveResult(py::module &mod); +void bindEvolveResult(nanobind::module_ &mod); } // namespace cudaq diff --git a/python/runtime/common/py_ExecutionContext.cpp b/python/runtime/common/py_ExecutionContext.cpp index 83b71fbea5e..132462462de 100644 --- a/python/runtime/common/py_ExecutionContext.cpp +++ b/python/runtime/common/py_ExecutionContext.cpp @@ -12,10 +12,10 @@ #include "cudaq/utils/cudaq_utils.h" #include "mlir/ExecutionEngine/ExecutionEngine.h" #include -#include -#include - -namespace py = pybind11; +#include +#include +#include +#include namespace nvqir { std::string_view getQirOutputLog(); @@ -28,30 +28,29 @@ class PersistJITEngine {}; namespace cudaq { -void bindExecutionContext(py::module &mod) { - py::class_(mod, "ExecutionContext") - .def(py::init()) - .def(py::init(), py::arg("name"), - py::arg("shots"), py::arg("qpu_id") = 0) - .def_readwrite("kernelName", &cudaq::ExecutionContext::kernelName) - .def_readonly("result", &cudaq::ExecutionContext::result) - .def_readwrite("asyncExec", &cudaq::ExecutionContext::asyncExec) - .def_readonly("asyncResult", &cudaq::ExecutionContext::asyncResult) - .def_readwrite("hasConditionalsOnMeasureResults", - &cudaq::ExecutionContext::hasConditionalsOnMeasureResults) - .def_readwrite("totalIterations", - &cudaq::ExecutionContext::totalIterations) - .def_readwrite("batchIteration", &cudaq::ExecutionContext::batchIteration) - .def_readwrite("numberTrajectories", - &cudaq::ExecutionContext::numberTrajectories) - .def_readwrite("explicitMeasurements", - &cudaq::ExecutionContext::explicitMeasurements) - .def_readwrite("allowJitEngineCaching", - &cudaq::ExecutionContext::allowJitEngineCaching) - .def_readwrite("useParametricJit", - &cudaq::ExecutionContext::useParametricJit) - .def_readonly("invocationResultBuffer", - &cudaq::ExecutionContext::invocationResultBuffer) +void bindExecutionContext(nanobind::module_ &mod) { + nanobind::class_(mod, "ExecutionContext") + .def(nanobind::init()) + .def(nanobind::init(), + nanobind::arg("name"), nanobind::arg("shots"), + nanobind::arg("qpu_id") = 0) + .def_rw("kernelName", &cudaq::ExecutionContext::kernelName) + .def_ro("result", &cudaq::ExecutionContext::result) + .def_rw("asyncExec", &cudaq::ExecutionContext::asyncExec) + .def_ro("asyncResult", &cudaq::ExecutionContext::asyncResult) + .def_rw("hasConditionalsOnMeasureResults", + &cudaq::ExecutionContext::hasConditionalsOnMeasureResults) + .def_rw("totalIterations", &cudaq::ExecutionContext::totalIterations) + .def_rw("batchIteration", &cudaq::ExecutionContext::batchIteration) + .def_rw("numberTrajectories", + &cudaq::ExecutionContext::numberTrajectories) + .def_rw("explicitMeasurements", + &cudaq::ExecutionContext::explicitMeasurements) + .def_rw("allowJitEngineCaching", + &cudaq::ExecutionContext::allowJitEngineCaching) + .def_rw("useParametricJit", &cudaq::ExecutionContext::useParametricJit) + .def_ro("invocationResultBuffer", + &cudaq::ExecutionContext::invocationResultBuffer) .def("unset_jit_engine", [&](cudaq::ExecutionContext &execCtx) { if (execCtx.jitEng) { @@ -68,44 +67,50 @@ void bindExecutionContext(py::module &mod) { [](cudaq::ExecutionContext &ctx) { return ctx.expectationValue; }) // ----- Context management using with blocks ----- // Unlike in C++, we do not support nested execution contexts in Python. - .def("__enter__", - [](cudaq::ExecutionContext &ctx) -> ExecutionContext & { - if (cudaq::getExecutionContext()) { - throw std::runtime_error("Context already set. Nested execution " - "contexts are not supported in Python"); - } - auto &platform = cudaq::get_platform(); - platform.configureExecutionContext(ctx); - cudaq::detail::setExecutionContext(&ctx); - platform.beginExecution(); - return ctx; - }) - .def("__exit__", [](cudaq::ExecutionContext &ctx, py::object type, - py::object value, py::object traceback) { - if (type.is_none()) { - // Normal exit: finalize results, clean up the simulator, - // and reset the context (guaranteed even if finalize throws). - auto &platform = cudaq::get_platform(); - detail::try_finally( - [&] { + .def( + "__enter__", + [](cudaq::ExecutionContext &ctx) -> ExecutionContext & { + if (cudaq::getExecutionContext()) { + throw std::runtime_error("Context already set. Nested execution " + "contexts are not supported in Python"); + } + auto &platform = cudaq::get_platform(); + platform.configureExecutionContext(ctx); + cudaq::detail::setExecutionContext(&ctx); + platform.beginExecution(); + return ctx; + }, + nanobind::rv_policy::reference) + .def( + "__exit__", + [](cudaq::ExecutionContext &ctx, nanobind::object type, + nanobind::object value, nanobind::object traceback) { + if (type.is_none()) { + // Normal exit: finalize results, clean up the simulator, + // and reset the context (guaranteed even if finalize throws). + auto &platform = cudaq::get_platform(); + detail::try_finally( + [&] { + platform.finalizeExecutionContext(ctx); + platform.endExecution(); + }, + detail::resetExecutionContext); + } else { + // The kernel threw. Still need to tear down the platform so + // the simulator doesn't carry stale state into the next run. + // Separate invoke_no_throw so the context reset always runs. + detail::invoke_no_throw([&] { + auto &platform = cudaq::get_platform(); platform.finalizeExecutionContext(ctx); platform.endExecution(); - }, - detail::resetExecutionContext); - } else { - // The kernel threw. Still need to tear down the platform so - // the simulator doesn't carry stale state into the next run. - // Separate invoke_no_throw so the context reset always runs. - detail::invoke_no_throw([&] { - auto &platform = cudaq::get_platform(); - platform.finalizeExecutionContext(ctx); - platform.endExecution(); - }); - // Always reset context, even if the above cleanup failed. - detail::invoke_no_throw(detail::resetExecutionContext); - } - return false; - }); + }); + // Always reset context, even if the above cleanup failed. + detail::invoke_no_throw(detail::resetExecutionContext); + } + return false; + }, + nanobind::arg("type").none(), nanobind::arg("value").none(), + nanobind::arg("traceback").none()); mod.def("supportsExplicitMeasurements", []() { auto &platform = cudaq::get_platform(); return platform.supports_explicit_measurements(); @@ -121,33 +126,35 @@ void bindExecutionContext(py::module &mod) { return !isRemoteSimulator && (platform.is_remote() || platform.is_emulated()); }, - py::arg("qpuId") = 0); + nanobind::arg("qpuId") = 0); mod.def("getQirOutputLog", []() { return nvqir::getQirOutputLog(); }); mod.def("clearQirOutputLog", []() { nvqir::clearQirOutputLog(); }); mod.def("decodeQirOutputLog", - [](const std::string &outputLog, py::buffer decodedResults) { + [](const std::string &outputLog, nanobind::bytearray decodedResults) { cudaq::RecordLogParser parser; parser.parse(outputLog); - auto info = decodedResults.request(); - // Get the buffer and length of buffer (in bytes) from the parser. auto *origBuffer = parser.getBufferPtr(); const std::size_t bufferSize = parser.getBufferSize(); - std::memcpy(info.ptr, origBuffer, bufferSize); + std::memcpy(decodedResults.data(), origBuffer, bufferSize); }); - py::class_( + nanobind::class_( mod, "reuse_compiler_artifacts", "Within this context, CUDAQ will blindly reuse compiled objects." "It is up to the user to ensure that there are never two distinct" "computations launched within a single context.") - .def(py::init()) + .def(nanobind::init<>()) .def("__enter__", [](PersistJITEngine &ctx) -> void { cudaq::compiler_artifact::enablePersistentJITEngine(); }) - .def("__exit__", [](PersistJITEngine &ctx, py::object type, - py::object value, py::object traceback) { - cudaq::compiler_artifact::disablePersistentJITEngine(); - }); + .def( + "__exit__", + [](PersistJITEngine &ctx, nanobind::object type, + nanobind::object value, nanobind::object traceback) { + cudaq::compiler_artifact::disablePersistentJITEngine(); + }, + nanobind::arg("type").none(), nanobind::arg("value").none(), + nanobind::arg("traceback").none()); } } // namespace cudaq diff --git a/python/runtime/common/py_ExecutionContext.h b/python/runtime/common/py_ExecutionContext.h index d4004941135..7df4e909b43 100644 --- a/python/runtime/common/py_ExecutionContext.h +++ b/python/runtime/common/py_ExecutionContext.h @@ -8,10 +8,8 @@ #pragma once -#include - -namespace py = pybind11; +#include namespace cudaq { -void bindExecutionContext(py::module &mod); +void bindExecutionContext(nanobind::module_ &mod); } // namespace cudaq diff --git a/python/runtime/common/py_NoiseModel.cpp b/python/runtime/common/py_NoiseModel.cpp index 82563f86284..cf4f96b85cc 100644 --- a/python/runtime/common/py_NoiseModel.cpp +++ b/python/runtime/common/py_NoiseModel.cpp @@ -10,43 +10,41 @@ #include "common/NoiseModel.h" #include "cudaq.h" #include -#include -#include -#include +#include +#include +#include +#include +#include +#include namespace cudaq { -/// @brief Extract the array data from a buffer_info into our +/// @brief Extract the array data from a 2-d ndarray into our /// own allocated data pointer. /// This supports 2-d array in either row or column major. -void extractKrausData(py::buffer_info &info, complex *data) { - if (info.format != py::format_descriptor::format()) - throw std::runtime_error( - "Incompatible buffer format, must be np.complex128."); - - if (info.ndim != 2) - throw std::runtime_error("Incompatible buffer shape " + - std::to_string(info.ndim) + "."); +void extractKrausData(nanobind::ndarray, nanobind::ndim<2>, + nanobind::c_contig> + arr, + complex *data) { + auto rows = arr.shape(0); + auto cols = arr.shape(1); + auto *srcData = static_cast *>(arr.data()); constexpr bool rowMajor = true; - typedef Eigen::MatrixXcd::Scalar Scalar; typedef Eigen::Matrix, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor> RowMajorMat; auto strides = Eigen::Stride( - info.strides[rowMajor ? 0 : 1] / (py::ssize_t)sizeof(Scalar), - info.strides[rowMajor ? 1 : 0] / (py::ssize_t)sizeof(Scalar)); - auto map = - Eigen::Map>( - static_cast(info.ptr), info.shape[0], info.shape[1], - strides); + arr.stride(rowMajor ? 0 : 1), arr.stride(rowMajor ? 1 : 0)); + auto map = Eigen::Map>( + srcData, rows, cols, strides); RowMajorMat eigenMat(map); - memcpy(data, eigenMat.data(), - sizeof(complex) * (info.shape[0] * info.shape[1])); + memcpy(data, eigenMat.data(), sizeof(complex) * (rows * cols)); } /// @brief Bind the cudaq::noise_model, kraus_op, and kraus_channel. -void bindNoiseModel(py::module &mod) { +void bindNoiseModel(nanobind::module_ &mod) { mod.def("set_noise", &set_noise, "Set the underlying noise model."); mod.def("unset_noise", &unset_noise, @@ -54,86 +52,87 @@ void bindNoiseModel(py::module &mod) { mod.def( "get_noise", []() { return cudaq::get_platform().get_noise(); }, "Get the underlying noise model."); - py::class_( + nanobind::class_( mod, "NoiseModel", "The `NoiseModel` defines a set of :class:`KrausChannel`'s applied to " "specific qubits after the invocation of specified quantum operations.") - .def(py::init<>([mod]() { - // Create the noise model - auto model = std::make_unique(); - - // Define a map of channel names to generator functions - static std::map &)>> - channelGenerators = { - {"DepolarizationChannel", - [](const std::vector &p) -> kraus_channel { - return depolarization_channel(p); - }}, - {"AmplitudeDampingChannel", - [](const std::vector &p) -> kraus_channel { - return amplitude_damping_channel(p); - }}, - {"BitFlipChannel", - [](const std::vector &p) -> kraus_channel { - return bit_flip_channel(p); - }}, - {"PhaseFlipChannel", - [](const std::vector &p) -> kraus_channel { - return phase_flip_channel(p); - }}, - {"XError", - [](const std::vector &p) -> kraus_channel { - return x_error(p); - }}, - {"YError", - [](const std::vector &p) -> kraus_channel { - return y_error(p); - }}, - {"ZError", - [](const std::vector &p) -> kraus_channel { - return z_error(p); - }}, - {"PhaseDamping", - [](const std::vector &p) -> kraus_channel { - return phase_damping(p); - }}, - {"Pauli1", - [](const std::vector &p) -> kraus_channel { - return pauli1(p); - }}, - {"Pauli2", - [](const std::vector &p) -> kraus_channel { - return pauli2(p); - }}, - {"Depolarization1", - [](const std::vector &p) -> kraus_channel { - return depolarization1(p); - }}, - {"Depolarization2", - [](const std::vector &p) -> kraus_channel { - return depolarization2(p); - }}}; - - // Register each channel generator - for (const auto &[name, generator] : channelGenerators) { - if (py::hasattr(mod, name.c_str())) { - py::type channelType = py::getattr(mod, name.c_str()); - auto key = py::hash(channelType); - model->register_channel(key, generator); - } - } - - return model; - }), - "Construct a noise model with all built-in channels pre-registered.") + .def( + "__init__", + [mod](noise_model *self) { + new (self) noise_model(); + + // Define a map of channel names to generator functions + static std::map &)>> + channelGenerators = { + {"DepolarizationChannel", + [](const std::vector &p) -> kraus_channel { + return depolarization_channel(p); + }}, + {"AmplitudeDampingChannel", + [](const std::vector &p) -> kraus_channel { + return amplitude_damping_channel(p); + }}, + {"BitFlipChannel", + [](const std::vector &p) -> kraus_channel { + return bit_flip_channel(p); + }}, + {"PhaseFlipChannel", + [](const std::vector &p) -> kraus_channel { + return phase_flip_channel(p); + }}, + {"XError", + [](const std::vector &p) -> kraus_channel { + return x_error(p); + }}, + {"YError", + [](const std::vector &p) -> kraus_channel { + return y_error(p); + }}, + {"ZError", + [](const std::vector &p) -> kraus_channel { + return z_error(p); + }}, + {"PhaseDamping", + [](const std::vector &p) -> kraus_channel { + return phase_damping(p); + }}, + {"Pauli1", + [](const std::vector &p) -> kraus_channel { + return pauli1(p); + }}, + {"Pauli2", + [](const std::vector &p) -> kraus_channel { + return pauli2(p); + }}, + {"Depolarization1", + [](const std::vector &p) -> kraus_channel { + return depolarization1(p); + }}, + {"Depolarization2", + [](const std::vector &p) -> kraus_channel { + return depolarization2(p); + }}}; + + // Register each channel generator + for (const auto &[name, generator] : channelGenerators) { + if (nanobind::hasattr(mod, name.c_str())) { + nanobind::type_object channelType = + nanobind::borrow( + nanobind::getattr(mod, name.c_str())); + auto key = nanobind::hash(channelType); + self->register_channel(key, generator); + } + } + }, + "Construct a noise model with all built-in channels pre-registered.") .def( "register_channel", - [](noise_model &self, const py::type krausT) { - auto key = py::hash(krausT); + [](noise_model &self, const nanobind::type_object krausT) { + auto key = nanobind::hash(krausT); std::function &)> lambda = [krausT](const std::vector &p) -> kraus_channel { - return krausT(p).cast(); + return nanobind::cast(krausT(p)); }; self.register_channel(key, lambda); }, @@ -144,7 +143,8 @@ void bindNoiseModel(py::module &mod) { std::vector &qubits, kraus_channel &channel) { self.add_channel(opName, qubits, channel); }, - py::arg("operator"), py::arg("qubits"), py::arg("channel"), + nanobind::arg("operator"), nanobind::arg("qubits"), + nanobind::arg("channel"), R"#(Add the given :class:`KrausChannel` to be applied after invocation of the specified quantum operation. @@ -159,7 +159,7 @@ of the specified quantum operation. const noise_model::PredicateFuncTy &pre) { self.add_channel(opName, pre); }, - py::arg("operator"), py::arg("pre"), + nanobind::arg("operator"), nanobind::arg("pre"), R"#(Add the given :class:`KrausChannel` generator callback to be applied after invocation of the specified quantum operation. @@ -173,7 +173,8 @@ of the specified quantum operation. std::size_t num_controls = 0) { self.add_all_qubit_channel(opName, channel, num_controls); }, - py::arg("operator"), py::arg("channel"), py::arg("num_controls") = 0, + nanobind::arg("operator"), nanobind::arg("channel"), + nanobind::arg("num_controls") = 0, R"#(Add the given :class:`KrausChannel` to be applied after invocation of the specified quantum operation on arbitrary qubits. @@ -189,7 +190,7 @@ of the specified quantum operation on arbitrary qubits. const std::vector &qubits) { return self.get_channels(op, qubits); }, - py::arg("operator"), py::arg("qubits"), + nanobind::arg("operator"), nanobind::arg("qubits"), "Return the :class:`KrausChannel`'s that make up this noise model.") .def( "get_channels", @@ -198,35 +199,44 @@ of the specified quantum operation on arbitrary qubits. const std::vector &controls) { return self.get_channels(op, qubits, controls); }, - py::arg("operator"), py::arg("qubits"), py::arg("controls"), + nanobind::arg("operator"), nanobind::arg("qubits"), + nanobind::arg("controls"), "Return the :class:`KrausChannel`'s that make up this noise model."); } -void bindKrausOp(py::module &mod) { - py::class_( - mod, "KrausOperator", py::buffer_protocol(), +void bindKrausOp(nanobind::module_ &mod) { + nanobind::class_( + mod, "KrausOperator", "The `KrausOperator` is represented by a matrix and serves as an element " "of a quantum channel such that :code:`Sum Ki Ki^dag = I.`") - .def_buffer([](kraus_op &op) -> py::buffer_info { - return py::buffer_info(op.data.data(), sizeof(complex), - py::format_descriptor::format(), 2, - {op.nRows, op.nCols}, - {sizeof(complex) * op.nCols, sizeof(complex)}); - }) - .def(py::init([](const py::buffer &b) { - py::buffer_info info = b.request(); - std::vector v(info.shape[0] * info.shape[1]); - extractKrausData(info, v.data()); - return kraus_op(v); - }), - "Create a :class:`KrausOperator` from a buffer of data, like a " - "numpy array.") - .def_readonly("row_count", &kraus_op::nRows, - "The number of rows in the matrix representation of this " - ":class:`KrausOperator`.") - .def_readonly("col_count", &kraus_op::nCols, - "The number of columns in the matrix representation of " - "this :class:`KrausOperator`."); + .def( + "__array__", + [](kraus_op &op, nanobind::object dtype_obj, + nanobind::object copy_obj) { + size_t shape[2] = {op.nRows, op.nCols}; + return nanobind::ndarray>( + op.data.data(), 2, shape, nanobind::handle()); + }, + nanobind::arg("dtype") = nanobind::none(), + nanobind::arg("copy") = nanobind::none()) + .def( + "__init__", + [](kraus_op *self, + nanobind::ndarray, nanobind::ndim<2>, + nanobind::c_contig> + arr) { + std::vector v(arr.shape(0) * arr.shape(1)); + extractKrausData(arr, v.data()); + new (self) kraus_op(v); + }, + "Create a :class:`KrausOperator` from a buffer of data, like a " + "numpy array.") + .def_ro("row_count", &kraus_op::nRows, + "The number of rows in the matrix representation of this " + ":class:`KrausOperator`.") + .def_ro("col_count", &kraus_op::nCols, + "The number of columns in the matrix representation of " + "this :class:`KrausOperator`."); } // Need a trampoline class to make this sub-class-able from Python @@ -235,8 +245,8 @@ class PyKrausChannel : public kraus_channel { using kraus_channel::kraus_channel; }; -void bindNoiseChannels(py::module &mod) { - py::enum_(mod, "NoiseModelType") +void bindNoiseChannels(nanobind::module_ &mod) { + nanobind::enum_(mod, "NoiseModelType") .value("Unknown", cudaq::noise_model_type::unknown) .value("DepolarizationChannel", cudaq::noise_model_type::depolarization_channel) @@ -254,46 +264,50 @@ void bindNoiseChannels(py::module &mod) { .value("Depolarization1", cudaq::noise_model_type::depolarization1) .value("Depolarization2", cudaq::noise_model_type::depolarization2); - py::class_( - mod, "KrausChannel", py::dynamic_attr(), + nanobind::class_( + mod, "KrausChannel", "The `KrausChannel` is composed of a list of " ":class:`KrausOperator`'s and " "is applied to a specific qubit or set of qubits.") - .def(py::init<>(), "Create an empty :class:`KrausChannel`") - .def(py::init &>(), + .def(nanobind::init<>(), "Create an empty :class:`KrausChannel`") + .def(nanobind::init &>(), "Create a :class:`KrausChannel` composed of a list of " ":class:`KrausOperator`'s.") - .def(py::init([](py::list ops) { - std::vector kops; - for (std::size_t i = 0; i < ops.size(); i++) { - auto buffer = ops[i].cast(); - auto info = buffer.request(); - auto shape = info.shape; - std::vector v(shape[0] * shape[1]); - extractKrausData(info, v.data()); - kops.emplace_back(v); - } - return kraus_channel(kops); - }), - "Create a :class:`KrausChannel` given a list of " - ":class:`KrausOperator`'s.") - .def_readwrite("parameters", &kraus_channel::parameters) - .def_readwrite("noise_type", &kraus_channel::noise_type) + .def( + "__init__", + [](kraus_channel *self, nanobind::list ops) { + std::vector kops; + for (std::size_t i = 0; i < ops.size(); i++) { + auto arr = nanobind::cast, nanobind::ndim<2>, nanobind::c_contig>>( + ops[i]); + auto rows = arr.shape(0); + auto cols = arr.shape(1); + std::vector v(rows * cols); + extractKrausData(arr, v.data()); + kops.emplace_back(v); + } + new (self) kraus_channel(kops); + }, + "Create a :class:`KrausChannel` given a list of " + ":class:`KrausOperator`'s.") + .def_rw("parameters", &kraus_channel::parameters) + .def_rw("noise_type", &kraus_channel::noise_type) .def("get_ops", &kraus_channel::get_ops, "Return the :class:`KrausOperator`'s in this :class:`KrausChannel`.") .def( "__getitem__", [](kraus_channel &self, std::size_t idx) { return self[idx]; }, - py::arg("index"), + nanobind::arg("index"), "Return the :class:`KrausOperator` at the given index in this " ":class:`KrausChannel`.") .def( "append", [](kraus_channel &self, kraus_op op) { self.push_back(op); }, - py::arg("operator"), + nanobind::arg("operator"), "Add a :class:`KrausOperator` to this :class:`KrausChannel`."); - py::class_( + nanobind::class_( mod, "DepolarizationChannel", R"#(Models the decoherence of the qubit state and phase into a mixture " of the computational basis states, `|0>` and `|1>`. @@ -318,15 +332,15 @@ void bindNoiseChannels(py::module &mod) { For `probability = 0.0`, the channel will behave noise-free. For `probability = 0.75`, the channel will fully depolarize the state. For `probability = 1.0`, the channel will be uniform.)#") - .def(py::init>()) - .def(py::init(), py::arg("probability"), + .def(nanobind::init>()) + .def(nanobind::init(), nanobind::arg("probability"), "Initialize the `DepolarizationChannel` with the provided " "`probability`.") - .def_readonly_static( + .def_ro_static( "num_parameters", &depolarization_channel::num_parameters, "The number of parameters this channel requires at construction."); - py::class_( + nanobind::class_( mod, "AmplitudeDampingChannel", R"#(Models the dissipation of energy due to system interactions with the environment. @@ -341,15 +355,15 @@ void bindNoiseChannels(py::module &mod) { representing the probability that the qubit will decay to its ground state. The probability of the qubit remaining in the same state is therefore `1 - probability`.)#") - .def(py::init>()) - .def(py::init(), py::arg("probability"), + .def(nanobind::init>()) + .def(nanobind::init(), nanobind::arg("probability"), "Initialize the `AmplitudeDampingChannel` with the provided " "`probability`.") - .def_readonly_static( + .def_ro_static( "num_parameters", &litude_damping_channel::num_parameters, "The number of parameters this channel requires at construction."); - py::class_( + nanobind::class_( mod, "BitFlipChannel", R"#(Models the decoherence of the qubit state. Its constructor expects a float value, `probability`, representing the probability that the qubit @@ -364,14 +378,14 @@ void bindNoiseChannels(py::module &mod) { The probability of the qubit remaining in the same state is therefore `1 - probability`.)#") - .def(py::init>()) - .def(py::init(), py::arg("probability"), + .def(nanobind::init>()) + .def(nanobind::init(), nanobind::arg("probability"), "Initialize the `BitFlipChannel` with the provided `probability`.") - .def_readonly_static( + .def_ro_static( "num_parameters", &bit_flip_channel::num_parameters, "The number of parameters this channel requires at construction."); - py::class_( + nanobind::class_( mod, "PhaseFlipChannel", R"#(Models the decoherence of the qubit phase. Its constructor expects a float value, `probability`, representing the probability of a random @@ -385,95 +399,95 @@ void bindNoiseChannels(py::module &mod) { The probability of the qubit phase remaining untouched is therefore `1 - probability`.)#") - .def(py::init>()) - .def(py::init(), py::arg("probability"), + .def(nanobind::init>()) + .def(nanobind::init(), nanobind::arg("probability"), "Initialize the `PhaseFlipChannel` with the provided `probability`.") - .def_readonly_static( + .def_ro_static( "num_parameters", &phase_flip_channel::num_parameters, "The number of parameters this channel requires at construction."); - py::class_( + nanobind::class_( mod, "PhaseDamping", R"#(A Kraus channel that models the single-qubit phase damping error. This is similar to AmplitudeDamping, but for phase.)#") - .def(py::init>()) - .def(py::init()) - .def_readonly_static( + .def(nanobind::init>()) + .def(nanobind::init()) + .def_ro_static( "num_parameters", &phase_damping::num_parameters, "The number of parameters this channel requires at construction."); - py::class_( + nanobind::class_( mod, "ZError", R"#(A Pauli error that applies the Z operator when an error occurs. It is the same as PhaseFlipChannel.)#") - .def(py::init>()) - .def(py::init()) - .def_readonly_static( + .def(nanobind::init>()) + .def(nanobind::init()) + .def_ro_static( "num_parameters", &z_error::num_parameters, "The number of parameters this channel requires at construction."); - py::class_( + nanobind::class_( mod, "XError", R"#(A Pauli error that applies the X operator when an error occurs. It is the same as BitFlipChannel.)#") - .def(py::init>()) - .def(py::init()) - .def_readonly_static( + .def(nanobind::init>()) + .def(nanobind::init()) + .def_ro_static( "num_parameters", &x_error::num_parameters, "The number of parameters this channel requires at construction."); - py::class_( + nanobind::class_( mod, "YError", R"#(A Pauli error that applies the Y operator when an error occurs.)#") - .def(py::init>()) - .def(py::init()) - .def_readonly_static( + .def(nanobind::init>()) + .def(nanobind::init()) + .def_ro_static( "num_parameters", &y_error::num_parameters, "The number of parameters this channel requires at construction."); - py::class_( + nanobind::class_( mod, "Pauli1", R"#(A single-qubit Pauli error that applies either an X error, Y error, or Z error. The probability of each X, Y, or Z error is supplied as a parameter.)#") - .def(py::init>()) - .def_readonly_static( + .def(nanobind::init>()) + .def_ro_static( "num_parameters", &pauli1::num_parameters, "The number of parameters this channel requires at construction."); - py::class_( + nanobind::class_( mod, "Pauli2", R"#(A 2-qubit Pauli error that applies one of the following errors, with the probabilities specified as a vector. Possible errors: IX, IY, IZ, XI, XX, XY, XZ, YI, YX, YY, YZ, ZI, ZX, ZY, and ZZ.)#") - .def(py::init>()) - .def_readonly_static( + .def(nanobind::init>()) + .def_ro_static( "num_parameters", &pauli2::num_parameters, "The number of parameters this channel requires at construction."); - py::class_( + nanobind::class_( mod, "Depolarization1", R"#(The same as DepolarizationChannel (single qubit depolarization))#") - .def(py::init>()) - .def(py::init()) - .def_readonly_static( + .def(nanobind::init>()) + .def(nanobind::init()) + .def_ro_static( "num_parameters", &depolarization1::num_parameters, "The number of parameters this channel requires at construction."); - py::class_( + nanobind::class_( mod, "Depolarization2", R"#(A 2-qubit depolarization error that applies one of the following errors. Possible errors: IX, IY, IZ, XI, XX, XY, XZ, YI, YX, YY, YZ, ZI, ZX, ZY, and ZZ.)#") - .def(py::init>()) - .def(py::init()) - .def_readonly_static( + .def(nanobind::init>()) + .def(nanobind::init()) + .def_ro_static( "num_parameters", &depolarization2::num_parameters, "The number of parameters this channel requires at construction."); } -void bindNoise(py::module &mod) { +void bindNoise(nanobind::module_ &mod) { bindNoiseModel(mod); bindKrausOp(mod); bindNoiseChannels(mod); diff --git a/python/runtime/common/py_NoiseModel.h b/python/runtime/common/py_NoiseModel.h index 75c0f0b8df7..cc03a52e138 100644 --- a/python/runtime/common/py_NoiseModel.h +++ b/python/runtime/common/py_NoiseModel.h @@ -6,11 +6,9 @@ * the terms of the Apache License 2.0 which accompanies this distribution. * ******************************************************************************/ -#include - -namespace py = pybind11; +#include namespace cudaq { /// @brief Bind the cudaq::noise_model data-type to Python. -void bindNoise(py::module &mod); +void bindNoise(nanobind::module_ &mod); } // namespace cudaq diff --git a/python/runtime/common/py_ObserveResult.cpp b/python/runtime/common/py_ObserveResult.cpp index d26185673ee..5383391b9dc 100644 --- a/python/runtime/common/py_ObserveResult.cpp +++ b/python/runtime/common/py_ObserveResult.cpp @@ -11,21 +11,23 @@ #include "common/ObserveResult.h" #include "cudaq/algorithms/observe.h" -namespace py = pybind11; +#include +#include + namespace { // FIXME(OperatorCpp): Remove this when the operator class is implemented in // C++ -cudaq::spin_op to_spin_op(py::object &obj) { - if (py::hasattr(obj, "_to_spinop")) - return obj.attr("_to_spinop")().cast(); - return obj.cast(); +cudaq::spin_op to_spin_op(nanobind::object &obj) { + if (nanobind::hasattr(obj, "_to_spinop")) + return nanobind::cast(obj.attr("_to_spinop")()); + return nanobind::cast(obj); } -cudaq::spin_op to_spin_op_term(py::object &obj) { +cudaq::spin_op to_spin_op_term(nanobind::object &obj) { auto op = cudaq::spin_op::empty(); - if (py::hasattr(obj, "_to_spinop")) - op = obj.attr("_to_spinop")().cast(); + if (nanobind::hasattr(obj, "_to_spinop")) + op = nanobind::cast(obj.attr("_to_spinop")()); else - op = obj.cast(); + op = nanobind::cast(obj); if (op.num_terms() != 1) throw std::invalid_argument("expecting a spin op with a single term"); return *op.begin(); @@ -46,21 +48,23 @@ namespace cudaq { /// @brief Bind the `cudaq::observe_result` and `cudaq::async_observe_result` /// data classes to python as `cudaq.ObserveResult` and /// `cudaq.AsyncObserveResult`. -void bindObserveResult(py::module &mod) { - py::class_( +void bindObserveResult(nanobind::module_ &mod) { + nanobind::class_( mod, "ObserveResult", "A data-type containing the results of a call to :func:`observe`. " "This includes any measurement counts data, as well as the global " "expectation value of the user-defined `spin_operator`.\n") - .def(py::init()) - .def(py::init( - [](double exp_val, const spin_op &spin_op, sample_result result) { - return observe_result(exp_val, spin_op, result); - })) - .def(py::init( - [](double exp_val, py::object spin_op, sample_result result) { - return observe_result(exp_val, to_spin_op(spin_op), result); - })) + .def(nanobind::init()) + .def("__init__", + [](observe_result *self, double exp_val, const spin_op &spin_op, + sample_result result) { + new (self) observe_result(exp_val, spin_op, result); + }) + .def("__init__", + [](observe_result *self, double exp_val, nanobind::object spin_op, + sample_result result) { + new (self) observe_result(exp_val, to_spin_op(spin_op), result); + }) /// @brief Bind the member functions of `cudaq.ObserveResult`. .def("dump", &observe_result::dump, "Dump the raw data from the :class:`SampleResult` that are stored " @@ -79,18 +83,18 @@ void bindObserveResult(py::module &mod) { [](observe_result &self, const spin_op_term &sub_term) { return self.counts(sub_term); }, - py::arg("sub_term"), "") + nanobind::arg("sub_term"), "") .def( "counts", - [](observe_result &self, py::object sub_term) { + [](observe_result &self, nanobind::object sub_term) { return self.counts(to_spin_op_term(sub_term)); }, - py::arg("sub_term"), - R"#(Given a `sub_term` of the global `spin_operator` that was passed + nanobind::arg("sub_term"), + R"#(Given a `sub_term` of the global `spin_operator` that was passed to :func:`observe`, return its measurement counts. Args: - sub_term (`SpinOperator`): An individual sub-term of the + sub_term (`SpinOperator`): An individual sub-term of the `spin_operator`. Returns: @@ -104,7 +108,7 @@ to :func:`observe`, return its measurement counts. 1); return self.counts(sub_term); }, - py::arg("sub_term"), + nanobind::arg("sub_term"), "Deprecated - ensure to pass a SpinOperatorTerm instead of a " "SpinOperator") .def( @@ -117,22 +121,22 @@ to :func:`observe`, return its measurement counts. [](observe_result &self, const spin_op_term &spin_term) { return self.expectation(spin_term); }, - py::arg("sub_term"), "") + nanobind::arg("sub_term"), "") .def( "expectation", - [](observe_result &self, py::object spin_term) { + [](observe_result &self, nanobind::object spin_term) { return self.expectation(to_spin_op_term(spin_term)); }, - py::arg("sub_term"), - R"#(Return the expectation value of an individual `sub_term` of the + nanobind::arg("sub_term"), + R"#(Return the expectation value of an individual `sub_term` of the global `spin_operator` that was passed to :func:`observe`. Args: - sub_term (:class:`SpinOperatorTerm`): An individual sub-term of the + sub_term (:class:`SpinOperatorTerm`): An individual sub-term of the `spin_operator`. Returns: - float : The expectation value of the `sub_term` with respect to the + float : The expectation value of the `sub_term` with respect to the :class:`Kernel` that was passed to :func:`observe`.)#") .def( "expectation", @@ -144,36 +148,37 @@ global `spin_operator` that was passed to :func:`observe`. return self.expectation(spin_term); }, - py::arg("sub_term"), + nanobind::arg("sub_term"), "Deprecated - ensure to pass a SpinOperatorTerm instead of a " "SpinOperator"); - py::class_( + nanobind::class_( mod, "AsyncObserveResult", - R"#(A data-type containing the results of a call to :func:`observe_async`. - -The `AsyncObserveResult` contains a future, whose :class:`ObserveResult` -may be returned via an invocation of the `get` method. + R"#(A data-type containing the results of a call to :func:`observe_async`. + +The `AsyncObserveResult` contains a future, whose :class:`ObserveResult` +may be returned via an invocation of the `get` method. This kicks off a wait on the current thread until the results are available. See `future `_ for more information on this programming pattern.)#") - .def(py::init([](std::string inJson, spin_op op) { - async_observe_result f(&op); - std::istringstream is(inJson); - is >> f; - return f; - })) - .def(py::init([](std::string inJson, py::object op) { - auto as_spin_op = to_spin_op(op); - async_observe_result f(&as_spin_op); - std::istringstream is(inJson); - is >> f; - return f; - })) + .def("__init__", + [](async_observe_result *self, std::string inJson, spin_op op) { + new (self) async_observe_result(&op); + std::istringstream is(inJson); + is >> *self; + }) + .def("__init__", + [](async_observe_result *self, std::string inJson, + nanobind::object op) { + auto as_spin_op = to_spin_op(op); + new (self) async_observe_result(&as_spin_op); + std::istringstream is(inJson); + is >> *self; + }) .def("get", &async_observe_result::get, - py::call_guard(), + nanobind::call_guard(), "Returns the :class:`ObserveResult` from the asynchronous observe " "execution.") .def("__str__", [](async_observe_result &self) { diff --git a/python/runtime/common/py_ObserveResult.h b/python/runtime/common/py_ObserveResult.h index 920a09c78e1..823d0b0ee6a 100644 --- a/python/runtime/common/py_ObserveResult.h +++ b/python/runtime/common/py_ObserveResult.h @@ -6,11 +6,9 @@ * the terms of the Apache License 2.0 which accompanies this distribution. * ******************************************************************************/ -#include - -namespace py = pybind11; +#include namespace cudaq { /// @brief Binds `cudaq.ObserveResult` and `cudaq.AsyncObserveResult`. -void bindObserveResult(py::module &mod); +void bindObserveResult(nanobind::module_ &mod); } // namespace cudaq diff --git a/python/runtime/common/py_Resources.cpp b/python/runtime/common/py_Resources.cpp index c777b185aaa..07098a83377 100644 --- a/python/runtime/common/py_Resources.cpp +++ b/python/runtime/common/py_Resources.cpp @@ -6,8 +6,11 @@ * the terms of the Apache License 2.0 which accompanies this distribution. * ******************************************************************************/ -#include -#include +#include +#include +#include +#include +#include #include "py_Resources.h" @@ -17,14 +20,14 @@ namespace cudaq { -void bindResources(py::module &mod) { +void bindResources(nanobind::module_ &mod) { using namespace cudaq; - py::class_( + nanobind::class_( mod, "Resources", - R"#(A data-type containing the results of a call to :func:`estimate_resources`. + R"#(A data-type containing the results of a call to :func:`estimate_resources`. This includes all gate counts.)#") - .def(py::init<>()) + .def(nanobind::init<>()) .def( "dump", [](Resources &self) { self.dump(); }, "Print a string of the raw resource counts data to the " @@ -59,41 +62,33 @@ This includes all gate counts.)#") "to_dict", [](Resources &self) { return self.gateCounts(); }, "Return a dictionary of the raw resource counts that are stored in " "`self`.\n") - .def_property_readonly( - "num_qubits", &Resources::getNumQubits, - "The total number of qubits allocated in the kernel.\n") - .def_property_readonly( - "num_used_qubits", &Resources::getNumUsedQubits, - "The number of qubits touched by at least one quantum " - "operation.\n") - .def_property_readonly( - "depth", &Resources::getCircuitDepth, - "The circuit depth (longest gate chain on any qubit).\n") - .def_property_readonly( + .def_prop_ro("num_qubits", &Resources::getNumQubits, + "The total number of qubits allocated in the kernel.\n") + .def_prop_ro("num_used_qubits", &Resources::getNumUsedQubits, + "The number of qubits touched by at least one quantum " + "operation.\n") + .def_prop_ro("depth", &Resources::getCircuitDepth, + "The circuit depth (longest gate chain on any qubit).\n") + .def_prop_ro( "gate_count_by_arity", - [](Resources &self) { - return py::dict(py::cast(self.getGateCountsByArity())); - }, + [](Resources &self) { return self.getGateCountsByArity(); }, "Gate counts by qubit arity, as a dict mapping arity to count.\n") .def("gate_count_for_arity", &Resources::getGateCountByArity, - py::arg("arity"), + nanobind::arg("arity"), "Get gate count for a specific qubit arity (total qubits " "including controls and targets). Returns 0 if no gates of " "that arity exist.") - .def("depth_for_arity", &Resources::getDepthByArity, py::arg("arity"), + .def("depth_for_arity", &Resources::getDepthByArity, + nanobind::arg("arity"), "Get circuit depth considering only gates of a specific qubit " "arity. Returns 0 if no gates of that arity exist.") - .def_property_readonly("multi_qubit_gate_count", - &Resources::getMultiQubitGateCount, - "Total count of gates with 2 or more qubits.\n") - .def_property_readonly("multi_qubit_depth", - &Resources::getMultiQubitDepth, - "Max depth across all gate widths >= 2.\n") - .def_property_readonly( + .def_prop_ro("multi_qubit_gate_count", &Resources::getMultiQubitGateCount, + "Total count of gates with 2 or more qubits.\n") + .def_prop_ro("multi_qubit_depth", &Resources::getMultiQubitDepth, + "Max depth across all gate widths >= 2.\n") + .def_prop_ro( "per_qubit_depth", - [](Resources &self) { - return py::dict(py::cast(self.getPerQubitDepth())); - }, + [](Resources &self) { return self.getPerQubitDepth(); }, "Per-qubit circuit depth (all gates), as a dict mapping qubit " "index to depth.\n") .def("clear", &Resources::clear, "Clear out all metadata from `self`.\n"); diff --git a/python/runtime/common/py_Resources.h b/python/runtime/common/py_Resources.h index cf5f6e7fdaf..4ea7546e1a3 100644 --- a/python/runtime/common/py_Resources.h +++ b/python/runtime/common/py_Resources.h @@ -5,11 +5,9 @@ * This source code and the accompanying materials are made available under * * the terms of the Apache License 2.0 which accompanies this distribution. * ******************************************************************************/ -#include - -namespace py = pybind11; +#include namespace cudaq { /// @brief Bind `cudaq.Resources` to python. -void bindResources(py::module &mod); +void bindResources(nanobind::module_ &mod); } // namespace cudaq diff --git a/python/runtime/common/py_SampleResult.cpp b/python/runtime/common/py_SampleResult.cpp index 6196502bb63..47b65d5226e 100644 --- a/python/runtime/common/py_SampleResult.cpp +++ b/python/runtime/common/py_SampleResult.cpp @@ -6,8 +6,11 @@ * the terms of the Apache License 2.0 which accompanies this distribution. * ******************************************************************************/ -#include -#include +#include +#include +#include +#include +#include #include "py_SampleResult.h" @@ -17,26 +20,22 @@ namespace cudaq { -void bindMeasureCounts(py::module &mod) { +void bindMeasureCounts(nanobind::module_ &mod) { using namespace cudaq; // TODO Bind the variants of this functions that take the register name // as input. - py::class_( + nanobind::class_( mod, "SampleResult", - R"#(A data-type containing the results of a call to :func:`sample`. -This includes all measurement counts data from both mid-circuit and + R"#(A data-type containing the results of a call to :func:`sample`. +This includes all measurement counts data from both mid-circuit and terminal measurements. Note: - Conditional logic on mid-circuit measurements is no longer supported with - `sample`. Use `run` instead. - -Attributes: - register_names (List[str]): A list of the names of each measurement - register that are stored in `self`.)#") - .def_property_readonly("register_names", &sample_result::register_names) - .def(py::init<>()) + Conditional logic on mid-circuit measurements is no longer supported with + `sample`. Use `run` instead.)#") + .def_prop_ro("register_names", &sample_result::register_names) + .def(nanobind::init<>()) .def( "dump", [](sample_result &self) { self.dump(); }, "Print a string of the raw measurement counts data to the " @@ -63,19 +62,19 @@ terminal measurements. auto map = self.to_map(); auto iter = map.find(bitstring); if (iter == map.end()) - throw py::key_error("bitstring '" + bitstring + - "' does not exist"); + throw nanobind::key_error( + ("bitstring '" + bitstring + "' does not exist").c_str()); return iter->second; }, - py::arg("bitstring"), + nanobind::arg("bitstring"), R"#(Return the measurement counts for the given `bitstring`. Args: bitstring (str): The binary string to return the measurement data of. Returns: - float: The number of times the given `bitstring` was measured + float: The number of times the given `bitstring` was measured during the `shots_count` number of executions on the QPU.)#") .def( "__len__", [](sample_result &self) { return self.to_map().size(); }, @@ -84,12 +83,14 @@ terminal measurements. .def( "__iter__", [](sample_result &self) { - return py::make_key_iterator(self.begin(), self.end()); + return nanobind::make_key_iterator(nanobind::type(), + "key_iterator", self.begin(), + self.end()); }, - py::keep_alive<0, 1>(), + nanobind::keep_alive<0, 1>(), "Iterate through the :class:`SampleResult` dictionary.\n") .def("expectation", &sample_result::expectation, - py::arg("register_name") = GlobalRegisterName, + nanobind::arg("register_name") = GlobalRegisterName, "Return the expectation value in the Z-basis of the :class:`Kernel` " "that was sampled.\n") .def( @@ -102,45 +103,46 @@ terminal measurements. 1); return self.expectation(); }, - py::arg("register_name") = GlobalRegisterName, + nanobind::arg("register_name") = GlobalRegisterName, "Return the expectation value in the Z-basis of the :class:`Kernel` " "that was sampled.\n") .def("probability", &sample_result::probability, "Return the probability of observing the given bit string.\n", - py::arg("bitstring"), py::arg("register_name") = GlobalRegisterName, + nanobind::arg("bitstring"), + nanobind::arg("register_name") = GlobalRegisterName, R"#(Return the probability of measuring the given `bitstring`. Args: - bitstring (str): The binary string to return the measurement + bitstring (str): The binary string to return the measurement probability of. - register_name (Optional[str]): The optional measurement register - name to extract the probability from. Defaults to the '__global__' + register_name (Optional[str]): The optional measurement register + name to extract the probability from. Defaults to the '__global__' register. Returns: - float: - The probability of measuring the given `bitstring`. Equivalent - to the proportion of the total times the bitstring was measured + float: + The probability of measuring the given `bitstring`. Equivalent + to the proportion of the total times the bitstring was measured vs. the number of experiments (`shots_count`).)#") .def("most_probable", &sample_result::most_probable, - py::arg("register_name") = GlobalRegisterName, - R"#(Return the bitstring that was measured most frequently in the + nanobind::arg("register_name") = GlobalRegisterName, + R"#(Return the bitstring that was measured most frequently in the experiment. Args: - register_name (Optional[str]): The optional measurement register - name to extract the most probable bitstring from. Defaults to the + register_name (Optional[str]): The optional measurement register + name to extract the most probable bitstring from. Defaults to the '__global__' register. Returns: str: The most frequently measured binary string during the experiment.)#") - .def("count", &sample_result::count, py::arg("bitstring"), - py::arg("register_name") = GlobalRegisterName, + .def("count", &sample_result::count, nanobind::arg("bitstring"), + nanobind::arg("register_name") = GlobalRegisterName, R"#(Return the number of times the given bitstring was observed. Args: bitstring (str): The binary string to return the measurement counts for. - register_name (Optional[str]): The optional measurement register name to + register_name (Optional[str]): The optional measurement register name to extract the probability from. Defaults to the '__global__' register. Returns: @@ -149,21 +151,21 @@ experiment. static_cast &, const std::string_view) const>( &sample_result::get_marginal), - py::arg("marginal_indices"), py::kw_only(), - py::arg("register_name") = GlobalRegisterName, - R"#(Extract the measurement counts data for the provided subset of + nanobind::arg("marginal_indices"), nanobind::kw_only(), + nanobind::arg("register_name") = GlobalRegisterName, + R"#(Extract the measurement counts data for the provided subset of qubits (`marginal_indices`). Args: - marginal_indices (list[int]): A list of the qubit indices to extract the + marginal_indices (list[int]): A list of the qubit indices to extract the measurement data from. - register_name (Optional[str]): The optional measurement register name to extract + register_name (Optional[str]): The optional measurement register name to extract the counts data from. Defaults to the '__global__' register. Returns: - :class:`SampleResult`: + :class:`SampleResult`: A new `SampleResult` dictionary containing the extracted measurement data.)#") .def("get_sequential_data", &sample_result::sequential_data, - py::arg("register_name") = GlobalRegisterName, + nanobind::arg("register_name") = GlobalRegisterName, "Return the data from the given register (`register_name`) as it " "was collected sequentially. A list of measurement results, not " "collated into a map.\n") @@ -174,26 +176,30 @@ qubits (`marginal_indices`). ExecutionResult res(cd); return sample_result(res); }, - py::arg("register_name"), + nanobind::arg("register_name"), "Extract the provided sub-register (`register_name`) as a new " ":class:`SampleResult`.\n") .def( "items", [](sample_result &self) { - return py::make_iterator(self.begin(), self.end()); + return nanobind::make_iterator(nanobind::type(), + "item_iterator", self.begin(), + self.end()); }, - py::keep_alive<0, 1>(), + nanobind::keep_alive<0, 1>(), "Return the key/value pairs in this :class:`SampleResult` " "dictionary.\n") .def( "values", [](sample_result &self) { - return py::make_value_iterator(self.begin(), self.end()); + return nanobind::make_value_iterator( + nanobind::type(), "value_iterator", self.begin(), + self.end()); }, - py::keep_alive<0, 1>(), + nanobind::keep_alive<0, 1>(), "Return all values (the counts) in this :class:`SampleResult` " "dictionary.\n") - .def(py::self += py::self) + .def(nanobind::self += nanobind::self) .def("clear", &sample_result::clear, "Clear out all metadata from `self`.\n"); } diff --git a/python/runtime/common/py_SampleResult.h b/python/runtime/common/py_SampleResult.h index 2cc72487900..832acf3e40c 100644 --- a/python/runtime/common/py_SampleResult.h +++ b/python/runtime/common/py_SampleResult.h @@ -5,13 +5,11 @@ * This source code and the accompanying materials are made available under * * the terms of the Apache License 2.0 which accompanies this distribution. * ******************************************************************************/ -#include +#include #include "utils/LinkedLibraryHolder.h" -namespace py = pybind11; - namespace cudaq { /// @brief Bind `cudaq.MeasureCounts` to python. -void bindMeasureCounts(py::module &mod); +void bindMeasureCounts(nanobind::module_ &mod); } // namespace cudaq diff --git a/python/runtime/cudaq/algorithms/py_draw.cpp b/python/runtime/cudaq/algorithms/py_draw.cpp index be13796efd9..94d01c1b151 100644 --- a/python/runtime/cudaq/algorithms/py_draw.cpp +++ b/python/runtime/cudaq/algorithms/py_draw.cpp @@ -11,14 +11,12 @@ #include "cudaq/platform/nvqpp_interface.h" #include "runtime/cudaq/platform/py_alt_launch_kernel.h" -namespace py = pybind11; - /// @brief Run `cudaq::contrib::draw`'s string overload on the provided kernel. /// \p kernel is a kernel decorator object and \p args are the arguments to /// launch \p kernel. static std::string pyDraw(const std::string &format, const std::string &shortName, MlirModule mod, - py::args runtimeArgs) { + nanobind::args runtimeArgs) { if (format != "ascii" && format != "latex") throw std::runtime_error("format argument must be \"ascii\" or \"latex\"."); @@ -31,11 +29,11 @@ static std::string pyDraw(const std::string &format, } /// @brief Bind the draw cudaq function -void cudaq::bindPyDraw(py::module &mod) { +void cudaq::bindPyDraw(nanobind::module_ &mod) { mod.def( "draw_impl", [](const std::string &format, const std::string &shortName, - MlirModule mod, py::args runtimeArgs) { + MlirModule mod, nanobind::args runtimeArgs) { return pyDraw(format, shortName, mod, runtimeArgs); }, R"#( @@ -47,7 +45,7 @@ string. Args: format (str): The format of the output. Can be 'ascii' or 'latex'. kernel (:class:`Kernel`): The :class:`Kernel` to draw. - *arguments (Optional[Any]): The concrete values to evaluate the kernel + *arguments (Optional[Any]): The concrete values to evaluate the kernel function at. Leave empty if the kernel doesn't accept any arguments. Returns: @@ -66,12 +64,12 @@ string. mz(q) print(cudaq.draw(bell_pair)) # Output - # ╭───╮ + # ╭───╮ # q0 : ┤ h ├──●── # ╰───╯╭─┴─╮ # q1 : ─────┤ x ├ # ╰───╯ - + # Example with arguments import cudaq @cudaq.kernel diff --git a/python/runtime/cudaq/algorithms/py_draw.h b/python/runtime/cudaq/algorithms/py_draw.h index f6bd76d4a3b..cc5c37df87e 100644 --- a/python/runtime/cudaq/algorithms/py_draw.h +++ b/python/runtime/cudaq/algorithms/py_draw.h @@ -9,8 +9,8 @@ #pragma once #include "utils/OpaqueArguments.h" -#include +#include namespace cudaq { -void bindPyDraw(pybind11::module &mod); +void bindPyDraw(nanobind::module_ &mod); } diff --git a/python/runtime/cudaq/algorithms/py_evolve.cpp b/python/runtime/cudaq/algorithms/py_evolve.cpp index b243287a0db..80e54f3edc7 100644 --- a/python/runtime/cudaq/algorithms/py_evolve.cpp +++ b/python/runtime/cudaq/algorithms/py_evolve.cpp @@ -11,13 +11,17 @@ #include "cudaq/algorithms/evolve_internal.h" #include "cudaq/runtime/logger/logger.h" #include "runtime/cudaq/platform/py_alt_launch_kernel.h" +#include "utils/NanobindAdaptors.h" #include "utils/OpaqueArguments.h" -#include "mlir/Bindings/Python/PybindAdaptors.h" #include "mlir/CAPI/IR.h" -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include namespace cudaq { @@ -26,17 +30,18 @@ using spin_op_creator = std::function)>; // Helper to determine if an object is a Python kernel builder object (PyKernel) -static bool isPyKernelObject(py::object &kernel) { +static bool isPyKernelObject(nanobind::object &kernel) { const std::string kernelTypeName = - py::hasattr(kernel, "__class__") - ? kernel.attr("__class__").attr("__name__").cast() + nanobind::hasattr(kernel, "__class__") + ? nanobind::cast( + kernel.attr("__class__").attr("__name__")) : ""; return (kernelTypeName == "PyKernel"); } template evolve_result -pyEvolve(state initial_state, py::object kernel, +pyEvolve(state initial_state, nanobind::object kernel, std::map params, std::vector> observables = {}, int shots_count = -1) { @@ -44,11 +49,11 @@ pyEvolve(state initial_state, py::object kernel, throw std::runtime_error( "The provided kernel to pyEvolve is not a valid PyKernel object."); - if (py::hasattr(kernel, "compile")) + if (nanobind::hasattr(kernel, "compile")) kernel.attr("compile")(); - auto kernelName = kernel.attr("name").cast(); - auto kernelMod = unwrap(kernel.attr("module").cast()); + auto kernelName = nanobind::cast(kernel.attr("name")); + auto kernelMod = unwrap(nanobind::cast(kernel.attr("module"))); std::vector spin_ops = {}; for (auto &observable : observables) { @@ -70,23 +75,24 @@ pyEvolve(state initial_state, py::object kernel, template evolve_result -pyEvolve(state initial_state, std::vector kernels, +pyEvolve(state initial_state, std::vector kernels, std::vector> params, std::vector> observables = {}, int shots_count = -1, bool save_intermediate_states = true) { - if (!std::all_of(kernels.begin(), kernels.end(), - [](py::object &kernel) { return isPyKernelObject(kernel); })) + if (!std::all_of( + kernels.begin(), kernels.end(), + [](nanobind::object &kernel) { return isPyKernelObject(kernel); })) throw std::runtime_error( "One or more of the provided kernels to pyEvolve is not a valid " "PyKernel object."); std::vector> launchFcts = {}; - for (py::object kernel : kernels) { - if (py::hasattr(kernel, "compile")) + for (nanobind::object kernel : kernels) { + if (nanobind::hasattr(kernel, "compile")) kernel.attr("compile")(); - auto kernelName = kernel.attr("name").cast(); - auto kernelMod = unwrap(kernel.attr("module").cast()); + auto kernelName = nanobind::cast(kernel.attr("name")); + auto kernelMod = unwrap(nanobind::cast(kernel.attr("module"))); launchFcts.push_back([kernelMod, kernelName](state state) mutable { auto *argData = new cudaq::OpaqueArguments(); @@ -112,7 +118,7 @@ pyEvolve(state initial_state, std::vector kernels, template async_evolve_result -pyEvolveAsync(state initial_state, py::object kernel, +pyEvolveAsync(state initial_state, nanobind::object kernel, std::map params, std::vector> observables = {}, std::size_t qpu_id = 0, @@ -122,18 +128,19 @@ pyEvolveAsync(state initial_state, py::object kernel, throw std::runtime_error( "The provided kernel to pyEvolveAsync is not a valid PyKernel object."); - if (py::hasattr(kernel, "compile")) + if (nanobind::hasattr(kernel, "compile")) kernel.attr("compile")(); - auto kernelMod = unwrap(kernel.attr("module").cast()).clone(); - auto kernelName = kernel.attr("name").cast(); + auto kernelMod = + unwrap(nanobind::cast(kernel.attr("module"))).clone(); + auto kernelName = nanobind::cast(kernel.attr("name")); std::vector spin_ops = {}; for (auto observable : observables) { spin_ops.push_back(observable(params)); } - py::gil_scoped_release release; + nanobind::gil_scoped_release release; return __internal__::evolve_async( initial_state, [kernelMod, kernelName](state state) mutable { @@ -148,27 +155,29 @@ pyEvolveAsync(state initial_state, py::object kernel, template async_evolve_result -pyEvolveAsync(state initial_state, std::vector kernels, +pyEvolveAsync(state initial_state, std::vector kernels, std::vector> params, std::vector> observables = {}, std::size_t qpu_id = 0, std::optional noise_model = std::nullopt, int shots_count = -1, bool save_intermediate_states = true) { - if (!std::all_of(kernels.begin(), kernels.end(), - [](py::object &kernel) { return isPyKernelObject(kernel); })) + if (!std::all_of( + kernels.begin(), kernels.end(), + [](nanobind::object &kernel) { return isPyKernelObject(kernel); })) throw std::runtime_error( "One or more of the provided kernels to pyEvolveAsync is not a valid " "PyKernel object."); std::vector> launchFcts = {}; - for (py::object kernel : kernels) { - if (py::hasattr(kernel, "compile")) + for (nanobind::object kernel : kernels) { + if (nanobind::hasattr(kernel, "compile")) kernel.attr("compile")(); // IMPORTANT: we need to make sure no Python data is accessed in the async. // functor. - auto kernelMod = unwrap(kernel.attr("module").cast()).clone(); - auto kernelName = kernel.attr("name").cast(); + auto kernelMod = + unwrap(nanobind::cast(kernel.attr("module"))).clone(); + auto kernelName = nanobind::cast(kernel.attr("name")); launchFcts.push_back( [kernelMod = std::move(kernelMod), kernelName](state state) mutable { cudaq::OpaqueArguments argData; @@ -187,7 +196,7 @@ pyEvolveAsync(state initial_state, std::vector kernels, spin_ops.push_back(std::move(ops)); } - py::gil_scoped_release release; + nanobind::gil_scoped_release release; return __internal__::evolve_async(initial_state, launchFcts, spin_ops, qpu_id, noise_model, shots_count, save_intermediate_states); @@ -196,7 +205,7 @@ pyEvolveAsync(state initial_state, std::vector kernels, #define DEFINE_PARAM_TYPE_OVERLOAD_VEC(type, pyMod) \ pyMod.def( \ "evolve", \ - [](state initial_state, std::vector kernels, \ + [](state initial_state, std::vector kernels, \ std::vector> params = {}, \ std::vector> observables = {}, \ int shots_count = -1, bool save_intermediate_states = true) { \ @@ -205,16 +214,16 @@ pyEvolveAsync(state initial_state, std::vector kernels, }, \ "Evolve the given initial_state with the provided kernel and " \ "parameters.", \ - py::arg("initial_state"), py::arg("kernels"), \ - py::arg("params") = std::vector>{}, \ - py::arg("observables") = std::vector>{}, \ - py::arg("shots_count") = -1, \ - py::arg("save_intermediate_states") = true); + nanobind::arg("initial_state"), nanobind::arg("kernels"), \ + nanobind::arg("params") = std::vector>{}, \ + nanobind::arg("observables") = std::vector>{}, \ + nanobind::arg("shots_count") = -1, \ + nanobind::arg("save_intermediate_states") = true); #define DEFINE_PARAM_TYPE_OVERLOAD(type, pyMod) \ pyMod.def( \ "evolve", \ - [](state initial_state, py::object kernel, \ + [](state initial_state, nanobind::object kernel, \ std::map params = {}, \ std::vector> observables = {}, \ int shots_count = -1) { \ @@ -223,15 +232,15 @@ pyEvolveAsync(state initial_state, std::vector kernels, }, \ "Evolve the given initial_state with the provided kernel and " \ "parameters.", \ - py::arg("initial_state"), py::arg("kernels"), \ - py::arg("params") = std::map{}, \ - py::arg("observables") = std::vector>{}, \ - py::arg("shots_count") = -1); + nanobind::arg("initial_state"), nanobind::arg("kernels"), \ + nanobind::arg("params") = std::map{}, \ + nanobind::arg("observables") = std::vector>{}, \ + nanobind::arg("shots_count") = -1); #define DEFINE_ASYNC_PARAM_TYPE_OVERLOAD_VEC(type, pyMod) \ pyMod.def( \ "evolve_async", \ - [](state initial_state, std::vector kernels, \ + [](state initial_state, std::vector kernels, \ std::vector> params = {}, \ std::vector> observables = {}, \ std::size_t qpu_id = 0, \ @@ -243,17 +252,18 @@ pyEvolveAsync(state initial_state, std::vector kernels, }, \ "Asynchronously evolve the given initial_state with " \ "the provided kernel and parameters.", \ - py::arg("initial_state"), py::arg("kernels"), \ - py::arg("params") = std::vector>{}, \ - py::arg("observables") = std::vector>{}, \ - py::arg("qpu_id") = 0, py::arg("noise_model") = std::nullopt, \ - py::arg("shots_count") = -1, \ - py::arg("save_intermediate_states") = true); + nanobind::arg("initial_state"), nanobind::arg("kernels"), \ + nanobind::arg("params") = std::vector>{}, \ + nanobind::arg("observables") = std::vector>{}, \ + nanobind::arg("qpu_id") = 0, \ + nanobind::arg("noise_model") = std::nullopt, \ + nanobind::arg("shots_count") = -1, \ + nanobind::arg("save_intermediate_states") = true); #define DEFINE_ASYNC_PARAM_TYPE_OVERLOAD(type, pyMod) \ pyMod.def( \ "evolve_async", \ - [](state initial_state, py::object kernel, \ + [](state initial_state, nanobind::object kernel, \ std::map params = {}, \ std::vector> observables = {}, \ std::size_t qpu_id = 0, \ @@ -264,14 +274,15 @@ pyEvolveAsync(state initial_state, std::vector kernels, }, \ "Asynchronously evolve the given initial_state with " \ "the provided kernel and parameters.", \ - py::arg("initial_state"), py::arg("kernels"), \ - py::arg("params") = std::map{}, \ - py::arg("observables") = std::vector>{}, \ - py::arg("qpu_id") = 0, py::arg("noise_model") = std::nullopt, \ - py::arg("shots_count") = -1); + nanobind::arg("initial_state"), nanobind::arg("kernels"), \ + nanobind::arg("params") = std::map{}, \ + nanobind::arg("observables") = std::vector>{}, \ + nanobind::arg("qpu_id") = 0, \ + nanobind::arg("noise_model") = std::nullopt, \ + nanobind::arg("shots_count") = -1); /// @brief Bind the evolve cudaq function for circuit simulator -void bindPyEvolve(py::module &mod) { +void bindPyEvolve(nanobind::module_ &mod) { // Sync evolve overloads DEFINE_PARAM_TYPE_OVERLOAD_VEC(long, mod); DEFINE_PARAM_TYPE_OVERLOAD_VEC(double, mod); diff --git a/python/runtime/cudaq/algorithms/py_evolve.h b/python/runtime/cudaq/algorithms/py_evolve.h index 869806c41b4..4af37da5b0c 100644 --- a/python/runtime/cudaq/algorithms/py_evolve.h +++ b/python/runtime/cudaq/algorithms/py_evolve.h @@ -8,10 +8,8 @@ #pragma once -#include - -namespace py = pybind11; +#include namespace cudaq { -void bindPyEvolve(py::module &mod); +void bindPyEvolve(nanobind::module_ &mod); } // namespace cudaq diff --git a/python/runtime/cudaq/algorithms/py_observe_async.cpp b/python/runtime/cudaq/algorithms/py_observe_async.cpp index 58c5ce37454..19586bce198 100644 --- a/python/runtime/cudaq/algorithms/py_observe_async.cpp +++ b/python/runtime/cudaq/algorithms/py_observe_async.cpp @@ -13,14 +13,15 @@ #include "cudaq/Todo.h" #include "cudaq/algorithms/observe.h" #include "runtime/cudaq/platform/py_alt_launch_kernel.h" +#include "utils/NanobindAdaptors.h" #include "utils/OpaqueArguments.h" -#include "mlir/Bindings/Python/PybindAdaptors.h" #include "mlir/CAPI/IR.h" #include "mlir/Dialect/Func/IR/FuncOps.h" #include -#include - -namespace py = pybind11; +#include +#include +#include +#include using namespace cudaq; @@ -67,14 +68,14 @@ static async_observe_result pyObserveAsync(const std::string &shortName, mlir::ModuleOp mod, const spin_op &spin_operator, std::size_t qpu_id, int shots, - py::args args) { + nanobind::args args) { auto &platform = get_platform(); args = simplifiedValidateInputArguments(args); auto fnOp = getKernelFuncOp(mod, shortName); auto opaques = marshal_arguments_for_module_launch(mod, args, fnOp); // Launch the asynchronous execution. - py::gil_scoped_release release; + nanobind::gil_scoped_release release; return details::runObservationAsync( detail::make_copyable_function([opaques = std::move(opaques), shortName, mod = mod.clone()]() mutable { @@ -86,17 +87,16 @@ static async_observe_result pyObserveAsync(const std::string &shortName, spin_operator, platform, shots, shortName, qpu_id); } -static async_observe_result observe_async_impl(const std::string &shortName, - MlirModule module, - py::object &spin_operator_obj, - std::size_t qpu_id, int shots, - py::args args) { +static async_observe_result +observe_async_impl(const std::string &shortName, MlirModule module, + nanobind::object &spin_operator_obj, std::size_t qpu_id, + int shots, nanobind::args args) { // FIXME(OperatorCpp): Remove this when the operator class is implemented in // C++ - spin_op spin_operator = [](py::object &obj) -> spin_op { - if (py::hasattr(obj, "_to_spinop")) - return obj.attr("_to_spinop")().cast(); - return obj.cast(); + spin_op spin_operator = [](nanobind::object &obj) -> spin_op { + if (nanobind::hasattr(obj, "_to_spinop")) + return nanobind::cast(obj.attr("_to_spinop")()); + return nanobind::cast(obj); }(spin_operator_obj); auto mod = unwrap(module); return pyObserveAsync(shortName, mod, spin_operator, qpu_id, shots, args); @@ -106,7 +106,7 @@ static async_observe_result observe_async_impl(const std::string &shortName, static observe_result pyObservePar(const PyParType &type, const std::string &shortName, mlir::ModuleOp module, spin_op &spin_operator, int shots, - std::optional noise, py::args args) { + std::optional noise, nanobind::args args) { // Ensure the user input is correct. auto &platform = get_platform(); if (!platform.supports_task_distribution()) @@ -163,11 +163,14 @@ pyObservePar(const PyParType &type, const std::string &shortName, /// Observe can be a single observe call, a parallel observe call, or a observe /// broadcast. All these variants are handled here. -static observe_result -observe_parallel_impl(const std::string &shortName, MlirModule module, - py::type execution, spin_op &spin_operator, int shots, - std::optional noise, py::args arguments) { - std::string applicatorKey = py::str(execution.attr("__name__")); +static observe_result observe_parallel_impl(const std::string &shortName, + MlirModule module, + nanobind::type_object execution, + spin_op &spin_operator, int shots, + std::optional noise, + nanobind::args arguments) { + std::string applicatorKey = + nanobind::cast(execution.attr("__name__")); auto mod = unwrap(module); if (applicatorKey == "thread") return pyObservePar(PyParType::thread, shortName, mod, spin_operator, shots, @@ -178,14 +181,14 @@ observe_parallel_impl(const std::string &shortName, MlirModule module, throw std::runtime_error("invalid parallel execution context"); } -void cudaq::bindObserveAsync(py::module &mod) { +void cudaq::bindObserveAsync(nanobind::module_ &mod) { auto parallelSubmodule = mod.def_submodule("parallel"); - py::class_( + nanobind::class_( parallelSubmodule, "mpi", "Type indicating that the :func:`observe` function should distribute its " "expectation value computations across available MPI ranks and GPUs for " "each term."); - py::class_( + nanobind::class_( parallelSubmodule, "thread", "Type indicating that the :func:`observe` function should distribute its " "term " diff --git a/python/runtime/cudaq/algorithms/py_observe_async.h b/python/runtime/cudaq/algorithms/py_observe_async.h index 44cb0a63048..ebd599b6ab3 100644 --- a/python/runtime/cudaq/algorithms/py_observe_async.h +++ b/python/runtime/cudaq/algorithms/py_observe_async.h @@ -8,8 +8,8 @@ #pragma once -#include +#include namespace cudaq { -void bindObserveAsync(pybind11::module &mod); +void bindObserveAsync(nanobind::module_ &mod); } // namespace cudaq diff --git a/python/runtime/cudaq/algorithms/py_optimizer.cpp b/python/runtime/cudaq/algorithms/py_optimizer.cpp index 79064fbf867..339b33e81ae 100644 --- a/python/runtime/cudaq/algorithms/py_optimizer.cpp +++ b/python/runtime/cudaq/algorithms/py_optimizer.cpp @@ -5,8 +5,12 @@ * This source code and the accompanying materials are made available under * * the terms of the Apache License 2.0 which accompanies this distribution. * ******************************************************************************/ -#include -#include +#include +#include +#include +#include +#include +#include #include "common/JsonConvert.h" #include "cudaq/algorithms/gradients/central_difference.h" @@ -19,23 +23,26 @@ namespace cudaq { -/// @brief Bind the `cudaq::optimization_result` typedef. -void bindOptimizationResult(py::module &mod) { - py::class_(mod, "OptimizationResult"); +/// @brief optimization_result is a typedef for std::tuple> which is automatically converted by nanobind's +/// stl/tuple type caster. +void bindOptimizationResult(nanobind::module_ &mod) { + mod.attr("OptimizationResult") = + nanobind::handle(reinterpret_cast(&PyTuple_Type)); } -void bindGradientStrategies(py::module &mod) { +void bindGradientStrategies(nanobind::module_ &mod) { // Binding under the `cudaq.gradients` namespace in python. auto gradients_submodule = mod.def_submodule("gradients"); // Have to bind the parent class, `cudaq::gradient`, to allow // for the passing of arbitrary `cudaq::gradients::` around. // Note: this class lives under `cudaq.gradients.gradient` // in python. - py::class_(gradients_submodule, "gradient"); + nanobind::class_(gradients_submodule, "gradient"); // Gradient strategies derive from the `cudaq::gradient` class. - py::class_(gradients_submodule, - "CentralDifference") - .def(py::init<>()) + nanobind::class_(gradients_submodule, + "CentralDifference") + .def(nanobind::init<>()) .def( "to_json", [](const gradients::central_difference &p) { return json(p).dump(); }, @@ -51,18 +58,20 @@ void bindGradientStrategies(py::module &mod) { .def( "compute", [](cudaq::gradient &grad, const std::vector &x, - py::function &func, double funcAtX) { + nanobind::callable &func, double funcAtX) { auto function = - func.cast)>>(); + nanobind::cast)>>( + func); return grad.compute(x, function, funcAtX); }, - py::arg("parameter_vector"), py::arg("function"), py::arg("funcAtX"), + nanobind::arg("parameter_vector"), nanobind::arg("function"), + nanobind::arg("funcAtX"), "Compute the gradient of the provided `parameter_vector` with " "respect to " "its loss function, using the `CentralDifference` method.\n"); - py::class_(gradients_submodule, - "ForwardDifference") - .def(py::init<>()) + nanobind::class_(gradients_submodule, + "ForwardDifference") + .def(nanobind::init<>()) .def( "to_json", [](const gradients::forward_difference &p) { return json(p).dump(); }, @@ -78,18 +87,20 @@ void bindGradientStrategies(py::module &mod) { .def( "compute", [](cudaq::gradient &grad, const std::vector &x, - py::function &func, double funcAtX) { + nanobind::callable &func, double funcAtX) { auto function = - func.cast)>>(); + nanobind::cast)>>( + func); return grad.compute(x, function, funcAtX); }, - py::arg("parameter_vector"), py::arg("function"), py::arg("funcAtX"), + nanobind::arg("parameter_vector"), nanobind::arg("function"), + nanobind::arg("funcAtX"), "Compute the gradient of the provided `parameter_vector` with " "respect to " "its loss function, using the `ForwardDifference` method.\n"); - py::class_(gradients_submodule, - "ParameterShift") - .def(py::init<>()) + nanobind::class_(gradients_submodule, + "ParameterShift") + .def(nanobind::init<>()) .def( "to_json", [](const gradients::parameter_shift &p) { return json(p).dump(); }, @@ -105,12 +116,14 @@ void bindGradientStrategies(py::module &mod) { .def( "compute", [](cudaq::gradient &grad, const std::vector &x, - py::function &func, double funcAtX) { + nanobind::callable &func, double funcAtX) { auto function = - func.cast)>>(); + nanobind::cast)>>( + func); return grad.compute(x, function, funcAtX); }, - py::arg("parameter_vector"), py::arg("function"), py::arg("funcAtX"), + nanobind::arg("parameter_vector"), nanobind::arg("function"), + nanobind::arg("funcAtX"), "Compute the gradient of the provided `parameter_vector` with " "respect to " "its loss function, using the `ParameterShift` method.\n"); @@ -121,9 +134,10 @@ void bindGradientStrategies(py::module &mod) { /// Can now define its member functions on /// that submodule. template -py::class_ addPyOptimizer(py::module &mod, std::string &&name) { - return py::class_(mod, name.c_str()) - .def(py::init<>()) +nanobind::class_ addPyOptimizer(nanobind::module_ &mod, + std::string &&name) { + return nanobind::class_(mod, name.c_str()) + .def(nanobind::init<>()) .def( "to_json", [](const OptimizerT &p) { return json(p).dump(); }, "Convert optimizer to JSON string") @@ -135,15 +149,15 @@ py::class_ addPyOptimizer(py::module &mod, std::string &&name) { return p; }, "Convert JSON string to optimizer") - .def_readwrite("max_iterations", &OptimizerT::max_eval, R"doc( + .def_rw("max_iterations", &OptimizerT::max_eval, R"doc( int: Maximum number of optimizer iterations (default: unlimited). Sets an upper bound on the number of function evaluations or iterations the optimizer will perform. If not set, the optimizer may run until convergence or until another stopping criterion is met. )doc") - .def_readwrite("initial_parameters", &OptimizerT::initial_parameters, - R"doc( + .def_rw("initial_parameters", &OptimizerT::initial_parameters, + R"doc( list[float]: Initial values for the optimization parameters (optional). Provides a starting point for the optimization. If not specified, the @@ -156,7 +170,7 @@ py::class_ addPyOptimizer(py::module &mod, std::string &&name) { optimizer.initial_parameters = [0.5, -0.3, 1.2] )doc") - .def_readwrite("lower_bounds", &OptimizerT::lower_bounds, R"doc( + .def_rw("lower_bounds", &OptimizerT::lower_bounds, R"doc( list[float]: Lower bounds for optimization parameters (optional). Constrains the search space by specifying minimum allowed values for @@ -168,7 +182,7 @@ py::class_ addPyOptimizer(py::module &mod, std::string &&name) { optimizer.lower_bounds = [-2.0, -2.0] # For 2D problem )doc") - .def_readwrite("upper_bounds", &OptimizerT::upper_bounds, R"doc( + .def_rw("upper_bounds", &OptimizerT::upper_bounds, R"doc( list[float]: Upper bounds for optimization parameters (optional). Constrains the search space by specifying maximum allowed values for @@ -197,21 +211,22 @@ py::class_ addPyOptimizer(py::module &mod, std::string &&name) { )doc") .def( "optimize", - [](OptimizerT &opt, const int dim, py::function &func) { + [](OptimizerT &opt, const int dim, nanobind::callable &func) { return opt.optimize(dim, [&](std::vector x, std::vector &grad) { // Call the function. auto ret = func(x); // Does it return a tuple? - auto isTupleReturn = py::isinstance(ret); + auto isTupleReturn = nanobind::isinstance(ret); // If we don't need gradients, and it does, just grab the value // and return. if (!opt.requiresGradients() && isTupleReturn) - return ret.cast()[0].cast(); + return nanobind::cast( + nanobind::cast(ret)[0]); // If we don't need gradients and it doesn't return tuple, then // just pass what we got. if (!opt.requiresGradients() && !isTupleReturn) - return ret.cast(); + return nanobind::cast(ret); // Throw an error if we need gradients and they weren't provided. if (opt.requiresGradients() && !isTupleReturn) @@ -220,16 +235,16 @@ py::class_ addPyOptimizer(py::module &mod, std::string &&name) { "(float, list[float]) for gradient-based optimizers"); // If here, we require gradients, and the signature is right. - auto tuple = ret.cast(); + auto tuple = nanobind::cast(ret); auto val = tuple[0]; - auto gradIn = tuple[1].cast(); + auto gradIn = nanobind::cast(tuple[1]); for (std::size_t i = 0; i < gradIn.size(); i++) - grad[i] = gradIn[i].cast(); + grad[i] = nanobind::cast(gradIn[i]); - return val.cast(); + return nanobind::cast(val); }); }, - py::arg("dimensions"), py::arg("function"), R"doc( + nanobind::arg("dimensions"), nanobind::arg("function"), R"doc( Run the optimization procedure. Args: @@ -267,14 +282,14 @@ Run the optimization procedure. )doc"); } -void bindOptimizers(py::module &mod) { +void bindOptimizers(nanobind::module_ &mod) { // Binding the `cudaq::optimizers` class to `_pycudaq` as a submodule // so it's accessible directly in the cudaq namespace. auto optimizers_submodule = mod.def_submodule("optimizers"); - py::class_(optimizers_submodule, "optimizer"); + nanobind::class_(optimizers_submodule, "optimizer"); addPyOptimizer(optimizers_submodule, "COBYLA") - .def(py::init<>(), R"doc( + .def(nanobind::init<>(), R"doc( Constrained Optimization BY Linear Approximations (COBYLA). COBYLA is a gradient-free derivative-free optimization algorithm that uses @@ -297,7 +312,7 @@ This optimizer does not require gradients from the objective function. )doc"); addPyOptimizer(optimizers_submodule, "NelderMead") - .def(py::init<>(), R"doc( + .def(nanobind::init<>(), R"doc( Nelder-Mead simplex optimization algorithm. The Nelder-Mead method is a gradient-free simplex-based optimization algorithm @@ -320,7 +335,7 @@ This optimizer does not require gradients from the objective function. )doc"); addPyOptimizer(optimizers_submodule, "LBFGS") - .def(py::init<>(), R"doc( + .def(nanobind::init<>(), R"doc( Limited-memory Broyden-Fletcher-Goldfarb-Shanno (L-BFGS) optimizer. L-BFGS is a quasi-Newton method that approximates the Hessian matrix using @@ -346,7 +361,7 @@ This optimizer requires gradients from the objective function. addPyOptimizer(optimizers_submodule, "GradientDescent") - .def(py::init<>(), R"doc( + .def(nanobind::init<>(), R"doc( Basic gradient descent optimization algorithm. Gradient descent iteratively moves in the direction of steepest descent @@ -373,7 +388,7 @@ This optimizer requires gradients from the objective function. // Have to bind extra optimizer parameters to the following manually: auto py_spsa = addPyOptimizer(optimizers_submodule, "SPSA") - .def(py::init<>(), R"doc( + .def(nanobind::init<>(), R"doc( Simultaneous Perturbation Stochastic Approximation (SPSA) optimizer. SPSA is a gradient-free optimization algorithm that uses simultaneous @@ -404,15 +419,15 @@ This optimizer does not require gradients from the objective function. function=objective ) )doc"); - py_spsa.def_readwrite("gamma", &cudaq::optimizers::spsa::gamma, R"doc( + py_spsa.def_rw("gamma", &cudaq::optimizers::spsa::gamma, R"doc( float: Scaling exponent for the step size schedule (default: 0.101). Controls how the step size decreases over iterations. The step size at iteration k is proportional to (A + k + 1)^(-gamma), where A is a stability constant. Common values are in the range [0.1, 0.6]. )doc"); - py_spsa.def_readwrite("step_size", &cudaq::optimizers::spsa::eval_step_size, - R"doc( + py_spsa.def_rw("step_size", &cudaq::optimizers::spsa::eval_step_size, + R"doc( float: Evaluation step size for gradient approximation (default: 0.3). Controls the magnitude of perturbations used to approximate gradients. @@ -421,7 +436,7 @@ to noise. Typical values range from 0.1 to 0.5. )doc"); auto py_adam = addPyOptimizer(optimizers_submodule, "Adam") - .def(py::init<>(), R"doc( + .def(nanobind::init<>(), R"doc( Adaptive Moment Estimation (Adam) optimizer. Adam is an adaptive learning rate optimization algorithm that computes @@ -458,8 +473,8 @@ function must return a tuple of (value, gradient_vector). function=objective_with_grad ) )doc"); - py_adam.def_readwrite("batch_size", &cudaq::optimizers::adam::batch_size, - R"doc( + py_adam.def_rw("batch_size", &cudaq::optimizers::adam::batch_size, + R"doc( int: Number of samples per batch (default: 1). For stochastic optimization, determines how many samples are used to @@ -467,28 +482,28 @@ compute each gradient estimate. Batch size of 1 corresponds to online learning. Larger batch sizes can provide more stable gradient estimates but require more computation per iteration. )doc"); - py_adam.def_readwrite("beta1", &cudaq::optimizers::adam::beta1, R"doc( + py_adam.def_rw("beta1", &cudaq::optimizers::adam::beta1, R"doc( float: Exponential decay rate for the first moment estimates (default: 0.9). Controls the exponential moving average of past gradients (momentum term). Values are typically in the range [0.9, 0.999]. Higher values give more weight to past gradients, providing smoother updates but slower adaptation. )doc"); - py_adam.def_readwrite("beta2", &cudaq::optimizers::adam::beta2, R"doc( + py_adam.def_rw("beta2", &cudaq::optimizers::adam::beta2, R"doc( float: Exponential decay rate for the second moment estimates (default: 0.999). Controls the exponential moving average of past squared gradients. Values are typically in the range [0.99, 0.9999]. Higher values provide more stable learning rates but slower adaptation to changing gradient magnitudes. )doc"); - py_adam.def_readwrite("epsilon", &cudaq::optimizers::adam::eps, R"doc( + py_adam.def_rw("epsilon", &cudaq::optimizers::adam::eps, R"doc( float: Small constant for numerical stability (default: 1e-8). Added to the denominator to prevent division by zero when computing adaptive learning rates. Should be a small positive value, typically between 1e-8 and 1e-6. )doc"); - py_adam.def_readwrite("step_size", &cudaq::optimizers::adam::step_size, R"doc( + py_adam.def_rw("step_size", &cudaq::optimizers::adam::step_size, R"doc( float: Learning rate (step size) for parameter updates (default: 0.01). Controls the magnitude of parameter updates at each iteration. Typical @@ -496,7 +511,7 @@ values range from 0.001 to 0.1. The effective learning rate is adapted per parameter based on gradient history. Start with 0.001 or 0.01 and adjust based on convergence behavior. )doc"); - py_adam.def_readwrite("f_tol", &cudaq::optimizers::adam::f_tol, R"doc( + py_adam.def_rw("f_tol", &cudaq::optimizers::adam::f_tol, R"doc( float: Convergence tolerance on the objective function value (default: 1e-4). Optimization terminates when the change in objective function value between @@ -505,7 +520,7 @@ convergence but may require more iterations. )doc"); auto py_sgd = addPyOptimizer(optimizers_submodule, "SGD") - .def(py::init<>(), R"doc( + .def(nanobind::init<>(), R"doc( Stochastic Gradient Descent (SGD) optimizer. SGD is a fundamental optimization algorithm that updates parameters by taking @@ -539,7 +554,7 @@ function must return a tuple of (value, gradient_vector). function=objective_with_grad ) )doc"); - py_sgd.def_readwrite("batch_size", &cudaq::optimizers::sgd::batch_size, R"doc( + py_sgd.def_rw("batch_size", &cudaq::optimizers::sgd::batch_size, R"doc( int: Number of samples per batch (default: 1). For stochastic optimization, determines how many samples are used to @@ -548,7 +563,7 @@ stochastic gradient descent. Larger batch sizes (mini-batch SGD) can provide more stable gradient estimates but require more computation per iteration. )doc"); - py_sgd.def_readwrite("step_size", &cudaq::optimizers::sgd::step_size, R"doc( + py_sgd.def_rw("step_size", &cudaq::optimizers::sgd::step_size, R"doc( float: Learning rate (step size) for parameter updates (default: 0.01). Controls the magnitude of parameter updates at each iteration. The update @@ -556,7 +571,7 @@ rule is: x_new = x_old - step_size * gradient. Typical values range from 0.001 to 0.1. Too large values can cause divergence, while too small values lead to slow convergence. )doc"); - py_sgd.def_readwrite("f_tol", &cudaq::optimizers::sgd::f_tol, R"doc( + py_sgd.def_rw("f_tol", &cudaq::optimizers::sgd::f_tol, R"doc( float: Convergence tolerance on the objective function value (default: 1e-4). Optimization terminates when the change in objective function value between @@ -566,7 +581,7 @@ gradients, convergence may be noisy. )doc"); } -void bindOptimizerWrapper(py::module &mod) { +void bindOptimizerWrapper(nanobind::module_ &mod) { bindOptimizationResult(mod); bindGradientStrategies(mod); bindOptimizers(mod); diff --git a/python/runtime/cudaq/algorithms/py_optimizer.h b/python/runtime/cudaq/algorithms/py_optimizer.h index bd90e44e3af..10ec35d46cd 100644 --- a/python/runtime/cudaq/algorithms/py_optimizer.h +++ b/python/runtime/cudaq/algorithms/py_optimizer.h @@ -8,11 +8,9 @@ #pragma once -#include - -namespace py = pybind11; +#include namespace cudaq { /// @brief Bind the `cudaq::optimizers::` to python. -void bindOptimizerWrapper(py::module &mod); +void bindOptimizerWrapper(nanobind::module_ &mod); } // namespace cudaq diff --git a/python/runtime/cudaq/algorithms/py_resource_count.cpp b/python/runtime/cudaq/algorithms/py_resource_count.cpp index 2f30d7d6b87..53af2405cf5 100644 --- a/python/runtime/cudaq/algorithms/py_resource_count.cpp +++ b/python/runtime/cudaq/algorithms/py_resource_count.cpp @@ -10,17 +10,16 @@ #include "common/Resources.h" #include "runtime/cudaq/platform/py_alt_launch_kernel.h" #include "utils/LinkedLibraryHolder.h" -#include "mlir/Bindings/Python/PybindAdaptors.h" -#include - -namespace py = pybind11; +#include "utils/NanobindAdaptors.h" +#include +#include using namespace cudaq; static Resources estimate_resources_impl(const std::string &kernelName, MlirModule kernelMod, std::optional> choice, - py::args args) { + nanobind::args args) { auto &platform = cudaq::get_platform(); args = simplifiedValidateInputArguments(args); @@ -60,7 +59,7 @@ estimate_resources_impl(const std::string &kernelName, MlirModule kernelMod, return counts; } -void cudaq::bindCountResources(py::module &mod) { +void cudaq::bindCountResources(nanobind::module_ &mod) { mod.def("estimate_resources_impl", estimate_resources_impl, "See python documentation for estimate_resources."); } diff --git a/python/runtime/cudaq/algorithms/py_resource_count.h b/python/runtime/cudaq/algorithms/py_resource_count.h index af03edd8476..d307c83ed9c 100644 --- a/python/runtime/cudaq/algorithms/py_resource_count.h +++ b/python/runtime/cudaq/algorithms/py_resource_count.h @@ -8,8 +8,8 @@ #pragma once -#include +#include namespace cudaq { -void bindCountResources(pybind11::module &mod); +void bindCountResources(nanobind::module_ &mod); } diff --git a/python/runtime/cudaq/algorithms/py_run.cpp b/python/runtime/cudaq/algorithms/py_run.cpp index 153d9c50fbc..5609ebe325a 100644 --- a/python/runtime/cudaq/algorithms/py_run.cpp +++ b/python/runtime/cudaq/algorithms/py_run.cpp @@ -11,26 +11,27 @@ #include "cudaq/algorithms/run.h" #include "cudaq_internal/compiler/LayoutInfo.h" #include "runtime/cudaq/platform/py_alt_launch_kernel.h" +#include "utils/NanobindAdaptors.h" #include "utils/OpaqueArguments.h" -#include "mlir/Bindings/Python/PybindAdaptors.h" #include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include using namespace cudaq; using namespace cudaq_internal::compiler; -static std::vector readRunResults(mlir::ModuleOp module, - mlir::Type ty, - details::RunResultSpan &results, - std::size_t count) { - std::vector ret; +static std::vector +readRunResults(mlir::ModuleOp module, mlir::Type ty, + details::RunResultSpan &results, std::size_t count) { + std::vector ret; std::size_t byteSize = results.lengthInBytes / count; for (std::size_t i = 0; i < results.lengthInBytes; i += byteSize) { - py::object obj = convertResult(module, ty, results.data + i); + nanobind::object obj = convertResult(module, ty, results.data + i); ret.push_back(obj); } return ret; @@ -90,19 +91,18 @@ pyRunTheKernel(const std::string &name, quantum_platform &platform, return results; } -static std::vector pyReadResults(details::RunResultSpan results, - mlir::ModuleOp mod, - std::size_t shots_count, - const std::string &name) { +static std::vector +pyReadResults(details::RunResultSpan results, mlir::ModuleOp mod, + std::size_t shots_count, const std::string &name) { auto returnTy = recoverReturnType(mod, name); return readRunResults(mod, returnTy, results, shots_count); } /// @brief Run `cudaq::run` on the provided kernel. -static std::vector +static std::vector run_impl(const std::string &shortName, MlirModule module, std::size_t shots_count, std::optional noise_model, - std::size_t qpu_id, py::args runtimeArgs) { + std::size_t qpu_id, nanobind::args runtimeArgs) { if (shots_count == 0) return {}; @@ -133,7 +133,7 @@ namespace { // When the `ready` future is set, the content of the buffer is filled. struct async_run_result { std::future ready; - std::vector *results; + std::vector *results; std::string *error; }; } // namespace @@ -142,7 +142,7 @@ struct async_run_result { static async_run_result run_async_impl(const std::string &shortName, MlirModule module, std::size_t shots_count, std::optional noise_model, - std::size_t qpu_id, py::args runtimeArgs) { + std::size_t qpu_id, nanobind::args runtimeArgs) { if (!shots_count) return {}; @@ -162,7 +162,7 @@ run_async_impl(const std::string &shortName, MlirModule module, "Noise model is not supported on remote platforms."); async_run_result result; - result.results = new std::vector(); + result.results = new std::vector(); result.error = new std::string(); if (shots_count == 0) { @@ -184,7 +184,7 @@ run_async_impl(const std::string &shortName, MlirModule module, { // Release GIL to allow c++ threads, all code inside the scope is c++, so // there is no need to re-acquire the GIL inside the thread. - py::gil_scoped_release gil_release{}; + nanobind::gil_scoped_release gil_release{}; QuantumTask wrapped = detail::make_copyable_function( [sp = std::move(spanPromise), ep = std::move(errorPromise), noise_model = std::move(noise_model), qpu_id, name = shortName, @@ -214,7 +214,7 @@ run_async_impl(const std::string &shortName, MlirModule module, { // Release GIL to allow c++ threads, re-acquire for conversion of the // results to python objects. - py::gil_scoped_release gil_release{}; + nanobind::gil_scoped_release gil_release{}; auto resultFuture = std::async(std::launch::deferred, [sf = std::move(spanFuture), ef = std::move(errorFuture), @@ -224,7 +224,7 @@ run_async_impl(const std::string &shortName, MlirModule module, std::swap(*errorPtr, error); if (error.empty()) { auto span = sf.get(); - py::gil_scoped_acquire gil{}; + nanobind::gil_scoped_acquire gil{}; auto results = pyReadResults(span, mod, shots_count, shortName); std::swap(*resultsPtr, results); @@ -237,7 +237,7 @@ run_async_impl(const std::string &shortName, MlirModule module, } /// @brief Bind the run cudaq function. -void cudaq::bindPyRun(py::module &mod) { +void cudaq::bindPyRun(nanobind::module_ &mod) { mod.def("run_impl", run_impl, R"#( Run the provided `kernel` with the given kernel arguments over the specified @@ -255,8 +255,8 @@ number of circuit executions (`shots_count`). } /// @brief Bind the run_async cudaq function. -void cudaq::bindPyRunAsync(py::module &mod) { - py::class_(mod, "AsyncRunResultImpl", "") +void cudaq::bindPyRunAsync(nanobind::module_ &mod) { + nanobind::class_(mod, "AsyncRunResultImpl", "") .def( "get", [](async_run_result &self) { diff --git a/python/runtime/cudaq/algorithms/py_run.h b/python/runtime/cudaq/algorithms/py_run.h index 3e2c10df6b4..c1070f1f443 100644 --- a/python/runtime/cudaq/algorithms/py_run.h +++ b/python/runtime/cudaq/algorithms/py_run.h @@ -8,9 +8,9 @@ #pragma once -#include +#include namespace cudaq { -void bindPyRun(pybind11::module &mod); -void bindPyRunAsync(pybind11::module &mod); +void bindPyRun(nanobind::module_ &mod); +void bindPyRunAsync(nanobind::module_ &mod); } // namespace cudaq diff --git a/python/runtime/cudaq/algorithms/py_sample_async.cpp b/python/runtime/cudaq/algorithms/py_sample_async.cpp index dfe50fcb4b5..43deba6c1ce 100644 --- a/python/runtime/cudaq/algorithms/py_sample_async.cpp +++ b/python/runtime/cudaq/algorithms/py_sample_async.cpp @@ -10,21 +10,21 @@ #include "common/DeviceCodeRegistry.h" #include "cudaq/algorithms/sample.h" #include "runtime/cudaq/platform/py_alt_launch_kernel.h" +#include "utils/NanobindAdaptors.h" #include "utils/OpaqueArguments.h" -#include "mlir/Bindings/Python/PybindAdaptors.h" #include "mlir/CAPI/IR.h" #include "mlir/Dialect/Func/IR/FuncOps.h" #include -#include - -namespace py = pybind11; +#include +#include +#include using namespace cudaq; static async_sample_result sample_async_impl( const std::string &shortName, MlirModule module, std::size_t shots_count, std::optional noise_model, bool explicit_measurements, - std::size_t qpu_id, py::args runtimeArgs) { + std::size_t qpu_id, nanobind::args runtimeArgs) { mlir::ModuleOp mod = unwrap(module); runtimeArgs = simplifiedValidateInputArguments(runtimeArgs); @@ -40,7 +40,7 @@ static async_sample_result sample_async_impl( auto opaques = marshal_arguments_for_module_launch(mod, runtimeArgs, fnOp); // Should only have C++ going on here, safe to release the GIL - py::gil_scoped_release release; + nanobind::gil_scoped_release release; // Use runSamplingAsync with noise model support. // The noise_model is passed by value to runSamplingAsync, which captures @@ -60,7 +60,7 @@ static async_sample_result sample_async_impl( std::move(noise_model)); } -void cudaq::bindSampleAsync(py::module &mod) { +void cudaq::bindSampleAsync(nanobind::module_ &mod) { // Async. result wrapper for Python kernels, which also holds the Python MLIR // context. // @@ -74,8 +74,8 @@ void cudaq::bindSampleAsync(py::module &mod) { // then track a reference (ref count) to the context of the temporary (rval) // kernel. - py::class_(mod, "AsyncSampleResultImpl", - R"#( + nanobind::class_(mod, "AsyncSampleResultImpl", + R"#( A data-type containing the results of a call to :func:`sample_async`. The `AsyncSampleResult` models a future-like type, whose :class:`SampleResult` may be returned via an invocation of the `get` method. This kicks off a wait on the @@ -83,14 +83,15 @@ current thread until the results are available. See `future `_ for more information on this programming pattern. )#") - .def(py::init([](std::string inJson) { - async_sample_result f; - std::istringstream is(inJson); - is >> f; - return f; - })) + .def("__init__", + [](async_sample_result *self, std::string inJson) { + async_sample_result f; + std::istringstream is(inJson); + is >> f; + new (self) async_sample_result(std::move(f)); + }) .def("get", &async_sample_result::get, - py::call_guard(), + nanobind::call_guard(), "Return the :class:`SampleResult` from the asynchronous sample " "execution.\n") .def( diff --git a/python/runtime/cudaq/algorithms/py_sample_async.h b/python/runtime/cudaq/algorithms/py_sample_async.h index b494b2631bb..ec1c69476ac 100644 --- a/python/runtime/cudaq/algorithms/py_sample_async.h +++ b/python/runtime/cudaq/algorithms/py_sample_async.h @@ -8,10 +8,8 @@ #pragma once -#include - -namespace py = pybind11; +#include namespace cudaq { -void bindSampleAsync(py::module &mod); +void bindSampleAsync(nanobind::module_ &mod); } // namespace cudaq diff --git a/python/runtime/cudaq/algorithms/py_sample_ptsbe.cpp b/python/runtime/cudaq/algorithms/py_sample_ptsbe.cpp index 4bf979ac8d9..064672787bc 100644 --- a/python/runtime/cudaq/algorithms/py_sample_ptsbe.cpp +++ b/python/runtime/cudaq/algorithms/py_sample_ptsbe.cpp @@ -20,13 +20,15 @@ #include "cudaq/ptsbe/strategies/OrderedSamplingStrategy.h" #include "cudaq/ptsbe/strategies/ProbabilisticSamplingStrategy.h" #include "runtime/cudaq/platform/py_alt_launch_kernel.h" +#include "utils/NanobindAdaptors.h" #include "utils/OpaqueArguments.h" -#include "mlir/Bindings/Python/PybindAdaptors.h" #include "mlir/CAPI/IR.h" #include "mlir/Dialect/Func/IR/FuncOps.h" -#include - -namespace py = pybind11; +#include +#include +#include +#include +#include using namespace cudaq; @@ -38,9 +40,9 @@ static ptsbe::sample_result pySamplePTSBE(const std::string &shortName, MlirModule module, std::size_t shots_count, noise_model noiseModel, std::optional max_trajectories, - py::object sampling_strategy, py::object shot_allocation_obj, - bool return_execution_data, bool include_sequential_data, - py::args runtimeArgs) { + nanobind::object sampling_strategy, + nanobind::object shot_allocation_obj, bool return_execution_data, + bool include_sequential_data, nanobind::args runtimeArgs) { if (shots_count == 0) return ptsbe::sample_result(); @@ -51,11 +53,12 @@ pySamplePTSBE(const std::string &shortName, MlirModule module, if (!sampling_strategy.is_none()) ptsbe_options.strategy = - sampling_strategy.cast>(); + nanobind::cast>( + sampling_strategy); if (!shot_allocation_obj.is_none()) ptsbe_options.shot_allocation = - shot_allocation_obj.cast(); + nanobind::cast(shot_allocation_obj); auto mod = unwrap(module); runtimeArgs = simplifiedValidateInputArguments(runtimeArgs); @@ -104,13 +107,12 @@ struct AsyncPTSBESampleResultImpl { } // namespace /// @brief Run PTSBE sampling asynchronously from Python. -static AsyncPTSBESampleResultImpl -pySampleAsyncPTSBE(const std::string &shortName, MlirModule module, - std::size_t shots_count, noise_model &noiseModel, - std::optional max_trajectories, - py::object sampling_strategy, py::object shot_allocation_obj, - bool return_execution_data, bool include_sequential_data, - py::args runtimeArgs) { +static AsyncPTSBESampleResultImpl pySampleAsyncPTSBE( + const std::string &shortName, MlirModule module, std::size_t shots_count, + noise_model &noiseModel, std::optional max_trajectories, + nanobind::object sampling_strategy, nanobind::object shot_allocation_obj, + bool return_execution_data, bool include_sequential_data, + nanobind::args runtimeArgs) { ptsbe::PTSBEOptions ptsbe_options; ptsbe_options.return_execution_data = return_execution_data; @@ -119,11 +121,12 @@ pySampleAsyncPTSBE(const std::string &shortName, MlirModule module, if (!sampling_strategy.is_none()) ptsbe_options.strategy = - sampling_strategy.cast>(); + nanobind::cast>( + sampling_strategy); if (!shot_allocation_obj.is_none()) ptsbe_options.shot_allocation = - shot_allocation_obj.cast(); + nanobind::cast(shot_allocation_obj); auto mod = unwrap(module); runtimeArgs = simplifiedValidateInputArguments(runtimeArgs); @@ -135,7 +138,7 @@ pySampleAsyncPTSBE(const std::string &shortName, MlirModule module, std::string kernelName = shortName; // Release GIL before launching async C++ work - py::gil_scoped_release release; + nanobind::gil_scoped_release release; return AsyncPTSBESampleResultImpl(ptsbe::detail::runSamplingAsyncPTSBE( [opaques = std::move(opaques), kernelName, mod = mod.clone()]() mutable { [[maybe_unused]] auto result = @@ -145,20 +148,19 @@ pySampleAsyncPTSBE(const std::string &shortName, MlirModule module, noiseModel)); } -void cudaq::bindSamplePTSBE(py::module &mod) { +void cudaq::bindSamplePTSBE(nanobind::module_ &mod) { auto ptsbe = mod.def_submodule( "ptsbe", "PTSBE (Pre-Trajectory Sampling with Batch Execution)"); // Base strategy class (abstract, not directly constructible) - py::class_>( + nanobind::class_( ptsbe, "PTSSamplingStrategy", "Base class for trajectory sampling strategies.") .def("name", &ptsbe::PTSSamplingStrategy::name, "Get the name of this strategy."); // Shot allocation strategy - py::enum_( + nanobind::enum_( ptsbe, "ShotAllocationType", "Strategy type for allocating shots across trajectories.") .value("PROPORTIONAL", ptsbe::ShotAllocationStrategy::Type::PROPORTIONAL, @@ -172,33 +174,36 @@ void cudaq::bindSamplePTSBE(py::module &mod) { ptsbe::ShotAllocationStrategy::Type::HIGH_WEIGHT_BIAS, "Bias toward high-weight error trajectories."); - py::class_( + nanobind::class_( ptsbe, "ShotAllocationStrategy", "Strategy for allocating shots across selected trajectories.") - .def(py::init<>(), "Create a default (PROPORTIONAL) strategy.") - .def(py::init([](ptsbe::ShotAllocationStrategy::Type t, double bias, - std::optional seed) { - return ptsbe::ShotAllocationStrategy(t, bias, seed); - }), - py::arg("type"), py::arg("bias_strength") = 2.0, - py::arg("seed") = py::none(), - "Create a strategy with specified type, optional bias strength, " - "and optional random seed. When seed is None (default), uses " - "CUDA-Q's global random seed.") - .def_readwrite("type", &ptsbe::ShotAllocationStrategy::type, - "The allocation strategy type.") - .def_readwrite( - "bias_strength", &ptsbe::ShotAllocationStrategy::bias_strength, - "Bias factor for weighted strategies. Default value is 2.0."); + .def(nanobind::init<>(), "Create a default (PROPORTIONAL) strategy.") + .def( + "__init__", + [](ptsbe::ShotAllocationStrategy *self, + ptsbe::ShotAllocationStrategy::Type t, double bias, + std::optional seed) { + new (self) ptsbe::ShotAllocationStrategy(t, bias, seed); + }, + nanobind::arg("type"), nanobind::arg("bias_strength") = 2.0, + nanobind::arg("seed") = nanobind::none(), + "Create a strategy with specified type, optional bias strength, " + "and optional random seed. When seed is None (default), uses " + "CUDA-Q's global random seed.") + .def_rw("type", &ptsbe::ShotAllocationStrategy::type, + "The allocation strategy type.") + .def_rw("bias_strength", &ptsbe::ShotAllocationStrategy::bias_strength, + "Bias factor for weighted strategies. Default value is 2.0."); // Concrete strategies - py::class_>( + nanobind::class_( ptsbe, "ProbabilisticSamplingStrategy", "Sample trajectories randomly based on their occurrence probabilities.") - .def(py::init, std::optional>(), - py::arg("seed") = py::none(), - py::arg("max_trajectory_samples") = py::none(), + .def(nanobind::init, + std::optional>(), + nanobind::arg("seed") = nanobind::none(), + nanobind::arg("max_trajectory_samples") = nanobind::none(), "Create a probabilistic strategy with optional random seed and " "max trajectory sample count. When seed is None (default), uses " "CUDA-Q's global random seed. " @@ -206,20 +211,19 @@ void cudaq::bindSamplePTSBE(py::module &mod) { "The loop stops early once max_trajectories unique patterns are " "found. When None (default), a budget is auto-calculated."); - py::class_>( + nanobind::class_( ptsbe, "OrderedSamplingStrategy", "Sample trajectories sorted by probability in descending order.") - .def(py::init<>(), "Create an ordered strategy."); + .def(nanobind::init<>(), "Create an ordered strategy."); - py::class_>( + nanobind::class_( ptsbe, "ExhaustiveSamplingStrategy", "Enumerate all possible trajectories in lexicographic order.") - .def(py::init<>(), "Create an exhaustive strategy."); + .def(nanobind::init<>(), "Create an exhaustive strategy."); // Trace instruction type enum - py::enum_( + nanobind::enum_( ptsbe, "TraceInstructionType", "Type discriminator for trace instructions.") .value("Gate", ptsbe::TraceInstructionType::Gate) @@ -228,47 +232,48 @@ void cudaq::bindSamplePTSBE(py::module &mod) { .export_values(); // Trace instruction - py::class_( + nanobind::class_( ptsbe, "TraceInstruction", "Single operation in the execution trace.") - .def_property_readonly( + .def_prop_ro( "type", [](const ptsbe::TraceInstruction &self) { return self.type; }) - .def_property_readonly( + .def_prop_ro( "name", [](const ptsbe::TraceInstruction &self) { return self.name; }) - .def_property_readonly("targets", - [](const ptsbe::TraceInstruction &self) { - return std::vector( - self.targets.begin(), self.targets.end()); - }) - .def_property_readonly("controls", - [](const ptsbe::TraceInstruction &self) { - return std::vector( - self.controls.begin(), self.controls.end()); - }) - .def_property_readonly("params", - [](const ptsbe::TraceInstruction &self) { - return std::vector(self.params.begin(), - self.params.end()); - }) + .def_prop_ro("targets", + [](const ptsbe::TraceInstruction &self) { + return std::vector(self.targets.begin(), + self.targets.end()); + }) + .def_prop_ro("controls", + [](const ptsbe::TraceInstruction &self) { + return std::vector(self.controls.begin(), + self.controls.end()); + }) + .def_prop_ro("params", + [](const ptsbe::TraceInstruction &self) { + return std::vector(self.params.begin(), + self.params.end()); + }) .def("__repr__", [](const ptsbe::TraceInstruction &self) { return "TraceInstruction(" + self.name + " on " + std::to_string(self.targets.size()) + " qubits)"; }); // Kraus selection (cudaq:: namespace) - py::class_(ptsbe, "KrausSelection", - "Reference to a single Kraus operator selection.") - .def_property_readonly( + nanobind::class_( + ptsbe, "KrausSelection", + "Reference to a single Kraus operator selection.") + .def_prop_ro( "circuit_location", [](const KrausSelection &self) { return self.circuit_location; }) - .def_property_readonly( + .def_prop_ro( "kraus_operator_index", [](const KrausSelection &self) { return self.kraus_operator_index; }) - .def_property_readonly( - "is_error", [](const KrausSelection &self) { return self.is_error; }) - .def_property_readonly( - "qubits", [](const KrausSelection &self) { return self.qubits; }) - .def_property_readonly( - "op_name", [](const KrausSelection &self) { return self.op_name; }) + .def_prop_ro("is_error", + [](const KrausSelection &self) { return self.is_error; }) + .def_prop_ro("qubits", + [](const KrausSelection &self) { return self.qubits; }) + .def_prop_ro("op_name", + [](const KrausSelection &self) { return self.op_name; }) .def("__repr__", [](const KrausSelection &self) { return "KrausSelection(loc=" + std::to_string(self.circuit_location) + ", idx=" + std::to_string(self.kraus_operator_index) + @@ -276,27 +281,25 @@ void cudaq::bindSamplePTSBE(py::module &mod) { }); // Kraus trajectory (cudaq:: namespace) - py::class_( + nanobind::class_( ptsbe, "KrausTrajectory", "Complete specification of one noise trajectory with outcomes.") - .def_property_readonly( + .def_prop_ro( "trajectory_id", [](const KrausTrajectory &self) { return self.trajectory_id; }) - .def_property_readonly( - "probability", - [](const KrausTrajectory &self) { return self.probability; }) - .def_property_readonly( - "num_shots", - [](const KrausTrajectory &self) { return self.num_shots; }) - .def_readonly("multiplicity", &KrausTrajectory::multiplicity, - "Number of times this trajectory was sampled.") - .def_readonly("weight", &KrausTrajectory::weight, - "Allocation weight for shot distribution.") - .def_property_readonly( + .def_prop_ro("probability", + [](const KrausTrajectory &self) { return self.probability; }) + .def_prop_ro("num_shots", + [](const KrausTrajectory &self) { return self.num_shots; }) + .def_ro("multiplicity", &KrausTrajectory::multiplicity, + "Number of times this trajectory was sampled.") + .def_ro("weight", &KrausTrajectory::weight, + "Allocation weight for shot distribution.") + .def_prop_ro( "kraus_selections", [](const KrausTrajectory &self) { return self.kraus_selections; }, - py::return_value_policy::reference_internal) - .def_property_readonly( + nanobind::rv_policy::reference_internal) + .def_prop_ro( "measurement_counts", [](const KrausTrajectory &self) { return self.measurement_counts; }) .def("__repr__", [](const KrausTrajectory &self) { @@ -306,34 +309,35 @@ void cudaq::bindSamplePTSBE(py::module &mod) { }); // PTSBE execution data container - py::class_( + nanobind::class_( ptsbe, "PTSBEExecutionData", "Container for PTSBE execution data including circuit structure, " "trajectory specifications, and per-trajectory measurement outcomes.") - .def_property_readonly( + .def_prop_ro( "instructions", [](const ptsbe::PTSBEExecutionData &self) -> const std::vector & { return self.instructions; }, - py::return_value_policy::reference_internal) - .def_property_readonly( + nanobind::rv_policy::reference_internal) + .def_prop_ro( "trajectories", [](const ptsbe::PTSBEExecutionData &self) -> const std::vector & { return self.trajectories; }, - py::return_value_policy::reference_internal) + nanobind::rv_policy::reference_internal) .def( "count_instructions", [](const ptsbe::PTSBEExecutionData &self, - ptsbe::TraceInstructionType type, py::object name) -> std::size_t { + ptsbe::TraceInstructionType type, + nanobind::object name) -> std::size_t { std::optional nameOpt; if (!name.is_none()) - nameOpt = name.cast(); + nameOpt = nanobind::cast(name); return self.count_instructions(type, nameOpt); }, - py::arg("type"), py::arg("name") = py::none(), + nanobind::arg("type"), nanobind::arg("name") = nanobind::none(), "Count instructions of a given type.") .def( "get_trajectory", @@ -344,7 +348,8 @@ void cudaq::bindSamplePTSBE(py::module &mod) { return nullptr; return &result.value().get(); }, - py::return_value_policy::reference_internal, py::arg("trajectory_id"), + nanobind::rv_policy::reference_internal, + nanobind::arg("trajectory_id"), "Look up a trajectory by its ID. Returns None if not found.") .def("__repr__", [](const ptsbe::PTSBEExecutionData &self) { @@ -358,10 +363,10 @@ void cudaq::bindSamplePTSBE(py::module &mod) { }); // PTSBE sample result (subclass of sample_result) - py::class_( + nanobind::class_( ptsbe, "PTSBESampleResult", "PTSBE sample result with optional execution data.") - .def_property_readonly( + .def_prop_ro( "ptsbe_execution_data", [](const ptsbe::sample_result &self) -> const ptsbe::PTSBEExecutionData * { @@ -371,31 +376,36 @@ void cudaq::bindSamplePTSBE(py::module &mod) { }, // reference_internal ties the returned object's lifetime to self, // so the pointer into internal data stays valid. - py::return_value_policy::reference_internal, + nanobind::rv_policy::reference_internal, "PTSBE execution data if return_execution_data was True, None " "otherwise.") .def("has_execution_data", &ptsbe::sample_result::has_execution_data, "Check if execution data is available."); // Async PTSBE sample result wrapper - py::class_( + nanobind::class_( ptsbe, "AsyncSampleResultImpl", "Future-like wrapper for asynchronous PTSBE sampling.") .def("get", &AsyncPTSBESampleResultImpl::get, - py::call_guard(), + nanobind::call_guard(), "Block until the PTSBE sampling result is available and return it."); // PTSBE sample implementation - ptsbe.def("sample_impl", pySamplePTSBE, + ptsbe.def("sample_impl", pySamplePTSBE, nanobind::arg("kernel_name"), + nanobind::arg("module"), nanobind::arg("shots_count"), + nanobind::arg("noise_model"), nanobind::arg("max_trajectories"), + nanobind::arg("sampling_strategy").none(), + nanobind::arg("shot_allocation").none(), + nanobind::arg("return_execution_data"), + nanobind::arg("include_sequential_data"), R"pbdoc( Run PTSBE sampling on the provided kernel. Args: kernel_name: The kernel name. module: The MLIR module. - return_type: The MLIR return type. shots_count: The number of shots. - noise_model: Optional noise model for gate-based noise; may be None. + noise_model: The noise model. max_trajectories: Maximum unique trajectories, or None to use shots. sampling_strategy: Sampling strategy or None for default (probabilistic). shot_allocation: Shot allocation strategy or None for default (proportional). @@ -409,6 +419,13 @@ Run PTSBE sampling on the provided kernel. // PTSBE async sample implementation ptsbe.def("sample_async_impl", pySampleAsyncPTSBE, + nanobind::arg("kernel_name"), nanobind::arg("module"), + nanobind::arg("shots_count"), nanobind::arg("noise_model"), + nanobind::arg("max_trajectories"), + nanobind::arg("sampling_strategy").none(), + nanobind::arg("shot_allocation").none(), + nanobind::arg("return_execution_data"), + nanobind::arg("include_sequential_data"), "Run PTSBE sampling asynchronously. Returns an " "AsyncSampleResultImpl."); } diff --git a/python/runtime/cudaq/algorithms/py_sample_ptsbe.h b/python/runtime/cudaq/algorithms/py_sample_ptsbe.h index 2c5f2869486..ad8386efd64 100644 --- a/python/runtime/cudaq/algorithms/py_sample_ptsbe.h +++ b/python/runtime/cudaq/algorithms/py_sample_ptsbe.h @@ -8,8 +8,8 @@ #pragma once -#include +#include namespace cudaq { -void bindSamplePTSBE(pybind11::module &mod); +void bindSamplePTSBE(nanobind::module_ &mod); } // namespace cudaq diff --git a/python/runtime/cudaq/algorithms/py_state.cpp b/python/runtime/cudaq/algorithms/py_state.cpp index 38fa842f020..d1099e692be 100644 --- a/python/runtime/cudaq/algorithms/py_state.cpp +++ b/python/runtime/cudaq/algorithms/py_state.cpp @@ -13,8 +13,16 @@ #include "cudaq/algorithms/get_state.h" #include "cudaq/runtime/logger/logger.h" #include "runtime/cudaq/platform/py_alt_launch_kernel.h" +#include "utils/NanobindAdaptors.h" #include "utils/OpaqueArguments.h" -#include "mlir/Bindings/Python/PybindAdaptors.h" +#include +#include +#include +#include +#include +#include +#include +#include using namespace cudaq; @@ -41,7 +49,7 @@ static std::vector bitStringToIntVec(const std::string &bitString) { /// @brief Run `cudaq::get_state` on the provided kernel and spin operator. static state get_state_impl(const std::string &shortName, MlirModule mod, - py::args args) { + nanobind::args args) { auto closure = [=]() { return marshal_and_launch_module(shortName, mod, args); }; @@ -51,7 +59,7 @@ static state get_state_impl(const std::string &shortName, MlirModule mod, static std::future get_state_async_impl(const std::string &shortName, MlirModule module, std::size_t qpu_id, - py::args args) { + nanobind::args args) { // Launch the asynchronous execution. auto mod = unwrap(module); std::string kernelName = shortName; @@ -59,7 +67,7 @@ static std::future get_state_async_impl(const std::string &shortName, auto fnOp = getKernelFuncOp(mod, shortName); auto opaques = marshal_arguments_for_module_launch(mod, args, fnOp); - py::gil_scoped_release release; + nanobind::gil_scoped_release release; return details::runGetStateAsync( detail::make_copyable_function([opaques = std::move(opaques), kernelName, mod = mod.clone()]() mutable { @@ -131,12 +139,12 @@ class PyRemoteSimulationState : public RemoteSimulationState { /// @brief Run `cudaq::get_state` for remote execution targets on the provided /// kernel and args -state pyGetStateRemote(py::object kernel, py::args args) { - if (py::hasattr(kernel, "compile")) +state pyGetStateRemote(nanobind::object kernel, nanobind::args args) { + if (nanobind::hasattr(kernel, "compile")) kernel.attr("compile")(); - auto kernelName = kernel.attr("uniqName").cast(); - auto kernelMod = kernel.attr("qkeModule").cast(); + auto kernelName = nanobind::cast(kernel.attr("uniqName")); + auto kernelMod = nanobind::cast(kernel.attr("qkeModule")); args = simplifiedValidateInputArguments(args); auto *argData = toOpaqueArgs(args, kernelMod, kernelName); #if 0 @@ -170,7 +178,7 @@ class PyQPUState : public QPUState { /// @brief Run `cudaq::get_state` for qpu targets on the provided /// kernel and args state pyGetStateQPU(const std::string &kernelName, MlirModule kernelMod, - py::args args) { + nanobind::args args) { auto moduleOp = unwrap(kernelMod); std::string mlirCode; llvm::raw_string_ostream outStr(mlirCode); @@ -182,45 +190,62 @@ state pyGetStateQPU(const std::string &kernelName, MlirModule kernelMod, return state(new PyQPUState(kernelName, mlirCode, argData)); } -state pyGetStateLibraryMode(py::object kernel, py::args args) { +state pyGetStateLibraryMode(nanobind::object kernel, nanobind::args args) { return details::extractState([&]() mutable { if (0 == args.size()) kernel(); else { - std::vector argsData; + std::vector argsData; for (size_t i = 0; i < args.size(); i++) { - py::object arg = args[i]; - argsData.emplace_back(std::forward(arg)); + nanobind::object arg = args[i]; + argsData.emplace_back(std::forward(arg)); } kernel(std::move(argsData)); } }); } -static py::buffer_info getCupyBufferInfo(py::buffer cupy_buffer) { - // Note: cupy 13.5+ arrays will bind (overload resolution) to a py::buffer - // type. However, we cannot access the underlying buffer info via a +/// @brief Helper struct to hold buffer metadata, analogous to Python's +/// buffer_info. +struct BufferInfo { + void *ptr = nullptr; + std::size_t itemsize = 0; + std::string format; + std::size_t ndim = 0; + std::vector shape; + std::vector strides; + bool readonly = false; + std::size_t size = 0; // total number of elements +}; + +static BufferInfo getCupyBufferInfo(nanobind::object cupy_buffer) { + // Note: cupy 13.5+ arrays will bind (overload resolution) to a + // nanobind::object type. However, we cannot access the underlying buffer info + // via a // `.request()` as it will throw unless that is managed memory. Here, we - // retrieve and construct buffer_info from the CuPy array interface. + // retrieve and construct BufferInfo from the CuPy array interface. - if (!py::hasattr(cupy_buffer, "__cuda_array_interface__")) { + if (!nanobind::hasattr(cupy_buffer, "__cuda_array_interface__")) { throw std::runtime_error("Buffer is not a CuPy array"); } - py::dict cupy_array_info = cupy_buffer.attr("__cuda_array_interface__"); + nanobind::dict cupy_array_info = nanobind::cast( + cupy_buffer.attr("__cuda_array_interface__")); // Ref: https://numba.readthedocs.io/en/stable/cuda/cuda_array_interface.html // example: {'shape': (2, 2), 'typestr': '(); - void *dataPtr = (void *)dataInfo[0].cast(); - const bool readOnly = dataInfo[1].cast(); - auto shapeTuple = cupy_array_info["shape"].cast(); + nanobind::tuple dataInfo = + nanobind::cast(cupy_array_info["data"]); + void *dataPtr = (void *)nanobind::cast(dataInfo[0]); + const bool readOnly = nanobind::cast(dataInfo[1]); + auto shapeTuple = nanobind::cast(cupy_array_info["shape"]); std::vector extents; for (std::size_t i = 0; i < shapeTuple.size(); i++) { - extents.push_back(shapeTuple[i].cast()); + extents.push_back(nanobind::cast(shapeTuple[i])); } - const std::string typeStr = cupy_array_info["typestr"].cast(); + const std::string typeStr = + nanobind::cast(cupy_array_info["typestr"]); if (typeStr != "), - py::format_descriptor>::format()) - : std::make_tuple( - sizeof(std::complex), - py::format_descriptor>::format()); + std::size_t dataTypeSize = isDoublePrecision ? sizeof(std::complex) + : sizeof(std::complex); + std::string desc = isDoublePrecision ? "Zd" : "Zf"; std::vector strides(extents.size(), dataTypeSize); for (size_t i = 1; i < extents.size(); ++i) strides[i] = strides[i - 1] * extents[i - 1]; - return py::buffer_info(dataPtr, dataTypeSize, /*itemsize */ - desc, extents.size(), /* ndim */ - extents, /* shape */ - strides, /* strides */ - readOnly /* readonly */ - ); + std::size_t totalSize = 1; + for (auto e : extents) + totalSize *= e; + + BufferInfo info; + info.ptr = dataPtr; + info.itemsize = dataTypeSize; + info.format = desc; + info.ndim = extents.size(); + info.shape = extents; + info.strides = strides; + info.readonly = readOnly; + info.size = totalSize; + return info; } -static cudaq::state createStateFromPyBuffer(py::buffer data, +/// @brief Helper to get BufferInfo from a numpy array via Python buffer +/// protocol. +static BufferInfo getNumpyBufferInfo(nanobind::object numpy_array) { + nanobind::module_ np = nanobind::module_::import_("numpy"); + auto dtype = numpy_array.attr("dtype"); + std::string dtypeStr = nanobind::cast(dtype.attr("name")); + + BufferInfo info; + if (dtypeStr == "complex64") { + info.itemsize = sizeof(std::complex); + info.format = "Zf"; + } else if (dtypeStr == "complex128") { + info.itemsize = sizeof(std::complex); + info.format = "Zd"; + } else { + info.format = dtypeStr; + info.itemsize = nanobind::cast(dtype.attr("itemsize")); + } + auto shapeTuple = nanobind::cast(numpy_array.attr("shape")); + info.ndim = shapeTuple.size(); + info.size = 1; + for (std::size_t i = 0; i < shapeTuple.size(); i++) { + auto ext = nanobind::cast(shapeTuple[i]); + info.shape.push_back(ext); + info.size *= ext; + } + auto stridesTuple = + nanobind::cast(numpy_array.attr("strides")); + for (std::size_t i = 0; i < stridesTuple.size(); i++) { + info.strides.push_back(nanobind::cast(stridesTuple[i])); + } + // Get the raw data pointer via numpy's ctypes interface + info.ptr = reinterpret_cast( + nanobind::cast(numpy_array.attr("ctypes").attr("data"))); + info.readonly = false; + return info; +} + +static cudaq::state createStateFromPyBuffer(nanobind::object data, LinkedLibraryHolder &holder) { - const bool isHostData = !py::hasattr(data, "__cuda_array_interface__"); + const bool isHostData = !nanobind::hasattr(data, "__cuda_array_interface__"); // Check that the target is GPU-based, i.e., can handle device // pointer. if (!holder.getTarget().config.GpuRequired && !isHostData) @@ -259,12 +325,11 @@ static cudaq::state createStateFromPyBuffer(py::buffer data, fmt::format("Current target '{}' does not support CuPy arrays.", holder.getTarget().name)); - auto info = isHostData ? data.request() : getCupyBufferInfo(data); + auto info = isHostData ? getNumpyBufferInfo(data) : getCupyBufferInfo(data); if (info.shape.size() > 2) throw std::runtime_error( "state.from_data only supports 1D or 2D array data."); - if (info.format != py::format_descriptor>::format() && - info.format != py::format_descriptor>::format()) + if (info.format != "Zf" && info.format != "Zd") throw std::runtime_error( "A numpy array with only floating point elements passed to " "`state.from_data`. Input must be of complex float type. Please add to " @@ -273,7 +338,7 @@ static cudaq::state createStateFromPyBuffer(py::buffer data, "`dtype=cudaq.complex()` for precision-agnostic code."); if (!isHostData || info.shape.size() == 1) { - if (info.format == py::format_descriptor>::format()) + if (info.format == "Zf") return state::from_data(std::make_pair( reinterpret_cast *>(info.ptr), info.size)); @@ -286,8 +351,7 @@ static cudaq::state createStateFromPyBuffer(py::buffer data, throw std::runtime_error( "state.from_data 2D array (density matrix) input must be " "square matrix data."); - const bool isDoublePrecision = - info.format == py::format_descriptor>::format(); + const bool isDoublePrecision = (info.format == "Zd"); const int64_t dataSize = isDoublePrecision ? sizeof(std::complex) : sizeof(std::complex); const bool rowMajor = @@ -313,15 +377,15 @@ static cudaq::state createStateFromPyBuffer(py::buffer data, } /// @brief Bind the get_state cudaq function -void cudaq::bindPyState(py::module &mod, LinkedLibraryHolder &holder) { - py::enum_(mod, "InitialStateType", - "Enumeration describing the initial state " - "type to be created in the backend") +void cudaq::bindPyState(nanobind::module_ &mod, LinkedLibraryHolder &holder) { + nanobind::enum_(mod, "InitialStateType", + "Enumeration describing the initial state " + "type to be created in the backend") .value("ZERO", InitialState::ZERO) .value("UNIFORM", InitialState::UNIFORM) .export_values(); - py::class_( + nanobind::class_( mod, "Tensor", "The `Tensor` describes a pointer to simulation data as well as the rank " "and extents for that tensorial data it represents.") @@ -329,87 +393,112 @@ void cudaq::bindPyState(py::module &mod, LinkedLibraryHolder &holder) { [](SimulationState::Tensor &tensor) { return reinterpret_cast(tensor.data); }) - .def_readonly("extents", &SimulationState::Tensor::extents) + .def_ro("extents", &SimulationState::Tensor::extents) .def("get_rank", &SimulationState::Tensor::get_rank) .def("get_element_size", &SimulationState::Tensor::element_size) .def("get_num_elements", &SimulationState::Tensor::get_num_elements); - py::class_( - mod, "State", py::buffer_protocol(), + nanobind::class_( + mod, "State", "A data-type representing the quantum state of the internal simulator. " "This type is not user-constructible and instances can only be retrieved " "via the `cudaq.get_state(...)` function or the static " "`cudaq.State.from_data()` method.\n") - .def_buffer([](const state &self) { - if (self.get_num_tensors() != 1) - throw std::runtime_error("Numpy interop is only supported for vector " - "and matrix state data."); - - // This method is used by Pybind to enable interoperability with NumPy - // array data. We therefore must be careful since the state data may - // actually be on GPU device. - - // Get the data pointer. - // Data may be on GPU device, if so we must make a copy to host. - // If users do not want this copy, they will have to operate apart - // from Numpy - void *dataPtr = nullptr; - auto stateVector = self.get_tensor(); - auto precision = self.get_precision(); - if (self.is_on_gpu()) { - // This is device data, transfer to host, which gives us - // ownership of a new data pointer on host. Store it globally - // here so we ensure that it gets cleaned up. - auto numElements = stateVector.get_num_elements(); - if (precision == SimulationState::precision::fp32) { - auto *hostData = new std::complex[numElements]; - self.to_host(hostData, numElements); - dataPtr = reinterpret_cast(hostData); - } else { - auto *hostData = new std::complex[numElements]; - self.to_host(hostData, numElements); - dataPtr = reinterpret_cast(hostData); - } - hostDataFromDevice.emplace_back(dataPtr, [precision](void *data) { - CUDAQ_INFO("freeing data that was copied from GPU device for " - "compatibility with NumPy"); - // Use delete[] to match new[] allocation (not free()) - if (precision == SimulationState::precision::fp32) - delete[] static_cast *>(data); - else - delete[] static_cast *>(data); - }); - } else { - dataPtr = self.get_tensor().data; - } - - // We need to know the precision of the simulation data to get the - // data type size and the format descriptor - auto [dataTypeSize, desc] = - precision == SimulationState::precision::fp32 - ? std::make_tuple( - sizeof(std::complex), - py::format_descriptor>::format()) - : std::make_tuple( - sizeof(std::complex), - py::format_descriptor>::format()); - - // Get the shape of the data. Return buffer info in a correctly - // shaped manner. - auto shape = self.get_tensor().extents; - if (shape.size() != 1) - return py::buffer_info(dataPtr, dataTypeSize, /*itemsize */ - desc, 2, /* ndim */ - {shape[0], shape[1]}, /* shape */ - {dataTypeSize * static_cast(shape[1]), - dataTypeSize}, /* strides */ - true /* readonly */ - ); - return py::buffer_info(dataPtr, dataTypeSize, /*itemsize */ - desc, 1, /* ndim */ - {shape[0]}, /* shape */ - {dataTypeSize}); - }) + .def( + "__array__", + [](const state &self, nanobind::object dtype_obj, + nanobind::object copy_obj) { + if (self.get_num_tensors() != 1) + throw std::runtime_error( + "Numpy interop is only supported for vector " + "and matrix state data."); + + // This method enables interoperability with NumPy array data. + // We must be careful since the state data may actually be on GPU + // device. + + nanobind::module_ np = nanobind::module_::import_("numpy"); + auto stateVector = self.get_tensor(); + auto precision = self.get_precision(); + auto shape = self.get_tensor().extents; + + // Determine numpy dtype + nanobind::object np_dtype = + precision == SimulationState::precision::fp32 + ? np.attr("complex64") + : np.attr("complex128"); + + if (self.is_on_gpu()) { + // This is device data, transfer to host + auto numElements = stateVector.get_num_elements(); + nanobind::object arr; + if (precision == SimulationState::precision::fp32) { + auto *hostData = new std::complex[numElements]; + self.to_host(hostData, numElements); + // Create numpy array and copy data + if (shape.size() != 1) { + nanobind::tuple np_shape = + nanobind::make_tuple(shape[0], shape[1]); + arr = np.attr("empty")(np_shape, np_dtype); + } else { + nanobind::tuple np_shape = nanobind::make_tuple(shape[0]); + arr = np.attr("empty")(np_shape, np_dtype); + } + auto *destPtr = reinterpret_cast *>( + nanobind::cast(arr.attr("ctypes").attr("data"))); + std::memcpy(destPtr, hostData, + numElements * sizeof(std::complex)); + delete[] hostData; + } else { + auto *hostData = new std::complex[numElements]; + self.to_host(hostData, numElements); + if (shape.size() != 1) { + nanobind::tuple np_shape = + nanobind::make_tuple(shape[0], shape[1]); + arr = np.attr("empty")(np_shape, np_dtype); + } else { + nanobind::tuple np_shape = nanobind::make_tuple(shape[0]); + arr = np.attr("empty")(np_shape, np_dtype); + } + auto *destPtr = reinterpret_cast *>( + nanobind::cast(arr.attr("ctypes").attr("data"))); + std::memcpy(destPtr, hostData, + numElements * sizeof(std::complex)); + delete[] hostData; + } + return arr; + } + + // Host data path - wrap existing memory + void *dataPtr = self.get_tensor().data; + auto numElements = stateVector.get_num_elements(); + if (shape.size() != 1) { + nanobind::tuple np_shape = + nanobind::make_tuple(shape[0], shape[1]); + // Use np.frombuffer-like approach: create array from pointer + nanobind::object arr = np.attr("empty")(np_shape, np_dtype); + auto *destPtr = reinterpret_cast( + nanobind::cast(arr.attr("ctypes").attr("data"))); + std::size_t dataTypeSize = + precision == SimulationState::precision::fp32 + ? sizeof(std::complex) + : sizeof(std::complex); + std::memcpy(destPtr, dataPtr, numElements * dataTypeSize); + return arr; + } + nanobind::tuple np_shape = nanobind::make_tuple(shape[0]); + nanobind::object arr = np.attr("empty")(np_shape, np_dtype); + auto *destPtr = reinterpret_cast( + nanobind::cast(arr.attr("ctypes").attr("data"))); + std::size_t dataTypeSize = + precision == SimulationState::precision::fp32 + ? sizeof(std::complex) + : sizeof(std::complex); + std::memcpy(destPtr, dataPtr, numElements * dataTypeSize); + return arr; + }, + nanobind::arg("dtype") = nanobind::none(), + nanobind::arg("copy") = nanobind::none()) .def( "__len__", [](state &self) { @@ -433,16 +522,28 @@ void cudaq::bindPyState(py::module &mod, LinkedLibraryHolder &holder) { "Convert the address of the state object to an integer.") .def_static( "from_data", - [&](py::buffer data) { + [&](nanobind::object data) { + // Reject Python sequences (list/tuple) overload — they should be + // dispatched to the vector overload below. In pybind11, py::buffer + // excluded lists; nanobind::object accepts anything, so we must + // guard explicitly. + if (nanobind::isinstance(data) || + nanobind::isinstance(data)) + throw nanobind::next_overload(); return createStateFromPyBuffer(data, holder); }, "Return a state from data.") .def_static( "from_data", - [&holder](const std::vector &tensors) { + [&holder](const std::vector &tensors) { + // Reject SimulationState::Tensor objects overload — they're handled + // by the next overload and don't have numpy/cupy buffer attributes. + if (!tensors.empty() && + nanobind::isinstance(tensors[0])) + throw nanobind::next_overload(); const bool isHostData = tensors.empty() || - !py::hasattr(tensors[0], "__cuda_array_interface__"); + !nanobind::hasattr(tensors[0], "__cuda_array_interface__"); // Check that the target is GPU-based, i.e., can handle device // pointer. if (!holder.getTarget().config.GpuRequired && !isHostData) @@ -451,8 +552,8 @@ void cudaq::bindPyState(py::module &mod, LinkedLibraryHolder &holder) { holder.getTarget().name)); TensorStateData tensorData; for (auto &tensor : tensors) { - auto info = - isHostData ? tensor.request() : getCupyBufferInfo(tensor); + auto info = isHostData ? getNumpyBufferInfo(tensor) + : getCupyBufferInfo(tensor); const std::vector extents(info.shape.begin(), info.shape.end()); tensorData.emplace_back( @@ -477,36 +578,38 @@ void cudaq::bindPyState(py::module &mod, LinkedLibraryHolder &holder) { "Return a state from matrix product state tensor data.") .def_static( "from_data", - [](const py::list &tensors) { - // Note: we must use Python type (py::list) for proper overload - // resolution. The overload for py::object, intended for cupy arrays - // (implementing Python array interface), may be overshadowed by any - // std::vector overloads. + [](const nanobind::list &tensors) { + // Note: we must use Python type (nanobind::list) for proper + // overload resolution. The overload for nanobind::object, intended + // for cupy arrays (implementing Python array interface), may be + // overshadowed by any std::vector overloads. TensorStateData tensorData; - for (auto &tensor : tensors) { + for (auto tensor : tensors) { // Make sure this is a CuPy array - if (!py::hasattr(tensor, "data")) + if (!nanobind::hasattr(tensor, "data")) throw std::runtime_error( - "invalid from_data operation on py::object - " + "invalid from_data operation on nanobind::object - " "only cupy array supported."); auto data = tensor.attr("data"); - if (!py::hasattr(data, "ptr")) + if (!nanobind::hasattr(data, "ptr")) throw std::runtime_error( - "invalid from_data operation on py::object tensors - " + "invalid from_data operation on nanobind::object tensors - " "only cupy array supported."); // We know this is a cupy device pointer. Start by ensuring it is // of proper complex type - auto typeStr = py::str(tensor.attr("dtype")).cast(); + auto typeStr = nanobind::cast( + tensor.attr("dtype").attr("name")); if (typeStr != "complex128") throw std::runtime_error( - "invalid from_data operation on py::object tensors - " + "invalid from_data operation on nanobind::object tensors - " "only cupy complex128 tensors supported."); - auto shape = tensor.attr("shape").cast(); + auto shape = + nanobind::cast(tensor.attr("shape")); std::vector extents; for (auto el : shape) - extents.emplace_back(el.cast()); - long ptr = data.attr("ptr").cast(); + extents.emplace_back(nanobind::cast(el)); + long ptr = nanobind::cast(data.attr("ptr")); tensorData.emplace_back( std::pair>{ reinterpret_cast *>(ptr), extents}); @@ -517,24 +620,24 @@ void cudaq::bindPyState(py::module &mod, LinkedLibraryHolder &holder) { "ndarray).") .def_static( "from_data", - [&holder](py::object opaqueData) { + [&holder](nanobind::object opaqueData) { // Note: This overload is no longer needed from cupy 13.5+ onward. // We can remove it in future releases. // Make sure this is a CuPy array - if (!py::hasattr(opaqueData, "data")) + if (!nanobind::hasattr(opaqueData, "data")) throw std::runtime_error( - "invalid from_data operation on py::object - " + "invalid from_data operation on nanobind::object - " "only cupy array supported."); auto data = opaqueData.attr("data"); - if (!py::hasattr(data, "ptr")) + if (!nanobind::hasattr(data, "ptr")) throw std::runtime_error( - "invalid from_data operation on py::object - " + "invalid from_data operation on nanobind::object - " "only cupy array supported."); // We know this is a cupy device pointer. Start by ensuring it is of // complex type - auto typeStr = - py::str(opaqueData.attr("dtype")).cast(); + auto typeStr = nanobind::cast( + opaqueData.attr("dtype").attr("name")); if (typeStr.find("float") != std::string::npos) throw std::runtime_error( "CuPy array with only floating point elements passed to " @@ -546,16 +649,17 @@ void cudaq::bindPyState(py::module &mod, LinkedLibraryHolder &holder) { // Compute the number of elements in the array std::vector extents; auto numElements = [&]() { - auto shape = opaqueData.attr("shape").cast(); + auto shape = + nanobind::cast(opaqueData.attr("shape")); std::size_t numElements = 1; for (auto el : shape) { - numElements *= el.cast(); - extents.emplace_back(el.cast()); + numElements *= nanobind::cast(el); + extents.emplace_back(nanobind::cast(el)); } return numElements; }(); - long ptr = data.attr("ptr").cast(); + long ptr = nanobind::cast(data.attr("ptr")); if (holder.getTarget().name == "dynamics") { // For dynamics, we need to send on the extents to distinguish // state vector vs density matrix. @@ -587,7 +691,7 @@ void cudaq::bindPyState(py::module &mod, LinkedLibraryHolder &holder) { .def( "getTensor", [](state &self, std::size_t idx) { return self.get_tensor(idx); }, - py::arg("idx") = 0, + nanobind::arg("idx") = 0, "Return the `idx` tensor making up this state representation.") .def( "getTensors", [](state &self) { return self.get_tensors(); }, @@ -699,7 +803,7 @@ index pair. [](state &self) { std::stringstream ss; self.dump(ss); - py::print(ss.str()); + nanobind::print(ss.str().c_str()); }, "Print the state to the console.") .def("__str__", @@ -714,7 +818,7 @@ index pair. "Compute the overlap between the provided :class:`State`'s.") .def( "overlap", - [&holder](state &self, py::buffer &other) { + [&holder](state &self, nanobind::object &other) { if (self.get_num_tensors() != 1) throw std::runtime_error("overlap NumPy interop only supported " "for vector and matrix state data."); @@ -724,24 +828,25 @@ index pair. "Compute the overlap between the provided :class:`State`'s.") .def( "overlap", - [](state &self, py::object other) { + [](state &self, nanobind::object other) { // Note: This overload is no longer needed from cupy 13.5+ onward. // We can remove it in future releases. Make sure this is a CuPy // array - if (!py::hasattr(other, "data")) + if (!nanobind::hasattr(other, "data")) throw std::runtime_error( - "invalid overlap operation on py::object - " + "invalid overlap operation on nanobind::object - " "only cupy array supported."); auto data = other.attr("data"); - if (!py::hasattr(data, "ptr")) + if (!nanobind::hasattr(data, "ptr")) throw std::runtime_error( - "invalid overlap operation on py::object - " + "invalid overlap operation on nanobind::object - " "only cupy array supported."); // We know this is a cupy device pointer. // Start by ensuring it is of complex type - auto typeStr = py::str(other.attr("dtype")).cast(); + auto typeStr = + nanobind::cast(other.attr("dtype").attr("name")); if (typeStr.find("float") != std::string::npos) throw std::runtime_error( "CuPy array with only floating point elements passed to " @@ -765,15 +870,15 @@ index pair. // Compute the number of elements in the other array auto numOtherElements = [&]() { - auto shape = other.attr("shape").cast(); + auto shape = nanobind::cast(other.attr("shape")); std::size_t numElements = 1; for (auto el : shape) - numElements *= el.cast(); + numElements *= nanobind::cast(el); return numElements; }(); // Cast the device ptr and perform the overlap - long ptr = data.attr("ptr").cast(); + long ptr = nanobind::cast(data.attr("ptr")); if (precision == SimulationState::precision::fp32) return self.overlap(state::from_data( std::make_pair(reinterpret_cast *>(ptr), @@ -787,7 +892,8 @@ index pair. mod.def( "get_state_impl", - [&](const std::string &shortName, MlirModule module, py::args args) { + [&](const std::string &shortName, MlirModule module, + nanobind::args args) { // Check for unsupported cases. if (holder.getTarget().name == "remote-mqpu" || holder.getTarget().name == "orca-photonics") @@ -800,7 +906,7 @@ index pair. }, "See the python documentation for get_state."); - py::class_( + nanobind::class_( mod, "AsyncStateResult", R"#(A data-type containing the results of a call to :func:`get_state_async`. The `AsyncStateResult` models a future-like type, whose @@ -810,14 +916,14 @@ See `future `_ for more information on this programming pattern.)#") .def( "get", [](async_state_result &self) { return self.get(); }, - py::call_guard(), + nanobind::call_guard(), "Return the :class:`State` from the asynchronous `get_state` " "accessor execution.\n"); mod.def( "get_state_async_impl", [&](const std::string &shortName, MlirModule module, std::size_t qpu_id, - py::args args) { + nanobind::args args) { // Check for unsupported cases. if (holder.getTarget().name == "remote-mqpu" || holder.getTarget().name == "nvqc" || diff --git a/python/runtime/cudaq/algorithms/py_state.h b/python/runtime/cudaq/algorithms/py_state.h index e290aa35e1a..7a7152f8d1f 100644 --- a/python/runtime/cudaq/algorithms/py_state.h +++ b/python/runtime/cudaq/algorithms/py_state.h @@ -8,11 +8,11 @@ #pragma once -#include -#include +#include +#include namespace cudaq { class LinkedLibraryHolder; -void bindPyState(pybind11::module &mod, LinkedLibraryHolder &holder); +void bindPyState(nanobind::module_ &mod, LinkedLibraryHolder &holder); } // namespace cudaq diff --git a/python/runtime/cudaq/algorithms/py_translate.cpp b/python/runtime/cudaq/algorithms/py_translate.cpp index 4d5f834ed0c..503cbc38cce 100644 --- a/python/runtime/cudaq/algorithms/py_translate.cpp +++ b/python/runtime/cudaq/algorithms/py_translate.cpp @@ -13,8 +13,8 @@ #include "cudaq/platform/default/python/QPU.h" #include "cudaq/runtime/logger/logger.h" #include "runtime/cudaq/platform/py_alt_launch_kernel.h" +#include "utils/NanobindAdaptors.h" #include "utils/OpaqueArguments.h" -#include "mlir/Bindings/Python/PybindAdaptors.h" #include "mlir/Pass/PassManager.h" #include "mlir/Target/LLVMIR/Export.h" @@ -23,7 +23,7 @@ using namespace mlir; /// @brief Run `cudaq::translate` on the provided kernel. static std::string translate_impl(const std::string &shortName, MlirModule module, const std::string &format, - py::args runtimeArguments) { + nanobind::args runtimeArguments) { StringRef format_ = format; auto formatPair = format_.split(':'); auto mod = unwrap(module); @@ -66,7 +66,7 @@ static std::string translate_impl(const std::string &shortName, } /// @brief Bind the translate cudaq function -void cudaq::bindPyTranslate(py::module &mod) { +void cudaq::bindPyTranslate(nanobind::module_ &mod) { mod.def("translate_impl", translate_impl, "See python documentation for translate."); // Internal translation to QIR for testing and internal use. Not intended to diff --git a/python/runtime/cudaq/algorithms/py_translate.h b/python/runtime/cudaq/algorithms/py_translate.h index 67b43598744..041167f7017 100644 --- a/python/runtime/cudaq/algorithms/py_translate.h +++ b/python/runtime/cudaq/algorithms/py_translate.h @@ -8,10 +8,8 @@ #pragma once -#include - -namespace py = pybind11; +#include namespace cudaq { -void bindPyTranslate(py::module &mod); +void bindPyTranslate(nanobind::module_ &mod); } // namespace cudaq diff --git a/python/runtime/cudaq/algorithms/py_unitary.cpp b/python/runtime/cudaq/algorithms/py_unitary.cpp index fad6bd1d0c7..3aefbbc957d 100644 --- a/python/runtime/cudaq/algorithms/py_unitary.cpp +++ b/python/runtime/cudaq/algorithms/py_unitary.cpp @@ -10,26 +10,25 @@ #include "cudaq/algorithms/unitary.h" #include "runtime/cudaq/operators/py_helpers.h" #include "runtime/cudaq/platform/py_alt_launch_kernel.h" -#include "mlir/Bindings/Python/PybindAdaptors.h" - -namespace py = pybind11; +#include "utils/NanobindAdaptors.h" using namespace cudaq; /// Compute the unitary of this kernel module. -static py::array get_unitary_impl(const std::string &shortName, - MlirModule module, py::args args) { +static nanobind::object get_unitary_impl(const std::string &shortName, + MlirModule module, + nanobind::args args) { auto f = [=]() { return cudaq::marshal_and_launch_module(shortName, module, args); }; // Return as numpy array (dim, dim), complex128 auto temp = contrib::get_unitary_cmat(std::move(f)); - return details::cmat_to_numpy(temp); + return nanobind::cast(details::cmat_to_numpy(temp)); } /// Bind the get_unitary cudaq function -void cudaq::bindPyUnitary(py::module &mod) { +void cudaq::bindPyUnitary(nanobind::module_ &mod) { mod.def("get_unitary_impl", get_unitary_impl, "See python documentation for get_unitary()."); } diff --git a/python/runtime/cudaq/algorithms/py_unitary.h b/python/runtime/cudaq/algorithms/py_unitary.h index ea2ffeca055..fccac11e42b 100644 --- a/python/runtime/cudaq/algorithms/py_unitary.h +++ b/python/runtime/cudaq/algorithms/py_unitary.h @@ -8,10 +8,8 @@ #pragma once -#include - -namespace py = pybind11; +#include namespace cudaq { -void bindPyUnitary(py::module &mod); +void bindPyUnitary(nanobind::module_ &mod); } // namespace cudaq diff --git a/python/runtime/cudaq/algorithms/py_utils.cpp b/python/runtime/cudaq/algorithms/py_utils.cpp index 0c6b16ec7c7..e396f93c3a5 100644 --- a/python/runtime/cudaq/algorithms/py_utils.cpp +++ b/python/runtime/cudaq/algorithms/py_utils.cpp @@ -8,52 +8,56 @@ #include "py_utils.h" #include "cudaq/utils/cudaq_utils.h" -#include -#include +#include +#include +#include +#include +#include namespace cudaq { -py::dict get_serializable_var_dict() { - py::object json = py::module_::import("json"); - py::dict serialized_dict; +nanobind::dict get_serializable_var_dict() { + nanobind::object json = nanobind::module_::import_("json"); + nanobind::dict serialized_dict; auto try_to_add_item = [&](const auto item) { try { auto key = item.first; auto value = item.second; - if (key.template cast().starts_with("__")) { + if (nanobind::cast(key).starts_with("__")) { // Ignore items that start with "__" (like Python __builtins__, etc.) - } else if (py::hasattr(value, "to_json")) { - auto type = value.get_type(); + } else if (nanobind::hasattr(value, "to_json")) { + auto type = value.type(); std::string module = - type.attr("__module__").template cast(); - std::string name = type.attr("__name__").template cast(); - auto type_name = py::str(module + "." + name); - auto json_key_name = py::str(key) + py::str("/") + type_name; + nanobind::cast(type.attr("__module__")); + std::string name = nanobind::cast(type.attr("__name__")); + auto type_name = nanobind::str((module + "." + name).c_str()); + auto json_key_name = nanobind::str(nanobind::str(key).c_str()) + + nanobind::str("/") + type_name; serialized_dict[json_key_name] = json.attr("loads")(value.attr("to_json")()); - } else if (py::hasattr(value, "tolist")) { + } else if (nanobind::hasattr(value, "tolist")) { serialized_dict[key] = json.attr("loads")(json.attr("dumps")(value.attr("tolist")())); } else { serialized_dict[key] = json.attr("loads")(json.attr("dumps")(value)); } - } catch (const py::error_already_set &e) { + } catch (const nanobind::python_error &e) { // Uncomment the following lines for debug, but all this really means is // that we won't send this to the remote server. // std::cout << "Failed to serialize key '" - // << item.first.template cast() + // << nanobind::cast(item.first) // << "' : " + std::string(e.what()) << std::endl; } }; - for (const auto item : py::globals()) + for (const auto item : nanobind::globals()) try_to_add_item(item); - py::object inspect = py::module::import("inspect"); - std::vector frame_vec; + nanobind::object inspect = nanobind::module_::import_("inspect"); + std::vector frame_vec; auto current_frame = inspect.attr("currentframe")(); while (current_frame && !current_frame.is_none()) { frame_vec.push_back(current_frame); @@ -64,7 +68,8 @@ py::dict get_serializable_var_dict() { // globals first to locals last. This ensures that the overwrites give // precedence to closest-to-locals. for (auto it = frame_vec.rbegin(); it != frame_vec.rend(); ++it) { - py::dict f_locals = it->attr("f_locals"); + nanobind::dict f_locals = + nanobind::cast(it->attr("f_locals")); for (const auto item : f_locals) try_to_add_item(item); } @@ -104,56 +109,60 @@ static std::size_t strip_leading_whitespace(std::string &source_code) { return min_indent; } -std::string get_source_code(const py::function &func) { +std::string get_source_code(const nanobind::callable &func) { // Get the source code - py::module_ analysis = py::module_::import("cudaq.kernel.analysis"); - py::object FetchDepFuncsSourceCode = analysis.attr("FetchDepFuncsSourceCode"); - py::object source_code; + nanobind::module_ analysis = + nanobind::module_::import_("cudaq.kernel.analysis"); + nanobind::object FetchDepFuncsSourceCode = + analysis.attr("FetchDepFuncsSourceCode"); + nanobind::object source_code; try { source_code = FetchDepFuncsSourceCode.attr("fetch")(func); - } catch (py::error_already_set &e) { + } catch (nanobind::python_error &e) { throw std::runtime_error("Failed to get source code: " + std::string(e.what())); } - std::string source = source_code.cast(); + std::string source = nanobind::cast(source_code); strip_leading_whitespace(source); return source; } -std::string get_var_name_for_handle(const py::handle &h) { - py::object inspect = py::module::import("inspect"); +std::string get_var_name_for_handle(const nanobind::handle &h) { + nanobind::object inspect = nanobind::module_::import_("inspect"); // Search locals first, walking up the call stack auto current_frame = inspect.attr("currentframe")(); while (current_frame && !current_frame.is_none()) { - py::dict f_locals = current_frame.attr("f_locals"); + nanobind::dict f_locals = + nanobind::cast(current_frame.attr("f_locals")); for (auto item : f_locals) if (item.second.is(h)) - return py::str(item.first); + return nanobind::cast(nanobind::str(item.first)); current_frame = current_frame.attr("f_back"); } // Search globals now current_frame = inspect.attr("currentframe")(); - py::dict f_globals = current_frame.attr("f_globals"); + nanobind::dict f_globals = + nanobind::cast(current_frame.attr("f_globals")); for (auto item : f_globals) if (item.second.is(h)) - return py::str(item.first); + return nanobind::cast(nanobind::str(item.first)); return std::string(); } -std::unordered_map> +std::unordered_map> DataClassRegistry::classes{}; /// @brief Bind the dataclass registry -void bindPyDataClassRegistry(py::module &mod) { - py::class_(mod, "DataClassRegistry", - R"#(Registry for dataclasses used in kernels)#") +void bindPyDataClassRegistry(nanobind::module_ &mod) { + nanobind::class_( + mod, "DataClassRegistry", R"#(Registry for dataclasses used in kernels)#") .def_static("registerClass", &DataClassRegistry::registerClass, "Register class\n") .def_static("isRegisteredClass", &DataClassRegistry::isRegisteredClass, "Is class registered\n") .def_static("getClassAttributes", &DataClassRegistry::getClassAttributes, "Find registered class and its attributes\n") - .def_readonly_static("classes", &DataClassRegistry::classes); + .def_ro_static("classes", &DataClassRegistry::classes); } } // namespace cudaq diff --git a/python/runtime/cudaq/algorithms/py_utils.h b/python/runtime/cudaq/algorithms/py_utils.h index 84dc1e6455c..2abd81d122a 100644 --- a/python/runtime/cudaq/algorithms/py_utils.h +++ b/python/runtime/cudaq/algorithms/py_utils.h @@ -8,35 +8,36 @@ #pragma once -#include +#include +#include #include #include -namespace py = pybind11; - namespace cudaq { /// @brief Get a JSON-encoded dictionary of a combination of all local /// and global variables that are JSON compatible -py::dict get_serializable_var_dict(); +nanobind::dict get_serializable_var_dict(); -/// @brief Fetch the Python source code from a `py::function` -std::string get_source_code(const py::function &func); +/// @brief Fetch the Python source code from a `nanobind::callable` +std::string get_source_code(const nanobind::callable &func); /// @brief Find the variable name for a given Python object handle. It searches /// locally first, walks up the call stack, and finally checks the global /// namespace. If not found, it returns an empty string. -std::string get_var_name_for_handle(const py::handle &h); +std::string get_var_name_for_handle(const nanobind::handle &h); /// @brief Registry for python data classes used in kernels class DataClassRegistry { public: - static std::unordered_map> + static std::unordered_map> classes; /// @brief Register class object - static void registerClass(std::string &name, py::object cls) { - classes[name] = {cls, cls.attr("__annotations__").cast()}; + static void registerClass(std::string &name, nanobind::object cls) { + classes[name] = { + cls, nanobind::cast(cls.attr("__annotations__"))}; } /// @brief Is data class name registered @@ -45,12 +46,12 @@ class DataClassRegistry { } /// @brief Find registered data class object and its attributes - static std::tuple + static std::tuple getClassAttributes(std::string &name) { return classes[name]; } }; -void bindPyDataClassRegistry(py::module &mod); +void bindPyDataClassRegistry(nanobind::module_ &mod); } // namespace cudaq diff --git a/python/runtime/cudaq/domains/plugins/CMakeLists.txt b/python/runtime/cudaq/domains/plugins/CMakeLists.txt index 675919e25ca..3bd2e991655 100644 --- a/python/runtime/cudaq/domains/plugins/CMakeLists.txt +++ b/python/runtime/cudaq/domains/plugins/CMakeLists.txt @@ -15,10 +15,12 @@ else() endif() add_library(cudaq-pyscf SHARED PySCFDriver.cpp) +target_compile_options(cudaq-pyscf PRIVATE -Wno-cast-qual) + if (SKBUILD) target_link_libraries(cudaq-pyscf PRIVATE - pybind11::pybind11 Python::Module + nanobind-static Python::Module cudaq-chemistry cudaq-operator cudaq cudaq-py-utils cudaq-platform-default) # Apple's linker (ld64) doesn't support --unresolved-symbols flag if (NOT APPLE) @@ -31,7 +33,7 @@ else() endif() target_link_libraries(cudaq-pyscf PRIVATE - Python::Python pybind11::pybind11 + nanobind-static Python::Python cudaq-chemistry cudaq-operator cudaq cudaq-py-utils cudaq-platform-default) endif() diff --git a/python/runtime/cudaq/domains/plugins/PySCFDriver.cpp b/python/runtime/cudaq/domains/plugins/PySCFDriver.cpp index 1cd6e142a83..8f99b59e231 100644 --- a/python/runtime/cudaq/domains/plugins/PySCFDriver.cpp +++ b/python/runtime/cudaq/domains/plugins/PySCFDriver.cpp @@ -9,20 +9,18 @@ #include "cudaq/domains/chemistry/MoleculePackageDriver.h" #include "cudaq/target_control.h" #include -#include +#include +#include +#include -namespace py = pybind11; using namespace cudaq; namespace { -/// @brief Reference to the pybind11 scoped interpreter -thread_local static std::unique_ptr interp; - -/// @brief Map an OpenFermion QubitOperator represented as a py::object +/// @brief Map an OpenFermion QubitOperator represented as a nanobind::object /// to a CUDA-Q spin_op -spin_op fromOpenFermionQubitOperator(const py::object &op) { - if (!py::hasattr(op, "terms")) +spin_op fromOpenFermionQubitOperator(const nanobind::object &op) { + if (!nanobind::hasattr(op, "terms")) throw std::runtime_error( "This is not an openfermion operator, must have 'terms' attribute."); std::map> creatorMap{ @@ -32,20 +30,21 @@ spin_op fromOpenFermionQubitOperator(const py::object &op) { auto terms = op.attr("terms"); auto H = spin_op::empty(); for (auto term : terms) { - auto termTuple = term.cast(); + auto termTuple = nanobind::cast(term); auto localTerm = spin_op::identity(); - for (auto &element : termTuple) { - auto casted = element.cast>(); + for (auto element : termTuple) { + auto casted = + nanobind::cast>(element); localTerm *= creatorMap[casted.second](casted.first); } - H += terms[term].cast() * localTerm; + H += nanobind::cast(terms[term]) * localTerm; } return H; } /// @brief Implement the CUDA-Q MoleculePackageDriver interface /// with support for generating molecular Hamiltonians via PySCF. We -/// achieve this via Pybind11's embedded interpreter capabilities. +/// achieve this via nanobind's Python API wrappers. class PySCFPackageDriver : public MoleculePackageDriver { protected: /// @brief The name of the chemistry python module. @@ -62,82 +61,83 @@ class PySCFPackageDriver : public MoleculePackageDriver { int multiplicity, int charge, std::optional nActiveElectrons = std::nullopt, std::optional nActiveOrbitals = std::nullopt) override { - if (!interp) - interp = std::make_unique(); + if (!Py_IsInitialized()) + Py_Initialize(); // Convert the molecular_geometry to a list[tuple(str,tuple)] - py::list pyGeometry(geometry.size()); - for (std::size_t counter = 0; auto &atom : geometry) { - py::tuple coordinate(3); + nanobind::list pyGeometry; + for (auto &atom : geometry) { + nanobind::object coordinate = nanobind::steal(PyTuple_New(3)); for (int i = 0; i < 3; i++) - coordinate[i] = atom.coordinates[i]; + PyTuple_SET_ITEM(coordinate.ptr(), i, + nanobind::cast(atom.coordinates[i]).release().ptr()); - pyGeometry[counter++] = py::make_tuple(atom.name, coordinate); + pyGeometry.append(nanobind::make_tuple(atom.name, coordinate)); } // We don't want to modify the platform, indicate so cudaq::__internal__::disableTargetModification(); // Import the cudaq python chemistry module - auto cudaqModule = py::module_::import(ChemistryModuleName); + auto cudaqModule = nanobind::module_::import_(ChemistryModuleName); // Reset it cudaq::__internal__::enableTargetModification(); // Setup the active space if requested. - py::object nElectrons = py::none(); - py::object nActive = py::none(); + nanobind::object nElectrons = nanobind::none(); + nanobind::object nActive = nanobind::none(); if (nActiveElectrons.has_value()) - nElectrons = py::int_(nActiveElectrons.value()); + nElectrons = nanobind::int_(nActiveElectrons.value()); if (nActiveOrbitals.has_value()) - nActive = py::int_(nActiveOrbitals.value()); + nActive = nanobind::int_(nActiveOrbitals.value()); // Run the openfermion-pyscf wrapper to create the hamiltonian + metadata auto hamiltonianGen = cudaqModule.attr(CreatorFunctionName); - auto resultTuple = hamiltonianGen(pyGeometry, basis, multiplicity, charge, - nElectrons, nActive) - .cast(); + auto resultTuple = nanobind::cast(hamiltonianGen( + pyGeometry, basis, multiplicity, charge, nElectrons, nActive)); // Get the spin_op representation - auto spinOp = fromOpenFermionQubitOperator(resultTuple[0]); + auto spinOp = + fromOpenFermionQubitOperator(nanobind::borrow(resultTuple[0])); // Get the OpenFermion molecule representation - auto openFermionMolecule = resultTuple[1]; + auto openFermionMolecule = nanobind::borrow(resultTuple[1]); // Extract the one-body integrals auto pyOneBody = openFermionMolecule.attr("one_body_integrals"); - auto shape = pyOneBody.attr("shape").cast(); - one_body_integrals oneBody( - {shape[0].cast(), shape[1].cast()}); + auto shape = nanobind::cast(pyOneBody.attr("shape")); + one_body_integrals oneBody({nanobind::cast(shape[0]), + nanobind::cast(shape[1])}); for (std::size_t i = 0; i < oneBody.shape[0]; i++) for (std::size_t j = 0; j < oneBody.shape[1]; j++) - oneBody(i, j) = - pyOneBody.attr("__getitem__")(py::make_tuple(i, j)).cast(); + oneBody(i, j) = nanobind::cast( + pyOneBody.attr("__getitem__")(nanobind::make_tuple(i, j))); // Extract the two-body integrals auto pyTwoBody = openFermionMolecule.attr("two_body_integrals"); - shape = pyTwoBody.attr("shape").cast(); - two_body_integals twoBody( - {shape[0].cast(), shape[1].cast(), - shape[2].cast(), shape[3].cast()}); + shape = nanobind::cast(pyTwoBody.attr("shape")); + two_body_integals twoBody({nanobind::cast(shape[0]), + nanobind::cast(shape[1]), + nanobind::cast(shape[2]), + nanobind::cast(shape[3])}); for (std::size_t i = 0; i < twoBody.shape[0]; i++) for (std::size_t j = 0; j < twoBody.shape[1]; j++) for (std::size_t k = 0; k < twoBody.shape[2]; k++) for (std::size_t l = 0; l < twoBody.shape[3]; l++) - twoBody(i, j, k, l) = - pyTwoBody.attr("__getitem__")(py::make_tuple(i, j, k, l)) - .cast(); + twoBody(i, j, k, l) = nanobind::cast(pyTwoBody.attr( + "__getitem__")(nanobind::make_tuple(i, j, k, l))); // return a new molecular_hamiltonian return molecular_hamiltonian{ spinOp, std::move(oneBody), std::move(twoBody), - openFermionMolecule.attr("n_electrons").cast(), - openFermionMolecule.attr("n_orbitals").cast(), - openFermionMolecule.attr("nuclear_repulsion").cast(), - openFermionMolecule.attr("hf_energy").cast(), - openFermionMolecule.attr("fci_energy").cast()}; + nanobind::cast(openFermionMolecule.attr("n_electrons")), + nanobind::cast(openFermionMolecule.attr("n_orbitals")), + nanobind::cast(openFermionMolecule.attr("nuclear_repulsion")), + nanobind::cast(openFermionMolecule.attr("hf_energy")), + nanobind::cast(openFermionMolecule.attr("fci_energy"))}; } }; diff --git a/python/runtime/cudaq/dynamics/CMakeLists.txt b/python/runtime/cudaq/dynamics/CMakeLists.txt index c56a4c3672d..d7910fdf586 100644 --- a/python/runtime/cudaq/dynamics/CMakeLists.txt +++ b/python/runtime/cudaq/dynamics/CMakeLists.txt @@ -7,13 +7,9 @@ # ============================================================================ # find_package(Python COMPONENTS Interpreter Development) -find_package(pybind11 CONFIG) -pybind11_add_module(nvqir_dynamics_bindings pyDynamics.cpp) -target_include_directories(nvqir_dynamics_bindings PRIVATE - ${PYTHON_INCLUDE_DIRS} - ${pybind11_INCLUDE_DIRS} -) +nanobind_add_module(nvqir_dynamics_bindings NB_STATIC pyDynamics.cpp) + target_include_directories(nvqir_dynamics_bindings PRIVATE ${CMAKE_SOURCE_DIR}/runtime diff --git a/python/runtime/cudaq/dynamics/pyDynamics.cpp b/python/runtime/cudaq/dynamics/pyDynamics.cpp index 5a4ee1380ac..1fdccbedcaa 100644 --- a/python/runtime/cudaq/dynamics/pyDynamics.cpp +++ b/python/runtime/cudaq/dynamics/pyDynamics.cpp @@ -15,10 +15,14 @@ #include "cudaq/algorithms/base_integrator.h" #include "cudaq/algorithms/integrator.h" #include "cudaq/schedule.h" -#include -#include +#include +#include +#include +#include +#include +#include +#include -namespace py = pybind11; namespace { cudaq::CuDensityMatState *asCudmState(cudaq::state &cudaqState) { auto *simState = cudaq::state_helper::getSimulationState(&cudaqState); @@ -30,7 +34,7 @@ cudaq::CuDensityMatState *asCudmState(cudaq::state &cudaqState) { } // namespace // Internal dynamics bindings -PYBIND11_MODULE(nvqir_dynamics_bindings, m) { +NB_MODULE(nvqir_dynamics_bindings, m) { class PyCuDensityMatTimeStepper : public cudaq::CuDensityMatTimeStepper { public: PyCuDensityMatTimeStepper(cudensitymatHandle_t handle, @@ -42,76 +46,80 @@ PYBIND11_MODULE(nvqir_dynamics_bindings, m) { }; // Time stepper bindings - py::class_(m, "TimeStepper") - .def(py::init( - [](cudaq::schedule schedule, std::vector modeExtents, - cudaq::sum_op hamiltonian, - std::vector> collapse_ops, - bool is_master_equation) { - std::unordered_map> params; - for (const auto ¶m : schedule.get_parameters()) { - params[param] = schedule.get_value_function()(param, 0.0); - } - auto liouvillian = cudaq::dynamics::Context::getCurrentContext() - ->getOpConverter() - .constructLiouvillian( - {hamiltonian}, {collapse_ops}, - modeExtents, params, is_master_equation); - return PyCuDensityMatTimeStepper( - cudaq::dynamics::Context::getCurrentContext()->getHandle(), - liouvillian, schedule); - })) - .def(py::init([](cudaq::schedule schedule, - std::vector modeExtents, - cudaq::super_op superOp) { - std::unordered_map> params; - for (const auto ¶m : schedule.get_parameters()) { - params[param] = schedule.get_value_function()(param, 0.0); - } - auto liouvillian = - cudaq::dynamics::Context::getCurrentContext() - ->getOpConverter() - .constructLiouvillian({superOp}, modeExtents, params); - return PyCuDensityMatTimeStepper( - cudaq::dynamics::Context::getCurrentContext()->getHandle(), - liouvillian, schedule); - })) - .def(py::init([](cudaq::schedule schedule, - std::vector modeExtents, - const std::vector> - &hamiltonians, - const std::vector< - std::vector>> - &list_collapse_ops, - bool is_master_equation) { - std::unordered_map> params; - for (const auto ¶m : schedule.get_parameters()) { - params[param] = schedule.get_value_function()(param, 0.0); - } - auto liouvillian = - cudaq::dynamics::Context::getCurrentContext() - ->getOpConverter() - .constructLiouvillian(hamiltonians, list_collapse_ops, - modeExtents, params, is_master_equation); - return PyCuDensityMatTimeStepper( - cudaq::dynamics::Context::getCurrentContext()->getHandle(), - liouvillian, schedule); - })) - .def(py::init([](cudaq::schedule schedule, - std::vector modeExtents, - const std::vector &superOps) { - std::unordered_map> params; - for (const auto ¶m : schedule.get_parameters()) { - params[param] = schedule.get_value_function()(param, 0.0); - } - auto liouvillian = - cudaq::dynamics::Context::getCurrentContext() - ->getOpConverter() - .constructLiouvillian(superOps, modeExtents, params); - return PyCuDensityMatTimeStepper( - cudaq::dynamics::Context::getCurrentContext()->getHandle(), - liouvillian, schedule); - })) + nanobind::class_(m, "TimeStepper") + .def("__init__", + [](PyCuDensityMatTimeStepper *self, cudaq::schedule schedule, + std::vector modeExtents, + cudaq::sum_op hamiltonian, + std::vector> collapse_ops, + bool is_master_equation) { + std::unordered_map> params; + for (const auto ¶m : schedule.get_parameters()) { + params[param] = schedule.get_value_function()(param, 0.0); + } + auto liouvillian = + cudaq::dynamics::Context::getCurrentContext() + ->getOpConverter() + .constructLiouvillian({hamiltonian}, {collapse_ops}, + modeExtents, params, + is_master_equation); + new (self) PyCuDensityMatTimeStepper( + cudaq::dynamics::Context::getCurrentContext()->getHandle(), + liouvillian, schedule); + }) + .def("__init__", + [](PyCuDensityMatTimeStepper *self, cudaq::schedule schedule, + std::vector modeExtents, cudaq::super_op superOp) { + std::unordered_map> params; + for (const auto ¶m : schedule.get_parameters()) { + params[param] = schedule.get_value_function()(param, 0.0); + } + auto liouvillian = + cudaq::dynamics::Context::getCurrentContext() + ->getOpConverter() + .constructLiouvillian({superOp}, modeExtents, params); + new (self) PyCuDensityMatTimeStepper( + cudaq::dynamics::Context::getCurrentContext()->getHandle(), + liouvillian, schedule); + }) + .def("__init__", + [](PyCuDensityMatTimeStepper *self, cudaq::schedule schedule, + std::vector modeExtents, + const std::vector> + &hamiltonians, + const std::vector>> &list_collapse_ops, + bool is_master_equation) { + std::unordered_map> params; + for (const auto ¶m : schedule.get_parameters()) { + params[param] = schedule.get_value_function()(param, 0.0); + } + auto liouvillian = + cudaq::dynamics::Context::getCurrentContext() + ->getOpConverter() + .constructLiouvillian(hamiltonians, list_collapse_ops, + modeExtents, params, + is_master_equation); + new (self) PyCuDensityMatTimeStepper( + cudaq::dynamics::Context::getCurrentContext()->getHandle(), + liouvillian, schedule); + }) + .def("__init__", + [](PyCuDensityMatTimeStepper *self, cudaq::schedule schedule, + std::vector modeExtents, + const std::vector &superOps) { + std::unordered_map> params; + for (const auto ¶m : schedule.get_parameters()) { + params[param] = schedule.get_value_function()(param, 0.0); + } + auto liouvillian = + cudaq::dynamics::Context::getCurrentContext() + ->getOpConverter() + .constructLiouvillian(superOps, modeExtents, params); + new (self) PyCuDensityMatTimeStepper( + cudaq::dynamics::Context::getCurrentContext()->getHandle(), + liouvillian, schedule); + }) .def("compute", [](PyCuDensityMatTimeStepper &self, cudaq::state &inputState, double t) { @@ -124,7 +132,6 @@ PYBIND11_MODULE(nvqir_dynamics_bindings, m) { .def("compute", [](PyCuDensityMatTimeStepper &self, cudaq::state &inputState, double t, cudaq::state &outputState) { - // Compute into the provided output state std::unordered_map> params; for (const auto ¶m : self.m_schedule.get_parameters()) { params[param] = self.m_schedule.get_value_function()(param, t); @@ -151,24 +158,26 @@ PYBIND11_MODULE(nvqir_dynamics_bindings, m) { }); // System dynamics data class - py::class_(m, "SystemDynamics") - .def(py::init<>()) - .def_readwrite("modeExtents", &cudaq::SystemDynamics::modeExtents) - .def_readwrite("hamiltonian", &cudaq::SystemDynamics::hamiltonian) - .def_readwrite("collapseOps", &cudaq::SystemDynamics::collapseOps) - .def_readwrite("parameters", &cudaq::SystemDynamics::parameters) - .def_readwrite("superOp", &cudaq::SystemDynamics::superOp); + nanobind::class_(m, "SystemDynamics") + .def(nanobind::init<>()) + .def_rw("modeExtents", &cudaq::SystemDynamics::modeExtents) + .def_rw("hamiltonian", &cudaq::SystemDynamics::hamiltonian) + .def_rw("collapseOps", &cudaq::SystemDynamics::collapseOps) + .def_rw("parameters", &cudaq::SystemDynamics::parameters) + .def_rw("superOp", &cudaq::SystemDynamics::superOp); // Expectation calculation - py::class_(m, "CuDensityMatExpectation") - .def(py::init([](cudaq::sum_op &obs, - const std::vector &modeExtents) { - return cudaq::CuDensityMatExpectation( - cudaq::dynamics::Context::getCurrentContext()->getHandle(), - cudaq::dynamics::Context::getCurrentContext() - ->getOpConverter() - .convertToCudensitymatOperator({}, obs, modeExtents)); - })) + nanobind::class_(m, "CuDensityMatExpectation") + .def("__init__", + [](cudaq::CuDensityMatExpectation *self, + cudaq::sum_op &obs, + const std::vector &modeExtents) { + new (self) cudaq::CuDensityMatExpectation( + cudaq::dynamics::Context::getCurrentContext()->getHandle(), + cudaq::dynamics::Context::getCurrentContext() + ->getOpConverter() + .convertToCudensitymatOperator({}, obs, modeExtents)); + }) .def("prepare", [](cudaq::CuDensityMatExpectation &self, cudaq::state &state) { auto *cudmState = asCudmState(state); @@ -187,9 +196,9 @@ PYBIND11_MODULE(nvqir_dynamics_bindings, m) { }); // Schedule class - py::class_(m, "Schedule") - .def(py::init &, - const std::vector &>()); + nanobind::class_(m, "Schedule") + .def(nanobind::init &, + const std::vector &>()); // Helper to initialize a data buffer state m.def("initializeState", @@ -287,23 +296,24 @@ PYBIND11_MODULE(nvqir_dynamics_bindings, m) { return cudaq::__internal__::checkBatchingCompatibility(hamOps, listCollapseOps); }, - py::arg("hamiltonians"), py::arg("collapse_operators")); + nanobind::arg("hamiltonians"), nanobind::arg("collapse_operators")); m.def( "checkSuperOpBatchingCompatibility", [](const std::vector &super_operators) { return cudaq::__internal__::checkBatchingCompatibility(super_operators); }, - py::arg("super_operators")); + nanobind::arg("super_operators")); auto integratorsSubmodule = m.def_submodule("integrators"); // Runge-Kutta integrator - py::class_(integratorsSubmodule, - "runge_kutta") - .def(py::init>(), py::kw_only(), - py::arg("order") = cudaq::integrators::runge_kutta::default_order, - py::arg("max_step_size") = py::none()) + nanobind::class_(integratorsSubmodule, + "runge_kutta") + .def(nanobind::init>(), nanobind::kw_only(), + nanobind::arg("order") = + cudaq::integrators::runge_kutta::default_order, + nanobind::arg("max_step_size") = nanobind::none()) .def("setState", [](cudaq::integrators::runge_kutta &self, cudaq::state &state, double t) { self.setState(state, t); }) diff --git a/python/runtime/cudaq/operators/py_boson_op.cpp b/python/runtime/cudaq/operators/py_boson_op.cpp index 5a23d29a9af..6df75bd5a27 100644 --- a/python/runtime/cudaq/operators/py_boson_op.cpp +++ b/python/runtime/cudaq/operators/py_boson_op.cpp @@ -7,10 +7,17 @@ ******************************************************************************/ #include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include "cudaq/operators.h" #include "cudaq/operators/serialization.h" @@ -19,7 +26,7 @@ namespace cudaq { -void bindBosonModule(py::module &mod) { +void bindBosonModule(nanobind::module_ &mod) { // Binding the functions in `cudaq::boson` as `_pycudaq` submodule // so it's accessible directly in the cudaq namespace. auto boson_submodule = mod.def_submodule("boson"); @@ -32,31 +39,32 @@ void bindBosonModule(py::module &mod) { "Returns product operator with constant value 1."); boson_submodule.def( "identity", [](std::size_t target) { return boson_op::identity(target); }, - py::arg("target"), + nanobind::arg("target"), "Returns an identity operator on the given target index."); boson_submodule.def( "identities", [](std::size_t first, std::size_t last) { return boson_op_term(first, last); }, - py::arg("first"), py::arg("last"), + nanobind::arg("first"), nanobind::arg("last"), "Creates a product operator that applies an identity operation to all " "degrees of " "freedom in the open range [first, last)."); boson_submodule.def( - "create", &boson_op::create, py::arg("target"), + "create", &boson_op::create, nanobind::arg("target"), "Returns a bosonic creation operator on the given target index."); boson_submodule.def( - "annihilate", &boson_op::annihilate, py::arg("target"), + "annihilate", &boson_op::annihilate, + nanobind::arg("target"), "Returns a bosonic annihilation operator on the given target index."); boson_submodule.def( - "number", &boson_op::number, py::arg("target"), + "number", &boson_op::number, nanobind::arg("target"), "Returns a bosonic number operator on the given target index."); boson_submodule.def( - "position", &boson_op::position, py::arg("target"), + "position", &boson_op::position, nanobind::arg("target"), "Returns a bosonic position operator on the given target index."); boson_submodule.def( - "momentum", &boson_op::momentum, py::arg("target"), + "momentum", &boson_op::momentum, nanobind::arg("target"), "Returns a bosonic momentum operator on the given target index."); boson_submodule.def( "canonicalized", @@ -90,50 +98,52 @@ void bindBosonModule(py::module &mod) { "degrees of freedom."); } -void bindBosonOperator(py::module &mod) { +void bindBosonOperator(nanobind::module_ &mod) { - auto boson_op_class = py::class_(mod, "BosonOperator"); + auto boson_op_class = nanobind::class_(mod, "BosonOperator"); auto boson_op_term_class = - py::class_(mod, "BosonOperatorTerm"); + nanobind::class_(mod, "BosonOperatorTerm"); boson_op_class .def( "__iter__", [](boson_op &self) { - return py::make_iterator(self.begin(), self.end()); + return nanobind::make_iterator(nanobind::type(), + "iterator", self.begin(), + self.end()); }, - py::keep_alive<0, 1>(), "Loop through each term of the operator.") + nanobind::keep_alive<0, 1>(), + "Loop through each term of the operator.") // properties - .def_property_readonly("parameters", - &boson_op::get_parameter_descriptions, - "Returns a dictionary that maps each parameter " - "name to its description.") - .def_property_readonly("degrees", &boson_op::degrees, - "Returns a vector that lists all degrees of " - "freedom that the operator targets. " - "The order of degrees is from smallest to largest " - "and reflects the ordering of " - "the matrix returned by `to_matrix`. " - "Specifically, the indices of a statevector " - "with two qubits are {00, 01, 10, 11}. An " - "ordering of degrees {0, 1} then indicates " - "that a state where the qubit with index 0 equals " - "1 with probability 1 is given by " - "the vector {0., 1., 0., 0.}.") - .def_property_readonly("min_degree", &boson_op::min_degree, - "Returns the smallest index of the degrees of " - "freedom that the operator targets.") - .def_property_readonly("max_degree", &boson_op::max_degree, - "Returns the smallest index of the degrees of " - "freedom that the operator targets.") - .def_property_readonly("term_count", &boson_op::num_terms, - "Returns the number of terms in the operator.") + .def_prop_ro("parameters", &boson_op::get_parameter_descriptions, + "Returns a dictionary that maps each parameter " + "name to its description.") + .def_prop_ro("degrees", &boson_op::degrees, + "Returns a vector that lists all degrees of " + "freedom that the operator targets. " + "The order of degrees is from smallest to largest " + "and reflects the ordering of " + "the matrix returned by `to_matrix`. " + "Specifically, the indices of a statevector " + "with two qubits are {00, 01, 10, 11}. An " + "ordering of degrees {0, 1} then indicates " + "that a state where the qubit with index 0 equals " + "1 with probability 1 is given by " + "the vector {0., 1., 0., 0.}.") + .def_prop_ro("min_degree", &boson_op::min_degree, + "Returns the smallest index of the degrees of " + "freedom that the operator targets.") + .def_prop_ro("max_degree", &boson_op::max_degree, + "Returns the smallest index of the degrees of " + "freedom that the operator targets.") + .def_prop_ro("term_count", &boson_op::num_terms, + "Returns the number of terms in the operator.") // constructors - .def(py::init<>(), + .def(nanobind::init<>(), "Creates a default instantiated sum. A default instantiated " "sum has no value; it will take a value the first time an " "arithmetic operation " @@ -142,12 +152,12 @@ void bindBosonOperator(py::module &mod) { "identity. To construct a `0` value in the mathematical sense " "(neutral element " "for addition), use `empty()` instead.") - .def(py::init(), + .def(nanobind::init(), "Creates a sum operator with no terms, reserving " "space for the given number of terms.") - .def(py::init(), + .def(nanobind::init(), "Creates a sum operator with the given term.") - .def(py::init(), "Copy constructor.") + .def(nanobind::init(), "Copy constructor.") .def( "copy", [](const boson_op &self) { return boson_op(self); }, "Creates a copy of the operator.") @@ -161,9 +171,9 @@ void bindBosonOperator(py::module &mod) { auto cmat = self.to_matrix(dimensions, params, invert_order); return details::cmat_to_numpy(cmat); }, - py::arg("dimensions") = dimension_map(), - py::arg("parameters") = parameter_map(), - py::arg("invert_order") = false, + nanobind::arg("dimensions") = dimension_map(), + nanobind::arg("parameters") = parameter_map(), + nanobind::arg("invert_order") = false, "Returns the matrix representation of the operator." "The matrix is ordered according to the convention (endianness) " "used in CUDA-Q, and the ordering returned by `degrees`. This order " @@ -173,13 +183,13 @@ void bindBosonOperator(py::module &mod) { .def( "to_matrix", [](const boson_op &self, dimension_map &dimensions, bool invert_order, - const py::kwargs &kwargs) { + const nanobind::kwargs &kwargs) { auto cmat = self.to_matrix( dimensions, details::kwargs_to_param_map(kwargs), invert_order); return details::cmat_to_numpy(cmat); }, - py::arg("dimensions") = dimension_map(), - py::arg("invert_order") = false, + nanobind::arg("dimensions") = dimension_map(), + nanobind::arg("invert_order") = false, nanobind::arg("kwargs"), "Returns the matrix representation of the operator." "The matrix is ordered according to the convention (endianness) " "used in CUDA-Q, and the ordering returned by `degrees`. This order " @@ -192,9 +202,9 @@ void bindBosonOperator(py::module &mod) { const parameter_map ¶ms, bool invert_order) { return self.to_sparse_matrix(dimensions, params, invert_order); }, - py::arg("dimensions") = dimension_map(), - py::arg("parameters") = parameter_map(), - py::arg("invert_order") = false, + nanobind::arg("dimensions") = dimension_map(), + nanobind::arg("parameters") = parameter_map(), + nanobind::arg("invert_order") = false, "Return the sparse matrix representation of the operator. This " "representation is a " "`Tuple[list[complex], list[int], list[int]]`, encoding the " @@ -208,12 +218,12 @@ void bindBosonOperator(py::module &mod) { .def( "to_sparse_matrix", [](const boson_op &self, dimension_map &dimensions, bool invert_order, - const py::kwargs &kwargs) { + const nanobind::kwargs &kwargs) { return self.to_sparse_matrix( dimensions, details::kwargs_to_param_map(kwargs), invert_order); }, - py::arg("dimensions") = dimension_map(), - py::arg("invert_order") = false, + nanobind::arg("dimensions") = dimension_map(), + nanobind::arg("invert_order") = false, nanobind::arg("kwargs"), "Return the sparse matrix representation of the operator. This " "representation is a " "`Tuple[list[complex], list[int], list[int]]`, encoding the " @@ -227,7 +237,7 @@ void bindBosonOperator(py::module &mod) { // comparisons - .def("__eq__", &boson_op::operator==, py::is_operator(), + .def("__eq__", &boson_op::operator==, nanobind::is_operator(), "Return true if the two operators are equivalent. The equivalence " "check takes " "commutation relations into account. Operators acting on different " @@ -239,91 +249,91 @@ void bindBosonOperator(py::module &mod) { [](const boson_op &self, const boson_op_term &other) { return self.num_terms() == 1 && *self.begin() == other; }, - py::is_operator(), "Return true if the two operators are equivalent.") + nanobind::is_operator(), + "Return true if the two operators are equivalent.") // unary operators - .def(-py::self, py::is_operator()) - .def(+py::self, py::is_operator()) + .def(-nanobind::self, nanobind::is_operator()) + .def(+nanobind::self, nanobind::is_operator()) // in-place arithmetics - .def(py::self /= int(), py::is_operator()) - .def(py::self *= int(), py::is_operator()) - .def(py::self += int(), py::is_operator()) - .def(py::self -= int(), py::is_operator()) - .def(py::self /= double(), py::is_operator()) - .def(py::self *= double(), py::is_operator()) - .def(py::self += double(), py::is_operator()) - .def(py::self -= double(), py::is_operator()) - .def(py::self /= std::complex(), py::is_operator()) - .def(py::self *= std::complex(), py::is_operator()) - .def(py::self += std::complex(), py::is_operator()) - .def(py::self -= std::complex(), py::is_operator()) - .def(py::self /= scalar_operator(), py::is_operator()) - .def(py::self *= scalar_operator(), py::is_operator()) - .def(py::self += scalar_operator(), py::is_operator()) - .def(py::self -= scalar_operator(), py::is_operator()) - .def(py::self *= boson_op_term(), py::is_operator()) - .def(py::self += boson_op_term(), py::is_operator()) - .def(py::self -= boson_op_term(), py::is_operator()) - .def(py::self *= py::self, py::is_operator()) - .def(py::self += py::self, py::is_operator()) -// see issue https://github.com/pybind/pybind11/issues/1893 + .def(nanobind::self /= int(), nanobind::is_operator()) + .def(nanobind::self *= int(), nanobind::is_operator()) + .def(nanobind::self += int(), nanobind::is_operator()) + .def(nanobind::self -= int(), nanobind::is_operator()) + .def(nanobind::self /= double(), nanobind::is_operator()) + .def(nanobind::self *= double(), nanobind::is_operator()) + .def(nanobind::self += double(), nanobind::is_operator()) + .def(nanobind::self -= double(), nanobind::is_operator()) + .def(nanobind::self /= std::complex(), nanobind::is_operator()) + .def(nanobind::self *= std::complex(), nanobind::is_operator()) + .def(nanobind::self += std::complex(), nanobind::is_operator()) + .def(nanobind::self -= std::complex(), nanobind::is_operator()) + .def(nanobind::self /= scalar_operator(), nanobind::is_operator()) + .def(nanobind::self *= scalar_operator(), nanobind::is_operator()) + .def(nanobind::self += scalar_operator(), nanobind::is_operator()) + .def(nanobind::self -= scalar_operator(), nanobind::is_operator()) + .def(nanobind::self *= boson_op_term(), nanobind::is_operator()) + .def(nanobind::self += boson_op_term(), nanobind::is_operator()) + .def(nanobind::self -= boson_op_term(), nanobind::is_operator()) + .def(nanobind::self *= nanobind::self, nanobind::is_operator()) + .def(nanobind::self += nanobind::self, nanobind::is_operator()) #ifdef __clang__ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wself-assign-overloaded" #endif - .def(py::self -= py::self, py::is_operator()) + .def(nanobind::self -= nanobind::self, nanobind::is_operator()) #ifdef __clang__ #pragma clang diagnostic pop #endif // right-hand arithmetics - .def(py::self / int(), py::is_operator()) - .def(py::self * int(), py::is_operator()) - .def(py::self + int(), py::is_operator()) - .def(py::self - int(), py::is_operator()) - .def(py::self / double(), py::is_operator()) - .def(py::self * double(), py::is_operator()) - .def(py::self + double(), py::is_operator()) - .def(py::self - double(), py::is_operator()) - .def(py::self / std::complex(), py::is_operator()) - .def(py::self * std::complex(), py::is_operator()) - .def(py::self + std::complex(), py::is_operator()) - .def(py::self - std::complex(), py::is_operator()) - .def(py::self / scalar_operator(), py::is_operator()) - .def(py::self * scalar_operator(), py::is_operator()) - .def(py::self + scalar_operator(), py::is_operator()) - .def(py::self - scalar_operator(), py::is_operator()) - .def(py::self * boson_op_term(), py::is_operator()) - .def(py::self + boson_op_term(), py::is_operator()) - .def(py::self - boson_op_term(), py::is_operator()) - .def(py::self * py::self, py::is_operator()) - .def(py::self + py::self, py::is_operator()) - .def(py::self - py::self, py::is_operator()) - .def(py::self * matrix_op_term(), py::is_operator()) - .def(py::self + matrix_op_term(), py::is_operator()) - .def(py::self - matrix_op_term(), py::is_operator()) - .def(py::self * matrix_op(), py::is_operator()) - .def(py::self + matrix_op(), py::is_operator()) - .def(py::self - matrix_op(), py::is_operator()) + .def(nanobind::self / int(), nanobind::is_operator()) + .def(nanobind::self * int(), nanobind::is_operator()) + .def(nanobind::self + int(), nanobind::is_operator()) + .def(nanobind::self - int(), nanobind::is_operator()) + .def(nanobind::self / double(), nanobind::is_operator()) + .def(nanobind::self * double(), nanobind::is_operator()) + .def(nanobind::self + double(), nanobind::is_operator()) + .def(nanobind::self - double(), nanobind::is_operator()) + .def(nanobind::self / std::complex(), nanobind::is_operator()) + .def(nanobind::self * std::complex(), nanobind::is_operator()) + .def(nanobind::self + std::complex(), nanobind::is_operator()) + .def(nanobind::self - std::complex(), nanobind::is_operator()) + .def(nanobind::self / scalar_operator(), nanobind::is_operator()) + .def(nanobind::self * scalar_operator(), nanobind::is_operator()) + .def(nanobind::self + scalar_operator(), nanobind::is_operator()) + .def(nanobind::self - scalar_operator(), nanobind::is_operator()) + .def(nanobind::self * boson_op_term(), nanobind::is_operator()) + .def(nanobind::self + boson_op_term(), nanobind::is_operator()) + .def(nanobind::self - boson_op_term(), nanobind::is_operator()) + .def(nanobind::self * nanobind::self, nanobind::is_operator()) + .def(nanobind::self + nanobind::self, nanobind::is_operator()) + .def(nanobind::self - nanobind::self, nanobind::is_operator()) + .def(nanobind::self * matrix_op_term(), nanobind::is_operator()) + .def(nanobind::self + matrix_op_term(), nanobind::is_operator()) + .def(nanobind::self - matrix_op_term(), nanobind::is_operator()) + .def(nanobind::self * matrix_op(), nanobind::is_operator()) + .def(nanobind::self + matrix_op(), nanobind::is_operator()) + .def(nanobind::self - matrix_op(), nanobind::is_operator()) // left-hand arithmetics - .def(int() * py::self, py::is_operator()) - .def(int() + py::self, py::is_operator()) - .def(int() - py::self, py::is_operator()) - .def(double() * py::self, py::is_operator()) - .def(double() + py::self, py::is_operator()) - .def(double() - py::self, py::is_operator()) - .def(std::complex() * py::self, py::is_operator()) - .def(std::complex() + py::self, py::is_operator()) - .def(std::complex() - py::self, py::is_operator()) - .def(scalar_operator() * py::self, py::is_operator()) - .def(scalar_operator() + py::self, py::is_operator()) - .def(scalar_operator() - py::self, py::is_operator()) + .def(int() * nanobind::self, nanobind::is_operator()) + .def(int() + nanobind::self, nanobind::is_operator()) + .def(int() - nanobind::self, nanobind::is_operator()) + .def(double() * nanobind::self, nanobind::is_operator()) + .def(double() + nanobind::self, nanobind::is_operator()) + .def(double() - nanobind::self, nanobind::is_operator()) + .def(std::complex() * nanobind::self, nanobind::is_operator()) + .def(std::complex() + nanobind::self, nanobind::is_operator()) + .def(std::complex() - nanobind::self, nanobind::is_operator()) + .def(scalar_operator() * nanobind::self, nanobind::is_operator()) + .def(scalar_operator() + nanobind::self, nanobind::is_operator()) + .def(scalar_operator() - nanobind::self, nanobind::is_operator()) // common operators @@ -351,17 +361,17 @@ void bindBosonOperator(py::module &mod) { .def("dump", &boson_op::dump, "Prints the string representation of the operator to the standard " "output.") - .def("trim", &boson_op::trim, py::arg("tol") = 0.0, - py::arg("parameters") = parameter_map(), + .def("trim", &boson_op::trim, nanobind::arg("tol") = 0.0, + nanobind::arg("parameters") = parameter_map(), "Removes all terms from the sum for which the absolute value of the " "coefficient is below " "the given tolerance.") .def( "trim", - [](boson_op &self, double tol, const py::kwargs &kwargs) { + [](boson_op &self, double tol, const nanobind::kwargs &kwargs) { return self.trim(tol, details::kwargs_to_param_map(kwargs)); }, - py::arg("tol") = 0.0, + nanobind::arg("tol") = 0.0, nanobind::arg("kwargs"), "Removes all terms from the sum for which the absolute value of the " "coefficient is below " "the given tolerance.") @@ -386,42 +396,44 @@ void bindBosonOperator(py::module &mod) { .def( "__iter__", [](boson_op_term &self) { - return py::make_iterator(self.begin(), self.end()); + return nanobind::make_iterator(nanobind::type(), + "iterator", self.begin(), + self.end()); }, - py::keep_alive<0, 1>(), "Loop through each term of the operator.") + nanobind::keep_alive<0, 1>(), + "Loop through each term of the operator.") // properties - .def_property_readonly("parameters", - &boson_op_term::get_parameter_descriptions, - "Returns a dictionary that maps each parameter " - "name to its description.") - .def_property_readonly("degrees", &boson_op_term::degrees, - "Returns a vector that lists all degrees of " - "freedom that the operator targets. " - "The order of degrees is from smallest to largest " - "and reflects the ordering of " - "the matrix returned by `to_matrix`. " - "Specifically, the indices of a statevector " - "with two qubits are {00, 01, 10, 11}. An " - "ordering of degrees {0, 1} then indicates " - "that a state where the qubit with index 0 equals " - "1 with probability 1 is given by " - "the vector {0., 1., 0., 0.}.") - .def_property_readonly("min_degree", &boson_op_term::min_degree, - "Returns the smallest index of the degrees of " - "freedom that the operator targets.") - .def_property_readonly("max_degree", &boson_op_term::max_degree, - "Returns the smallest index of the degrees of " - "freedom that the operator targets.") - .def_property_readonly("ops_count", &boson_op_term::num_ops, - "Returns the number of operators in the product.") - .def_property_readonly( + .def_prop_ro("parameters", &boson_op_term::get_parameter_descriptions, + "Returns a dictionary that maps each parameter " + "name to its description.") + .def_prop_ro("degrees", &boson_op_term::degrees, + "Returns a vector that lists all degrees of " + "freedom that the operator targets. " + "The order of degrees is from smallest to largest " + "and reflects the ordering of " + "the matrix returned by `to_matrix`. " + "Specifically, the indices of a statevector " + "with two qubits are {00, 01, 10, 11}. An " + "ordering of degrees {0, 1} then indicates " + "that a state where the qubit with index 0 equals " + "1 with probability 1 is given by " + "the vector {0., 1., 0., 0.}.") + .def_prop_ro("min_degree", &boson_op_term::min_degree, + "Returns the smallest index of the degrees of " + "freedom that the operator targets.") + .def_prop_ro("max_degree", &boson_op_term::max_degree, + "Returns the smallest index of the degrees of " + "freedom that the operator targets.") + .def_prop_ro("ops_count", &boson_op_term::num_ops, + "Returns the number of operators in the product.") + .def_prop_ro( "term_id", &boson_op_term::get_term_id, "The term id uniquely identifies the operators and targets (degrees) " "that they act on, " "but does not include information about the coefficient.") - .def_property_readonly( + .def_prop_ro( "coefficient", &boson_op_term::get_coefficient, "Returns the unevaluated coefficient of the operator. The " "coefficient is a " @@ -429,30 +441,32 @@ void bindBosonOperator(py::module &mod) { // constructors - .def(py::init<>(), + .def(nanobind::init<>(), "Creates a product operator with constant value 1. The returned " "operator does not target any degrees of freedom but merely " "represents a constant.") - .def(py::init(), py::arg("first_degree"), - py::arg("last_degree"), + .def(nanobind::init(), + nanobind::arg("first_degree"), nanobind::arg("last_degree"), "Creates a product operator that applies an identity operation to " "all degrees of " "freedom in the range [first_degree, last_degree).") - .def(py::init(), + .def(nanobind::init(), "Creates a product operator with the given constant value. " "The returned operator does not target any degrees of freedom.") - .def(py::init>(), + .def(nanobind::init>(), "Creates a product operator with the given " "constant value. The returned operator does not target any degrees " "of freedom.") - .def(py::init([](const scalar_operator &scalar) { - return boson_op_term() * scalar; - }), - "Creates a product operator with non-constant scalar value.") - .def(py::init(), + .def( + "__init__", + [](boson_op_term *self, const scalar_operator &scalar) { + new (self) boson_op_term(boson_op_term() * scalar); + }, + "Creates a product operator with non-constant scalar value.") + .def(nanobind::init(), "Creates a product operator with the given elementary operator.") - .def(py::init(), py::arg("operator"), - py::arg("size") = 0, + .def(nanobind::init(), + nanobind::arg("operator"), nanobind::arg("size") = 0, "Creates a copy of the given operator and reserves space for " "storing the given " "number of product terms (if a size is provided).") @@ -463,7 +477,7 @@ void bindBosonOperator(py::module &mod) { // evaluations .def("evaluate_coefficient", &boson_op_term::evaluate_coefficient, - py::arg("parameters") = parameter_map(), + nanobind::arg("parameters") = parameter_map(), "Returns the evaluated coefficient of the product operator. The " "parameters is a map of parameter names to their concrete, complex " "values.") @@ -474,9 +488,9 @@ void bindBosonOperator(py::module &mod) { auto cmat = self.to_matrix(dimensions, params, invert_order); return details::cmat_to_numpy(cmat); }, - py::arg("dimensions") = dimension_map(), - py::arg("parameters") = parameter_map(), - py::arg("invert_order") = false, + nanobind::arg("dimensions") = dimension_map(), + nanobind::arg("parameters") = parameter_map(), + nanobind::arg("invert_order") = false, "Returns the matrix representation of the operator." "The matrix is ordered according to the convention (endianness) " "used in CUDA-Q, and the ordering returned by `degrees`. This order " @@ -486,13 +500,13 @@ void bindBosonOperator(py::module &mod) { .def( "to_matrix", [](const boson_op_term &self, dimension_map &dimensions, - bool invert_order, const py::kwargs &kwargs) { + bool invert_order, const nanobind::kwargs &kwargs) { auto cmat = self.to_matrix( dimensions, details::kwargs_to_param_map(kwargs), invert_order); return details::cmat_to_numpy(cmat); }, - py::arg("dimensions") = dimension_map(), - py::arg("invert_order") = false, + nanobind::arg("dimensions") = dimension_map(), + nanobind::arg("invert_order") = false, nanobind::arg("kwargs"), "Returns the matrix representation of the operator." "The matrix is ordered according to the convention (endianness) " "used in CUDA-Q, and the ordering returned by `degrees`. This order " @@ -505,9 +519,9 @@ void bindBosonOperator(py::module &mod) { const parameter_map ¶ms, bool invert_order) { return self.to_sparse_matrix(dimensions, params, invert_order); }, - py::arg("dimensions") = dimension_map(), - py::arg("parameters") = parameter_map(), - py::arg("invert_order") = false, + nanobind::arg("dimensions") = dimension_map(), + nanobind::arg("parameters") = parameter_map(), + nanobind::arg("invert_order") = false, "Return the sparse matrix representation of the operator. This " "representation is a " "`Tuple[list[complex], list[int], list[int]]`, encoding the " @@ -521,12 +535,12 @@ void bindBosonOperator(py::module &mod) { .def( "to_sparse_matrix", [](const boson_op_term &self, dimension_map &dimensions, - bool invert_order, const py::kwargs &kwargs) { + bool invert_order, const nanobind::kwargs &kwargs) { return self.to_sparse_matrix( dimensions, details::kwargs_to_param_map(kwargs), invert_order); }, - py::arg("dimensions") = dimension_map(), - py::arg("invert_order") = false, + nanobind::arg("dimensions") = dimension_map(), + nanobind::arg("invert_order") = false, nanobind::arg("kwargs"), "Return the sparse matrix representation of the operator. This " "representation is a " "`Tuple[list[complex], list[int], list[int]]`, encoding the " @@ -540,7 +554,7 @@ void bindBosonOperator(py::module &mod) { // comparisons - .def("__eq__", &boson_op_term::operator==, py::is_operator(), + .def("__eq__", &boson_op_term::operator==, nanobind::is_operator(), "Return true if the two operators are equivalent. The equivalence " "check takes " "commutation relations into account. Operators acting on different " @@ -552,77 +566,78 @@ void bindBosonOperator(py::module &mod) { [](const boson_op_term &self, const boson_op &other) { return other.num_terms() == 1 && *other.begin() == self; }, - py::is_operator(), "Return true if the two operators are equivalent.") + nanobind::is_operator(), + "Return true if the two operators are equivalent.") // unary operators - .def(-py::self, py::is_operator()) - .def(+py::self, py::is_operator()) + .def(-nanobind::self, nanobind::is_operator()) + .def(+nanobind::self, nanobind::is_operator()) // in-place arithmetics - .def(py::self /= int(), py::is_operator()) - .def(py::self *= int(), py::is_operator()) - .def(py::self /= double(), py::is_operator()) - .def(py::self *= double(), py::is_operator()) - .def(py::self /= std::complex(), py::is_operator()) - .def(py::self *= std::complex(), py::is_operator()) - .def(py::self /= scalar_operator(), py::is_operator()) - .def(py::self *= scalar_operator(), py::is_operator()) - .def(py::self *= py::self, py::is_operator()) + .def(nanobind::self /= int(), nanobind::is_operator()) + .def(nanobind::self *= int(), nanobind::is_operator()) + .def(nanobind::self /= double(), nanobind::is_operator()) + .def(nanobind::self *= double(), nanobind::is_operator()) + .def(nanobind::self /= std::complex(), nanobind::is_operator()) + .def(nanobind::self *= std::complex(), nanobind::is_operator()) + .def(nanobind::self /= scalar_operator(), nanobind::is_operator()) + .def(nanobind::self *= scalar_operator(), nanobind::is_operator()) + .def(nanobind::self *= nanobind::self, nanobind::is_operator()) // right-hand arithmetics - .def(py::self / int(), py::is_operator()) - .def(py::self * int(), py::is_operator()) - .def(py::self + int(), py::is_operator()) - .def(py::self - int(), py::is_operator()) - .def(py::self / double(), py::is_operator()) - .def(py::self * double(), py::is_operator()) - .def(py::self + double(), py::is_operator()) - .def(py::self - double(), py::is_operator()) - .def(py::self / std::complex(), py::is_operator()) - .def(py::self * std::complex(), py::is_operator()) - .def(py::self + std::complex(), py::is_operator()) - .def(py::self - std::complex(), py::is_operator()) - .def(py::self / scalar_operator(), py::is_operator()) - .def(py::self * scalar_operator(), py::is_operator()) - .def(py::self + scalar_operator(), py::is_operator()) - .def(py::self - scalar_operator(), py::is_operator()) - .def(py::self * py::self, py::is_operator()) - .def(py::self + py::self, py::is_operator()) - .def(py::self - py::self, py::is_operator()) - .def(py::self * boson_op(), py::is_operator()) - .def(py::self + boson_op(), py::is_operator()) - .def(py::self - boson_op(), py::is_operator()) - .def(py::self * matrix_op_term(), py::is_operator()) - .def(py::self + matrix_op_term(), py::is_operator()) - .def(py::self - matrix_op_term(), py::is_operator()) - .def(py::self * matrix_op(), py::is_operator()) - .def(py::self + matrix_op(), py::is_operator()) - .def(py::self - matrix_op(), py::is_operator()) + .def(nanobind::self / int(), nanobind::is_operator()) + .def(nanobind::self * int(), nanobind::is_operator()) + .def(nanobind::self + int(), nanobind::is_operator()) + .def(nanobind::self - int(), nanobind::is_operator()) + .def(nanobind::self / double(), nanobind::is_operator()) + .def(nanobind::self * double(), nanobind::is_operator()) + .def(nanobind::self + double(), nanobind::is_operator()) + .def(nanobind::self - double(), nanobind::is_operator()) + .def(nanobind::self / std::complex(), nanobind::is_operator()) + .def(nanobind::self * std::complex(), nanobind::is_operator()) + .def(nanobind::self + std::complex(), nanobind::is_operator()) + .def(nanobind::self - std::complex(), nanobind::is_operator()) + .def(nanobind::self / scalar_operator(), nanobind::is_operator()) + .def(nanobind::self * scalar_operator(), nanobind::is_operator()) + .def(nanobind::self + scalar_operator(), nanobind::is_operator()) + .def(nanobind::self - scalar_operator(), nanobind::is_operator()) + .def(nanobind::self * nanobind::self, nanobind::is_operator()) + .def(nanobind::self + nanobind::self, nanobind::is_operator()) + .def(nanobind::self - nanobind::self, nanobind::is_operator()) + .def(nanobind::self * boson_op(), nanobind::is_operator()) + .def(nanobind::self + boson_op(), nanobind::is_operator()) + .def(nanobind::self - boson_op(), nanobind::is_operator()) + .def(nanobind::self * matrix_op_term(), nanobind::is_operator()) + .def(nanobind::self + matrix_op_term(), nanobind::is_operator()) + .def(nanobind::self - matrix_op_term(), nanobind::is_operator()) + .def(nanobind::self * matrix_op(), nanobind::is_operator()) + .def(nanobind::self + matrix_op(), nanobind::is_operator()) + .def(nanobind::self - matrix_op(), nanobind::is_operator()) // left-hand arithmetics - .def(int() * py::self, py::is_operator()) - .def(int() + py::self, py::is_operator()) - .def(int() - py::self, py::is_operator()) - .def(double() * py::self, py::is_operator()) - .def(double() + py::self, py::is_operator()) - .def(double() - py::self, py::is_operator()) - .def(std::complex() * py::self, py::is_operator()) - .def(std::complex() + py::self, py::is_operator()) - .def(std::complex() - py::self, py::is_operator()) - .def(scalar_operator() * py::self, py::is_operator()) - .def(scalar_operator() + py::self, py::is_operator()) - .def(scalar_operator() - py::self, py::is_operator()) + .def(int() * nanobind::self, nanobind::is_operator()) + .def(int() + nanobind::self, nanobind::is_operator()) + .def(int() - nanobind::self, nanobind::is_operator()) + .def(double() * nanobind::self, nanobind::is_operator()) + .def(double() + nanobind::self, nanobind::is_operator()) + .def(double() - nanobind::self, nanobind::is_operator()) + .def(std::complex() * nanobind::self, nanobind::is_operator()) + .def(std::complex() + nanobind::self, nanobind::is_operator()) + .def(std::complex() - nanobind::self, nanobind::is_operator()) + .def(scalar_operator() * nanobind::self, nanobind::is_operator()) + .def(scalar_operator() + nanobind::self, nanobind::is_operator()) + .def(scalar_operator() - nanobind::self, nanobind::is_operator()) // general utility functions .def("is_identity", &boson_op_term::is_identity, "Checks if all operators in the product are the identity. " - "Note: this function returns true regardless of the value of the " - "coefficient.") + "Note that this function returns true regardless of the value of " + "the coefficient.") .def( "__str__", [](const boson_op_term &self) { return self.to_string(); }, "Returns the string representation of the operator.") @@ -645,12 +660,12 @@ void bindBosonOperator(py::module &mod) { "of freedom that are not included in the given set."); } -void bindBosonWrapper(py::module &mod) { +void bindBosonWrapper(nanobind::module_ &mod) { bindBosonOperator(mod); - py::implicitly_convertible(); - py::implicitly_convertible, boson_op_term>(); - py::implicitly_convertible(); - py::implicitly_convertible(); + nanobind::implicitly_convertible(); + nanobind::implicitly_convertible, boson_op_term>(); + nanobind::implicitly_convertible(); + nanobind::implicitly_convertible(); bindBosonModule(mod); } diff --git a/python/runtime/cudaq/operators/py_boson_op.h b/python/runtime/cudaq/operators/py_boson_op.h index 7952fcab9cf..7f74e49cbc0 100644 --- a/python/runtime/cudaq/operators/py_boson_op.h +++ b/python/runtime/cudaq/operators/py_boson_op.h @@ -6,12 +6,10 @@ * the terms of the Apache License 2.0 which accompanies this distribution. * ******************************************************************************/ -#include - -namespace py = pybind11; +#include namespace cudaq { /// @brief Wrapper function for exposing the bindings of bosonic /// operators to python. -void bindBosonWrapper(py::module &mod); +void bindBosonWrapper(nanobind::module_ &mod); } // namespace cudaq diff --git a/python/runtime/cudaq/operators/py_fermion_op.cpp b/python/runtime/cudaq/operators/py_fermion_op.cpp index 7c0e315d6f5..621f39c873f 100644 --- a/python/runtime/cudaq/operators/py_fermion_op.cpp +++ b/python/runtime/cudaq/operators/py_fermion_op.cpp @@ -7,10 +7,18 @@ ******************************************************************************/ #include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include "cudaq/operators.h" #include "cudaq/operators/serialization.h" @@ -19,7 +27,7 @@ namespace cudaq { -void bindFermionModule(py::module &mod) { +void bindFermionModule(nanobind::module_ &mod) { // Binding the functions in `cudaq::fermion` as `_pycudaq` submodule // so it's accessible directly in the cudaq namespace. auto fermion_submodule = mod.def_submodule("fermion"); @@ -33,25 +41,26 @@ void bindFermionModule(py::module &mod) { fermion_submodule.def( "identity", [](std::size_t target) { return fermion_op::identity(target); }, - py::arg("target"), + nanobind::arg("target"), "Returns an identity operator on the given target index."); fermion_submodule.def( "identities", [](std::size_t first, std::size_t last) { return fermion_op_term(first, last); }, - py::arg("first"), py::arg("last"), + nanobind::arg("first"), nanobind::arg("last"), "Creates a product operator that applies an identity operation to all " "degrees of " "freedom in the open range [first, last)."); fermion_submodule.def( - "create", &fermion_op::create, py::arg("target"), + "create", &fermion_op::create, nanobind::arg("target"), "Returns a fermionic creation operator on the given target index."); fermion_submodule.def( - "annihilate", &fermion_op::annihilate, py::arg("target"), + "annihilate", &fermion_op::annihilate, + nanobind::arg("target"), "Returns a fermionic annihilation operator on the given target index."); fermion_submodule.def( - "number", &fermion_op::number, py::arg("target"), + "number", &fermion_op::number, nanobind::arg("target"), "Returns a fermionic number operator on the given target index."); fermion_submodule.def( "canonicalized", @@ -85,50 +94,52 @@ void bindFermionModule(py::module &mod) { "degrees of freedom."); } -void bindFermionOperator(py::module &mod) { +void bindFermionOperator(nanobind::module_ &mod) { - auto fermion_op_class = py::class_(mod, "FermionOperator"); + auto fermion_op_class = nanobind::class_(mod, "FermionOperator"); auto fermion_op_term_class = - py::class_(mod, "FermionOperatorTerm"); + nanobind::class_(mod, "FermionOperatorTerm"); fermion_op_class .def( "__iter__", [](fermion_op &self) { - return py::make_iterator(self.begin(), self.end()); + return nanobind::make_iterator(nanobind::type(), + "iterator", self.begin(), + self.end()); }, - py::keep_alive<0, 1>(), "Loop through each term of the operator.") + nanobind::keep_alive<0, 1>(), + "Loop through each term of the operator.") // properties - .def_property_readonly("parameters", - &fermion_op::get_parameter_descriptions, - "Returns a dictionary that maps each parameter " - "name to its description.") - .def_property_readonly("degrees", &fermion_op::degrees, - "Returns a vector that lists all degrees of " - "freedom that the operator targets. " - "The order of degrees is from smallest to largest " - "and reflects the ordering of " - "the matrix returned by `to_matrix`. " - "Specifically, the indices of a statevector " - "with two qubits are {00, 01, 10, 11}. An " - "ordering of degrees {0, 1} then indicates " - "that a state where the qubit with index 0 equals " - "1 with probability 1 is given by " - "the vector {0., 1., 0., 0.}.") - .def_property_readonly("min_degree", &fermion_op::min_degree, - "Returns the smallest index of the degrees of " - "freedom that the operator targets.") - .def_property_readonly("max_degree", &fermion_op::max_degree, - "Returns the smallest index of the degrees of " - "freedom that the operator targets.") - .def_property_readonly("term_count", &fermion_op::num_terms, - "Returns the number of terms in the operator.") + .def_prop_ro("parameters", &fermion_op::get_parameter_descriptions, + "Returns a dictionary that maps each parameter " + "name to its description.") + .def_prop_ro("degrees", &fermion_op::degrees, + "Returns a vector that lists all degrees of " + "freedom that the operator targets. " + "The order of degrees is from smallest to largest " + "and reflects the ordering of " + "the matrix returned by `to_matrix`. " + "Specifically, the indices of a statevector " + "with two qubits are {00, 01, 10, 11}. An " + "ordering of degrees {0, 1} then indicates " + "that a state where the qubit with index 0 equals " + "1 with probability 1 is given by " + "the vector {0., 1., 0., 0.}.") + .def_prop_ro("min_degree", &fermion_op::min_degree, + "Returns the smallest index of the degrees of " + "freedom that the operator targets.") + .def_prop_ro("max_degree", &fermion_op::max_degree, + "Returns the smallest index of the degrees of " + "freedom that the operator targets.") + .def_prop_ro("term_count", &fermion_op::num_terms, + "Returns the number of terms in the operator.") // constructors - .def(py::init<>(), + .def(nanobind::init<>(), "Creates a default instantiated sum. A default instantiated " "sum has no value; it will take a value the first time an " "arithmetic operation " @@ -137,12 +148,12 @@ void bindFermionOperator(py::module &mod) { "identity. To construct a `0` value in the mathematical sense " "(neutral element " "for addition), use `empty()` instead.") - .def(py::init(), + .def(nanobind::init(), "Creates a sum operator with no terms, reserving " "space for the given number of terms.") - .def(py::init(), + .def(nanobind::init(), "Creates a sum operator with the given term.") - .def(py::init(), "Copy constructor.") + .def(nanobind::init(), "Copy constructor.") .def( "copy", [](const fermion_op &self) { return fermion_op(self); }, "Creates a copy of the operator.") @@ -156,9 +167,9 @@ void bindFermionOperator(py::module &mod) { auto cmat = self.to_matrix(dimensions, params, invert_order); return details::cmat_to_numpy(cmat); }, - py::arg("dimensions") = dimension_map(), - py::arg("parameters") = parameter_map(), - py::arg("invert_order") = false, + nanobind::arg("dimensions") = dimension_map(), + nanobind::arg("parameters") = parameter_map(), + nanobind::arg("invert_order") = false, "Returns the matrix representation of the operator." "The matrix is ordered according to the convention (endianness) " "used in CUDA-Q, and the ordering returned by `degrees`. This order " @@ -168,13 +179,13 @@ void bindFermionOperator(py::module &mod) { .def( "to_matrix", [](const fermion_op &self, dimension_map &dimensions, - bool invert_order, const py::kwargs &kwargs) { + bool invert_order, const nanobind::kwargs &kwargs) { auto cmat = self.to_matrix( dimensions, details::kwargs_to_param_map(kwargs), invert_order); return details::cmat_to_numpy(cmat); }, - py::arg("dimensions") = dimension_map(), - py::arg("invert_order") = false, + nanobind::arg("dimensions") = dimension_map(), + nanobind::arg("invert_order") = false, nanobind::arg("kwargs"), "Returns the matrix representation of the operator." "The matrix is ordered according to the convention (endianness) " "used in CUDA-Q, and the ordering returned by `degrees`. This order " @@ -187,9 +198,9 @@ void bindFermionOperator(py::module &mod) { const parameter_map ¶ms, bool invert_order) { return self.to_sparse_matrix(dimensions, params, invert_order); }, - py::arg("dimensions") = dimension_map(), - py::arg("parameters") = parameter_map(), - py::arg("invert_order") = false, + nanobind::arg("dimensions") = dimension_map(), + nanobind::arg("parameters") = parameter_map(), + nanobind::arg("invert_order") = false, "Return the sparse matrix representation of the operator. This " "representation is a " "`Tuple[list[complex], list[int], list[int]]`, encoding the " @@ -203,12 +214,12 @@ void bindFermionOperator(py::module &mod) { .def( "to_sparse_matrix", [](const fermion_op &self, dimension_map &dimensions, - bool invert_order, const py::kwargs &kwargs) { + bool invert_order, const nanobind::kwargs &kwargs) { return self.to_sparse_matrix( dimensions, details::kwargs_to_param_map(kwargs), invert_order); }, - py::arg("dimensions") = dimension_map(), - py::arg("invert_order") = false, + nanobind::arg("dimensions") = dimension_map(), + nanobind::arg("invert_order") = false, nanobind::arg("kwargs"), "Return the sparse matrix representation of the operator. This " "representation is a " "`Tuple[list[complex], list[int], list[int]]`, encoding the " @@ -222,7 +233,7 @@ void bindFermionOperator(py::module &mod) { // comparisons - .def("__eq__", &fermion_op::operator==, py::is_operator(), + .def("__eq__", &fermion_op::operator==, nanobind::is_operator(), "Return true if the two operators are equivalent. The equivalence " "check takes " "commutation relations into account. Operators acting on different " @@ -234,91 +245,91 @@ void bindFermionOperator(py::module &mod) { [](const fermion_op &self, const fermion_op_term &other) { return self.num_terms() == 1 && *self.begin() == other; }, - py::is_operator(), "Return true if the two operators are equivalent.") + nanobind::is_operator(), + "Return true if the two operators are equivalent.") // unary operators - .def(-py::self, py::is_operator()) - .def(+py::self, py::is_operator()) + .def(-nanobind::self, nanobind::is_operator()) + .def(+nanobind::self, nanobind::is_operator()) // in-place arithmetics - .def(py::self /= int(), py::is_operator()) - .def(py::self *= int(), py::is_operator()) - .def(py::self += int(), py::is_operator()) - .def(py::self -= int(), py::is_operator()) - .def(py::self /= double(), py::is_operator()) - .def(py::self *= double(), py::is_operator()) - .def(py::self += double(), py::is_operator()) - .def(py::self -= double(), py::is_operator()) - .def(py::self /= std::complex(), py::is_operator()) - .def(py::self *= std::complex(), py::is_operator()) - .def(py::self += std::complex(), py::is_operator()) - .def(py::self -= std::complex(), py::is_operator()) - .def(py::self /= scalar_operator(), py::is_operator()) - .def(py::self *= scalar_operator(), py::is_operator()) - .def(py::self += scalar_operator(), py::is_operator()) - .def(py::self -= scalar_operator(), py::is_operator()) - .def(py::self *= fermion_op_term(), py::is_operator()) - .def(py::self += fermion_op_term(), py::is_operator()) - .def(py::self -= fermion_op_term(), py::is_operator()) - .def(py::self *= py::self, py::is_operator()) - .def(py::self += py::self, py::is_operator()) -// see issue https://github.com/pybind/pybind11/issues/1893 + .def(nanobind::self /= int(), nanobind::is_operator()) + .def(nanobind::self *= int(), nanobind::is_operator()) + .def(nanobind::self += int(), nanobind::is_operator()) + .def(nanobind::self -= int(), nanobind::is_operator()) + .def(nanobind::self /= double(), nanobind::is_operator()) + .def(nanobind::self *= double(), nanobind::is_operator()) + .def(nanobind::self += double(), nanobind::is_operator()) + .def(nanobind::self -= double(), nanobind::is_operator()) + .def(nanobind::self /= std::complex(), nanobind::is_operator()) + .def(nanobind::self *= std::complex(), nanobind::is_operator()) + .def(nanobind::self += std::complex(), nanobind::is_operator()) + .def(nanobind::self -= std::complex(), nanobind::is_operator()) + .def(nanobind::self /= scalar_operator(), nanobind::is_operator()) + .def(nanobind::self *= scalar_operator(), nanobind::is_operator()) + .def(nanobind::self += scalar_operator(), nanobind::is_operator()) + .def(nanobind::self -= scalar_operator(), nanobind::is_operator()) + .def(nanobind::self *= fermion_op_term(), nanobind::is_operator()) + .def(nanobind::self += fermion_op_term(), nanobind::is_operator()) + .def(nanobind::self -= fermion_op_term(), nanobind::is_operator()) + .def(nanobind::self *= nanobind::self, nanobind::is_operator()) + .def(nanobind::self += nanobind::self, nanobind::is_operator()) #ifdef __clang__ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wself-assign-overloaded" #endif - .def(py::self -= py::self, py::is_operator()) + .def(nanobind::self -= nanobind::self, nanobind::is_operator()) #ifdef __clang__ #pragma clang diagnostic pop #endif // right-hand arithmetics - .def(py::self / int(), py::is_operator()) - .def(py::self * int(), py::is_operator()) - .def(py::self + int(), py::is_operator()) - .def(py::self - int(), py::is_operator()) - .def(py::self / double(), py::is_operator()) - .def(py::self * double(), py::is_operator()) - .def(py::self + double(), py::is_operator()) - .def(py::self - double(), py::is_operator()) - .def(py::self / std::complex(), py::is_operator()) - .def(py::self * std::complex(), py::is_operator()) - .def(py::self + std::complex(), py::is_operator()) - .def(py::self - std::complex(), py::is_operator()) - .def(py::self / scalar_operator(), py::is_operator()) - .def(py::self * scalar_operator(), py::is_operator()) - .def(py::self + scalar_operator(), py::is_operator()) - .def(py::self - scalar_operator(), py::is_operator()) - .def(py::self * fermion_op_term(), py::is_operator()) - .def(py::self + fermion_op_term(), py::is_operator()) - .def(py::self - fermion_op_term(), py::is_operator()) - .def(py::self * py::self, py::is_operator()) - .def(py::self + py::self, py::is_operator()) - .def(py::self - py::self, py::is_operator()) - .def(py::self * matrix_op_term(), py::is_operator()) - .def(py::self + matrix_op_term(), py::is_operator()) - .def(py::self - matrix_op_term(), py::is_operator()) - .def(py::self * matrix_op(), py::is_operator()) - .def(py::self + matrix_op(), py::is_operator()) - .def(py::self - matrix_op(), py::is_operator()) + .def(nanobind::self / int(), nanobind::is_operator()) + .def(nanobind::self * int(), nanobind::is_operator()) + .def(nanobind::self + int(), nanobind::is_operator()) + .def(nanobind::self - int(), nanobind::is_operator()) + .def(nanobind::self / double(), nanobind::is_operator()) + .def(nanobind::self * double(), nanobind::is_operator()) + .def(nanobind::self + double(), nanobind::is_operator()) + .def(nanobind::self - double(), nanobind::is_operator()) + .def(nanobind::self / std::complex(), nanobind::is_operator()) + .def(nanobind::self * std::complex(), nanobind::is_operator()) + .def(nanobind::self + std::complex(), nanobind::is_operator()) + .def(nanobind::self - std::complex(), nanobind::is_operator()) + .def(nanobind::self / scalar_operator(), nanobind::is_operator()) + .def(nanobind::self * scalar_operator(), nanobind::is_operator()) + .def(nanobind::self + scalar_operator(), nanobind::is_operator()) + .def(nanobind::self - scalar_operator(), nanobind::is_operator()) + .def(nanobind::self * fermion_op_term(), nanobind::is_operator()) + .def(nanobind::self + fermion_op_term(), nanobind::is_operator()) + .def(nanobind::self - fermion_op_term(), nanobind::is_operator()) + .def(nanobind::self * nanobind::self, nanobind::is_operator()) + .def(nanobind::self + nanobind::self, nanobind::is_operator()) + .def(nanobind::self - nanobind::self, nanobind::is_operator()) + .def(nanobind::self * matrix_op_term(), nanobind::is_operator()) + .def(nanobind::self + matrix_op_term(), nanobind::is_operator()) + .def(nanobind::self - matrix_op_term(), nanobind::is_operator()) + .def(nanobind::self * matrix_op(), nanobind::is_operator()) + .def(nanobind::self + matrix_op(), nanobind::is_operator()) + .def(nanobind::self - matrix_op(), nanobind::is_operator()) // left-hand arithmetics - .def(int() * py::self, py::is_operator()) - .def(int() + py::self, py::is_operator()) - .def(int() - py::self, py::is_operator()) - .def(double() * py::self, py::is_operator()) - .def(double() + py::self, py::is_operator()) - .def(double() - py::self, py::is_operator()) - .def(std::complex() * py::self, py::is_operator()) - .def(std::complex() + py::self, py::is_operator()) - .def(std::complex() - py::self, py::is_operator()) - .def(scalar_operator() * py::self, py::is_operator()) - .def(scalar_operator() + py::self, py::is_operator()) - .def(scalar_operator() - py::self, py::is_operator()) + .def(int() * nanobind::self, nanobind::is_operator()) + .def(int() + nanobind::self, nanobind::is_operator()) + .def(int() - nanobind::self, nanobind::is_operator()) + .def(double() * nanobind::self, nanobind::is_operator()) + .def(double() + nanobind::self, nanobind::is_operator()) + .def(double() - nanobind::self, nanobind::is_operator()) + .def(std::complex() * nanobind::self, nanobind::is_operator()) + .def(std::complex() + nanobind::self, nanobind::is_operator()) + .def(std::complex() - nanobind::self, nanobind::is_operator()) + .def(scalar_operator() * nanobind::self, nanobind::is_operator()) + .def(scalar_operator() + nanobind::self, nanobind::is_operator()) + .def(scalar_operator() - nanobind::self, nanobind::is_operator()) // common operators @@ -346,17 +357,17 @@ void bindFermionOperator(py::module &mod) { .def("dump", &fermion_op::dump, "Prints the string representation of the operator to the standard " "output.") - .def("trim", &fermion_op::trim, py::arg("tol") = 0.0, - py::arg("parameters") = parameter_map(), + .def("trim", &fermion_op::trim, nanobind::arg("tol") = 0.0, + nanobind::arg("parameters") = parameter_map(), "Removes all terms from the sum for which the absolute value of the " "coefficient is below " "the given tolerance.") .def( "trim", - [](fermion_op &self, double tol, const py::kwargs &kwargs) { + [](fermion_op &self, double tol, const nanobind::kwargs &kwargs) { return self.trim(tol, details::kwargs_to_param_map(kwargs)); }, - py::arg("tol") = 0.0, + nanobind::arg("tol") = 0.0, nanobind::arg("kwargs"), "Removes all terms from the sum for which the absolute value of the " "coefficient is below " "the given tolerance.") @@ -381,42 +392,44 @@ void bindFermionOperator(py::module &mod) { .def( "__iter__", [](fermion_op_term &self) { - return py::make_iterator(self.begin(), self.end()); + return nanobind::make_iterator(nanobind::type(), + "iterator", self.begin(), + self.end()); }, - py::keep_alive<0, 1>(), "Loop through each term of the operator.") + nanobind::keep_alive<0, 1>(), + "Loop through each term of the operator.") // properties - .def_property_readonly("parameters", - &fermion_op_term::get_parameter_descriptions, - "Returns a dictionary that maps each parameter " - "name to its description.") - .def_property_readonly("degrees", &fermion_op_term::degrees, - "Returns a vector that lists all degrees of " - "freedom that the operator targets. " - "The order of degrees is from smallest to largest " - "and reflects the ordering of " - "the matrix returned by `to_matrix`. " - "Specifically, the indices of a statevector " - "with two qubits are {00, 01, 10, 11}. An " - "ordering of degrees {0, 1} then indicates " - "that a state where the qubit with index 0 equals " - "1 with probability 1 is given by " - "the vector {0., 1., 0., 0.}.") - .def_property_readonly("min_degree", &fermion_op_term::min_degree, - "Returns the smallest index of the degrees of " - "freedom that the operator targets.") - .def_property_readonly("max_degree", &fermion_op_term::max_degree, - "Returns the smallest index of the degrees of " - "freedom that the operator targets.") - .def_property_readonly("ops_count", &fermion_op_term::num_ops, - "Returns the number of operators in the product.") - .def_property_readonly( + .def_prop_ro("parameters", &fermion_op_term::get_parameter_descriptions, + "Returns a dictionary that maps each parameter " + "name to its description.") + .def_prop_ro("degrees", &fermion_op_term::degrees, + "Returns a vector that lists all degrees of " + "freedom that the operator targets. " + "The order of degrees is from smallest to largest " + "and reflects the ordering of " + "the matrix returned by `to_matrix`. " + "Specifically, the indices of a statevector " + "with two qubits are {00, 01, 10, 11}. An " + "ordering of degrees {0, 1} then indicates " + "that a state where the qubit with index 0 equals " + "1 with probability 1 is given by " + "the vector {0., 1., 0., 0.}.") + .def_prop_ro("min_degree", &fermion_op_term::min_degree, + "Returns the smallest index of the degrees of " + "freedom that the operator targets.") + .def_prop_ro("max_degree", &fermion_op_term::max_degree, + "Returns the smallest index of the degrees of " + "freedom that the operator targets.") + .def_prop_ro("ops_count", &fermion_op_term::num_ops, + "Returns the number of operators in the product.") + .def_prop_ro( "term_id", &fermion_op_term::get_term_id, "The term id uniquely identifies the operators and targets (degrees) " "that they act on, " "but does not include information about the coefficient.") - .def_property_readonly( + .def_prop_ro( "coefficient", &fermion_op_term::get_coefficient, "Returns the unevaluated coefficient of the operator. The " "coefficient is a " @@ -424,30 +437,32 @@ void bindFermionOperator(py::module &mod) { // constructors - .def(py::init<>(), + .def(nanobind::init<>(), "Creates a product operator with constant value 1. The returned " "operator does not target any degrees of freedom but merely " "represents a constant.") - .def(py::init(), py::arg("first_degree"), - py::arg("last_degree"), + .def(nanobind::init(), + nanobind::arg("first_degree"), nanobind::arg("last_degree"), "Creates a product operator that applies an identity operation to " "all degrees of " "freedom in the range [first_degree, last_degree).") - .def(py::init(), + .def(nanobind::init(), "Creates a product operator with the given constant value. " "The returned operator does not target any degrees of freedom.") - .def(py::init>(), + .def(nanobind::init>(), "Creates a product operator with the given " "constant value. The returned operator does not target any degrees " "of freedom.") - .def(py::init([](const scalar_operator &scalar) { - return fermion_op_term() * scalar; - }), - "Creates a product operator with non-constant scalar value.") - .def(py::init(), + .def( + "__init__", + [](fermion_op_term *self, const scalar_operator &scalar) { + new (self) fermion_op_term(fermion_op_term() * scalar); + }, + "Creates a product operator with non-constant scalar value.") + .def(nanobind::init(), "Creates a product operator with the given elementary operator.") - .def(py::init(), - py::arg("operator"), py::arg("size") = 0, + .def(nanobind::init(), + nanobind::arg("operator"), nanobind::arg("size") = 0, "Creates a copy of the given operator and reserves space for " "storing the given " "number of product terms (if a size is provided).") @@ -459,7 +474,7 @@ void bindFermionOperator(py::module &mod) { // evaluations .def("evaluate_coefficient", &fermion_op_term::evaluate_coefficient, - py::arg("parameters") = parameter_map(), + nanobind::arg("parameters") = parameter_map(), "Returns the evaluated coefficient of the product operator. The " "parameters is a map of parameter names to their concrete, complex " "values.") @@ -470,9 +485,9 @@ void bindFermionOperator(py::module &mod) { auto cmat = self.to_matrix(dimensions, params, invert_order); return details::cmat_to_numpy(cmat); }, - py::arg("dimensions") = dimension_map(), - py::arg("parameters") = parameter_map(), - py::arg("invert_order") = false, + nanobind::arg("dimensions") = dimension_map(), + nanobind::arg("parameters") = parameter_map(), + nanobind::arg("invert_order") = false, "Returns the matrix representation of the operator." "The matrix is ordered according to the convention (endianness) " "used in CUDA-Q, and the ordering returned by `degrees`. This order " @@ -482,13 +497,13 @@ void bindFermionOperator(py::module &mod) { .def( "to_matrix", [](const fermion_op_term &self, dimension_map &dimensions, - bool invert_order, const py::kwargs &kwargs) { + bool invert_order, const nanobind::kwargs &kwargs) { auto cmat = self.to_matrix( dimensions, details::kwargs_to_param_map(kwargs), invert_order); return details::cmat_to_numpy(cmat); }, - py::arg("dimensions") = dimension_map(), - py::arg("invert_order") = false, + nanobind::arg("dimensions") = dimension_map(), + nanobind::arg("invert_order") = false, nanobind::arg("kwargs"), "Returns the matrix representation of the operator." "The matrix is ordered according to the convention (endianness) " "used in CUDA-Q, and the ordering returned by `degrees`. This order " @@ -501,9 +516,9 @@ void bindFermionOperator(py::module &mod) { const parameter_map ¶ms, bool invert_order) { return self.to_sparse_matrix(dimensions, params, invert_order); }, - py::arg("dimensions") = dimension_map(), - py::arg("parameters") = parameter_map(), - py::arg("invert_order") = false, + nanobind::arg("dimensions") = dimension_map(), + nanobind::arg("parameters") = parameter_map(), + nanobind::arg("invert_order") = false, "Return the sparse matrix representation of the operator. This " "representation is a " "`Tuple[list[complex], list[int], list[int]]`, encoding the " @@ -517,12 +532,12 @@ void bindFermionOperator(py::module &mod) { .def( "to_sparse_matrix", [](const fermion_op_term &self, dimension_map &dimensions, - bool invert_order, const py::kwargs &kwargs) { + bool invert_order, const nanobind::kwargs &kwargs) { return self.to_sparse_matrix( dimensions, details::kwargs_to_param_map(kwargs), invert_order); }, - py::arg("dimensions") = dimension_map(), - py::arg("invert_order") = false, + nanobind::arg("dimensions") = dimension_map(), + nanobind::arg("invert_order") = false, nanobind::arg("kwargs"), "Return the sparse matrix representation of the operator. This " "representation is a " "`Tuple[list[complex], list[int], list[int]]`, encoding the " @@ -536,7 +551,7 @@ void bindFermionOperator(py::module &mod) { // comparisons - .def("__eq__", &fermion_op_term::operator==, py::is_operator(), + .def("__eq__", &fermion_op_term::operator==, nanobind::is_operator(), "Return true if the two operators are equivalent. The equivalence " "check takes " "commutation relations into account. Operators acting on different " @@ -548,77 +563,78 @@ void bindFermionOperator(py::module &mod) { [](const fermion_op_term &self, const fermion_op &other) { return other.num_terms() == 1 && *other.begin() == self; }, - py::is_operator(), "Return true if the two operators are equivalent.") + nanobind::is_operator(), + "Return true if the two operators are equivalent.") // unary operators - .def(-py::self, py::is_operator()) - .def(+py::self, py::is_operator()) + .def(-nanobind::self, nanobind::is_operator()) + .def(+nanobind::self, nanobind::is_operator()) // in-place arithmetics - .def(py::self /= int(), py::is_operator()) - .def(py::self *= int(), py::is_operator()) - .def(py::self /= double(), py::is_operator()) - .def(py::self *= double(), py::is_operator()) - .def(py::self /= std::complex(), py::is_operator()) - .def(py::self *= std::complex(), py::is_operator()) - .def(py::self /= scalar_operator(), py::is_operator()) - .def(py::self *= scalar_operator(), py::is_operator()) - .def(py::self *= py::self, py::is_operator()) + .def(nanobind::self /= int(), nanobind::is_operator()) + .def(nanobind::self *= int(), nanobind::is_operator()) + .def(nanobind::self /= double(), nanobind::is_operator()) + .def(nanobind::self *= double(), nanobind::is_operator()) + .def(nanobind::self /= std::complex(), nanobind::is_operator()) + .def(nanobind::self *= std::complex(), nanobind::is_operator()) + .def(nanobind::self /= scalar_operator(), nanobind::is_operator()) + .def(nanobind::self *= scalar_operator(), nanobind::is_operator()) + .def(nanobind::self *= nanobind::self, nanobind::is_operator()) // right-hand arithmetics - .def(py::self / int(), py::is_operator()) - .def(py::self * int(), py::is_operator()) - .def(py::self + int(), py::is_operator()) - .def(py::self - int(), py::is_operator()) - .def(py::self / double(), py::is_operator()) - .def(py::self * double(), py::is_operator()) - .def(py::self + double(), py::is_operator()) - .def(py::self - double(), py::is_operator()) - .def(py::self / std::complex(), py::is_operator()) - .def(py::self * std::complex(), py::is_operator()) - .def(py::self + std::complex(), py::is_operator()) - .def(py::self - std::complex(), py::is_operator()) - .def(py::self / scalar_operator(), py::is_operator()) - .def(py::self * scalar_operator(), py::is_operator()) - .def(py::self + scalar_operator(), py::is_operator()) - .def(py::self - scalar_operator(), py::is_operator()) - .def(py::self * py::self, py::is_operator()) - .def(py::self + py::self, py::is_operator()) - .def(py::self - py::self, py::is_operator()) - .def(py::self * fermion_op(), py::is_operator()) - .def(py::self + fermion_op(), py::is_operator()) - .def(py::self - fermion_op(), py::is_operator()) - .def(py::self * matrix_op_term(), py::is_operator()) - .def(py::self + matrix_op_term(), py::is_operator()) - .def(py::self - matrix_op_term(), py::is_operator()) - .def(py::self * matrix_op(), py::is_operator()) - .def(py::self + matrix_op(), py::is_operator()) - .def(py::self - matrix_op(), py::is_operator()) + .def(nanobind::self / int(), nanobind::is_operator()) + .def(nanobind::self * int(), nanobind::is_operator()) + .def(nanobind::self + int(), nanobind::is_operator()) + .def(nanobind::self - int(), nanobind::is_operator()) + .def(nanobind::self / double(), nanobind::is_operator()) + .def(nanobind::self * double(), nanobind::is_operator()) + .def(nanobind::self + double(), nanobind::is_operator()) + .def(nanobind::self - double(), nanobind::is_operator()) + .def(nanobind::self / std::complex(), nanobind::is_operator()) + .def(nanobind::self * std::complex(), nanobind::is_operator()) + .def(nanobind::self + std::complex(), nanobind::is_operator()) + .def(nanobind::self - std::complex(), nanobind::is_operator()) + .def(nanobind::self / scalar_operator(), nanobind::is_operator()) + .def(nanobind::self * scalar_operator(), nanobind::is_operator()) + .def(nanobind::self + scalar_operator(), nanobind::is_operator()) + .def(nanobind::self - scalar_operator(), nanobind::is_operator()) + .def(nanobind::self * nanobind::self, nanobind::is_operator()) + .def(nanobind::self + nanobind::self, nanobind::is_operator()) + .def(nanobind::self - nanobind::self, nanobind::is_operator()) + .def(nanobind::self * fermion_op(), nanobind::is_operator()) + .def(nanobind::self + fermion_op(), nanobind::is_operator()) + .def(nanobind::self - fermion_op(), nanobind::is_operator()) + .def(nanobind::self * matrix_op_term(), nanobind::is_operator()) + .def(nanobind::self + matrix_op_term(), nanobind::is_operator()) + .def(nanobind::self - matrix_op_term(), nanobind::is_operator()) + .def(nanobind::self * matrix_op(), nanobind::is_operator()) + .def(nanobind::self + matrix_op(), nanobind::is_operator()) + .def(nanobind::self - matrix_op(), nanobind::is_operator()) // left-hand arithmetics - .def(int() * py::self, py::is_operator()) - .def(int() + py::self, py::is_operator()) - .def(int() - py::self, py::is_operator()) - .def(double() * py::self, py::is_operator()) - .def(double() + py::self, py::is_operator()) - .def(double() - py::self, py::is_operator()) - .def(std::complex() * py::self, py::is_operator()) - .def(std::complex() + py::self, py::is_operator()) - .def(std::complex() - py::self, py::is_operator()) - .def(scalar_operator() * py::self, py::is_operator()) - .def(scalar_operator() + py::self, py::is_operator()) - .def(scalar_operator() - py::self, py::is_operator()) + .def(int() * nanobind::self, nanobind::is_operator()) + .def(int() + nanobind::self, nanobind::is_operator()) + .def(int() - nanobind::self, nanobind::is_operator()) + .def(double() * nanobind::self, nanobind::is_operator()) + .def(double() + nanobind::self, nanobind::is_operator()) + .def(double() - nanobind::self, nanobind::is_operator()) + .def(std::complex() * nanobind::self, nanobind::is_operator()) + .def(std::complex() + nanobind::self, nanobind::is_operator()) + .def(std::complex() - nanobind::self, nanobind::is_operator()) + .def(scalar_operator() * nanobind::self, nanobind::is_operator()) + .def(scalar_operator() + nanobind::self, nanobind::is_operator()) + .def(scalar_operator() - nanobind::self, nanobind::is_operator()) // general utility functions .def("is_identity", &fermion_op_term::is_identity, "Checks if all operators in the product are the identity. " - "Note: this function returns true regardless of the value of the " - "coefficient.") + "Note that this function returns true regardless of the value of " + "the coefficient.") .def( "__str__", [](const fermion_op_term &self) { return self.to_string(); }, @@ -642,12 +658,12 @@ void bindFermionOperator(py::module &mod) { "of freedom that are not included in the given set."); } -void bindFermionWrapper(py::module &mod) { +void bindFermionWrapper(nanobind::module_ &mod) { bindFermionOperator(mod); - py::implicitly_convertible(); - py::implicitly_convertible, fermion_op_term>(); - py::implicitly_convertible(); - py::implicitly_convertible(); + nanobind::implicitly_convertible(); + nanobind::implicitly_convertible, fermion_op_term>(); + nanobind::implicitly_convertible(); + nanobind::implicitly_convertible(); bindFermionModule(mod); } diff --git a/python/runtime/cudaq/operators/py_fermion_op.h b/python/runtime/cudaq/operators/py_fermion_op.h index b54e406267a..45dbb8015d2 100644 --- a/python/runtime/cudaq/operators/py_fermion_op.h +++ b/python/runtime/cudaq/operators/py_fermion_op.h @@ -6,12 +6,10 @@ * the terms of the Apache License 2.0 which accompanies this distribution. * ******************************************************************************/ -#include - -namespace py = pybind11; +#include namespace cudaq { /// @brief Wrapper function for exposing the bindings of fermionic /// operators to python. -void bindFermionWrapper(py::module &mod); +void bindFermionWrapper(nanobind::module_ &mod); } // namespace cudaq diff --git a/python/runtime/cudaq/operators/py_handlers.cpp b/python/runtime/cudaq/operators/py_handlers.cpp index 71ca25a15aa..e8c2147e92b 100644 --- a/python/runtime/cudaq/operators/py_handlers.cpp +++ b/python/runtime/cudaq/operators/py_handlers.cpp @@ -7,11 +7,14 @@ ******************************************************************************/ #include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include #include "cudaq/operators.h" #include "py_handlers.h" @@ -19,53 +22,56 @@ namespace cudaq { -void bindPauli(py::module mod) { - py::enum_(mod, "Pauli", - "An enumeration representing the types of Pauli matrices.") +void bindPauli(nanobind::module_ mod) { + nanobind::enum_( + mod, "Pauli", "An enumeration representing the types of Pauli matrices.") .value("X", pauli::X) .value("Y", pauli::Y) .value("Z", pauli::Z) .value("I", pauli::I); } -void bindOperatorHandlers(py::module &mod) { +void bindOperatorHandlers(nanobind::module_ &mod) { using matrix_callback = std::function &, const parameter_map &)>; - py::class_(mod, "MatrixOperatorElement") - .def_property_readonly( + nanobind::class_(mod, "MatrixOperatorElement") + .def_prop_ro( "id", [](const matrix_handler &self) { return self.to_string(false); }, "Returns the id used to define and instantiate the operator.") - .def_property_readonly("degrees", &matrix_handler::degrees, - "Returns a vector that lists all degrees of " - "freedom that the operator targets.") - .def_property_readonly("parameters", - &matrix_handler::get_parameter_descriptions, - "Returns a dictionary that maps each parameter " - "name to its description.") - .def_property_readonly("expected_dimensions", - &matrix_handler::get_expected_dimensions, - "The number of levels, that is the dimension, for " - "each degree of freedom " - "in canonical order that the operator acts on. A " - "value of zero or less " - "indicates that the operator is defined for any " - "dimension of that degree.") - .def(py::init(), + .def_prop_ro("degrees", &matrix_handler::degrees, + "Returns a vector that lists all degrees of " + "freedom that the operator targets.") + .def_prop_ro("parameters", &matrix_handler::get_parameter_descriptions, + "Returns a dictionary that maps each parameter " + "name to its description.") + .def_prop_ro("expected_dimensions", + &matrix_handler::get_expected_dimensions, + "The number of levels, that is the dimension, for " + "each degree of freedom " + "in canonical order that the operator acts on. A " + "value of zero or less " + "indicates that the operator is defined for any " + "dimension of that degree.") + .def(nanobind::init(), "Creates an identity operator on the given target.") - .def(py::init([](std::string operator_id, - std::vector degrees) { - return matrix_handler(std::move(operator_id), std::move(degrees)); - }), - py::arg("id"), py::arg("degrees"), - "Creates the matrix operator with the given id acting on the given " - "degrees of " - "freedom. Throws a runtime exception if no operator with that id " - "has been defined.") - .def(py::init(), "Copy constructor.") - .def("__eq__", &matrix_handler::operator==, py::is_operator()) - .def("to_string", &matrix_handler::to_string, py::arg("include_degrees"), + .def( + "__init__", + [](matrix_handler *self, std::string operator_id, + std::vector degrees) { + new (self) + matrix_handler(std::move(operator_id), std::move(degrees)); + }, + nanobind::arg("id"), nanobind::arg("degrees"), + "Creates the matrix operator with the given id acting on the given " + "degrees of " + "freedom. Throws a runtime exception if no operator with that id " + "has been defined.") + .def(nanobind::init(), "Copy constructor.") + .def("__eq__", &matrix_handler::operator==, nanobind::is_operator()) + .def("to_string", &matrix_handler::to_string, + nanobind::arg("include_degrees"), "Returns the string representation of the operator.") .def( "to_matrix", @@ -74,18 +80,19 @@ void bindOperatorHandlers(py::module &mod) { auto cmat = self.to_matrix(dimensions, params); return details::cmat_to_numpy(cmat); }, - py::arg("dimensions") = dimension_map(), - py::arg("parameters") = parameter_map(), + nanobind::arg("dimensions") = dimension_map(), + nanobind::arg("parameters") = parameter_map(), "Returns the matrix representation of the operator.") .def( "to_matrix", [](const matrix_handler &self, dimension_map &dimensions, - const py::kwargs &kwargs) { + const nanobind::kwargs &kwargs) { auto cmat = self.to_matrix(dimensions, details::kwargs_to_param_map(kwargs)); return details::cmat_to_numpy(cmat); }, - py::arg("dimensions") = dimension_map(), + nanobind::arg("dimensions") = dimension_map(), + nanobind::arg("kwargs"), "Returns the matrix representation of the operator.") // tools for custom operators @@ -93,12 +100,12 @@ void bindOperatorHandlers(py::module &mod) { "_define", [](std::string operator_id, std::vector expected_dimensions, const matrix_callback &func, bool overwrite, - const py::kwargs &kwargs) { + const nanobind::kwargs &kwargs) { // we need to make sure the python function that is stored in // the static dictionary containing the operator definitions // is properly cleaned up - otherwise python will hang on exit... - auto atexit = py::module_::import("atexit"); - atexit.attr("register")(py::cpp_function([operator_id]() { + auto atexit = nanobind::module_::import_("atexit"); + atexit.attr("register")(nanobind::cpp_function([operator_id]() { matrix_handler::remove_definition(operator_id); })); if (overwrite) @@ -107,24 +114,25 @@ void bindOperatorHandlers(py::module &mod) { std::move(operator_id), std::move(expected_dimensions), func, details::kwargs_to_param_description(kwargs)); }, - py::arg("operator_id"), py::arg("expected_dimensions"), - py::arg("callback"), py::arg("overwrite") = false, + nanobind::arg("operator_id"), nanobind::arg("expected_dimensions"), + nanobind::arg("callback"), nanobind::arg("overwrite") = false, + nanobind::arg("kwargs"), "Defines a matrix operator with the given name and dimensions whose" "matrix representation can be obtained by invoking the given " "callback function."); - py::class_(mod, "BosonOperatorElement") - .def_property_readonly( - "target", &boson_handler::target, - "Returns the degree of freedom that the operator targets.") - .def_property_readonly("degrees", &boson_handler::degrees, - "Returns a vector that lists all degrees of " - "freedom that the operator targets.") - .def(py::init(), + nanobind::class_(mod, "BosonOperatorElement") + .def_prop_ro("target", &boson_handler::target, + "Returns the degree of freedom that the operator targets.") + .def_prop_ro("degrees", &boson_handler::degrees, + "Returns a vector that lists all degrees of " + "freedom that the operator targets.") + .def(nanobind::init(), "Creates an identity operator on the given target.") - .def(py::init(), "Copy constructor.") - .def("__eq__", &boson_handler::operator==, py::is_operator()) - .def("to_string", &boson_handler::to_string, py::arg("include_degrees"), + .def(nanobind::init(), "Copy constructor.") + .def("__eq__", &boson_handler::operator==, nanobind::is_operator()) + .def("to_string", &boson_handler::to_string, + nanobind::arg("include_degrees"), "Returns the string representation of the operator.") .def( "to_matrix", @@ -133,32 +141,33 @@ void bindOperatorHandlers(py::module &mod) { auto cmat = self.to_matrix(dimensions, params); return details::cmat_to_numpy(cmat); }, - py::arg("dimensions") = dimension_map(), - py::arg("parameters") = parameter_map(), + nanobind::arg("dimensions") = dimension_map(), + nanobind::arg("parameters") = parameter_map(), "Returns the matrix representation of the operator.") .def( "to_matrix", [](const boson_handler &self, dimension_map &dimensions, - const py::kwargs &kwargs) { + const nanobind::kwargs &kwargs) { auto cmat = self.to_matrix(dimensions, details::kwargs_to_param_map(kwargs)); return details::cmat_to_numpy(cmat); }, - py::arg("dimensions") = dimension_map(), + nanobind::arg("dimensions") = dimension_map(), + nanobind::arg("kwargs"), "Returns the matrix representation of the operator."); - py::class_(mod, "FermionOperatorElement") - .def_property_readonly( - "target", &fermion_handler::target, - "Returns the degree of freedom that the operator targets.") - .def_property_readonly("degrees", &fermion_handler::degrees, - "Returns a vector that lists all degrees of " - "freedom that the operator targets.") - .def(py::init(), + nanobind::class_(mod, "FermionOperatorElement") + .def_prop_ro("target", &fermion_handler::target, + "Returns the degree of freedom that the operator targets.") + .def_prop_ro("degrees", &fermion_handler::degrees, + "Returns a vector that lists all degrees of " + "freedom that the operator targets.") + .def(nanobind::init(), "Creates an identity operator on the given target.") - .def(py::init(), "Copy constructor.") - .def("__eq__", &fermion_handler::operator==, py::is_operator()) - .def("to_string", &fermion_handler::to_string, py::arg("include_degrees"), + .def(nanobind::init(), "Copy constructor.") + .def("__eq__", &fermion_handler::operator==, nanobind::is_operator()) + .def("to_string", &fermion_handler::to_string, + nanobind::arg("include_degrees"), "Returns the string representation of the operator.") .def( "to_matrix", @@ -167,34 +176,35 @@ void bindOperatorHandlers(py::module &mod) { auto cmat = self.to_matrix(dimensions, params); return details::cmat_to_numpy(cmat); }, - py::arg("dimensions") = dimension_map(), - py::arg("parameters") = parameter_map(), + nanobind::arg("dimensions") = dimension_map(), + nanobind::arg("parameters") = parameter_map(), "Returns the matrix representation of the operator.") .def( "to_matrix", [](const fermion_handler &self, dimension_map &dimensions, - const py::kwargs &kwargs) { + const nanobind::kwargs &kwargs) { auto cmat = self.to_matrix(dimensions, details::kwargs_to_param_map(kwargs)); return details::cmat_to_numpy(cmat); }, - py::arg("dimensions") = dimension_map(), + nanobind::arg("dimensions") = dimension_map(), + nanobind::arg("kwargs"), "Returns the matrix representation of the operator."); - py::class_(mod, "SpinOperatorElement") - .def_property_readonly( - "target", &spin_handler::target, - "Returns the degree of freedom that the operator targets.") - .def_property_readonly("degrees", &spin_handler::degrees, - "Returns a vector that lists all degrees of " - "freedom that the operator targets.") - .def(py::init(), + nanobind::class_(mod, "SpinOperatorElement") + .def_prop_ro("target", &spin_handler::target, + "Returns the degree of freedom that the operator targets.") + .def_prop_ro("degrees", &spin_handler::degrees, + "Returns a vector that lists all degrees of " + "freedom that the operator targets.") + .def(nanobind::init(), "Creates an identity operator on the given target.") - .def(py::init(), "Copy constructor.") - .def("__eq__", &spin_handler::operator==, py::is_operator()) + .def(nanobind::init(), "Copy constructor.") + .def("__eq__", &spin_handler::operator==, nanobind::is_operator()) .def("as_pauli", &spin_handler::as_pauli, "Returns the Pauli representation of the operator.") - .def("to_string", &spin_handler::to_string, py::arg("include_degrees"), + .def("to_string", &spin_handler::to_string, + nanobind::arg("include_degrees"), "Returns the string representation of the operator.") .def( "to_matrix", @@ -203,22 +213,23 @@ void bindOperatorHandlers(py::module &mod) { auto cmat = self.to_matrix(dimensions, params); return details::cmat_to_numpy(cmat); }, - py::arg("dimensions") = dimension_map(), - py::arg("parameters") = parameter_map(), + nanobind::arg("dimensions") = dimension_map(), + nanobind::arg("parameters") = parameter_map(), "Returns the matrix representation of the operator.") .def( "to_matrix", [](const spin_handler &self, dimension_map &dimensions, - const py::kwargs &kwargs) { + const nanobind::kwargs &kwargs) { auto cmat = self.to_matrix(dimensions, details::kwargs_to_param_map(kwargs)); return details::cmat_to_numpy(cmat); }, - py::arg("dimensions") = dimension_map(), + nanobind::arg("dimensions") = dimension_map(), + nanobind::arg("kwargs"), "Returns the matrix representation of the operator."); } -void bindHandlersWrapper(py::module &mod) { +void bindHandlersWrapper(nanobind::module_ &mod) { bindPauli(mod); bindOperatorHandlers(mod); } diff --git a/python/runtime/cudaq/operators/py_handlers.h b/python/runtime/cudaq/operators/py_handlers.h index 3bcde5ad205..cd82dd92e44 100644 --- a/python/runtime/cudaq/operators/py_handlers.h +++ b/python/runtime/cudaq/operators/py_handlers.h @@ -6,12 +6,10 @@ * the terms of the Apache License 2.0 which accompanies this distribution. * ******************************************************************************/ -#include - -namespace py = pybind11; +#include namespace cudaq { /// @brief Wrapper function for exposing the bindings of /// operator handlers to python. -void bindHandlersWrapper(py::module &mod); +void bindHandlersWrapper(nanobind::module_ &mod); } // namespace cudaq diff --git a/python/runtime/cudaq/operators/py_helpers.cpp b/python/runtime/cudaq/operators/py_helpers.cpp index aecc5811074..b34212bce6e 100644 --- a/python/runtime/cudaq/operators/py_helpers.cpp +++ b/python/runtime/cudaq/operators/py_helpers.cpp @@ -8,18 +8,19 @@ #include "py_helpers.h" #include "cudaq/operators.h" +#include #include -#include -#include -#include +#include +#include +#include namespace cudaq::details { -cudaq::parameter_map kwargs_to_param_map(const py::kwargs &kwargs) { +cudaq::parameter_map kwargs_to_param_map(const nanobind::kwargs &kwargs) { cudaq::parameter_map params; - for (auto &[keyPy, valuePy] : kwargs) { - std::string key = py::str(keyPy); - std::complex value = valuePy.cast>(); + for (auto [keyPy, valuePy] : kwargs) { + std::string key = nanobind::str(keyPy).c_str(); + std::complex value = nanobind::cast>(valuePy); params.insert(params.end(), std::pair>(key, value)); } @@ -27,29 +28,34 @@ cudaq::parameter_map kwargs_to_param_map(const py::kwargs &kwargs) { } std::unordered_map -kwargs_to_param_description(const py::kwargs &kwargs) { +kwargs_to_param_description(const nanobind::kwargs &kwargs) { std::unordered_map param_desc; - for (auto &[keyPy, valuePy] : kwargs) { - std::string key = py::str(keyPy); - std::string value = py::str(valuePy); + for (auto [keyPy, valuePy] : kwargs) { + std::string key = nanobind::str(keyPy).c_str(); + std::string value = nanobind::str(valuePy).c_str(); param_desc.insert(param_desc.end(), std::pair(key, value)); } return param_desc; } -py::array_t> cmat_to_numpy(complex_matrix &cmat) { +nanobind::ndarray> +cmat_to_numpy(complex_matrix &cmat) { auto rows = cmat.rows(); auto cols = cmat.cols(); - auto data = cmat.get_data(complex_matrix::order::row_major); - std::vector shape = {static_cast(rows), - static_cast(cols)}; - std::vector strides = { - static_cast(sizeof(std::complex) * cols), - static_cast(sizeof(std::complex))}; - - // Return a numpy array without copying data - return py::array_t>(shape, strides, data); -}; + auto *src = cmat.get_data(complex_matrix::order::row_major); + std::size_t n = rows * cols; + std::size_t shape[2] = {rows, cols}; + + auto *copy = new std::complex[n]; + std::copy(src, src + n, copy); + + nanobind::capsule owner(copy, [](void *p) noexcept { + delete[] static_cast *>(p); + }); + + return nanobind::ndarray>(copy, 2, + shape, owner); +} } // namespace cudaq::details diff --git a/python/runtime/cudaq/operators/py_helpers.h b/python/runtime/cudaq/operators/py_helpers.h index 33b7463ae9a..e712281784f 100644 --- a/python/runtime/cudaq/operators/py_helpers.h +++ b/python/runtime/cudaq/operators/py_helpers.h @@ -7,14 +7,13 @@ ******************************************************************************/ #include "cudaq/operators.h" -#include -#include - -namespace py = pybind11; +#include +#include namespace cudaq::details { -cudaq::parameter_map kwargs_to_param_map(const py::kwargs &kwargs); +cudaq::parameter_map kwargs_to_param_map(const nanobind::kwargs &kwargs); std::unordered_map -kwargs_to_param_description(const py::kwargs &kwargs); -py::array_t> cmat_to_numpy(complex_matrix &cmat); +kwargs_to_param_description(const nanobind::kwargs &kwargs); +nanobind::ndarray> +cmat_to_numpy(complex_matrix &cmat); } // namespace cudaq::details diff --git a/python/runtime/cudaq/operators/py_matrix.cpp b/python/runtime/cudaq/operators/py_matrix.cpp index 6b2828c2973..48d37891e7f 100644 --- a/python/runtime/cudaq/operators/py_matrix.cpp +++ b/python/runtime/cudaq/operators/py_matrix.cpp @@ -6,10 +6,12 @@ * the terms of the Apache License 2.0 which accompanies this distribution. * ******************************************************************************/ -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include #include "cudaq/operators/matrix.h" #include "py_helpers.h" @@ -19,44 +21,25 @@ namespace cudaq { -/// @brief Extract the array data from a buffer_info into our -/// own allocated data pointer. -void extractMatrixData(py::buffer_info &info, std::complex *data) { - if (info.format != py::format_descriptor>::format()) - throw std::runtime_error( - "Incompatible buffer format, must be np.complex128."); - - if (info.ndim != 2) - throw std::runtime_error("Incompatible buffer shape."); - - memcpy(data, info.ptr, - sizeof(std::complex) * (info.shape[0] * info.shape[1])); -} - -void bindComplexMatrix(py::module &mod) { - py::class_( - mod, "ComplexMatrix", py::buffer_protocol(), +void bindComplexMatrix(nanobind::module_ &mod) { + nanobind::class_( + mod, "ComplexMatrix", "The :class:`ComplexMatrix` is a thin wrapper around a " "matrix of complex elements.") - /// The following makes this fully compatible with NumPy - .def_buffer([](complex_matrix &op) -> py::buffer_info { - return py::buffer_info( - op.get_data(complex_matrix::order::row_major), - sizeof(std::complex), - py::format_descriptor>::format(), 2, - {op.rows(), op.cols()}, - {sizeof(std::complex) * op.cols(), - sizeof(std::complex)}); - }) - .def(py::init([](const py::buffer &b) { - py::buffer_info info = b.request(); - complex_matrix m(info.shape[0], info.shape[1]); - extractMatrixData(info, - m.get_data(complex_matrix::order::row_major)); - return m; - }), - "Create a :class:`ComplexMatrix` from a buffer of data, such as a " - "numpy.ndarray.") + .def( + "__init__", + [](complex_matrix *self, + nanobind::ndarray, nanobind::ndim<2>, + nanobind::c_contig, nanobind::numpy> + arr) { + auto rows = arr.shape(0); + auto cols = arr.shape(1); + new (self) complex_matrix(rows, cols); + memcpy(self->get_data(complex_matrix::order::row_major), arr.data(), + sizeof(std::complex) * rows * cols); + }, + "Create a :class:`ComplexMatrix` from a buffer of data, such as a " + "numpy.ndarray.") .def( "num_rows", [](complex_matrix &m) { return m.rows(); }, "Returns the number of rows in the matrix.") @@ -85,7 +68,7 @@ void bindComplexMatrix(py::module &mod) { [](const complex_matrix &lhs, const complex_matrix &rhs) { return lhs == rhs; }, - py::is_operator()) + nanobind::is_operator()) .def("__str__", &complex_matrix::to_string, "Returns the string representation of the matrix.") .def( diff --git a/python/runtime/cudaq/operators/py_matrix.h b/python/runtime/cudaq/operators/py_matrix.h index 022a74fdbf8..baf93260e9e 100644 --- a/python/runtime/cudaq/operators/py_matrix.h +++ b/python/runtime/cudaq/operators/py_matrix.h @@ -6,12 +6,10 @@ * the terms of the Apache License 2.0 which accompanies this distribution. * ******************************************************************************/ -#include - -namespace py = pybind11; +#include namespace cudaq { /// @brief Wrapper function for exposing the bindings of `cudaq::complex_matrix` /// to python. -void bindComplexMatrix(py::module &mod); +void bindComplexMatrix(nanobind::module_ &mod); } // namespace cudaq diff --git a/python/runtime/cudaq/operators/py_matrix_op.cpp b/python/runtime/cudaq/operators/py_matrix_op.cpp index 187ab99f746..3883f86c9bd 100644 --- a/python/runtime/cudaq/operators/py_matrix_op.cpp +++ b/python/runtime/cudaq/operators/py_matrix_op.cpp @@ -7,10 +7,15 @@ ******************************************************************************/ #include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include "cudaq/operators.h" #include "cudaq/operators/serialization.h" @@ -19,7 +24,7 @@ namespace cudaq { -void bindOperatorsModule(py::module &mod) { +void bindOperatorsModule(nanobind::module_ &mod) { // Binding the functions in `cudaq::operators` as `_pycudaq` submodule // so it's accessible directly in the cudaq namespace. auto operators_submodule = mod.def_submodule("operators"); @@ -33,34 +38,34 @@ void bindOperatorsModule(py::module &mod) { operators_submodule.def( "identity", [](std::size_t target) { return matrix_op::identity(target); }, - py::arg("target"), + nanobind::arg("target"), "Returns an identity operator on the given target index."); operators_submodule.def( "identities", [](std::size_t first, std::size_t last) { return matrix_op_term(first, last); }, - py::arg("first"), py::arg("last"), + nanobind::arg("first"), nanobind::arg("last"), "Creates a product operator that applies an identity operation to all " "degrees of " "freedom in the open range [first, last)."); operators_submodule.def( - "number", &matrix_op::number, py::arg("target"), + "number", &matrix_op::number, nanobind::arg("target"), "Returns a number operator on the given target index."); operators_submodule.def( - "parity", &matrix_op::parity, py::arg("target"), + "parity", &matrix_op::parity, nanobind::arg("target"), "Returns a parity operator on the given target index."); operators_submodule.def( - "position", &matrix_op::position, py::arg("target"), + "position", &matrix_op::position, nanobind::arg("target"), "Returns a position operator on the given target index."); operators_submodule.def( - "momentum", &matrix_op::momentum, py::arg("target"), + "momentum", &matrix_op::momentum, nanobind::arg("target"), "Returns a momentum operator on the given target index."); operators_submodule.def( - "squeeze", &matrix_op::squeeze, py::arg("target"), + "squeeze", &matrix_op::squeeze, nanobind::arg("target"), "Returns a squeezing operator on the given target index."); operators_submodule.def( - "displace", &matrix_op::displace, py::arg("target"), + "displace", &matrix_op::displace, nanobind::arg("target"), "Returns a displacement operator on the given target index."); operators_submodule.def( "canonicalized", @@ -94,41 +99,43 @@ void bindOperatorsModule(py::module &mod) { "degrees of freedom."); } -void bindMatrixOperator(py::module &mod) { +void bindMatrixOperator(nanobind::module_ &mod) { - auto matrix_op_class = py::class_(mod, "MatrixOperator"); + auto matrix_op_class = nanobind::class_(mod, "MatrixOperator"); auto matrix_op_term_class = - py::class_(mod, "MatrixOperatorTerm"); + nanobind::class_(mod, "MatrixOperatorTerm"); matrix_op_class .def( "__iter__", [](matrix_op &self) { - return py::make_iterator(self.begin(), self.end()); + return nanobind::make_iterator(nanobind::type(), + "iterator", self.begin(), + self.end()); }, - py::keep_alive<0, 1>(), "Loop through each term of the operator.") + nanobind::keep_alive<0, 1>(), + "Loop through each term of the operator.") // properties - .def_property_readonly("parameters", - &matrix_op::get_parameter_descriptions, - "Returns a dictionary that maps each parameter " - "name to its description.") - .def_property_readonly("degrees", &matrix_op::degrees, - "Returns a vector that lists all degrees of " - "freedom that the operator targets.") - .def_property_readonly("min_degree", &matrix_op::min_degree, - "Returns the smallest index of the degrees of " - "freedom that the operator targets.") - .def_property_readonly("max_degree", &matrix_op::max_degree, - "Returns the smallest index of the degrees of " - "freedom that the operator targets.") - .def_property_readonly("term_count", &matrix_op::num_terms, - "Returns the number of terms in the operator.") + .def_prop_ro("parameters", &matrix_op::get_parameter_descriptions, + "Returns a dictionary that maps each parameter " + "name to its description.") + .def_prop_ro("degrees", &matrix_op::degrees, + "Returns a vector that lists all degrees of " + "freedom that the operator targets.") + .def_prop_ro("min_degree", &matrix_op::min_degree, + "Returns the smallest index of the degrees of " + "freedom that the operator targets.") + .def_prop_ro("max_degree", &matrix_op::max_degree, + "Returns the smallest index of the degrees of " + "freedom that the operator targets.") + .def_prop_ro("term_count", &matrix_op::num_terms, + "Returns the number of terms in the operator.") // constructors - .def(py::init<>(), + .def(nanobind::init<>(), "Creates a default instantiated sum. A default instantiated " "sum has no value; it will take a value the first time an " "arithmetic operation " @@ -137,15 +144,15 @@ void bindMatrixOperator(py::module &mod) { "identity. To construct a `0` value in the mathematical sense " "(neutral element " "for addition), use `empty()` instead.") - .def(py::init(), + .def(nanobind::init(), "Creates a sum operator with no terms, reserving " "space for the given number of terms.") - .def(py::init()) - .def(py::init()) - .def(py::init()) - .def(py::init(), + .def(nanobind::init()) + .def(nanobind::init()) + .def(nanobind::init()) + .def(nanobind::init(), "Creates a sum operator with the given term.") - .def(py::init(), "Copy constructor.") + .def(nanobind::init(), "Copy constructor.") .def( "copy", [](const matrix_op &self) { return matrix_op(self); }, "Creates a copy of the operator.") @@ -159,9 +166,9 @@ void bindMatrixOperator(py::module &mod) { auto cmat = self.to_matrix(dimensions, params, invert_order); return details::cmat_to_numpy(cmat); }, - py::arg("dimensions") = dimension_map(), - py::arg("parameters") = parameter_map(), - py::arg("invert_order") = false, + nanobind::arg("dimensions") = dimension_map(), + nanobind::arg("parameters") = parameter_map(), + nanobind::arg("invert_order") = false, "Returns the matrix representation of the operator." "The matrix is ordered according to the convention (endianness) " "used in CUDA-Q, and the ordering returned by `degrees`. This order " @@ -172,13 +179,13 @@ void bindMatrixOperator(py::module &mod) { .def( "to_matrix", [](const matrix_op &self, dimension_map &dimensions, - bool invert_order, const py::kwargs &kwargs) { + bool invert_order, const nanobind::kwargs &kwargs) { auto cmat = self.to_matrix( dimensions, details::kwargs_to_param_map(kwargs), invert_order); return details::cmat_to_numpy(cmat); }, - py::arg("dimensions") = dimension_map(), - py::arg("invert_order") = false, + nanobind::arg("dimensions") = dimension_map(), + nanobind::arg("invert_order") = false, nanobind::arg("kwargs"), "Returns the matrix representation of the operator." "The matrix is ordered according to the convention (endianness) " "used in CUDA-Q, and the ordering returned by `degrees`. This order " @@ -188,7 +195,7 @@ void bindMatrixOperator(py::module &mod) { // comparisons - .def("__eq__", &matrix_op::operator==, py::is_operator(), + .def("__eq__", &matrix_op::operator==, nanobind::is_operator(), "Return true if the two operators are equivalent. The equivalence " "check takes " "into account that addition is commutative and so is multiplication " @@ -202,85 +209,85 @@ void bindMatrixOperator(py::module &mod) { [](const matrix_op &self, const matrix_op_term &other) { return self.num_terms() == 1 && *self.begin() == other; }, - py::is_operator(), "Return true if the two operators are equivalent.") + nanobind::is_operator(), + "Return true if the two operators are equivalent.") // unary operators - .def(-py::self, py::is_operator()) - .def(+py::self, py::is_operator()) + .def(-nanobind::self, nanobind::is_operator()) + .def(+nanobind::self, nanobind::is_operator()) // in-place arithmetics - .def(py::self /= int(), py::is_operator()) - .def(py::self *= int(), py::is_operator()) - .def(py::self += int(), py::is_operator()) - .def(py::self -= int(), py::is_operator()) - .def(py::self /= double(), py::is_operator()) - .def(py::self *= double(), py::is_operator()) - .def(py::self += double(), py::is_operator()) - .def(py::self -= double(), py::is_operator()) - .def(py::self /= std::complex(), py::is_operator()) - .def(py::self *= std::complex(), py::is_operator()) - .def(py::self += std::complex(), py::is_operator()) - .def(py::self -= std::complex(), py::is_operator()) - .def(py::self /= scalar_operator(), py::is_operator()) - .def(py::self *= scalar_operator(), py::is_operator()) - .def(py::self += scalar_operator(), py::is_operator()) - .def(py::self -= scalar_operator(), py::is_operator()) - .def(py::self *= matrix_op_term(), py::is_operator()) - .def(py::self += matrix_op_term(), py::is_operator()) - .def(py::self -= matrix_op_term(), py::is_operator()) - .def(py::self *= py::self, py::is_operator()) - .def(py::self += py::self, py::is_operator()) -// see issue https://github.com/pybind/pybind11/issues/1893 + .def(nanobind::self /= int(), nanobind::is_operator()) + .def(nanobind::self *= int(), nanobind::is_operator()) + .def(nanobind::self += int(), nanobind::is_operator()) + .def(nanobind::self -= int(), nanobind::is_operator()) + .def(nanobind::self /= double(), nanobind::is_operator()) + .def(nanobind::self *= double(), nanobind::is_operator()) + .def(nanobind::self += double(), nanobind::is_operator()) + .def(nanobind::self -= double(), nanobind::is_operator()) + .def(nanobind::self /= std::complex(), nanobind::is_operator()) + .def(nanobind::self *= std::complex(), nanobind::is_operator()) + .def(nanobind::self += std::complex(), nanobind::is_operator()) + .def(nanobind::self -= std::complex(), nanobind::is_operator()) + .def(nanobind::self /= scalar_operator(), nanobind::is_operator()) + .def(nanobind::self *= scalar_operator(), nanobind::is_operator()) + .def(nanobind::self += scalar_operator(), nanobind::is_operator()) + .def(nanobind::self -= scalar_operator(), nanobind::is_operator()) + .def(nanobind::self *= matrix_op_term(), nanobind::is_operator()) + .def(nanobind::self += matrix_op_term(), nanobind::is_operator()) + .def(nanobind::self -= matrix_op_term(), nanobind::is_operator()) + .def(nanobind::self *= nanobind::self, nanobind::is_operator()) + .def(nanobind::self += nanobind::self, nanobind::is_operator()) #ifdef __clang__ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wself-assign-overloaded" #endif - .def(py::self -= py::self, py::is_operator()) + .def(nanobind::self -= nanobind::self, nanobind::is_operator()) #ifdef __clang__ #pragma clang diagnostic pop #endif // right-hand arithmetics - .def(py::self / int(), py::is_operator()) - .def(py::self * int(), py::is_operator()) - .def(py::self + int(), py::is_operator()) - .def(py::self - int(), py::is_operator()) - .def(py::self / double(), py::is_operator()) - .def(py::self * double(), py::is_operator()) - .def(py::self + double(), py::is_operator()) - .def(py::self - double(), py::is_operator()) - .def(py::self / std::complex(), py::is_operator()) - .def(py::self * std::complex(), py::is_operator()) - .def(py::self + std::complex(), py::is_operator()) - .def(py::self - std::complex(), py::is_operator()) - .def(py::self / scalar_operator(), py::is_operator()) - .def(py::self * scalar_operator(), py::is_operator()) - .def(py::self + scalar_operator(), py::is_operator()) - .def(py::self - scalar_operator(), py::is_operator()) - .def(py::self * matrix_op_term(), py::is_operator()) - .def(py::self + matrix_op_term(), py::is_operator()) - .def(py::self - matrix_op_term(), py::is_operator()) - .def(py::self * py::self, py::is_operator()) - .def(py::self + py::self, py::is_operator()) - .def(py::self - py::self, py::is_operator()) + .def(nanobind::self / int(), nanobind::is_operator()) + .def(nanobind::self * int(), nanobind::is_operator()) + .def(nanobind::self + int(), nanobind::is_operator()) + .def(nanobind::self - int(), nanobind::is_operator()) + .def(nanobind::self / double(), nanobind::is_operator()) + .def(nanobind::self * double(), nanobind::is_operator()) + .def(nanobind::self + double(), nanobind::is_operator()) + .def(nanobind::self - double(), nanobind::is_operator()) + .def(nanobind::self / std::complex(), nanobind::is_operator()) + .def(nanobind::self * std::complex(), nanobind::is_operator()) + .def(nanobind::self + std::complex(), nanobind::is_operator()) + .def(nanobind::self - std::complex(), nanobind::is_operator()) + .def(nanobind::self / scalar_operator(), nanobind::is_operator()) + .def(nanobind::self * scalar_operator(), nanobind::is_operator()) + .def(nanobind::self + scalar_operator(), nanobind::is_operator()) + .def(nanobind::self - scalar_operator(), nanobind::is_operator()) + .def(nanobind::self * matrix_op_term(), nanobind::is_operator()) + .def(nanobind::self + matrix_op_term(), nanobind::is_operator()) + .def(nanobind::self - matrix_op_term(), nanobind::is_operator()) + .def(nanobind::self * nanobind::self, nanobind::is_operator()) + .def(nanobind::self + nanobind::self, nanobind::is_operator()) + .def(nanobind::self - nanobind::self, nanobind::is_operator()) // left-hand arithmetics - .def(int() * py::self, py::is_operator()) - .def(int() + py::self, py::is_operator()) - .def(int() - py::self, py::is_operator()) - .def(double() * py::self, py::is_operator()) - .def(double() + py::self, py::is_operator()) - .def(double() - py::self, py::is_operator()) - .def(std::complex() * py::self, py::is_operator()) - .def(std::complex() + py::self, py::is_operator()) - .def(std::complex() - py::self, py::is_operator()) - .def(scalar_operator() * py::self, py::is_operator()) - .def(scalar_operator() + py::self, py::is_operator()) - .def(scalar_operator() - py::self, py::is_operator()) + .def(int() * nanobind::self, nanobind::is_operator()) + .def(int() + nanobind::self, nanobind::is_operator()) + .def(int() - nanobind::self, nanobind::is_operator()) + .def(double() * nanobind::self, nanobind::is_operator()) + .def(double() + nanobind::self, nanobind::is_operator()) + .def(double() - nanobind::self, nanobind::is_operator()) + .def(std::complex() * nanobind::self, nanobind::is_operator()) + .def(std::complex() + nanobind::self, nanobind::is_operator()) + .def(std::complex() - nanobind::self, nanobind::is_operator()) + .def(scalar_operator() * nanobind::self, nanobind::is_operator()) + .def(scalar_operator() + nanobind::self, nanobind::is_operator()) + .def(scalar_operator() - nanobind::self, nanobind::is_operator()) // common operators @@ -308,17 +315,17 @@ void bindMatrixOperator(py::module &mod) { .def("dump", &matrix_op::dump, "Prints the string representation of the operator to the standard " "output.") - .def("trim", &matrix_op::trim, py::arg("tol") = 0.0, - py::arg("parameters") = parameter_map(), + .def("trim", &matrix_op::trim, nanobind::arg("tol") = 0.0, + nanobind::arg("parameters") = parameter_map(), "Removes all terms from the sum for which the absolute value of the " "coefficient is below " "the given tolerance.") .def( "trim", - [](matrix_op &self, double tol, const py::kwargs &kwargs) { + [](matrix_op &self, double tol, const nanobind::kwargs &kwargs) { return self.trim(tol, details::kwargs_to_param_map(kwargs)); }, - py::arg("tol") = 0.0, + nanobind::arg("tol") = 0.0, nanobind::arg("kwargs"), "Removes all terms from the sum for which the absolute value of the " "coefficient is below " "the given tolerance.") @@ -343,42 +350,44 @@ void bindMatrixOperator(py::module &mod) { .def( "__iter__", [](matrix_op_term &self) { - return py::make_iterator(self.begin(), self.end()); + return nanobind::make_iterator(nanobind::type(), + "iterator", self.begin(), + self.end()); }, - py::keep_alive<0, 1>(), "Loop through each term of the operator.") + nanobind::keep_alive<0, 1>(), + "Loop through each term of the operator.") // properties - .def_property_readonly("parameters", - &matrix_op_term::get_parameter_descriptions, - "Returns a dictionary that maps each parameter " - "name to its description.") - .def_property_readonly("degrees", &matrix_op_term::degrees, - "Returns a vector that lists all degrees of " - "freedom that the operator targets. " - "The order of degrees is from smallest to largest " - "and reflects the ordering of " - "the matrix returned by `to_matrix`. " - "Specifically, the indices of a statevector " - "with two qubits are {00, 01, 10, 11}. An " - "ordering of degrees {0, 1} then indicates " - "that a state where the qubit with index 0 equals " - "1 with probability 1 is given by " - "the vector {0., 1., 0., 0.}.") - .def_property_readonly("min_degree", &matrix_op_term::min_degree, - "Returns the smallest index of the degrees of " - "freedom that the operator targets.") - .def_property_readonly("max_degree", &matrix_op_term::max_degree, - "Returns the smallest index of the degrees of " - "freedom that the operator targets.") - .def_property_readonly("ops_count", &matrix_op_term::num_ops, - "Returns the number of operators in the product.") - .def_property_readonly( + .def_prop_ro("parameters", &matrix_op_term::get_parameter_descriptions, + "Returns a dictionary that maps each parameter " + "name to its description.") + .def_prop_ro("degrees", &matrix_op_term::degrees, + "Returns a vector that lists all degrees of " + "freedom that the operator targets. " + "The order of degrees is from smallest to largest " + "and reflects the ordering of " + "the matrix returned by `to_matrix`. " + "Specifically, the indices of a statevector " + "with two qubits are {00, 01, 10, 11}. An " + "ordering of degrees {0, 1} then indicates " + "that a state where the qubit with index 0 equals " + "1 with probability 1 is given by " + "the vector {0., 1., 0., 0.}.") + .def_prop_ro("min_degree", &matrix_op_term::min_degree, + "Returns the smallest index of the degrees of " + "freedom that the operator targets.") + .def_prop_ro("max_degree", &matrix_op_term::max_degree, + "Returns the smallest index of the degrees of " + "freedom that the operator targets.") + .def_prop_ro("ops_count", &matrix_op_term::num_ops, + "Returns the number of operators in the product.") + .def_prop_ro( "term_id", &matrix_op_term::get_term_id, "The term id uniquely identifies the operators and targets (degrees) " "that they act on, " "but does not include information about the coefficient.") - .def_property_readonly( + .def_prop_ro( "coefficient", &matrix_op_term::get_coefficient, "Returns the unevaluated coefficient of the operator. The " "coefficient is a " @@ -386,33 +395,35 @@ void bindMatrixOperator(py::module &mod) { // constructors - .def(py::init<>(), + .def(nanobind::init<>(), "Creates a product operator with constant value 1. The returned " "operator does not target any degrees of freedom but merely " "represents a constant.") - .def(py::init(), py::arg("first_degree"), - py::arg("last_degree"), + .def(nanobind::init(), + nanobind::arg("first_degree"), nanobind::arg("last_degree"), "Creates a product operator that applies an identity operation to " "all degrees of " "freedom in the range [first_degree, last_degree).") - .def(py::init(), + .def(nanobind::init(), "Creates a product operator with the given constant value. " "The returned operator does not target any degrees of freedom.") - .def(py::init>(), + .def(nanobind::init>(), "Creates a product operator with the given " "constant value. The returned operator does not target any degrees " "of freedom.") - .def(py::init([](const scalar_operator &scalar) { - return matrix_op_term() * scalar; - }), - "Creates a product operator with non-constant scalar value.") - .def(py::init(), + .def( + "__init__", + [](matrix_op_term *self, const scalar_operator &scalar) { + new (self) matrix_op_term(matrix_op_term() * scalar); + }, + "Creates a product operator with non-constant scalar value.") + .def(nanobind::init(), "Creates a product operator with the given elementary operator.") - .def(py::init()) - .def(py::init()) - .def(py::init()) - .def(py::init(), py::arg("operator"), - py::arg("size") = 0, + .def(nanobind::init()) + .def(nanobind::init()) + .def(nanobind::init()) + .def(nanobind::init(), + nanobind::arg("operator"), nanobind::arg("size") = 0, "Creates a copy of the given operator and reserves space for " "storing the given " "number of product terms (if a size is provided).") @@ -424,7 +435,7 @@ void bindMatrixOperator(py::module &mod) { // evaluations .def("evaluate_coefficient", &matrix_op_term::evaluate_coefficient, - py::arg("parameters") = parameter_map(), + nanobind::arg("parameters") = parameter_map(), "Returns the evaluated coefficient of the product operator. The " "parameters is a map of parameter names to their concrete, complex " "values.") @@ -435,9 +446,9 @@ void bindMatrixOperator(py::module &mod) { auto cmat = self.to_matrix(dimensions, params, invert_order); return details::cmat_to_numpy(cmat); }, - py::arg("dimensions") = dimension_map(), - py::arg("parameters") = parameter_map(), - py::arg("invert_order") = false, + nanobind::arg("dimensions") = dimension_map(), + nanobind::arg("parameters") = parameter_map(), + nanobind::arg("invert_order") = false, "Returns the matrix representation of the operator." "The matrix is ordered according to the convention (endianness) " "used in CUDA-Q, and the ordering returned by `degrees`. This order " @@ -447,13 +458,13 @@ void bindMatrixOperator(py::module &mod) { .def( "to_matrix", [](const matrix_op_term &self, dimension_map &dimensions, - bool invert_order, const py::kwargs &kwargs) { + bool invert_order, const nanobind::kwargs &kwargs) { auto cmat = self.to_matrix( dimensions, details::kwargs_to_param_map(kwargs), invert_order); return details::cmat_to_numpy(cmat); }, - py::arg("dimensions") = dimension_map(), - py::arg("invert_order") = false, + nanobind::arg("dimensions") = dimension_map(), + nanobind::arg("invert_order") = false, nanobind::arg("kwargs"), "Returns the matrix representation of the operator." "The matrix is ordered according to the convention (endianness) " "used in CUDA-Q, and the ordering returned by `degrees`. This order " @@ -463,7 +474,7 @@ void bindMatrixOperator(py::module &mod) { // comparisons - .def("__eq__", &matrix_op_term::operator==, py::is_operator(), + .def("__eq__", &matrix_op_term::operator==, nanobind::is_operator(), "Return true if the two operators are equivalent. The equivalence " "check takes " "into account that multiplication of operators that act on " @@ -476,71 +487,72 @@ void bindMatrixOperator(py::module &mod) { [](const matrix_op_term &self, const matrix_op &other) { return other.num_terms() == 1 && *other.begin() == self; }, - py::is_operator(), "Return true if the two operators are equivalent.") + nanobind::is_operator(), + "Return true if the two operators are equivalent.") // unary operators - .def(-py::self, py::is_operator()) - .def(+py::self, py::is_operator()) + .def(-nanobind::self, nanobind::is_operator()) + .def(+nanobind::self, nanobind::is_operator()) // in-place arithmetics - .def(py::self /= int(), py::is_operator()) - .def(py::self *= int(), py::is_operator()) - .def(py::self /= double(), py::is_operator()) - .def(py::self *= double(), py::is_operator()) - .def(py::self /= std::complex(), py::is_operator()) - .def(py::self *= std::complex(), py::is_operator()) - .def(py::self /= scalar_operator(), py::is_operator()) - .def(py::self *= scalar_operator(), py::is_operator()) - .def(py::self *= py::self, py::is_operator()) + .def(nanobind::self /= int(), nanobind::is_operator()) + .def(nanobind::self *= int(), nanobind::is_operator()) + .def(nanobind::self /= double(), nanobind::is_operator()) + .def(nanobind::self *= double(), nanobind::is_operator()) + .def(nanobind::self /= std::complex(), nanobind::is_operator()) + .def(nanobind::self *= std::complex(), nanobind::is_operator()) + .def(nanobind::self /= scalar_operator(), nanobind::is_operator()) + .def(nanobind::self *= scalar_operator(), nanobind::is_operator()) + .def(nanobind::self *= nanobind::self, nanobind::is_operator()) // right-hand arithmetics - .def(py::self / int(), py::is_operator()) - .def(py::self * int(), py::is_operator()) - .def(py::self + int(), py::is_operator()) - .def(py::self - int(), py::is_operator()) - .def(py::self / double(), py::is_operator()) - .def(py::self * double(), py::is_operator()) - .def(py::self + double(), py::is_operator()) - .def(py::self - double(), py::is_operator()) - .def(py::self / std::complex(), py::is_operator()) - .def(py::self * std::complex(), py::is_operator()) - .def(py::self + std::complex(), py::is_operator()) - .def(py::self - std::complex(), py::is_operator()) - .def(py::self / scalar_operator(), py::is_operator()) - .def(py::self * scalar_operator(), py::is_operator()) - .def(py::self + scalar_operator(), py::is_operator()) - .def(py::self - scalar_operator(), py::is_operator()) - .def(py::self * py::self, py::is_operator()) - .def(py::self + py::self, py::is_operator()) - .def(py::self - py::self, py::is_operator()) - .def(py::self * matrix_op(), py::is_operator()) - .def(py::self + matrix_op(), py::is_operator()) - .def(py::self - matrix_op(), py::is_operator()) + .def(nanobind::self / int(), nanobind::is_operator()) + .def(nanobind::self * int(), nanobind::is_operator()) + .def(nanobind::self + int(), nanobind::is_operator()) + .def(nanobind::self - int(), nanobind::is_operator()) + .def(nanobind::self / double(), nanobind::is_operator()) + .def(nanobind::self * double(), nanobind::is_operator()) + .def(nanobind::self + double(), nanobind::is_operator()) + .def(nanobind::self - double(), nanobind::is_operator()) + .def(nanobind::self / std::complex(), nanobind::is_operator()) + .def(nanobind::self * std::complex(), nanobind::is_operator()) + .def(nanobind::self + std::complex(), nanobind::is_operator()) + .def(nanobind::self - std::complex(), nanobind::is_operator()) + .def(nanobind::self / scalar_operator(), nanobind::is_operator()) + .def(nanobind::self * scalar_operator(), nanobind::is_operator()) + .def(nanobind::self + scalar_operator(), nanobind::is_operator()) + .def(nanobind::self - scalar_operator(), nanobind::is_operator()) + .def(nanobind::self * nanobind::self, nanobind::is_operator()) + .def(nanobind::self + nanobind::self, nanobind::is_operator()) + .def(nanobind::self - nanobind::self, nanobind::is_operator()) + .def(nanobind::self * matrix_op(), nanobind::is_operator()) + .def(nanobind::self + matrix_op(), nanobind::is_operator()) + .def(nanobind::self - matrix_op(), nanobind::is_operator()) // left-hand arithmetics - .def(int() * py::self, py::is_operator()) - .def(int() + py::self, py::is_operator()) - .def(int() - py::self, py::is_operator()) - .def(double() * py::self, py::is_operator()) - .def(double() + py::self, py::is_operator()) - .def(double() - py::self, py::is_operator()) - .def(std::complex() * py::self, py::is_operator()) - .def(std::complex() + py::self, py::is_operator()) - .def(std::complex() - py::self, py::is_operator()) - .def(scalar_operator() * py::self, py::is_operator()) - .def(scalar_operator() + py::self, py::is_operator()) - .def(scalar_operator() - py::self, py::is_operator()) + .def(int() * nanobind::self, nanobind::is_operator()) + .def(int() + nanobind::self, nanobind::is_operator()) + .def(int() - nanobind::self, nanobind::is_operator()) + .def(double() * nanobind::self, nanobind::is_operator()) + .def(double() + nanobind::self, nanobind::is_operator()) + .def(double() - nanobind::self, nanobind::is_operator()) + .def(std::complex() * nanobind::self, nanobind::is_operator()) + .def(std::complex() + nanobind::self, nanobind::is_operator()) + .def(std::complex() - nanobind::self, nanobind::is_operator()) + .def(scalar_operator() * nanobind::self, nanobind::is_operator()) + .def(scalar_operator() + nanobind::self, nanobind::is_operator()) + .def(scalar_operator() - nanobind::self, nanobind::is_operator()) // general utility functions .def("is_identity", &matrix_op_term::is_identity, "Checks if all operators in the product are the identity. " - "Note: this function returns true regardless of the value of the " - "coefficient.") + "Note that this function returns true regardless of the value of " + "the coefficient.") .def( "__str__", [](const matrix_op_term &self) { return self.to_string(); }, @@ -564,18 +576,18 @@ void bindMatrixOperator(py::module &mod) { "of freedom that are not included in the given set."); } -void bindOperatorsWrapper(py::module &mod) { +void bindOperatorsWrapper(nanobind::module_ &mod) { bindMatrixOperator(mod); - py::implicitly_convertible(); - py::implicitly_convertible, matrix_op_term>(); - py::implicitly_convertible(); - py::implicitly_convertible(); - py::implicitly_convertible(); - py::implicitly_convertible(); - py::implicitly_convertible(); - py::implicitly_convertible(); - py::implicitly_convertible(); - py::implicitly_convertible(); + nanobind::implicitly_convertible(); + nanobind::implicitly_convertible, matrix_op_term>(); + nanobind::implicitly_convertible(); + nanobind::implicitly_convertible(); + nanobind::implicitly_convertible(); + nanobind::implicitly_convertible(); + nanobind::implicitly_convertible(); + nanobind::implicitly_convertible(); + nanobind::implicitly_convertible(); + nanobind::implicitly_convertible(); bindOperatorsModule(mod); } diff --git a/python/runtime/cudaq/operators/py_matrix_op.h b/python/runtime/cudaq/operators/py_matrix_op.h index 97b154b720f..28df05d8efb 100644 --- a/python/runtime/cudaq/operators/py_matrix_op.h +++ b/python/runtime/cudaq/operators/py_matrix_op.h @@ -6,12 +6,10 @@ * the terms of the Apache License 2.0 which accompanies this distribution. * ******************************************************************************/ -#include - -namespace py = pybind11; +#include namespace cudaq { /// @brief Wrapper function for exposing the bindings of matrix /// operators to python. -void bindOperatorsWrapper(py::module &mod); +void bindOperatorsWrapper(nanobind::module_ &mod); } // namespace cudaq diff --git a/python/runtime/cudaq/operators/py_scalar_op.cpp b/python/runtime/cudaq/operators/py_scalar_op.cpp index fc92a836551..1ed437dc316 100644 --- a/python/runtime/cudaq/operators/py_scalar_op.cpp +++ b/python/runtime/cudaq/operators/py_scalar_op.cpp @@ -10,11 +10,15 @@ #include #include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include "cudaq/operators.h" #include "cudaq/operators/serialization.h" @@ -23,47 +27,127 @@ namespace cudaq { -void bindScalarOperator(py::module &mod) { - using scalar_callback = - std::function(const parameter_map &)>; +namespace { + +std::pair, bool> +introspectCallable(const nanobind::callable &func) { + nanobind::module_ inspect = nanobind::module_::import_("inspect"); + nanobind::object argSpec = inspect.attr("getfullargspec")(func); + + if (!argSpec.attr("varargs").is_none()) + throw std::invalid_argument( + "the function defining a scalar operator must not take *args"); + + nanobind::module_ helpers = + nanobind::module_::import_("cudaq.operators.helpers"); + nanobind::object paramDocsFn = helpers.attr("_parameter_docs"); + nanobind::object docstring = func.attr("__doc__"); + + std::unordered_map paramDesc; + for (nanobind::handle name : argSpec.attr("args")) { + std::string n = nanobind::cast(name); + std::string doc = nanobind::cast( + paramDocsFn(nanobind::str(n.c_str()), docstring)); + paramDesc[n] = doc; + } + for (nanobind::handle name : argSpec.attr("kwonlyargs")) { + std::string n = nanobind::cast(name); + std::string doc = nanobind::cast( + paramDocsFn(nanobind::str(n.c_str()), docstring)); + paramDesc[n] = doc; + } + + bool acceptsKwargs = !argSpec.attr("varkw").is_none(); + return {std::move(paramDesc), acceptsKwargs}; +} + +scalar_callback wrapPythonCallable(nanobind::callable func, + const std::vector ¶mNames, + bool acceptsKwargs) { + return [func = std::move(func), paramNames, + acceptsKwargs](const parameter_map ¶ms) -> std::complex { + nanobind::gil_scoped_acquire guard; + nanobind::dict pyKwargs; + if (acceptsKwargs) { + for (const auto &[k, v] : params) + pyKwargs[k.c_str()] = nanobind::cast(v); + } else { + for (const auto &name : paramNames) { + auto it = params.find(name); + if (it != params.end()) + pyKwargs[name.c_str()] = nanobind::cast(it->second); + } + } + nanobind::object result = func(**pyKwargs); + return nanobind::cast>(result); + }; +} + +} // anonymous namespace + +void bindScalarOperator(nanobind::module_ &mod) { - py::class_(mod, "ScalarOperator") + nanobind::class_(mod, "ScalarOperator") // properties - .def_property_readonly("parameters", - &scalar_operator::get_parameter_descriptions, - "Returns a dictionary that maps each parameter " - "name to its description.") + .def_prop_ro("parameters", &scalar_operator::get_parameter_descriptions, + "Returns a dictionary that maps each parameter " + "name to its description.") // constructors - .def(py::init<>(), "Creates a scalar operator with constant value 1.") - .def(py::init(), + .def(nanobind::init<>(), + "Creates a scalar operator with constant value 1.") + .def(nanobind::init(), "Creates a scalar operator with the given constant value.") - .def(py::init>(), + .def(nanobind::init>(), "Creates a scalar operator with the given constant value.") - .def(py::init([](const scalar_callback &func, const py::kwargs &kwargs) { - return scalar_operator( - func, details::kwargs_to_param_description(kwargs)); - }), - py::arg("callback"), - "Creates a scalar operator where the given callback function is " - "invoked during evaluation.") - .def(py::init(), "Copy constructor.") + .def( + "__init__", + [](scalar_operator *self, nanobind::callable func) { + auto [paramDesc, acceptsKwargs] = introspectCallable(func); + std::vector paramNames; + for (const auto &[k, v] : paramDesc) + paramNames.push_back(k); + auto callback = + wrapPythonCallable(std::move(func), paramNames, acceptsKwargs); + new (self) + scalar_operator(std::move(callback), std::move(paramDesc)); + }, + nanobind::arg("generator"), + "Creates a scalar operator from a callable. Parameter names are " + "introspected from the function signature.") + .def( + "__init__", + [](scalar_operator *self, nanobind::callable func, + const nanobind::kwargs &kwargs) { + auto [introspected, acceptsKwargs] = introspectCallable(func); + auto paramDesc = details::kwargs_to_param_description(kwargs); + std::vector paramNames; + for (const auto &[k, v] : paramDesc) + paramNames.push_back(k); + auto callback = + wrapPythonCallable(std::move(func), paramNames, acceptsKwargs); + new (self) + scalar_operator(std::move(callback), std::move(paramDesc)); + }, + "Creates a scalar operator from a callable with keyword argument " + "parameter descriptions.") + .def(nanobind::init(), "Copy constructor.") // evaluations .def( "evaluate", - [](const scalar_operator &self, const py::kwargs &kwargs) { + [](const scalar_operator &self, const nanobind::kwargs &kwargs) { return self.evaluate(details::kwargs_to_param_map(kwargs)); }, "Evaluated value of the operator.") // comparisons - .def("__eq__", &scalar_operator::operator==, py::is_operator()) + .def("__eq__", &scalar_operator::operator==, nanobind::is_operator()) // general utility functions @@ -73,10 +157,10 @@ void bindScalarOperator(py::module &mod) { "Returns the string representation of the operator."); } -void bindScalarWrapper(py::module &mod) { +void bindScalarWrapper(nanobind::module_ &mod) { bindScalarOperator(mod); - py::implicitly_convertible(); - py::implicitly_convertible, scalar_operator>(); + nanobind::implicitly_convertible(); + nanobind::implicitly_convertible, scalar_operator>(); } } // namespace cudaq diff --git a/python/runtime/cudaq/operators/py_scalar_op.h b/python/runtime/cudaq/operators/py_scalar_op.h index 046f23411c6..4197132a60c 100644 --- a/python/runtime/cudaq/operators/py_scalar_op.h +++ b/python/runtime/cudaq/operators/py_scalar_op.h @@ -6,12 +6,10 @@ * the terms of the Apache License 2.0 which accompanies this distribution. * ******************************************************************************/ -#include - -namespace py = pybind11; +#include namespace cudaq { /// @brief Wrapper function for exposing the bindings of `cudaq::spin` /// and `cudaq::spin_op` to python. -void bindScalarWrapper(py::module &mod); +void bindScalarWrapper(nanobind::module_ &mod); } // namespace cudaq diff --git a/python/runtime/cudaq/operators/py_spin_op.cpp b/python/runtime/cudaq/operators/py_spin_op.cpp index 0e35fb1e0d9..e901dcac0cd 100644 --- a/python/runtime/cudaq/operators/py_spin_op.cpp +++ b/python/runtime/cudaq/operators/py_spin_op.cpp @@ -7,10 +7,18 @@ ******************************************************************************/ #include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include "cudaq/operators.h" #include "cudaq/operators/serialization.h" @@ -20,8 +28,8 @@ namespace cudaq { /// @brief Map an OpenFermion operator to our own spin operator -spin_op fromOpenFermionQubitOperator(py::object &op) { - if (!py::hasattr(op, "terms")) +spin_op fromOpenFermionQubitOperator(nanobind::object &op) { + if (!nanobind::hasattr(op, "terms")) throw std::runtime_error( "This is not an openfermion operator, must have 'terms' attribute."); std::map> creatorMap{ @@ -31,18 +39,19 @@ spin_op fromOpenFermionQubitOperator(py::object &op) { auto terms = op.attr("terms"); auto H = spin_op::empty(); for (auto term : terms) { - auto termTuple = term.cast(); + auto termTuple = nanobind::cast(term); auto localTerm = spin_op::identity(); - for (auto &element : termTuple) { - auto casted = element.cast>(); + for (auto element : termTuple) { + auto casted = + nanobind::cast>(element); localTerm *= creatorMap[casted.second](casted.first); } - H += terms[term].cast() * localTerm; + H += nanobind::cast(terms[term]) * localTerm; } return H; } -void bindSpinModule(py::module &mod) { +void bindSpinModule(nanobind::module_ &mod) { // Binding the functions in `cudaq::spin` as `_pycudaq` submodule // so it's accessible directly in the cudaq namespace. auto spin_submodule = mod.def_submodule("spin"); @@ -56,33 +65,35 @@ void bindSpinModule(py::module &mod) { // here for consistency with other operators spin_submodule.def( "identity", [](std::size_t target) { return spin_op::identity(target); }, - py::arg("target"), + nanobind::arg("target"), "Returns an identity operator on the given target index."); spin_submodule.def( "identities", [](std::size_t first, std::size_t last) { return spin_op_term(first, last); }, - py::arg("first"), py::arg("last"), + nanobind::arg("first"), nanobind::arg("last"), "Creates a product operator that applies an identity operation to all " "degrees of " "freedom in the open range [first, last)."); - spin_submodule.def("i", &spin_op::i, py::arg("target"), + spin_submodule.def("i", &spin_op::i, nanobind::arg("target"), "Returns a Pauli I spin operator on the given " "target qubit index."); spin_submodule.def( - "x", &spin_op::x, py::arg("target"), + "x", &spin_op::x, nanobind::arg("target"), "Returns a Pauli X spin operator on the given target qubit index."); spin_submodule.def( - "y", &spin_op::y, py::arg("target"), + "y", &spin_op::y, nanobind::arg("target"), "Returns a Pauli Y spin operator on the given target qubit index."); spin_submodule.def( - "z", &spin_op::z, py::arg("target"), + "z", &spin_op::z, nanobind::arg("target"), "Returns a Pauli Z spin operator on the given target qubit index."); - spin_submodule.def("plus", &spin_op::plus, py::arg("target"), + spin_submodule.def("plus", &spin_op::plus, + nanobind::arg("target"), "Return a sigma plus spin operator on the given " "target qubit index."); - spin_submodule.def("minus", &spin_op::minus, py::arg("target"), + spin_submodule.def("minus", &spin_op::minus, + nanobind::arg("target"), "Return a sigma minus spin operator on the given " "target qubit index."); spin_submodule.def( @@ -115,52 +126,55 @@ void bindSpinModule(py::module &mod) { "degrees of freedom."); } -void bindSpinOperator(py::module &mod) { +void bindSpinOperator(nanobind::module_ &mod) { - auto spin_op_class = py::class_(mod, "SpinOperator"); - auto spin_op_term_class = py::class_(mod, "SpinOperatorTerm"); + auto spin_op_class = nanobind::class_(mod, "SpinOperator"); + auto spin_op_term_class = + nanobind::class_(mod, "SpinOperatorTerm"); spin_op_class .def( "__iter__", [](spin_op &self) { - return py::make_iterator(self.begin(), self.end()); + return nanobind::make_iterator(nanobind::type(), + "iterator", self.begin(), + self.end()); }, - py::keep_alive<0, 1>(), "Loop through each term of the operator.") + nanobind::keep_alive<0, 1>(), + "Loop through each term of the operator.") // properties - .def_property_readonly("parameters", &spin_op::get_parameter_descriptions, - "Returns a dictionary that maps each parameter " - "name to its description.") - .def_property_readonly("degrees", &spin_op::degrees, - "Returns a vector that lists all degrees of " - "freedom that the operator targets. " - "The order of degrees is from smallest to largest " - "and reflects the ordering of " - "the matrix returned by `to_matrix`. " - "Specifically, the indices of a statevector " - "with two qubits are {00, 01, 10, 11}. An " - "ordering of degrees {0, 1} then indicates " - "that a state where the qubit with index 0 equals " - "1 with probability 1 is given by " - "the vector {0., 1., 0., 0.}.") - .def_property_readonly("min_degree", &spin_op::min_degree, - "Returns the smallest index of the degrees of " - "freedom that the operator targets.") - .def_property_readonly("max_degree", &spin_op::max_degree, - "Returns the smallest index of the degrees of " - "freedom that the operator targets.") - .def_property_readonly("term_count", &spin_op::num_terms, - "Returns the number of terms in the operator.") + .def_prop_ro("parameters", &spin_op::get_parameter_descriptions, + "Returns a dictionary that maps each parameter " + "name to its description.") + .def_prop_ro("degrees", &spin_op::degrees, + "Returns a vector that lists all degrees of " + "freedom that the operator targets. " + "The order of degrees is from smallest to largest " + "and reflects the ordering of " + "the matrix returned by `to_matrix`. " + "Specifically, the indices of a statevector " + "with two qubits are {00, 01, 10, 11}. An " + "ordering of degrees {0, 1} then indicates " + "that a state where the qubit with index 0 equals " + "1 with probability 1 is given by " + "the vector {0., 1., 0., 0.}.") + .def_prop_ro("min_degree", &spin_op::min_degree, + "Returns the smallest index of the degrees of " + "freedom that the operator targets.") + .def_prop_ro("max_degree", &spin_op::max_degree, + "Returns the smallest index of the degrees of " + "freedom that the operator targets.") + .def_prop_ro("term_count", &spin_op::num_terms, + "Returns the number of terms in the operator.") // only exists for spin operators - .def_property_readonly( - "qubit_count", &spin_op::num_qubits, - "Return the number of qubits this operator acts on.") + .def_prop_ro("qubit_count", &spin_op::num_qubits, + "Return the number of qubits this operator acts on.") // constructors - .def(py::init<>(), + .def(nanobind::init<>(), "Creates a default instantiated sum. A default instantiated " "sum has no value; it will take a value the first time an " "arithmetic operation " @@ -169,27 +183,31 @@ void bindSpinOperator(py::module &mod) { "identity. To construct a `0` value in the mathematical sense " "(neutral element " "for addition), use `empty()` instead.") - .def(py::init(), py::arg("size"), + .def(nanobind::init(), nanobind::arg("size"), "Creates a sum operator with no terms, reserving " "space for the given number of terms (size).") // NOTE: only supported on spin ops so far - .def(py::init &>(), py::arg("data"), + .def(nanobind::init &>(), nanobind::arg("data"), "Creates an operator based on a serialized data representation.") // NOTE: only supported on spin ops so far - .def(py::init([](const std::string &fileName) { - binary_spin_op_reader reader; - return reader.read(fileName); - }), - "Creates an operator based on a serialized data representation in " - "the given file.") - .def(py::init(), + .def( + "__init__", + [](spin_op *self, const std::string &fileName) { + binary_spin_op_reader reader; + new (self) spin_op(reader.read(fileName)); + }, + "Creates an operator based on a serialized data representation in " + "the given file.") + .def(nanobind::init(), "Creates a sum operator with the given term.") - .def(py::init(), "Copy constructor.") + .def(nanobind::init(), "Copy constructor.") // NOTE: only supported on spin ops - .def(py::init([](py::object obj) { - return fromOpenFermionQubitOperator(obj); - }), - "Convert an OpenFermion operator to a CUDA-Q spin operator.") + .def( + "__init__", + [](spin_op *self, nanobind::object obj) { + new (self) spin_op(fromOpenFermionQubitOperator(obj)); + }, + "Convert an OpenFermion operator to a CUDA-Q spin operator.") .def( "copy", [](const spin_op &self) { return spin_op(self); }, "Creates a copy of the operator.") @@ -200,15 +218,16 @@ void bindSpinOperator(py::module &mod) { .def_static( "from_json", [](const std::string &json_str) { - py::object json = py::module_::import("json"); - auto data = py::list(json.attr("loads")(json_str)); - return spin_op(data.cast>()); + nanobind::object json = nanobind::module_::import_("json"); + auto data = nanobind::list(json.attr("loads")(json_str)); + return spin_op(nanobind::cast>(data)); }, "Convert JSON string ('[d1, d2, d3, ...]') to spin_op") // NOTE: only supported on spin ops .def_static( - "random", &spin_op::random, py::arg("qubit_count"), - py::arg("term_count"), py::arg("seed") = std::random_device{}(), + "random", &spin_op::random, + nanobind::arg("qubit_count"), nanobind::arg("term_count"), + nanobind::arg("seed") = std::random_device{}(), "Return a random spin operator with the given number of terms " "(`term_count`) where each term acts on all targets in the open " "range " @@ -223,9 +242,9 @@ void bindSpinOperator(py::module &mod) { auto cmat = self.to_matrix(dimensions, params, invert_order); return details::cmat_to_numpy(cmat); }, - py::arg("dimensions") = dimension_map(), - py::arg("parameters") = parameter_map(), - py::arg("invert_order") = false, + nanobind::arg("dimensions") = dimension_map(), + nanobind::arg("parameters") = parameter_map(), + nanobind::arg("invert_order") = false, "Returns the matrix representation of the operator." "The matrix is ordered according to the convention (endianness) " "used in CUDA-Q, and the ordering returned by `degrees`. This order " @@ -235,13 +254,13 @@ void bindSpinOperator(py::module &mod) { .def( "to_matrix", [](const spin_op &self, dimension_map &dimensions, bool invert_order, - const py::kwargs &kwargs) { + const nanobind::kwargs &kwargs) { auto cmat = self.to_matrix( dimensions, details::kwargs_to_param_map(kwargs), invert_order); return details::cmat_to_numpy(cmat); }, - py::arg("dimensions") = dimension_map(), - py::arg("invert_order") = false, + nanobind::arg("dimensions") = dimension_map(), + nanobind::arg("invert_order") = false, nanobind::arg("kwargs"), "Returns the matrix representation of the operator." "The matrix is ordered according to the convention (endianness) " "used in CUDA-Q, and the ordering returned by `degrees`. This order " @@ -254,9 +273,9 @@ void bindSpinOperator(py::module &mod) { const parameter_map ¶ms, bool invert_order) { return self.to_sparse_matrix(dimensions, params, invert_order); }, - py::arg("dimensions") = dimension_map(), - py::arg("parameters") = parameter_map(), - py::arg("invert_order") = false, + nanobind::arg("dimensions") = dimension_map(), + nanobind::arg("parameters") = parameter_map(), + nanobind::arg("invert_order") = false, "Return the sparse matrix representation of the operator. This " "representation is a " "`Tuple[list[complex], list[int], list[int]]`, encoding the " @@ -270,12 +289,12 @@ void bindSpinOperator(py::module &mod) { .def( "to_sparse_matrix", [](const spin_op &self, dimension_map &dimensions, bool invert_order, - const py::kwargs &kwargs) { + const nanobind::kwargs &kwargs) { return self.to_sparse_matrix( dimensions, details::kwargs_to_param_map(kwargs), invert_order); }, - py::arg("dimensions") = dimension_map(), - py::arg("invert_order") = false, + nanobind::arg("dimensions") = dimension_map(), + nanobind::arg("invert_order") = false, nanobind::arg("kwargs"), "Return the sparse matrix representation of the operator. This " "representation is a " "`Tuple[list[complex], list[int], list[int]]`, encoding the " @@ -289,7 +308,7 @@ void bindSpinOperator(py::module &mod) { // comparisons - .def("__eq__", &spin_op::operator==, py::is_operator(), + .def("__eq__", &spin_op::operator==, nanobind::is_operator(), "Return true if the two operators are equivalent. The equivalence " "check takes " "commutation relations into account. Operators acting on different " @@ -301,91 +320,91 @@ void bindSpinOperator(py::module &mod) { [](const spin_op &self, const spin_op_term &other) { return self.num_terms() == 1 && *self.begin() == other; }, - py::is_operator(), "Return true if the two operators are equivalent.") + nanobind::is_operator(), + "Return true if the two operators are equivalent.") // unary operators - .def(-py::self, py::is_operator()) - .def(+py::self, py::is_operator()) + .def(-nanobind::self, nanobind::is_operator()) + .def(+nanobind::self, nanobind::is_operator()) // in-place arithmetics - .def(py::self /= int(), py::is_operator()) - .def(py::self *= int(), py::is_operator()) - .def(py::self += int(), py::is_operator()) - .def(py::self -= int(), py::is_operator()) - .def(py::self /= double(), py::is_operator()) - .def(py::self *= double(), py::is_operator()) - .def(py::self += double(), py::is_operator()) - .def(py::self -= double(), py::is_operator()) - .def(py::self /= std::complex(), py::is_operator()) - .def(py::self *= std::complex(), py::is_operator()) - .def(py::self += std::complex(), py::is_operator()) - .def(py::self -= std::complex(), py::is_operator()) - .def(py::self /= scalar_operator(), py::is_operator()) - .def(py::self *= scalar_operator(), py::is_operator()) - .def(py::self += scalar_operator(), py::is_operator()) - .def(py::self -= scalar_operator(), py::is_operator()) - .def(py::self *= spin_op_term(), py::is_operator()) - .def(py::self += spin_op_term(), py::is_operator()) - .def(py::self -= spin_op_term(), py::is_operator()) - .def(py::self *= py::self, py::is_operator()) - .def(py::self += py::self, py::is_operator()) -// see issue https://github.com/pybind/pybind11/issues/1893 + .def(nanobind::self /= int(), nanobind::is_operator()) + .def(nanobind::self *= int(), nanobind::is_operator()) + .def(nanobind::self += int(), nanobind::is_operator()) + .def(nanobind::self -= int(), nanobind::is_operator()) + .def(nanobind::self /= double(), nanobind::is_operator()) + .def(nanobind::self *= double(), nanobind::is_operator()) + .def(nanobind::self += double(), nanobind::is_operator()) + .def(nanobind::self -= double(), nanobind::is_operator()) + .def(nanobind::self /= std::complex(), nanobind::is_operator()) + .def(nanobind::self *= std::complex(), nanobind::is_operator()) + .def(nanobind::self += std::complex(), nanobind::is_operator()) + .def(nanobind::self -= std::complex(), nanobind::is_operator()) + .def(nanobind::self /= scalar_operator(), nanobind::is_operator()) + .def(nanobind::self *= scalar_operator(), nanobind::is_operator()) + .def(nanobind::self += scalar_operator(), nanobind::is_operator()) + .def(nanobind::self -= scalar_operator(), nanobind::is_operator()) + .def(nanobind::self *= spin_op_term(), nanobind::is_operator()) + .def(nanobind::self += spin_op_term(), nanobind::is_operator()) + .def(nanobind::self -= spin_op_term(), nanobind::is_operator()) + .def(nanobind::self *= nanobind::self, nanobind::is_operator()) + .def(nanobind::self += nanobind::self, nanobind::is_operator()) #ifdef __clang__ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wself-assign-overloaded" #endif - .def(py::self -= py::self, py::is_operator()) + .def(nanobind::self -= nanobind::self, nanobind::is_operator()) #ifdef __clang__ #pragma clang diagnostic pop #endif // right-hand arithmetics - .def(py::self / int(), py::is_operator()) - .def(py::self * int(), py::is_operator()) - .def(py::self + int(), py::is_operator()) - .def(py::self - int(), py::is_operator()) - .def(py::self / double(), py::is_operator()) - .def(py::self * double(), py::is_operator()) - .def(py::self + double(), py::is_operator()) - .def(py::self - double(), py::is_operator()) - .def(py::self / std::complex(), py::is_operator()) - .def(py::self * std::complex(), py::is_operator()) - .def(py::self + std::complex(), py::is_operator()) - .def(py::self - std::complex(), py::is_operator()) - .def(py::self / scalar_operator(), py::is_operator()) - .def(py::self * scalar_operator(), py::is_operator()) - .def(py::self + scalar_operator(), py::is_operator()) - .def(py::self - scalar_operator(), py::is_operator()) - .def(py::self * spin_op_term(), py::is_operator()) - .def(py::self + spin_op_term(), py::is_operator()) - .def(py::self - spin_op_term(), py::is_operator()) - .def(py::self * py::self, py::is_operator()) - .def(py::self + py::self, py::is_operator()) - .def(py::self - py::self, py::is_operator()) - .def(py::self * matrix_op_term(), py::is_operator()) - .def(py::self + matrix_op_term(), py::is_operator()) - .def(py::self - matrix_op_term(), py::is_operator()) - .def(py::self * matrix_op(), py::is_operator()) - .def(py::self + matrix_op(), py::is_operator()) - .def(py::self - matrix_op(), py::is_operator()) + .def(nanobind::self / int(), nanobind::is_operator()) + .def(nanobind::self * int(), nanobind::is_operator()) + .def(nanobind::self + int(), nanobind::is_operator()) + .def(nanobind::self - int(), nanobind::is_operator()) + .def(nanobind::self / double(), nanobind::is_operator()) + .def(nanobind::self * double(), nanobind::is_operator()) + .def(nanobind::self + double(), nanobind::is_operator()) + .def(nanobind::self - double(), nanobind::is_operator()) + .def(nanobind::self / std::complex(), nanobind::is_operator()) + .def(nanobind::self * std::complex(), nanobind::is_operator()) + .def(nanobind::self + std::complex(), nanobind::is_operator()) + .def(nanobind::self - std::complex(), nanobind::is_operator()) + .def(nanobind::self / scalar_operator(), nanobind::is_operator()) + .def(nanobind::self * scalar_operator(), nanobind::is_operator()) + .def(nanobind::self + scalar_operator(), nanobind::is_operator()) + .def(nanobind::self - scalar_operator(), nanobind::is_operator()) + .def(nanobind::self * spin_op_term(), nanobind::is_operator()) + .def(nanobind::self + spin_op_term(), nanobind::is_operator()) + .def(nanobind::self - spin_op_term(), nanobind::is_operator()) + .def(nanobind::self * nanobind::self, nanobind::is_operator()) + .def(nanobind::self + nanobind::self, nanobind::is_operator()) + .def(nanobind::self - nanobind::self, nanobind::is_operator()) + .def(nanobind::self * matrix_op_term(), nanobind::is_operator()) + .def(nanobind::self + matrix_op_term(), nanobind::is_operator()) + .def(nanobind::self - matrix_op_term(), nanobind::is_operator()) + .def(nanobind::self * matrix_op(), nanobind::is_operator()) + .def(nanobind::self + matrix_op(), nanobind::is_operator()) + .def(nanobind::self - matrix_op(), nanobind::is_operator()) // left-hand arithmetics - .def(int() * py::self, py::is_operator()) - .def(int() + py::self, py::is_operator()) - .def(int() - py::self, py::is_operator()) - .def(double() * py::self, py::is_operator()) - .def(double() + py::self, py::is_operator()) - .def(double() - py::self, py::is_operator()) - .def(std::complex() * py::self, py::is_operator()) - .def(std::complex() + py::self, py::is_operator()) - .def(std::complex() - py::self, py::is_operator()) - .def(scalar_operator() * py::self, py::is_operator()) - .def(scalar_operator() + py::self, py::is_operator()) - .def(scalar_operator() - py::self, py::is_operator()) + .def(int() * nanobind::self, nanobind::is_operator()) + .def(int() + nanobind::self, nanobind::is_operator()) + .def(int() - nanobind::self, nanobind::is_operator()) + .def(double() * nanobind::self, nanobind::is_operator()) + .def(double() + nanobind::self, nanobind::is_operator()) + .def(double() - nanobind::self, nanobind::is_operator()) + .def(std::complex() * nanobind::self, nanobind::is_operator()) + .def(std::complex() + nanobind::self, nanobind::is_operator()) + .def(std::complex() - nanobind::self, nanobind::is_operator()) + .def(scalar_operator() * nanobind::self, nanobind::is_operator()) + .def(scalar_operator() + nanobind::self, nanobind::is_operator()) + .def(scalar_operator() - nanobind::self, nanobind::is_operator()) // common operators @@ -420,22 +439,22 @@ void bindSpinOperator(py::module &mod) { .def( "to_json", [](const spin_op &self) { - py::object json = py::module_::import("json"); + nanobind::object json = nanobind::module_::import_("json"); auto data = self.get_data_representation(); return json.attr("dumps")(data); }, - "Convert spin_op to JSON string: '[d1, d2, d3, ...]'") - .def("trim", &spin_op::trim, py::arg("tol") = 0.0, - py::arg("parameters") = parameter_map(), + "Convert spin_op to a JSON string, e.g., '[d1, d2, d3, ...]'.") + .def("trim", &spin_op::trim, nanobind::arg("tol") = 0.0, + nanobind::arg("parameters") = parameter_map(), "Removes all terms from the sum for which the absolute value of the " "coefficient is below " "the given tolerance.") .def( "trim", - [](spin_op &self, double tol, const py::kwargs &kwargs) { + [](spin_op &self, double tol, const nanobind::kwargs &kwargs) { return self.trim(tol, details::kwargs_to_param_map(kwargs)); }, - py::arg("tol") = 0.0, + nanobind::arg("tol") = 0.0, nanobind::arg("kwargs"), "Removes all terms from the sum for which the absolute value of the " "coefficient is below " "the given tolerance.") @@ -522,33 +541,37 @@ void bindSpinOperator(py::module &mod) { "(product operator) in future releases.") // constructor for old serialization format .def( - py::init([](const std::vector &data, std::size_t num_qubits) { + "__init__", + [](spin_op *self, const std::vector &data, + std::size_t num_qubits) { PyErr_WarnEx( PyExc_DeprecationWarning, "serialization format changed - use the constructor without a " "size_t argument to create a spin_op from the new format", 1); - return spin_op(data, num_qubits); - }), - py::arg("data"), py::arg("num_qubits"), + new (self) spin_op(data, num_qubits); + }, + nanobind::arg("data"), nanobind::arg("num_qubits"), "Deprecated - use constructor without the `num_qubits` argument " "instead.") // new constructor with deprecation warning provided only for backwards // compatibility (matching the deprecated data constructor for the old // serialization format above) - .def(py::init([](const std::string &fileName, bool legacy) { - binary_spin_op_reader reader; - PyErr_WarnEx( - PyExc_DeprecationWarning, - "overload provided for compatibility with the deprecated " - "serialization format - please migrate to the new format and " - "use the constructor without boolean argument", - 1); - return reader.read(fileName, legacy); - }), - py::arg("filename"), py::arg("legacy"), - "Constructor available for loading deprecated data representations " - "from file - will be removed in future releases.") + .def( + "__init__", + [](spin_op *self, const std::string &fileName, bool legacy) { + binary_spin_op_reader reader; + PyErr_WarnEx( + PyExc_DeprecationWarning, + "overload provided for compatibility with the deprecated " + "serialization format - please migrate to the new format and " + "use the constructor without boolean argument", + 1); + new (self) spin_op(reader.read(fileName, legacy)); + }, + nanobind::arg("filename"), nanobind::arg("legacy"), + "Constructor available for loading deprecated data representations " + "from file - will be removed in future releases.") .def_static( "empty_op", []() { @@ -568,27 +591,28 @@ void bindSpinOperator(py::module &mod) { 1); return self.to_string(print_coefficient); }, - py::arg("print_coefficient") = true, + nanobind::arg("print_coefficient") = true, "Deprecated - use the standard `str` conversion or `get_pauli_word` " "on each term instead.") .def( "for_each_term", - [](spin_op &self, py::function functor) { + [](spin_op &self, nanobind::callable functor) { PyErr_WarnEx(PyExc_DeprecationWarning, "use standard iteration instead", 1); self.for_each_term(functor); }, - py::arg("function"), "Deprecated - use standard iteration instead.") + nanobind::arg("function"), + "Deprecated - use standard iteration instead.") .def( "for_each_pauli", - [](spin_op &self, py::function functor) { + [](spin_op &self, nanobind::callable functor) { PyErr_WarnEx(PyExc_DeprecationWarning, "iterate over the sum to get each term and then " "iterate over the term(s) instead", 1); self.for_each_pauli(functor); }, - py::arg("function"), + nanobind::arg("function"), "Deprecated - iterator over sum and then iterator over term " "instead."); #if (defined(__GNUC__) && !defined(__clang__) && !defined(__INTEL_COMPILER)) @@ -602,49 +626,50 @@ void bindSpinOperator(py::module &mod) { .def( "__iter__", [](spin_op_term &self) { - return py::make_iterator(self.begin(), self.end()); + return nanobind::make_iterator(nanobind::type(), + "iterator", self.begin(), + self.end()); }, - py::keep_alive<0, 1>(), "Loop through each term of the operator.") + nanobind::keep_alive<0, 1>(), + "Loop through each term of the operator.") // properties - .def_property_readonly("parameters", - &spin_op_term::get_parameter_descriptions, - "Returns a dictionary that maps each parameter " - "name to its description.") - .def_property_readonly("degrees", &spin_op_term::degrees, - "Returns a vector that lists all degrees of " - "freedom that the operator targets. " - "The order of degrees is from smallest to largest " - "and reflects the ordering of " - "the matrix returned by `to_matrix`. " - "Specifically, the indices of a statevector " - "with two qubits are {00, 01, 10, 11}. An " - "ordering of degrees {0, 1} then indicates " - "that a state where the qubit with index 0 equals " - "1 with probability 1 is given by " - "the vector {0., 1., 0., 0.}.") - .def_property_readonly("min_degree", &spin_op_term::min_degree, - "Returns the smallest index of the degrees of " - "freedom that the operator targets.") - .def_property_readonly("max_degree", &spin_op_term::max_degree, - "Returns the smallest index of the degrees of " - "freedom that the operator targets.") - .def_property_readonly("ops_count", &spin_op_term::num_ops, - "Returns the number of operators in the product.") - .def_property_readonly( + .def_prop_ro("parameters", &spin_op_term::get_parameter_descriptions, + "Returns a dictionary that maps each parameter " + "name to its description.") + .def_prop_ro("degrees", &spin_op_term::degrees, + "Returns a vector that lists all degrees of " + "freedom that the operator targets. " + "The order of degrees is from smallest to largest " + "and reflects the ordering of " + "the matrix returned by `to_matrix`. " + "Specifically, the indices of a statevector " + "with two qubits are {00, 01, 10, 11}. An " + "ordering of degrees {0, 1} then indicates " + "that a state where the qubit with index 0 equals " + "1 with probability 1 is given by " + "the vector {0., 1., 0., 0.}.") + .def_prop_ro("min_degree", &spin_op_term::min_degree, + "Returns the smallest index of the degrees of " + "freedom that the operator targets.") + .def_prop_ro("max_degree", &spin_op_term::max_degree, + "Returns the smallest index of the degrees of " + "freedom that the operator targets.") + .def_prop_ro("ops_count", &spin_op_term::num_ops, + "Returns the number of operators in the product.") + .def_prop_ro( "term_count", [](const spin_op_term &) { return 1; }, "Returns the number of terms in the operator. Always returns 1.") // only exists for spin operators - .def_property_readonly( - "qubit_count", &spin_op_term::num_qubits, - "Return the number of qubits this operator acts on.") - .def_property_readonly( + .def_prop_ro("qubit_count", &spin_op_term::num_qubits, + "Return the number of qubits this operator acts on.") + .def_prop_ro( "term_id", &spin_op_term::get_term_id, "The term id uniquely identifies the operators and targets (degrees) " "that they act on, " "but does not include information about the coefficient.") - .def_property_readonly( + .def_prop_ro( "coefficient", &spin_op_term::get_coefficient, "Returns the unevaluated coefficient of the operator. The " "coefficient is a " @@ -652,60 +677,66 @@ void bindSpinOperator(py::module &mod) { // constructors - .def(py::init<>(), + .def(nanobind::init<>(), "Creates a product operator with constant value 1. The returned " "operator does not target any degrees of freedom but merely " "represents a constant.") - .def(py::init(), py::arg("first_degree"), - py::arg("last_degree"), + .def(nanobind::init(), + nanobind::arg("first_degree"), nanobind::arg("last_degree"), "Creates a product operator that applies an identity operation to " "all degrees of " "freedom in the range [first_degree, last_degree).") // NOTE: only supported on spin ops so far - .def(py::init([](const std::vector &data) { - spin_op op(data); - if (op.num_terms() != 1) - throw std::runtime_error( - "invalid data representation for product operator"); - return *op.begin(); - }), - py::arg("data"), - "Creates an operator based on a serialized data representation.") + .def( + "__init__", + [](spin_op_term *self, const std::vector &data) { + spin_op op(data); + if (op.num_terms() != 1) + throw std::runtime_error( + "invalid data representation for product operator"); + new (self) spin_op_term(*op.begin()); + }, + nanobind::arg("data"), + "Creates an operator based on a serialized data representation.") // NOTE: only supported on spin ops so far - .def(py::init([](const std::string &fileName) { - binary_spin_op_reader reader; - spin_op op = reader.read(fileName); - if (op.num_terms() != 1) - throw std::runtime_error( - "invalid data representation for product operator"); - return *op.begin(); - }), - "Creates an operator based on a serialized data representation in " - "the given file.") - .def(py::init(), + .def( + "__init__", + [](spin_op_term *self, const std::string &fileName) { + binary_spin_op_reader reader; + spin_op op = reader.read(fileName); + if (op.num_terms() != 1) + throw std::runtime_error( + "invalid data representation for product operator"); + new (self) spin_op_term(*op.begin()); + }, + "Creates an operator based on a serialized data representation in " + "the given file.") + .def(nanobind::init(), "Creates a product operator with the given constant value. " "The returned operator does not target any degrees of freedom.") - .def(py::init>(), + .def(nanobind::init>(), "Creates a product operator with the given " "constant value. The returned operator does not target any degrees " "of freedom.") - .def(py::init([](const scalar_operator &scalar) { - return spin_op_term() * scalar; - }), - "Creates a product operator with non-constant scalar value.") - .def(py::init(), + .def( + "__init__", + [](spin_op_term *self, const scalar_operator &scalar) { + new (self) spin_op_term(spin_op_term() * scalar); + }, + "Creates a product operator with non-constant scalar value.") + .def(nanobind::init(), "Creates a product operator with the given elementary operator.") - .def(py::init(), py::arg("operator"), - py::arg("size") = 0, + .def(nanobind::init(), + nanobind::arg("operator"), nanobind::arg("size") = 0, "Creates a copy of the given operator and reserves space for " "storing the given " "number of product terms (if a size is provided).") .def_static( "from_json", [](const std::string &json_str) { - py::object json = py::module_::import("json"); - auto data = py::list(json.attr("loads")(json_str)); - spin_op op(data.cast>()); + nanobind::object json = nanobind::module_::import_("json"); + auto data = nanobind::list(json.attr("loads")(json_str)); + spin_op op(nanobind::cast>(data)); if (op.num_terms() != 1) throw std::runtime_error( "invalid data representation for product operator"); @@ -719,7 +750,7 @@ void bindSpinOperator(py::module &mod) { // evaluations .def("evaluate_coefficient", &spin_op_term::evaluate_coefficient, - py::arg("parameters") = parameter_map(), + nanobind::arg("parameters") = parameter_map(), "Returns the evaluated coefficient of the product operator. The " "parameters is a map of parameter names to their concrete, complex " "values.") @@ -730,9 +761,9 @@ void bindSpinOperator(py::module &mod) { auto cmat = self.to_matrix(dimensions, params, invert_order); return details::cmat_to_numpy(cmat); }, - py::arg("dimensions") = dimension_map(), - py::arg("parameters") = parameter_map(), - py::arg("invert_order") = false, + nanobind::arg("dimensions") = dimension_map(), + nanobind::arg("parameters") = parameter_map(), + nanobind::arg("invert_order") = false, "Returns the matrix representation of the operator." "The matrix is ordered according to the convention (endianness) " "used in CUDA-Q, and the ordering returned by `degrees`. This order " @@ -742,13 +773,13 @@ void bindSpinOperator(py::module &mod) { .def( "to_matrix", [](const spin_op_term &self, dimension_map &dimensions, - bool invert_order, const py::kwargs &kwargs) { + bool invert_order, const nanobind::kwargs &kwargs) { auto cmat = self.to_matrix( dimensions, details::kwargs_to_param_map(kwargs), invert_order); return details::cmat_to_numpy(cmat); }, - py::arg("dimensions") = dimension_map(), - py::arg("invert_order") = false, + nanobind::arg("dimensions") = dimension_map(), + nanobind::arg("invert_order") = false, nanobind::arg("kwargs"), "Returns the matrix representation of the operator." "The matrix is ordered according to the convention (endianness) " "used in CUDA-Q, and the ordering returned by `degrees`. This order " @@ -761,9 +792,9 @@ void bindSpinOperator(py::module &mod) { const parameter_map ¶ms, bool invert_order) { return self.to_sparse_matrix(dimensions, params, invert_order); }, - py::arg("dimensions") = dimension_map(), - py::arg("parameters") = parameter_map(), - py::arg("invert_order") = false, + nanobind::arg("dimensions") = dimension_map(), + nanobind::arg("parameters") = parameter_map(), + nanobind::arg("invert_order") = false, "Return the sparse matrix representation of the operator. This " "representation is a " "`Tuple[list[complex], list[int], list[int]]`, encoding the " @@ -777,12 +808,12 @@ void bindSpinOperator(py::module &mod) { .def( "to_sparse_matrix", [](const spin_op_term &self, dimension_map &dimensions, - bool invert_order, const py::kwargs &kwargs) { + bool invert_order, const nanobind::kwargs &kwargs) { return self.to_sparse_matrix( dimensions, details::kwargs_to_param_map(kwargs), invert_order); }, - py::arg("dimensions") = dimension_map(), - py::arg("invert_order") = false, + nanobind::arg("dimensions") = dimension_map(), + nanobind::arg("invert_order") = false, nanobind::arg("kwargs"), "Return the sparse matrix representation of the operator. This " "representation is a " "`Tuple[list[complex], list[int], list[int]]`, encoding the " @@ -796,7 +827,7 @@ void bindSpinOperator(py::module &mod) { // comparisons - .def("__eq__", &spin_op_term::operator==, py::is_operator(), + .def("__eq__", &spin_op_term::operator==, nanobind::is_operator(), "Return true if the two operators are equivalent. The equivalence " "check takes " "commutation relations into account. Operators acting on different " @@ -808,77 +839,78 @@ void bindSpinOperator(py::module &mod) { [](const spin_op_term &self, const spin_op &other) { return other.num_terms() == 1 && *other.begin() == self; }, - py::is_operator(), "Return true if the two operators are equivalent.") + nanobind::is_operator(), + "Return true if the two operators are equivalent.") // unary operators - .def(-py::self, py::is_operator()) - .def(+py::self, py::is_operator()) + .def(-nanobind::self, nanobind::is_operator()) + .def(+nanobind::self, nanobind::is_operator()) // in-place arithmetics - .def(py::self /= int(), py::is_operator()) - .def(py::self *= int(), py::is_operator()) - .def(py::self /= double(), py::is_operator()) - .def(py::self *= double(), py::is_operator()) - .def(py::self /= std::complex(), py::is_operator()) - .def(py::self *= std::complex(), py::is_operator()) - .def(py::self /= scalar_operator(), py::is_operator()) - .def(py::self *= scalar_operator(), py::is_operator()) - .def(py::self *= py::self, py::is_operator()) + .def(nanobind::self /= int(), nanobind::is_operator()) + .def(nanobind::self *= int(), nanobind::is_operator()) + .def(nanobind::self /= double(), nanobind::is_operator()) + .def(nanobind::self *= double(), nanobind::is_operator()) + .def(nanobind::self /= std::complex(), nanobind::is_operator()) + .def(nanobind::self *= std::complex(), nanobind::is_operator()) + .def(nanobind::self /= scalar_operator(), nanobind::is_operator()) + .def(nanobind::self *= scalar_operator(), nanobind::is_operator()) + .def(nanobind::self *= nanobind::self, nanobind::is_operator()) // right-hand arithmetics - .def(py::self / int(), py::is_operator()) - .def(py::self * int(), py::is_operator()) - .def(py::self + int(), py::is_operator()) - .def(py::self - int(), py::is_operator()) - .def(py::self / double(), py::is_operator()) - .def(py::self * double(), py::is_operator()) - .def(py::self + double(), py::is_operator()) - .def(py::self - double(), py::is_operator()) - .def(py::self / std::complex(), py::is_operator()) - .def(py::self * std::complex(), py::is_operator()) - .def(py::self + std::complex(), py::is_operator()) - .def(py::self - std::complex(), py::is_operator()) - .def(py::self / scalar_operator(), py::is_operator()) - .def(py::self * scalar_operator(), py::is_operator()) - .def(py::self + scalar_operator(), py::is_operator()) - .def(py::self - scalar_operator(), py::is_operator()) - .def(py::self * py::self, py::is_operator()) - .def(py::self + py::self, py::is_operator()) - .def(py::self - py::self, py::is_operator()) - .def(py::self * spin_op(), py::is_operator()) - .def(py::self + spin_op(), py::is_operator()) - .def(py::self - spin_op(), py::is_operator()) - .def(py::self * matrix_op_term(), py::is_operator()) - .def(py::self + matrix_op_term(), py::is_operator()) - .def(py::self - matrix_op_term(), py::is_operator()) - .def(py::self * matrix_op(), py::is_operator()) - .def(py::self + matrix_op(), py::is_operator()) - .def(py::self - matrix_op(), py::is_operator()) + .def(nanobind::self / int(), nanobind::is_operator()) + .def(nanobind::self * int(), nanobind::is_operator()) + .def(nanobind::self + int(), nanobind::is_operator()) + .def(nanobind::self - int(), nanobind::is_operator()) + .def(nanobind::self / double(), nanobind::is_operator()) + .def(nanobind::self * double(), nanobind::is_operator()) + .def(nanobind::self + double(), nanobind::is_operator()) + .def(nanobind::self - double(), nanobind::is_operator()) + .def(nanobind::self / std::complex(), nanobind::is_operator()) + .def(nanobind::self * std::complex(), nanobind::is_operator()) + .def(nanobind::self + std::complex(), nanobind::is_operator()) + .def(nanobind::self - std::complex(), nanobind::is_operator()) + .def(nanobind::self / scalar_operator(), nanobind::is_operator()) + .def(nanobind::self * scalar_operator(), nanobind::is_operator()) + .def(nanobind::self + scalar_operator(), nanobind::is_operator()) + .def(nanobind::self - scalar_operator(), nanobind::is_operator()) + .def(nanobind::self * nanobind::self, nanobind::is_operator()) + .def(nanobind::self + nanobind::self, nanobind::is_operator()) + .def(nanobind::self - nanobind::self, nanobind::is_operator()) + .def(nanobind::self * spin_op(), nanobind::is_operator()) + .def(nanobind::self + spin_op(), nanobind::is_operator()) + .def(nanobind::self - spin_op(), nanobind::is_operator()) + .def(nanobind::self * matrix_op_term(), nanobind::is_operator()) + .def(nanobind::self + matrix_op_term(), nanobind::is_operator()) + .def(nanobind::self - matrix_op_term(), nanobind::is_operator()) + .def(nanobind::self * matrix_op(), nanobind::is_operator()) + .def(nanobind::self + matrix_op(), nanobind::is_operator()) + .def(nanobind::self - matrix_op(), nanobind::is_operator()) // left-hand arithmetics - .def(int() * py::self, py::is_operator()) - .def(int() + py::self, py::is_operator()) - .def(int() - py::self, py::is_operator()) - .def(double() * py::self, py::is_operator()) - .def(double() + py::self, py::is_operator()) - .def(double() - py::self, py::is_operator()) - .def(std::complex() * py::self, py::is_operator()) - .def(std::complex() + py::self, py::is_operator()) - .def(std::complex() - py::self, py::is_operator()) - .def(scalar_operator() * py::self, py::is_operator()) - .def(scalar_operator() + py::self, py::is_operator()) - .def(scalar_operator() - py::self, py::is_operator()) + .def(int() * nanobind::self, nanobind::is_operator()) + .def(int() + nanobind::self, nanobind::is_operator()) + .def(int() - nanobind::self, nanobind::is_operator()) + .def(double() * nanobind::self, nanobind::is_operator()) + .def(double() + nanobind::self, nanobind::is_operator()) + .def(double() - nanobind::self, nanobind::is_operator()) + .def(std::complex() * nanobind::self, nanobind::is_operator()) + .def(std::complex() + nanobind::self, nanobind::is_operator()) + .def(std::complex() - nanobind::self, nanobind::is_operator()) + .def(scalar_operator() * nanobind::self, nanobind::is_operator()) + .def(scalar_operator() + nanobind::self, nanobind::is_operator()) + .def(scalar_operator() - nanobind::self, nanobind::is_operator()) // general utility functions .def("is_identity", &spin_op_term::is_identity, "Checks if all operators in the product are the identity. " - "Note: this function returns true regardless of the value of the " - "coefficient.") + "Note that this function returns true regardless of the value of " + "the coefficient.") .def( "__str__", [](const spin_op_term &self) { return self.to_string(); }, "Returns the string representation of the operator.") @@ -896,18 +928,18 @@ void bindSpinOperator(py::module &mod) { .def( "to_json", [](const spin_op_term &self) { - py::object json = py::module_::import("json"); + nanobind::object json = nanobind::module_::import_("json"); auto data = spin_op(self).get_data_representation(); return json.attr("dumps")(data); }, - "Convert spin_op to JSON string: '[d1, d2, d3, ...]'") + "Convert spin_op to a JSON string, e.g., '[d1, d2, d3, ...]'.") // only exists for spin operators .def( "get_pauli_word", [](spin_op_term &op, std::size_t pad_identities) { return op.get_pauli_word(pad_identities); }, - py::arg("pad_identities") = 0, + nanobind::arg("pad_identities") = 0, "Gets the Pauli word representation of this product operator.") // only exists for spin operators .def("get_binary_symplectic_form", @@ -973,7 +1005,7 @@ void bindSpinOperator(py::module &mod) { 1); return self.to_string(print_coefficient); }, - py::arg("print_coefficient") = true, + nanobind::arg("print_coefficient") = true, "Deprecated - use the standard `str` conversion or use " "`get_pauli_word` instead.") .def( @@ -985,18 +1017,19 @@ void bindSpinOperator(py::module &mod) { 1); return spin_op(op).distribute_terms(chunks); }, - py::arg("chunk_count"), + nanobind::arg("chunk_count"), "Deprecated - instantiate a `SpinOperator` from this " "`SpinOperatorTerm` " "and call distribute_terms on that.") .def( "for_each_pauli", - [](spin_op_term &self, py::function functor) { + [](spin_op_term &self, nanobind::callable functor) { PyErr_WarnEx(PyExc_DeprecationWarning, "use standard iteration instead", 1); spin_op(self).for_each_pauli(functor); }, - py::arg("function"), "Deprecated - use standard iteration instead."); + nanobind::arg("function"), + "Deprecated - use standard iteration instead."); #if (defined(__GNUC__) && !defined(__clang__) && !defined(__INTEL_COMPILER)) #pragma GCC diagnostic pop #endif @@ -1005,12 +1038,12 @@ void bindSpinOperator(py::module &mod) { #endif } -void bindSpinWrapper(py::module &mod) { +void bindSpinWrapper(nanobind::module_ &mod) { bindSpinOperator(mod); - py::implicitly_convertible(); - py::implicitly_convertible, spin_op_term>(); - py::implicitly_convertible(); - py::implicitly_convertible(); + nanobind::implicitly_convertible(); + nanobind::implicitly_convertible, spin_op_term>(); + nanobind::implicitly_convertible(); + nanobind::implicitly_convertible(); bindSpinModule(mod); } diff --git a/python/runtime/cudaq/operators/py_spin_op.h b/python/runtime/cudaq/operators/py_spin_op.h index 592458ca681..3d0b7df7a8b 100644 --- a/python/runtime/cudaq/operators/py_spin_op.h +++ b/python/runtime/cudaq/operators/py_spin_op.h @@ -6,12 +6,10 @@ * the terms of the Apache License 2.0 which accompanies this distribution. * ******************************************************************************/ -#include - -namespace py = pybind11; +#include namespace cudaq { /// @brief Wrapper function for exposing the bindings of `cudaq::spin` /// and `cudaq::spin_op` to python. -void bindSpinWrapper(py::module &mod); +void bindSpinWrapper(nanobind::module_ &mod); } // namespace cudaq diff --git a/python/runtime/cudaq/operators/py_super_op.cpp b/python/runtime/cudaq/operators/py_super_op.cpp index 730064dbb72..2c18dfbc820 100644 --- a/python/runtime/cudaq/operators/py_super_op.cpp +++ b/python/runtime/cudaq/operators/py_super_op.cpp @@ -7,10 +7,14 @@ ******************************************************************************/ #include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include #include "cudaq/operators.h" #include "py_helpers.h" @@ -18,53 +22,54 @@ namespace cudaq { -void bindSuperOperatorWrapper(py::module &mod) { - auto super_op_class = py::class_(mod, "SuperOperator"); +void bindSuperOperatorWrapper(nanobind::module_ &mod) { + auto super_op_class = nanobind::class_(mod, "SuperOperator"); super_op_class - .def(py::init<>(), "Creates a default instantiated super-operator. A " - "default instantiated " - "super-operator means a no action linear map.") - .def_static( - "left_multiply", - py::overload_cast &>( - &super_op::left_multiply), - "Creates a super-operator representing a left " - "multiplication of the operator to the density matrix.") - .def_static( - "right_multiply", - py::overload_cast &>( - &super_op::right_multiply), - "Creates a super-operator representing a right " - "multiplication of the operator to the density matrix.") - .def_static( - "left_right_multiply", - py::overload_cast &, - const cudaq::product_op &>( - &super_op::left_right_multiply), - "Creates a super-operator representing a simultaneous left " - "multiplication of the first operator operand and right " - "multiplication of the second operator operand to the " - "density matrix.") + .def(nanobind::init<>(), + "Creates a default instantiated super-operator. A " + "default instantiated " + "super-operator means a no action linear map.") + .def_static("left_multiply", + nanobind::overload_cast< + const cudaq::product_op &>( + &super_op::left_multiply), + "Creates a super-operator representing a left " + "multiplication of the operator to the density matrix.") + .def_static("right_multiply", + nanobind::overload_cast< + const cudaq::product_op &>( + &super_op::right_multiply), + "Creates a super-operator representing a right " + "multiplication of the operator to the density matrix.") + .def_static("left_right_multiply", + nanobind::overload_cast< + const cudaq::product_op &, + const cudaq::product_op &>( + &super_op::left_right_multiply), + "Creates a super-operator representing a simultaneous left " + "multiplication of the first operator operand and right " + "multiplication of the second operator operand to the " + "density matrix.") .def_static( "left_multiply", - py::overload_cast &>( + nanobind::overload_cast &>( &super_op::left_multiply), "Creates a super-operator representing a left " "multiplication of the operator to the density matrix. The sum is " "distributed into a linear combination of super-operator actions.") .def_static( "right_multiply", - py::overload_cast &>( + nanobind::overload_cast &>( &super_op::right_multiply), "Creates a super-operator representing a right " "multiplication of the operator to the density matrix. The sum is " "distributed into a linear combination of super-operator actions.") .def_static( "left_right_multiply", - py::overload_cast &, - const cudaq::sum_op &>( + nanobind::overload_cast &, + const cudaq::sum_op &>( &super_op::left_right_multiply), "Creates a super-operator representing a simultaneous left " "multiplication of the first operator operand and right " @@ -74,11 +79,13 @@ void bindSuperOperatorWrapper(py::module &mod) { .def( "__iter__", [](super_op &self) { - return py::make_iterator(self.begin(), self.end()); + return nanobind::make_iterator(nanobind::type(), + "iterator", self.begin(), + self.end()); }, - py::keep_alive<0, 1>(), + nanobind::keep_alive<0, 1>(), "Loop through each term of the super-operator.") - .def(py::self += py::self, py::is_operator()); + .def(nanobind::self += nanobind::self, nanobind::is_operator()); } } // namespace cudaq diff --git a/python/runtime/cudaq/operators/py_super_op.h b/python/runtime/cudaq/operators/py_super_op.h index 32474d65639..da8c5e3ea3a 100644 --- a/python/runtime/cudaq/operators/py_super_op.h +++ b/python/runtime/cudaq/operators/py_super_op.h @@ -6,12 +6,10 @@ * the terms of the Apache License 2.0 which accompanies this distribution. * ******************************************************************************/ -#include - -namespace py = pybind11; +#include namespace cudaq { /// @brief Wrapper function for exposing the bindings of super-operator to /// python. -void bindSuperOperatorWrapper(py::module &mod); +void bindSuperOperatorWrapper(nanobind::module_ &mod); } // namespace cudaq diff --git a/python/runtime/cudaq/platform/py_alt_launch_kernel.cpp b/python/runtime/cudaq/platform/py_alt_launch_kernel.cpp index 9cf74e898c0..eb2dd7f63d5 100644 --- a/python/runtime/cudaq/platform/py_alt_launch_kernel.cpp +++ b/python/runtime/cudaq/platform/py_alt_launch_kernel.cpp @@ -9,7 +9,6 @@ #include "py_alt_launch_kernel.h" #include "common/AnalogHamiltonian.h" #include "common/ArgumentWrapper.h" -#include "common/CompiledModule.h" #include "common/Environment.h" #include "cudaq/Optimizer/Builder/Marshal.h" #include "cudaq/Optimizer/Builder/Runtime.h" @@ -24,6 +23,7 @@ #include "cudaq_internal/compiler/LayoutInfo.h" #include "runtime/cudaq/algorithms/py_utils.h" #include "utils/LinkedLibraryHolder.h" +#include "utils/NanobindAdaptors.h" #include "utils/OpaqueArguments.h" #include "utils/PyTypes.h" #include "llvm/MC/SubtargetFeature.h" @@ -31,7 +31,6 @@ #include "llvm/Support/Error.h" #include "llvm/Support/Host.h" #include "llvm/Target/TargetMachine.h" -#include "mlir/Bindings/Python/PybindAdaptors.h" #include "mlir/CAPI/ExecutionEngine.h" #include "mlir/Dialect/Func/IR/FuncOps.h" #include "mlir/ExecutionEngine/OptUtils.h" @@ -42,13 +41,20 @@ #include "mlir/Target/LLVMIR/Export.h" #include "mlir/Transforms/Passes.h" #include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include -namespace py = pybind11; using namespace mlir; using namespace cudaq_internal::compiler; using cudaq::JitEngine; +using cudaq::PackingStyle; static std::function getTransportLayer = []() -> std::string { throw std::runtime_error("binding for kernel launch is incomplete"); @@ -128,53 +134,55 @@ void cudaq::setDataLayout(MlirModule module) { // The section is the implementation of functions declared in OpaqueArguments.h //===----------------------------------------------------------------------===// -py::args cudaq::simplifiedValidateInputArguments(py::args &args) { - py::args processed = py::tuple(args.size()); +nanobind::args cudaq::simplifiedValidateInputArguments(nanobind::args &args) { + nanobind::args processed = + nanobind::steal(PyTuple_New((Py_ssize_t)args.size())); for (std::size_t i = 0; i < args.size(); ++i) { - auto arg = args[i]; + nanobind::object arg = nanobind::borrow(args[i]); // Check if it has tolist, so it might be a 1d buffer (array / numpy // ndarray) - if (py::hasattr(args[i], "tolist")) { + if (nanobind::hasattr(args[i], "tolist")) { // This is a valid ndarray if it has tolist and shape - if (!py::hasattr(args[i], "shape")) + if (!nanobind::hasattr(args[i], "shape")) throw std::runtime_error( "Invalid input argument type, could not get shape of array."); // This is an ndarray with tolist() and shape attributes // get the shape and check its size - auto shape = args[i].attr("shape").cast(); + auto shape = nanobind::cast(args[i].attr("shape")); if (shape.size() != 1) throw std::runtime_error("Cannot pass ndarray with shape != (N,)."); arg = args[i].attr("tolist")(); - } else if (py::isinstance(arg)) { - arg = py::cast(arg); - } else if (py::isinstance(arg)) { - py::list arg_list = py::cast(arg); + } else if (nanobind::isinstance(arg)) { + arg = nanobind::cast(nanobind::cast(arg)); + } else if (nanobind::isinstance(arg)) { + nanobind::list arg_list = nanobind::cast(arg); const bool all_strings = [&]() { - for (auto &item : arg_list) - if (!py::isinstance(item)) + for (auto item : arg_list) + if (!nanobind::isinstance(item)) return false; return true; }(); if (all_strings) { std::vector pw_list; pw_list.reserve(arg_list.size()); - for (auto &item : arg_list) - pw_list.emplace_back(py::cast(item)); - arg = std::move(pw_list); + for (auto item : arg_list) + pw_list.emplace_back(nanobind::cast(item)); + arg = nanobind::cast(std::move(pw_list)); } } - processed[i] = arg; + PyTuple_SET_ITEM(processed.ptr(), (Py_ssize_t)i, arg.inc_ref().ptr()); } return processed; } +template void cudaq::handleStructMemberVariable(void *data, std::size_t offset, mlir::Type memberType, - py::object value) { + nanobind::object value) { auto appendValue = [](void *data, auto &&value, std::size_t offset) { std::memcpy(((char *)data) + offset, &value, sizeof(std::remove_cvref_t)); @@ -182,30 +190,37 @@ void cudaq::handleStructMemberVariable(void *data, std::size_t offset, llvm::TypeSwitch(memberType) .Case([&](mlir::IntegerType ty) { if (ty.isInteger(1)) { - appendValue(data, (bool)value.cast(), offset); + appendValue(data, nanobind::cast(value), offset); return; } - appendValue(data, (std::int64_t)value.cast(), offset); + appendValue(data, nanobind::cast(value), offset); }) .Case([&](mlir::Float64Type ty) { - appendValue(data, (double)value.cast(), offset); + appendValue(data, nanobind::cast(value), offset); }) .Case([&](cudaq::cc::StdvecType ty) { - auto appendVectorValue = [](py::object value, void *data, - std::size_t offset, T) { - auto asList = value.cast(); + auto appendVectorValue = [](nanobind::object value, + void *data, std::size_t offset, + T) { + auto asList = nanobind::cast(value); // Use the correct element type T (not always double). auto *values = new std::vector(asList.size()); - for (std::size_t i = 0; auto &v : asList) - (*values)[i++] = v.cast(); - - std::memcpy(((char *)data) + offset, values, 16); + for (std::size_t i = 0; auto v : asList) + (*values)[i++] = nanobind::cast(v); + + // synthesis path: span {ptr, size_t} + // argsCreator path: std::vector {ptr, ptr, ptr} + constexpr std::size_t copySize = + sizeof(std::conditional_t