diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..05fbdeb --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,33 @@ +# Read the Docs configuration file for Sphinx projects +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the OS, Python version and other tools you might need +build: + os: ubuntu-24.04 + tools: + python: "3.13" + + +# Build documentation in the "doc/" directory with Sphinx +sphinx: + configuration: docs/conf.py + + +# Optionally build your docs in additional formats such as PDF and ePub +formats: + - pdf + - epub + +# Optional but recommended, declare the Python requirements required +# to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: docs/requirements.txt + # This installs the package qiskit-calculquebec to ensure the code is available for autodoc reponsible for + # generating the API reference documentation. + - method: pip + path: . \ No newline at end of file diff --git a/docs/FR/mitigation_exemple.ipynb b/docs/FR/mitigation_exemple.ipynb index 67336a3..ed1b500 100644 --- a/docs/FR/mitigation_exemple.ipynb +++ b/docs/FR/mitigation_exemple.ipynb @@ -70,7 +70,7 @@ " project_id=os.getenv(\"PROJECT_ID\"),\n", ")\n", "backend = MonarQBackend(\"yukon\", my_client)\n", - "shots = 1000" + "shots = 1000" ] }, { @@ -111,8 +111,7 @@ "# Qubits physiques après transpilation\n", "if transpiled_qc.layout and transpiled_qc.layout.final_layout:\n", " physical_qubits = [\n", - " transpiled_qc.layout.final_layout[q]\n", - " for q in transpiled_qc.qubits\n", + " transpiled_qc.layout.final_layout[q] for q in transpiled_qc.qubits\n", " ]\n", "else:\n", " physical_qubits = list(range(3))\n", @@ -120,8 +119,8 @@ "print(f\"Qubits physiques : {physical_qubits}\")\n", "\n", "# Exécution brute (référence)\n", - "sampler = Sampler(mode=backend)\n", - "job = sampler.run([transpiled_qc], shots=shots)\n", + "sampler = Sampler(mode=backend)\n", + "job = sampler.run([transpiled_qc], shots=shots)\n", "counts_raw = job.result()[0].data.meas.get_counts()\n", "print(f\"Counts bruts : {counts_raw}\")" ] @@ -167,21 +166,23 @@ } ], "source": [ - "rem_matrix = ReadoutMitigation(backend, method='matrix')\n", + "rem_matrix = ReadoutMitigation(backend, method=\"matrix\")\n", "rem_matrix.cals_from_system()\n", "\n", "# Afficher les fidélités\n", "print(\"Fidélités de lecture (method='matrix') :\")\n", "for q, info in zip(range(backend.target.num_qubits), rem_matrix.readout_fidelity()):\n", " if info is not None:\n", - " print(f\" qubit {q:2d} : P(0|0)={info['p00']:.4f} P(1|1)={info['p11']:.4f} moy={info['mean']:.4f}\")\n", + " print(\n", + " f\" qubit {q:2d} : P(0|0)={info['p00']:.4f} P(1|1)={info['p11']:.4f} moy={info['mean']:.4f}\"\n", + " )\n", "\n", "# Correction\n", "counts_rem_matrix = rem_matrix.apply_correction(counts_raw, qubits=physical_qubits)\n", "print(f\"\\nCounts bruts : {counts_raw}\")\n", "print(f\"Counts mitigés (matrix) : {counts_rem_matrix}\")\n", "\n", - "#plot_histogram([counts_raw,counts_rem_matrix], legend=[\"raw\",\"mitig\"])" + "# plot_histogram([counts_raw,counts_rem_matrix], legend=[\"raw\",\"mitig\"])" ] }, { @@ -210,14 +211,22 @@ "\n", "fig, ax = plt.subplots(figsize=(7, 5))\n", "im = ax.imshow(inv_A, cmap=\"RdBu\", vmin=-1, vmax=1)\n", - "ax.set_xticks(range(2**n)); ax.set_yticks(range(2**n))\n", + "ax.set_xticks(range(2**n))\n", + "ax.set_yticks(range(2**n))\n", "ax.set_xticklabels(labels, rotation=45, ha=\"right\")\n", "ax.set_yticklabels(labels)\n", "ax.set_title(f\"Matrice inverse de confusion ({2**n}×{2**n})\")\n", "for i in range(2**n):\n", " for j in range(2**n):\n", - " ax.text(j, i, f\"{inv_A[i,j]:.2f}\", ha=\"center\", va=\"center\", fontsize=7,\n", - " color=\"black\" if abs(inv_A[i,j]) < 0.5 else \"white\")\n", + " ax.text(\n", + " j,\n", + " i,\n", + " f\"{inv_A[i,j]:.2f}\",\n", + " ha=\"center\",\n", + " va=\"center\",\n", + " fontsize=7,\n", + " color=\"black\" if abs(inv_A[i, j]) < 0.5 else \"white\",\n", + " )\n", "plt.colorbar(im)\n", "plt.tight_layout()\n", "plt.show()" @@ -249,7 +258,7 @@ } ], "source": [ - "rem_m3 = ReadoutMitigation(backend, method='m3')\n", + "rem_m3 = ReadoutMitigation(backend, method=\"m3\")\n", "rem_m3.cals_from_system()\n", "\n", "# Correction M3\n", @@ -446,11 +455,19 @@ "from mitiq.zne.scaling import fold_global\n", "\n", "configs = [\n", - " (\"Richardson [1,2,3] + fold_random\", RichardsonFactory([1.0, 2.0, 3.0]), None),\n", - " (\"Richardson [1,3,5] + fold_random\", RichardsonFactory([1.0, 3.0, 5.0]), None),\n", - " (\"Linear [1,1.5,2,2.5,3] + fold_random\", LinearFactory([1.0, 1.5, 2.0, 2.5, 3.0]), None),\n", - " (\"Richardson [1,2,3] + fold_global\", RichardsonFactory([1.0, 2.0, 3.0]), fold_global),\n", - " (\"Linear [1,2,3] + fold_global\", LinearFactory([1.0, 2.0, 3.0]), fold_global),\n", + " (\"Richardson [1,2,3] + fold_random\", RichardsonFactory([1.0, 2.0, 3.0]), None),\n", + " (\"Richardson [1,3,5] + fold_random\", RichardsonFactory([1.0, 3.0, 5.0]), None),\n", + " (\n", + " \"Linear [1,1.5,2,2.5,3] + fold_random\",\n", + " LinearFactory([1.0, 1.5, 2.0, 2.5, 3.0]),\n", + " None,\n", + " ),\n", + " (\n", + " \"Richardson [1,2,3] + fold_global\",\n", + " RichardsonFactory([1.0, 2.0, 3.0]),\n", + " fold_global,\n", + " ),\n", + " (\"Linear [1,2,3] + fold_global\", LinearFactory([1.0, 2.0, 3.0]), fold_global),\n", "]\n", "\n", "print(f\"{'Config':50s} {'Mitigé':>8} {'Erreur':>8}\")\n", @@ -585,12 +602,14 @@ " qc.h(0)\n", " qc.cx(0, 1)\n", " for _ in range(depth):\n", - " qc.id(0); qc.id(1)\n", + " qc.id(0)\n", + " qc.id(1)\n", " qc.cx(0, 1)\n", " qc.h(0)\n", " qc.measure(range(n), range(n))\n", " return qc\n", "\n", + "\n", "DEPTHS = [4, 10, 30, 50]\n", "results_ddd = {d: {} for d in DEPTHS}\n", "\n", @@ -602,7 +621,9 @@ " raw = ddd.run_unmitigated(circuit)\n", " mit = ddd.run(circuit)\n", " results_ddd[depth][rule] = mit\n", - " print(f\" rule={rule:<5} : brut={raw:.4f} DDD={mit:.4f} err={abs(1.0-mit):.4f}\")" + " print(\n", + " f\" rule={rule:<5} : brut={raw:.4f} DDD={mit:.4f} err={abs(1.0-mit):.4f}\"\n", + " )" ] }, { @@ -619,7 +640,7 @@ "outputs": [], "source": [ "# Initialiser REM une seule fois\n", - "rem = ReadoutMitigation(backend, method='m3')\n", + "rem = ReadoutMitigation(backend, method=\"m3\")\n", "rem.cals_from_system()\n", "\n", "# Récupérer les qubits physiques du circuit idle\n", @@ -631,11 +652,11 @@ "else:\n", " phys = list(range(circuit_idle.num_qubits))\n", "\n", - "ddd_rem = DDDMitigation(backend, rule='xyxy', num_trials=3)\n", + "ddd_rem = DDDMitigation(backend, rule=\"xyxy\", num_trials=3)\n", "\n", - "val_raw = ddd_rem.run_unmitigated(circuit_idle)\n", - "val_ddd = ddd_rem.run(circuit_idle)\n", - "val_ddd_rem = ddd_rem.run(circuit_idle, rem=rem, qubits=phys)\n", + "val_raw = ddd_rem.run_unmitigated(circuit_idle)\n", + "val_ddd = ddd_rem.run(circuit_idle)\n", + "val_ddd_rem = ddd_rem.run(circuit_idle, rem=rem, qubits=phys)\n", "\n", "print(f\"Brut : {val_raw:.4f} (err={abs(1.0-val_raw):.4f})\")\n", "print(f\"DDD xyxy : {val_ddd:.4f} (err={abs(1.0-val_ddd):.4f})\")\n", @@ -695,8 +716,8 @@ "source": [ "pt_zne = PauliTwirlingMitigation(backend, num_variants=10)\n", "\n", - "raw = pt_zne.run_unmitigated(qc_pt)\n", - "pt_only = pt_zne.run(qc_pt)\n", + "raw = pt_zne.run_unmitigated(qc_pt)\n", + "pt_only = pt_zne.run(qc_pt)\n", "pt_zne_val = pt_zne.run_with_zne(qc_pt)\n", "\n", "print(f\"Brut : {raw:.4f}\")\n", @@ -705,6 +726,7 @@ "\n", "# Avec factory personnalisée\n", "from mitiq.zne.inference import RichardsonFactory\n", + "\n", "val_rich = pt_zne.run_with_zne(\n", " qc_pt,\n", " factory=RichardsonFactory([1.0, 2.0, 3.0]),\n", @@ -731,12 +753,14 @@ "# Résumé : P(|000⟩) pour le circuit GHZ 3 qubits (idéal = 0.5)\n", "ideal = 0.5\n", "\n", + "\n", "def p000_from_counts(counts, n_shots=shots):\n", " return counts.get(\"000\", 0) / n_shots\n", "\n", - "raw_p = p000_from_counts(counts_raw)\n", + "\n", + "raw_p = p000_from_counts(counts_raw)\n", "rem_matrix_p = p000_from_counts(counts_rem_matrix)\n", - "rem_m3_p = p000_from_counts(counts_rem_m3)\n", + "rem_m3_p = p000_from_counts(counts_rem_m3)\n", "\n", "print(f\"{'Technique':<30} {'P(|000⟩)':>10} {'Erreur':>8}\")\n", "print(\"-\" * 52)\n", diff --git a/docs/FR/prise_en_main.ipynb b/docs/FR/prise_en_main.ipynb index b28666b..e45623e 100644 --- a/docs/FR/prise_en_main.ipynb +++ b/docs/FR/prise_en_main.ipynb @@ -1,182 +1,182 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Monarq Backend\n", - "\n", - "Ce document a pour objectif d'expliquer ce dont vous avez besoin pour utiliser MonarqBackend.\n", - "\n", - "Ce \"backend\" vous permet de communiquer directement avec MonarQ.\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "vscode": { - "languageId": "plaintext" + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Monarq Backend\n", + "\n", + "Ce document a pour objectif d'expliquer ce dont vous avez besoin pour utiliser MonarqBackend.\n", + "\n", + "Ce \"backend\" vous permet de communiquer directement avec MonarQ.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "vscode": { + "languageId": "plaintext" + } + }, + "source": [ + "## Usage typique\n", + "\n", + "Voici la manière typique d'utiliser le device ``MonarQBackend`` :" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "0. Tout d'abord, assurez-vous d'avoir la version Python ```3.10.X``` d'installée." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "1. Installez le plugin (vous pouvez vous référer au README)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "2. Importez les dépendances :" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Cette ligne importe la librairie Qiskit. Elle contient tout ce dont vous avez besoin pour la programmation quantique.\n", + "import qiskit\n", + "\n", + "# Cette ligne importe la classe client, nécessaire pour s'authentifier sur MonarQ\n", + "from qiskit_calculquebec.API.client import MonarqClient\n", + "\n", + "# Cette ligne importe la classe MonarQBackend\n", + "from qiskit_calculquebec.backends import MonarQBackend" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "3. Créez un [client](https://github.com/calculquebec/qiskit-calculquebec/blob/main/doc/for_developers/using_client.ipynb) pour votre device." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Voilà comment créer un client. Changez les valeurs dans la parenthèse pour vos identifiants\n", + "# Assurez vous d'avoir créé un projet, et d'utiliser le nom exact de ce dernier\n", + "my_client = MonarqClient(\"your host\", \"your user\", \"your access token\", \"your project\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "4. Créez un device en utilisant votre objet client" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Il y a 1 argument obligatoire pour le device : \n", + "- le client\n", + "\n", + "Il y a 1 argument optionnel : \n", + "- Le nom de la machine: \"yukon\" ou \"monarq\" (\"monarq\" est par défaut)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Créer le backend MonarQ en utilisant le client créé précédemment\n", + "backend = MonarQBackend(client=my_client)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "5. Créez votre circuit" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Créer un circuit quantique simple\n", + "qc = qiskit.QuantumCircuit(2)\n", + "# Appliquer des portes quantiques\n", + "qc.h(0)\n", + "qc.cx(0, 1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Pour plus d'information à propos des opérations en Qiskit, cliquez [ici](https://quantum.cloud.ibm.com/docs/en/api/qiskit/circuit_library)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "6. Transpiler et exécutez le circuit et utilisez les résultats comme vous le désirez" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Transpile le circuit :\n", + "compiled_circuit = qiskit.transpile(qc, backend=backend)\n", + "# Exécute le circuit sur le backend MonarQ\n", + "job = backend.run(compiled_circuit, shots=1024)\n", + "# Récupérer les résultats\n", + "result = job.result()\n", + "# Afficher les résultats\n", + "print(result.get_counts())" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.11" } - }, - "source": [ - "## Usage typique\n", - "\n", - "Voici la manière typique d'utiliser le device ``MonarQBackend`` :" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "0. Tout d'abord, assurez-vous d'avoir la version Python ```3.10.X``` d'installée." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "1. Installez le plugin (vous pouvez vous référer au README)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "2. Importez les dépendances :" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Cette ligne importe la librairie Qiskit. Elle contient tout ce dont vous avez besoin pour la programmation quantique.\n", - "import qiskit\n", - "\n", - "# Cette ligne importe la classe client, nécessaire pour s'authentifier sur MonarQ\n", - "from qiskit_calculquebec.API.client import MonarqClient\n", - "\n", - "# Cette ligne importe la classe MonarQBackend\n", - "from qiskit_calculquebec.backends import MonarQBackend" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "3. Créez un [client](https://github.com/calculquebec/pennylane-calculquebec/blob/main/doc/for_developers/using_client.ipynb) pour votre device." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Voilà comment créer un client. Changez les valeurs dans la parenthèse pour vos identifiants\n", - "# Assurez vous d'avoir créé un projet, et d'utiliser le nom exact de ce dernier\n", - "my_client = MonarqClient(\"your host\", \"your user\", \"your access token\", \"your project\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "4. Créez un device en utilisant votre objet client" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Il y a 1 argument obligatoire pour le device : \n", - "- le client\n", - "\n", - "Il y a 1 argument optionnel : \n", - "- Le nom de la machine: \"yukon\" ou \"monarq\" (\"monarq\" est par défaut)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Créer le backend MonarQ en utilisant le client créé précédemment\n", - "backend = MonarQBackend(client=my_client)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "5. Créez votre circuit" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Créer un circuit quantique simple\n", - "qc = qiskit.QuantumCircuit(2)\n", - "# Appliquer des portes quantiques\n", - "qc.h(0)\n", - "qc.cx(0, 1)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Pour plus d'information à propos des opérations en Qiskit, cliquez [ici](https://quantum.cloud.ibm.com/docs/en/api/qiskit/circuit_library)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "6. Transpiler et exécutez le circuit et utilisez les résultats comme vous le désirez" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Transpile le circuit :\n", - "compiled_circuit = qiskit.transpile(qc, backend=backend)\n", - "# Exécute le circuit sur le backend MonarQ\n", - "job = backend.run(compiled_circuit, shots=1024)\n", - "# Récupérer les résultats\n", - "result = job.result()\n", - "# Afficher les résultats\n", - "print(result.get_counts())" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.11" - } - }, - "nbformat": 4, - "nbformat_minor": 2 + "nbformat": 4, + "nbformat_minor": 2 } diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_templates/autosummary/class.rst b/docs/_templates/autosummary/class.rst new file mode 100644 index 0000000..6443efa --- /dev/null +++ b/docs/_templates/autosummary/class.rst @@ -0,0 +1,90 @@ +{{ fullname | escape | underline }} + +.. autoclass:: {{ fullname }} + :show-inheritance: + + {% if '__init__' in methods %} + {% set caught_result = methods.remove('__init__') %} + {% endif %} + + {% block attributes_documentation %} + {% if attributes %} + + {% set collapse_id_suffix = (fullname | replace('.', '-') | replace('_', '-')) %} + + .. raw:: html + + +

+ Attributes +

+
+
+ + {% block attributes_summary %} + {% if attributes %} + + .. autosummary:: + + {% for item in attributes %} + ~{{ name }}.{{ item }} + {%- endfor %} + + {% endif %} + {% endblock %} + + {% for item in attributes %} + .. autoattribute:: {{ item }} + {%- endfor %} + + .. raw:: html + +
+ + {% endif %} + {% endblock %} + + {% block methods_documentation %} + {% if methods %} + + .. raw:: html + + +
+ + {% block methods_summary %} + {% if methods %} + + .. autosummary:: + {% for item in methods %} + ~{{ name }}.{{ item }} + {%- endfor %} + + {% endif %} + {% endblock %} + + {% for item in methods %} + .. automethod:: {{ item }} + {%- endfor %} + + .. raw:: html + +
+ + {% endif %} + {% endblock %} + + .. raw:: html + + diff --git a/docs/_templates/autosummary/module.rst b/docs/_templates/autosummary/module.rst new file mode 100644 index 0000000..190c15f --- /dev/null +++ b/docs/_templates/autosummary/module.rst @@ -0,0 +1,61 @@ +{{ fullname | escape | underline }} + +.. automodule:: {{ fullname }} + :members: + :undoc-members: + :inherited-members: + :show-inheritance: + :special-members: __init__ + :private-members: + +{% if modules %} +Submodules +---------- + +.. autosummary:: + :toctree: + :recursive: + +{% for item in modules %} + {{ item }} +{% endfor %} +{% endif %} + +{% if classes %} +Classes +------- + +.. autosummary:: + :toctree: + :recursive: + +{% for item in classes %} + {{ item }} +{% endfor %} +{% endif %} + +{% if functions %} +Functions +--------- + +.. autosummary:: + :toctree: + :recursive: + +{% for item in functions %} + {{ item }} +{% endfor %} +{% endif %} + +{% if attributes %} +Module Attributes +----------------- + +.. autosummary:: + :toctree: + :recursive: + +{% for item in attributes %} + {{ item }} +{% endfor %} +{% endif %} diff --git a/docs/adr/choose-documentation-pipeline.md b/docs/adr/choose-documentation-pipeline.md new file mode 100644 index 0000000..ed206e9 --- /dev/null +++ b/docs/adr/choose-documentation-pipeline.md @@ -0,0 +1,41 @@ +# Decision Record: Choice of Documentation Pipeline + +## Context + +The `qiskit-calculquebec` package is a software development project for which some basic documentation exists in [`docs/`](https://github.com/calculquebec/qiskit-calculquebec/tree/main/docs). + +This documentation is currently made up of Markdown pages and Jupyter notebooks, which are maintained manually. However, this process has several drawbacks: +- Pages written as notebooks produce diffs that can’t easily be peer-reviewed in a Pull Request. +- The API documentation is not updated automatically, which makes it likely to become outdated if it is missed during a refactor, for example. +- The documentation is susceptible to be neglected, especially if the developer resources are limited, as they will likely prioritize the development and maintenance of the package itself over the documentation. +- The documentation is not easily discoverable, as it is located in the source code repository and not published on a dedicated platform. This can make it difficult for users to find and access the documentation, especially if they are not familiar with the project or its structure. + +As the project grows, those pain points are becoming more significant, making the use, but also the maintenance, of the package as a whole more difficult. + +In accordance with the project's documentation conventions, the code already contains docstrings using reStructuredText (reST) markup, formatted in Google style. This means that the documentation pipeline should be able to parse and render these docstrings correctly, ensuring that the API documentation is generated accurately and consistently. + +This decision aims to select a free documentation pipeline that automates documentation generation, enforces a consistent structure and format, and reduces the maintenance burden on developers. It should support Python and Jupyter notebooks, integrate with existing developer tools and workflows, and remain easy to use and maintain even for contributors with limited experience with documentation tooling. + +## Decision + +After evaluating several documentation pipelines, it has been decided to use [Sphinx](https://www.sphinx-doc.org/en/master/) as the documentation generator and [Read the Docs](https://readthedocs.org/) as the hosting platform for the `qiskit-calculquebec` documentation. + +Sphinx is a widely used documentation generator that supports Python, but also Jupyter notebooks through extensions like `nbsphinx`. It can parse reST docstrings and generate API documentation automatically, ensuring that the documentation remains inline with the code and is less likely to become outdated. Sphinx allows for a high level of customization and the technical depth required to set it up and maintain it can be a burden. However, Read the Docs provides a simple and easy to digest documentation on its support for Sphinx, allowing to lower the learning curve and maintenance overhead. + +Read the Docs (RTD) is popular for hosting documentation and provides seamless integration with Sphinx and GitHub. It offers features like versioning, localization using multiple RTD projects, and automatic builds triggered by commits to the repository. This means the documentation will be automatically updated whenever changes are made to the codebase and the changes will be easy to review in a Pull Request, as the generated documentation will be added as a check in the PR. Read the Docs is free of charge for the usage we expect for this project, and it provides a user-friendly interface for browsing the documentation, making it more accessible to users. + +## Consequences + +**Positive:** +- Automation of documentation generation, ensuring that the API documentation is always up-to-date with the codebase. +- Improved discoverability and accessibility of the documentation through hosting on Read the Docs. +- Support for multi-version documentation and localization, allowing users to access the documentation in their preferred language. + +**Negative/Neutral:** +- Sphinx has a steeper learning curve compared to simpler tools, which may require additional time to set up properly. However, Read the Docs provides good examples and documentation to help with this process. +- While Read the Docs is free for the expected usage, it may have limitations in terms of build time and storage, which could become an issue if the documentation grows significantly. This is however unlikely to be a problem for the `qiskit-calculquebec` documentation in the foreseeable future. + +## Alternatives Considered + +- GitHub Pages: While GitHub Pages is a popular choice and very simple to set up, it does not provide the support for multi-version documentation unless the archive is manually maintained and stored in the repository. Additionally, it makes localization, the same way as multi-version documentation, more difficult to maintain, as it would require the use of branches for each language. +- Zensical: Zensical is the new kid on the block, and is a promising tool for documentation. It is built on top of MkDocs, and only supports Markdown, which is not ideal for our docstrings written in reST. Additionally, it is still in early development, and requires a heavy maintenance effort to keep up with the latest changes, which is not ideal for a project with limited developer resources. \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..76650d5 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,62 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = "qiskit-calculquebec" +copyright = "2026, Calcul Québec" +author = "Calcul Québec" + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [] + +templates_path = ["_templates"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = "alabaster" +html_static_path = ["_static"] + +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", + "sphinx.ext.autosummary", + "myst_parser", +] + +# Autodoc and Autosummary configuration +autodoc_default_options = { + "show-inheritance": True, +} +# Include both class docstring and __init__ docstring +autoclass_content = "both" +autosummary_generate = True + +templates_path = ["_templates"] +exclude_patterns = [ + "_build", + "Thumbs.db", + ".DS_Store", + "**/*.md", +] + + +language = "en" + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_static_path = ["_static"] +html_theme = "sphinx_book_theme" +html_theme_options = { + "repository_url": "https://github.com/calculquebec/qiskit-calculquebec", + "use_repository_button": True, +} diff --git a/docs/getting_started.ipynb b/docs/getting_started.ipynb new file mode 100644 index 0000000..99967fe --- /dev/null +++ b/docs/getting_started.ipynb @@ -0,0 +1,183 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Monarq Backend\n", + "\n", + "This document is aimed to explain what you need to know in order to use the MonarqBackend.\n", + "\n", + "This backend lets you communicate directly with MonarQ. \n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Pour la version française, visitez [cette page](https://github.com/calculquebec/qiskit-calculquebec/blob/main/doc/FR/prise_en_main.ipynb). " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "vscode": { + "languageId": "plaintext" + } + }, + "source": [ + "## Default usage\n", + "\n", + "Here is a typical workflow for using the ```MonarQBackend```: " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "0. First and foremost, you have to make sure to have python version ```3.10.x``` intalled" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "1. install the plugin (refer to the README)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "2. Import dependencies:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# this line imports the qiskit library. It contains everything necessary for quantum programming\n", + "import qiskit\n", + "\n", + "# this line imports the client class which is required for authenticating with MonarQ\n", + "from qiskit_calculquebec.API.client import MonarqClient\n", + "\n", + "# This line imports the MonarQBackend class\n", + "from qiskit_calculquebec.backends import MonarQBackend" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "3. create a client for your device" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# This is how you create a client. Change the values in the parentheses for your credentials\n", + "# project is optional. It will be set to default if you don't specify it\n", + "my_client = MonarqClient(\"your host\", \"your user\", \"your access token\", \"your project\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "4. create a device using your client" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There are 1 mandatory arguments for the backend : \n", + "- a client\n", + "\n", + "And one optionnal :\n", + "- The name of the machine you want to use \"yukon\" or \"monarq\" (MonarQ is the default) " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create your backend\n", + "backend = MonarQBackend(client=my_client)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "5. create your circuit" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "qc = qiskit.QuantumCircuit(2)\n", + "qc.h(0)\n", + "qc.cx(0, 1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For more information about qiskit operations, click [here](https://quantum.cloud.ibm.com/docs/en/api/qiskit/circuit_library)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "6. Transpile and run your circuit on MonarQ" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "compiled_circuit = qiskit.transpile(qc, backend=backend)\n", + "job = backend.run(compiled_circuit, shots=1024)\n", + "result = job.result()\n", + "print(result.get_counts())" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.11" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..a714a61 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,16 @@ +.. qiskit-calculquebec documentation master file, created by + sphinx-quickstart on Mon Mar 2 10:51:07 2026. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Documentation for ``qiskit-calculquebec`` +============================================ + + + +.. toctree:: + :maxdepth: 1 + :caption: Contents: + + rtd/readme + rtd/code_ref diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..954237b --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/mitigation_example.ipynb b/docs/mitigation_example.ipynb index cc01652..1090153 100644 --- a/docs/mitigation_example.ipynb +++ b/docs/mitigation_example.ipynb @@ -29,7 +29,15 @@ "metadata": {}, "outputs": [], "source": [ - "load_dotenv()\nmy_client = CalculQuebecClient(\n os.getenv(\"HOST\"),\n os.getenv(\"USER\"),\n os.getenv(\"ACCESS_TOKEN\"),\n project_id=os.getenv(\"PROJECT_ID\"),\n)\nbackend = MonarQBackend(\"yukon\", my_client)\nshots = 1000" + "load_dotenv()\n", + "my_client = CalculQuebecClient(\n", + " os.getenv(\"HOST\"),\n", + " os.getenv(\"USER\"),\n", + " os.getenv(\"ACCESS_TOKEN\"),\n", + " project_id=os.getenv(\"PROJECT_ID\"),\n", + ")\n", + "backend = MonarQBackend(\"yukon\", my_client)\n", + "shots = 1000" ] }, { @@ -54,7 +62,30 @@ } ], "source": [ - "qc = QuantumCircuit(3)\nqc.h(0)\nqc.cx(0, 1)\nqc.cx(1, 2)\nqc.measure_all()\n\npm = generate_preset_pass_manager(optimization_level=3, backend=backend)\ntranspiled_qc = pm.run(qc)\n\n# Physical qubits after transpilation\nif transpiled_qc.layout and transpiled_qc.layout.final_layout:\n physical_qubits = [\n transpiled_qc.layout.final_layout[q]\n for q in transpiled_qc.qubits\n ]\nelse:\n physical_qubits = list(range(3))\n\nprint(f\"Physical qubits: {physical_qubits}\")\n\n# Raw execution (reference baseline)\nsampler = Sampler(mode=backend)\njob = sampler.run([transpiled_qc], shots=shots)\ncounts_raw = job.result()[0].data.meas.get_counts()\nprint(f\"Raw counts: {counts_raw}\")" + "qc = QuantumCircuit(3)\n", + "qc.h(0)\n", + "qc.cx(0, 1)\n", + "qc.cx(1, 2)\n", + "qc.measure_all()\n", + "\n", + "pm = generate_preset_pass_manager(optimization_level=3, backend=backend)\n", + "transpiled_qc = pm.run(qc)\n", + "\n", + "# Physical qubits after transpilation\n", + "if transpiled_qc.layout and transpiled_qc.layout.final_layout:\n", + " physical_qubits = [\n", + " transpiled_qc.layout.final_layout[q] for q in transpiled_qc.qubits\n", + " ]\n", + "else:\n", + " physical_qubits = list(range(3))\n", + "\n", + "print(f\"Physical qubits: {physical_qubits}\")\n", + "\n", + "# Raw execution (reference baseline)\n", + "sampler = Sampler(mode=backend)\n", + "job = sampler.run([transpiled_qc], shots=shots)\n", + "counts_raw = job.result()[0].data.meas.get_counts()\n", + "print(f\"Raw counts: {counts_raw}\")" ] }, { @@ -94,7 +125,23 @@ } ], "source": [ - "rem_matrix = ReadoutMitigation(backend, method='matrix')\nrem_matrix.cals_from_system()\n\n# Display readout fidelities\nprint(\"Readout fidelities (method='matrix'):\")\nfor q, info in zip(range(backend.target.num_qubits), rem_matrix.readout_fidelity()):\n if info is not None:\n print(f\" qubit {q:2d} : P(0|0)={info['p00']:.4f} P(1|1)={info['p11']:.4f} avg={info['mean']:.4f}\")\n\n# Apply correction\ncounts_rem_matrix = rem_matrix.apply_correction(counts_raw, qubits=physical_qubits)\nprint(f\"\\nRaw counts : {counts_raw}\")\nprint(f\"Mitigated (matrix) : {counts_rem_matrix}\")\n\n#plot_histogram([counts_raw, counts_rem_matrix], legend=[\"raw\", \"mitigated\"])" + "rem_matrix = ReadoutMitigation(backend, method=\"matrix\")\n", + "rem_matrix.cals_from_system()\n", + "\n", + "# Display readout fidelities\n", + "print(\"Readout fidelities (method='matrix'):\")\n", + "for q, info in zip(range(backend.target.num_qubits), rem_matrix.readout_fidelity()):\n", + " if info is not None:\n", + " print(\n", + " f\" qubit {q:2d} : P(0|0)={info['p00']:.4f} P(1|1)={info['p11']:.4f} avg={info['mean']:.4f}\"\n", + " )\n", + "\n", + "# Apply correction\n", + "counts_rem_matrix = rem_matrix.apply_correction(counts_raw, qubits=physical_qubits)\n", + "print(f\"\\nRaw counts : {counts_raw}\")\n", + "print(f\"Mitigated (matrix) : {counts_rem_matrix}\")\n", + "\n", + "# plot_histogram([counts_raw, counts_rem_matrix], legend=[\"raw\", \"mitigated\"])" ] }, { @@ -114,7 +161,34 @@ } ], "source": [ - "# Visualise the inverse confusion matrix\nimport matplotlib.pyplot as plt\n\ninv_A = rem_matrix.get_inv_confusion_matrix(physical_qubits)\nn = len(physical_qubits)\nlabels = [format(i, f\"0{n}b\") for i in range(2**n)]\n\nfig, ax = plt.subplots(figsize=(7, 5))\nim = ax.imshow(inv_A, cmap=\"RdBu\", vmin=-1, vmax=1)\nax.set_xticks(range(2**n)); ax.set_yticks(range(2**n))\nax.set_xticklabels(labels, rotation=45, ha=\"right\")\nax.set_yticklabels(labels)\nax.set_title(f\"Inverse confusion matrix ({2**n}\\u00d7{2**n})\")\nfor i in range(2**n):\n for j in range(2**n):\n ax.text(j, i, f\"{inv_A[i,j]:.2f}\", ha=\"center\", va=\"center\", fontsize=7,\n color=\"black\" if abs(inv_A[i,j]) < 0.5 else \"white\")\nplt.colorbar(im)\nplt.tight_layout()\nplt.show()" + "# Visualise the inverse confusion matrix\n", + "import matplotlib.pyplot as plt\n", + "\n", + "inv_A = rem_matrix.get_inv_confusion_matrix(physical_qubits)\n", + "n = len(physical_qubits)\n", + "labels = [format(i, f\"0{n}b\") for i in range(2**n)]\n", + "\n", + "fig, ax = plt.subplots(figsize=(7, 5))\n", + "im = ax.imshow(inv_A, cmap=\"RdBu\", vmin=-1, vmax=1)\n", + "ax.set_xticks(range(2**n))\n", + "ax.set_yticks(range(2**n))\n", + "ax.set_xticklabels(labels, rotation=45, ha=\"right\")\n", + "ax.set_yticklabels(labels)\n", + "ax.set_title(f\"Inverse confusion matrix ({2**n}\\u00d7{2**n})\")\n", + "for i in range(2**n):\n", + " for j in range(2**n):\n", + " ax.text(\n", + " j,\n", + " i,\n", + " f\"{inv_A[i,j]:.2f}\",\n", + " ha=\"center\",\n", + " va=\"center\",\n", + " fontsize=7,\n", + " color=\"black\" if abs(inv_A[i, j]) < 0.5 else \"white\",\n", + " )\n", + "plt.colorbar(im)\n", + "plt.tight_layout()\n", + "plt.show()" ] }, { @@ -143,7 +217,29 @@ } ], "source": [ - "rem_m3 = ReadoutMitigation(backend, method='m3')\nrem_m3.cals_from_system()\n\n# Apply M3 correction\nquasi = rem_m3.apply_correction(counts_raw, qubits=physical_qubits)\n\n# Convert quasi-probabilities → counts\ncounts_rem_m3 = {\n k: int(round(v * shots))\n for k, v in quasi.nearest_probability_distribution().items()\n if round(v * shots) > 0\n}\n\nprint(f\"Raw counts : {counts_raw}\")\nprint(f\"M3 counts : {counts_rem_m3}\")\n\n# Solver details\nquasi_d, info = rem_m3.apply_correction(\n counts_raw, qubits=physical_qubits, details=True\n)\nprint(f\"\\nSolver used : {info['method']}\")\nprint(f\"Time (s) : {info['time']:.4f}\")\nprint(f\"Distinct bitstrings: {info['dimension']}\")" + "rem_m3 = ReadoutMitigation(backend, method=\"m3\")\n", + "rem_m3.cals_from_system()\n", + "\n", + "# Apply M3 correction\n", + "quasi = rem_m3.apply_correction(counts_raw, qubits=physical_qubits)\n", + "\n", + "# Convert quasi-probabilities → counts\n", + "counts_rem_m3 = {\n", + " k: int(round(v * shots))\n", + " for k, v in quasi.nearest_probability_distribution().items()\n", + " if round(v * shots) > 0\n", + "}\n", + "\n", + "print(f\"Raw counts : {counts_raw}\")\n", + "print(f\"M3 counts : {counts_rem_m3}\")\n", + "\n", + "# Solver details\n", + "quasi_d, info = rem_m3.apply_correction(\n", + " counts_raw, qubits=physical_qubits, details=True\n", + ")\n", + "print(f\"\\nSolver used : {info['method']}\")\n", + "print(f\"Time (s) : {info['time']:.4f}\")\n", + "print(f\"Distinct bitstrings: {info['dimension']}\")" ] }, { @@ -291,7 +387,32 @@ } ], "source": [ - "# Compare several ZNE configurations\nfrom mitiq.zne.inference import LinearFactory, RichardsonFactory\nfrom mitiq.zne.scaling import fold_global\n\nconfigs = [\n (\"Richardson [1,2,3] + fold_random\", RichardsonFactory([1.0, 2.0, 3.0]), None),\n (\"Richardson [1,3,5] + fold_random\", RichardsonFactory([1.0, 3.0, 5.0]), None),\n (\"Linear [1,1.5,2,2.5,3] + fold_random\", LinearFactory([1.0, 1.5, 2.0, 2.5, 3.0]), None),\n (\"Richardson [1,2,3] + fold_global\", RichardsonFactory([1.0, 2.0, 3.0]), fold_global),\n (\"Linear [1,2,3] + fold_global\", LinearFactory([1.0, 2.0, 3.0]), fold_global),\n]\n\nprint(f\"{'Config':50s} {'Mitigated':>10} {'Error':>8}\")\nprint(\"-\" * 72)\nfor label, factory, scale_noise in configs:\n zne_cfg = ZNEMitigation(backend, factory=factory, scale_noise=scale_noise)\n val = zne_cfg.run(qc_zne)\n print(f\"{label:50s} {val:10.4f} {abs(0.5 - val):8.4f}\")" + "# Compare several ZNE configurations\n", + "from mitiq.zne.inference import LinearFactory, RichardsonFactory\n", + "from mitiq.zne.scaling import fold_global\n", + "\n", + "configs = [\n", + " (\"Richardson [1,2,3] + fold_random\", RichardsonFactory([1.0, 2.0, 3.0]), None),\n", + " (\"Richardson [1,3,5] + fold_random\", RichardsonFactory([1.0, 3.0, 5.0]), None),\n", + " (\n", + " \"Linear [1,1.5,2,2.5,3] + fold_random\",\n", + " LinearFactory([1.0, 1.5, 2.0, 2.5, 3.0]),\n", + " None,\n", + " ),\n", + " (\n", + " \"Richardson [1,2,3] + fold_global\",\n", + " RichardsonFactory([1.0, 2.0, 3.0]),\n", + " fold_global,\n", + " ),\n", + " (\"Linear [1,2,3] + fold_global\", LinearFactory([1.0, 2.0, 3.0]), fold_global),\n", + "]\n", + "\n", + "print(f\"{'Config':50s} {'Mitigated':>10} {'Error':>8}\")\n", + "print(\"-\" * 72)\n", + "for label, factory, scale_noise in configs:\n", + " zne_cfg = ZNEMitigation(backend, factory=factory, scale_noise=scale_noise)\n", + " val = zne_cfg.run(qc_zne)\n", + " print(f\"{label:50s} {val:10.4f} {abs(0.5 - val):8.4f}\")" ] }, { @@ -376,7 +497,34 @@ "metadata": {}, "outputs": [], "source": [ - "# Circuit with idle windows (GHZ + idle + inverse GHZ)\ndef make_ghz_idle(depth: int, n: int = 2) -> QuantumCircuit:\n qc = QuantumCircuit(n, n)\n qc.h(0)\n qc.cx(0, 1)\n for _ in range(depth):\n qc.id(0); qc.id(1)\n qc.cx(0, 1)\n qc.h(0)\n qc.measure(range(n), range(n))\n return qc\n\nDEPTHS = [4, 10, 30, 50]\nresults_ddd = {d: {} for d in DEPTHS}\n\nfor depth in DEPTHS:\n circuit = make_ghz_idle(depth)\n print(f\"\\n── depth = {depth} ──────────────\")\n for rule in [\"xx\", \"yy\", \"xyxy\"]:\n ddd = DDDMitigation(backend, rule=rule, num_trials=3)\n raw = ddd.run_unmitigated(circuit)\n mit = ddd.run(circuit)\n results_ddd[depth][rule] = mit\n print(f\" rule={rule:<5} : raw={raw:.4f} DDD={mit:.4f} err={abs(1.0-mit):.4f}\")" + "# Circuit with idle windows (GHZ + idle + inverse GHZ)\n", + "def make_ghz_idle(depth: int, n: int = 2) -> QuantumCircuit:\n", + " qc = QuantumCircuit(n, n)\n", + " qc.h(0)\n", + " qc.cx(0, 1)\n", + " for _ in range(depth):\n", + " qc.id(0)\n", + " qc.id(1)\n", + " qc.cx(0, 1)\n", + " qc.h(0)\n", + " qc.measure(range(n), range(n))\n", + " return qc\n", + "\n", + "\n", + "DEPTHS = [4, 10, 30, 50]\n", + "results_ddd = {d: {} for d in DEPTHS}\n", + "\n", + "for depth in DEPTHS:\n", + " circuit = make_ghz_idle(depth)\n", + " print(f\"\\n── depth = {depth} ──────────────\")\n", + " for rule in [\"xx\", \"yy\", \"xyxy\"]:\n", + " ddd = DDDMitigation(backend, rule=rule, num_trials=3)\n", + " raw = ddd.run_unmitigated(circuit)\n", + " mit = ddd.run(circuit)\n", + " results_ddd[depth][rule] = mit\n", + " print(\n", + " f\" rule={rule:<5} : raw={raw:.4f} DDD={mit:.4f} err={abs(1.0-mit):.4f}\"\n", + " )" ] }, { @@ -392,7 +540,28 @@ "metadata": {}, "outputs": [], "source": [ - "# Initialise REM once\nrem = ReadoutMitigation(backend, method='m3')\nrem.cals_from_system()\n\n# Retrieve physical qubits for the idle circuit\ncircuit_idle = make_ghz_idle(10)\npm_ref = generate_preset_pass_manager(optimization_level=0, backend=backend)\nt = pm_ref.run(circuit_idle)\nif t.layout and t.layout.final_layout:\n phys = [t.layout.final_layout[q] for q in t.qubits]\nelse:\n phys = list(range(circuit_idle.num_qubits))\n\nddd_rem = DDDMitigation(backend, rule='xyxy', num_trials=3)\n\nval_raw = ddd_rem.run_unmitigated(circuit_idle)\nval_ddd = ddd_rem.run(circuit_idle)\nval_ddd_rem = ddd_rem.run(circuit_idle, rem=rem, qubits=phys)\n\nprint(f\"Raw : {val_raw:.4f} (err={abs(1.0-val_raw):.4f})\")\nprint(f\"DDD xyxy : {val_ddd:.4f} (err={abs(1.0-val_ddd):.4f})\")\nprint(f\"DDD + REM M3 : {val_ddd_rem:.4f} (err={abs(1.0-val_ddd_rem):.4f})\")" + "# Initialise REM once\n", + "rem = ReadoutMitigation(backend, method=\"m3\")\n", + "rem.cals_from_system()\n", + "\n", + "# Retrieve physical qubits for the idle circuit\n", + "circuit_idle = make_ghz_idle(10)\n", + "pm_ref = generate_preset_pass_manager(optimization_level=0, backend=backend)\n", + "t = pm_ref.run(circuit_idle)\n", + "if t.layout and t.layout.final_layout:\n", + " phys = [t.layout.final_layout[q] for q in t.qubits]\n", + "else:\n", + " phys = list(range(circuit_idle.num_qubits))\n", + "\n", + "ddd_rem = DDDMitigation(backend, rule=\"xyxy\", num_trials=3)\n", + "\n", + "val_raw = ddd_rem.run_unmitigated(circuit_idle)\n", + "val_ddd = ddd_rem.run(circuit_idle)\n", + "val_ddd_rem = ddd_rem.run(circuit_idle, rem=rem, qubits=phys)\n", + "\n", + "print(f\"Raw : {val_raw:.4f} (err={abs(1.0-val_raw):.4f})\")\n", + "print(f\"DDD xyxy : {val_ddd:.4f} (err={abs(1.0-val_ddd):.4f})\")\n", + "print(f\"DDD + REM M3 : {val_ddd_rem:.4f} (err={abs(1.0-val_ddd_rem):.4f})\")" ] }, { @@ -424,7 +593,24 @@ "metadata": {}, "outputs": [], "source": [ - "pt_zne = PauliTwirlingMitigation(backend, num_variants=10)\n\nraw = pt_zne.run_unmitigated(qc_pt)\npt_only = pt_zne.run(qc_pt)\npt_zne_val = pt_zne.run_with_zne(qc_pt)\n\nprint(f\"Raw : {raw:.4f}\")\nprint(f\"PT only : {pt_only:.4f}\")\nprint(f\"PT + ZNE : {pt_zne_val:.4f}\")\n\n# With a custom factory\nfrom mitiq.zne.inference import RichardsonFactory\nval_rich = pt_zne.run_with_zne(\n qc_pt,\n factory=RichardsonFactory([1.0, 2.0, 3.0]),\n)\nprint(f\"PT + ZNE (Richardson [1,2,3]) : {val_rich:.4f}\")" + "pt_zne = PauliTwirlingMitigation(backend, num_variants=10)\n", + "\n", + "raw = pt_zne.run_unmitigated(qc_pt)\n", + "pt_only = pt_zne.run(qc_pt)\n", + "pt_zne_val = pt_zne.run_with_zne(qc_pt)\n", + "\n", + "print(f\"Raw : {raw:.4f}\")\n", + "print(f\"PT only : {pt_only:.4f}\")\n", + "print(f\"PT + ZNE : {pt_zne_val:.4f}\")\n", + "\n", + "# With a custom factory\n", + "from mitiq.zne.inference import RichardsonFactory\n", + "\n", + "val_rich = pt_zne.run_with_zne(\n", + " qc_pt,\n", + " factory=RichardsonFactory([1.0, 2.0, 3.0]),\n", + ")\n", + "print(f\"PT + ZNE (Richardson [1,2,3]) : {val_rich:.4f}\")" ] }, { @@ -440,7 +626,26 @@ "metadata": {}, "outputs": [], "source": [ - "# Summary: P(|000⟩) for the 3-qubit GHZ circuit (ideal = 0.5)\nideal = 0.5\n\ndef p000_from_counts(counts, n_shots=shots):\n return counts.get(\"000\", 0) / n_shots\n\nraw_p = p000_from_counts(counts_raw)\nrem_matrix_p = p000_from_counts(counts_rem_matrix)\nrem_m3_p = p000_from_counts(counts_rem_m3)\n\nprint(f\"{'Technique':<30} {'P(|000⟩)':>10} {'Error':>8}\")\nprint(\"-\" * 52)\nprint(f\"{'Ideal':<30} {ideal:>10.4f} {'—':>8}\")\nprint(f\"{'Raw':<30} {raw_p:>10.4f} {abs(ideal-raw_p):>8.4f}\")\nprint(f\"{'REM matrix':<30} {rem_matrix_p:>10.4f} {abs(ideal-rem_matrix_p):>8.4f}\")\nprint(f\"{'REM M3':<30} {rem_m3_p:>10.4f} {abs(ideal-rem_m3_p):>8.4f}\")\nprint(f\"{'ZNE':<30} {mit_zne:>10.4f} {abs(ideal-mit_zne):>8.4f}\")\nprint(f\"{'ZNE + REM M3':<30} {mit_zne_rem:>10.4f} {abs(ideal-mit_zne_rem):>8.4f}\")" + "# Summary: P(|000⟩) for the 3-qubit GHZ circuit (ideal = 0.5)\n", + "ideal = 0.5\n", + "\n", + "\n", + "def p000_from_counts(counts, n_shots=shots):\n", + " return counts.get(\"000\", 0) / n_shots\n", + "\n", + "\n", + "raw_p = p000_from_counts(counts_raw)\n", + "rem_matrix_p = p000_from_counts(counts_rem_matrix)\n", + "rem_m3_p = p000_from_counts(counts_rem_m3)\n", + "\n", + "print(f\"{'Technique':<30} {'P(|000⟩)':>10} {'Error':>8}\")\n", + "print(\"-\" * 52)\n", + "print(f\"{'Ideal':<30} {ideal:>10.4f} {'—':>8}\")\n", + "print(f\"{'Raw':<30} {raw_p:>10.4f} {abs(ideal-raw_p):>8.4f}\")\n", + "print(f\"{'REM matrix':<30} {rem_matrix_p:>10.4f} {abs(ideal-rem_matrix_p):>8.4f}\")\n", + "print(f\"{'REM M3':<30} {rem_m3_p:>10.4f} {abs(ideal-rem_m3_p):>8.4f}\")\n", + "print(f\"{'ZNE':<30} {mit_zne:>10.4f} {abs(ideal-mit_zne):>8.4f}\")\n", + "print(f\"{'ZNE + REM M3':<30} {mit_zne_rem:>10.4f} {abs(ideal-mit_zne_rem):>8.4f}\")" ] } ], diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..616d018 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,3 @@ +sphinx==9.1.0 +sphinx-book-theme +myst-parser diff --git a/docs/rtd/code_ref.rst b/docs/rtd/code_ref.rst new file mode 100644 index 0000000..c8fe9f6 --- /dev/null +++ b/docs/rtd/code_ref.rst @@ -0,0 +1,16 @@ +Code Reference +============== + +This section contains the API reference for the ``qiskit_calculquebec`` package. + +.. currentmodule:: qiskit_calculquebec + +.. autosummary:: + :toctree: code_ref + :recursive: + + API + backends + custom_gates + mitigation + provider diff --git a/qiskit_calculquebec/API/adapter.py b/qiskit_calculquebec/API/adapter.py index 349c01d..da866eb 100644 --- a/qiskit_calculquebec/API/adapter.py +++ b/qiskit_calculquebec/API/adapter.py @@ -180,9 +180,7 @@ def get_project_id_by_name(project_name: str = "default") -> str: converted = json.loads(res.text) projects = converted.get(keys.ITEMS, []) - matching_projects = [ - p for p in projects if p.get(keys.NAME) == project_name - ] + matching_projects = [p for p in projects if p.get(keys.NAME) == project_name] if len(matching_projects) > 1: raise MultipleProjectsException(matching_projects) diff --git a/qiskit_calculquebec/API/client.py b/qiskit_calculquebec/API/client.py index ac96e38..2f6631c 100644 --- a/qiskit_calculquebec/API/client.py +++ b/qiskit_calculquebec/API/client.py @@ -11,6 +11,7 @@ class ProjectParameterError(ValueError): Either ``project_name`` or ``project_id`` must be provided, but not both. """ + pass @@ -165,6 +166,7 @@ def __init__( circuit_name, ) import warnings + warnings.warn( "MonarqClient is deprecated and will be removed in a future release. " "Use CalculQuebecClient instead.", diff --git a/qiskit_calculquebec/API/job.py b/qiskit_calculquebec/API/job.py index 82895ac..645c964 100644 --- a/qiskit_calculquebec/API/job.py +++ b/qiskit_calculquebec/API/job.py @@ -100,9 +100,7 @@ def run(self, max_tries: int = -1) -> dict: if status == "SUCCEEDED": return content["result"]["histogram"] - raise JobException( - f"Job did not complete. Last status: {current_status}" - ) + raise JobException(f"Job did not complete. Last status: {current_status}") def raise_api_error(self, response): """Parse an API error response and raise a ``JobException``. diff --git a/qiskit_calculquebec/backends/targets/anyon_target.py b/qiskit_calculquebec/backends/targets/anyon_target.py index c3a848f..acbd268 100644 --- a/qiskit_calculquebec/backends/targets/anyon_target.py +++ b/qiskit_calculquebec/backends/targets/anyon_target.py @@ -38,47 +38,51 @@ class AnyonTarget(Target, ABC): """Abstract base class describing a quantum hardware target for Anyon devices. - This class extends ``qiskit.transpiler.Target`` and provides a common - interface for defining the characteristics of Anyon-based quantum devices, - such as Yukon or MonarQ. + This class extends ``qiskit.transpiler.Target`` and provides a common + interface for defining the characteristics of Anyon-based quantum devices, + such as Yukon or MonarQ. - The target defines: + The target defines: - * The physical qubits available on the device - * The device coupling map - * The supported gate set - * Gate durations and error rates - * Measurement operations + * The physical qubits available on the device + * The device coupling map + * The supported gate set + * Gate durations and error rates + * Measurement operations - Hardware calibration data (gate errors, measurement errors, and coherence - times) are optionally retrieved through - ``qiskit_calculquebec.API.adapter.ApiAdapter``. + Hardware calibration data (gate errors, measurement errors, and coherence + times) are optionally retrieved through + ``qiskit_calculquebec.API.adapter.ApiAdapter``. - Subclasses must implement methods describing the hardware topology. + Subclasses must implement methods describing the hardware topology. - Note: - This class is abstract and cannot be instantiated directly. Concrete - subclasses must implement: + Note: + This class is abstract and cannot be instantiated directly. Concrete + subclasses must implement: - * ``coupling_map()`` - * ``qubits()`` - * ``device_name()`` + * ``coupling_map()`` + * ``qubits()`` + * ``device_name()`` - Example: - Example of a concrete device target: + Example: + Example of a concrete device target: - .. code-block:: python + .. code-block:: python - class Yukon(AnyonTarget): + class Yukon(AnyonTarget): - def coupling_map(self): - return [(0, 1), (1, 0), (1, 2), (2, 1)] + def coupling_map(self): + return [(0, 1), (1, 0), (1, 2), (2, 1)] - def qubits(self): - return list(range(6)) + def qubits(self): + return list(range(6)) - def device_name(self): - return "Yukon" + def device_name(self): + return "Yukon" + <<<<<<< HEAD + ... + ======= + >>>>>>> 07572de34a34dd9bf7983e36a48840d354bb88de """ @abstractmethod diff --git a/qiskit_calculquebec/mitigation/__init__.py b/qiskit_calculquebec/mitigation/__init__.py index 533e402..7d58e73 100644 --- a/qiskit_calculquebec/mitigation/__init__.py +++ b/qiskit_calculquebec/mitigation/__init__.py @@ -28,12 +28,15 @@ import warnings as _warnings + def _check_optional_deps(): missing = [] try: import mitiq # noqa: F401 except ImportError: - missing.append("mitiq (required for ZNEMitigation, DDDMitigation, PauliTwirlingMitigation, ReadoutMitigation(method='matrix'))") + missing.append( + "mitiq (required for ZNEMitigation, DDDMitigation, PauliTwirlingMitigation, ReadoutMitigation(method='matrix'))" + ) try: import mthree # noqa: F401 except ImportError: @@ -52,6 +55,7 @@ def _check_optional_deps(): stacklevel=2, ) + _check_optional_deps() from qiskit_calculquebec.mitigation.readout import ReadoutMitigation diff --git a/qiskit_calculquebec/mitigation/ddd.py b/qiskit_calculquebec/mitigation/ddd.py index 8a85b4b..8d20764 100644 --- a/qiskit_calculquebec/mitigation/ddd.py +++ b/qiskit_calculquebec/mitigation/ddd.py @@ -11,7 +11,6 @@ - ``'xyxy'`` : X-Y-X-Y sequence (recommended in general) """ - try: from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager from qiskit_ibm_runtime import SamplerV2 @@ -27,6 +26,7 @@ def _require_mitiq_ddd(): try: from mitiq.ddd import execute_with_ddd from mitiq.ddd.rules import xx, yy, xyxy + return execute_with_ddd, {"xx": xx, "yy": yy, "xyxy": xyxy} except ImportError: raise ImportError( @@ -128,7 +128,12 @@ def executor(circuit) -> MeasurementResult: if not isinstance(transpiled, list): transpiled = [transpiled] sampler = SamplerV2(mode=backend) - counts = sampler.run(transpiled, shots=shots).result()[0].join_data().get_counts() + counts = ( + sampler.run(transpiled, shots=shots) + .result()[0] + .join_data() + .get_counts() + ) # Normalize multi-register keys (e.g. "0 0" → "00") counts = {"".join(k.split()): v for k, v in counts.items()} @@ -152,6 +157,7 @@ def executor(circuit) -> MeasurementResult: return MeasurementResult(np.array(bitstrings, dtype=int)) else: + def executor(circuit): # mitiq may pass a circuit without measurements after DDD insertion — re-add them circ = circuit.copy() @@ -163,7 +169,12 @@ def executor(circuit): if not isinstance(transpiled, list): transpiled = [transpiled] sampler = SamplerV2(mode=backend) - counts = sampler.run(transpiled, shots=shots).result()[0].join_data().get_counts() + counts = ( + sampler.run(transpiled, shots=shots) + .result()[0] + .join_data() + .get_counts() + ) # Normalize multi-register keys (e.g. "0 0" → "00") counts = {"".join(k.split()): v for k, v in counts.items()} n = circuit.num_qubits diff --git a/qiskit_calculquebec/mitigation/pauli_twirling.py b/qiskit_calculquebec/mitigation/pauli_twirling.py index cdc2793..e441b8e 100644 --- a/qiskit_calculquebec/mitigation/pauli_twirling.py +++ b/qiskit_calculquebec/mitigation/pauli_twirling.py @@ -22,6 +22,7 @@ def _require_mitiq_pt(): try: from mitiq.pt import generate_pauli_twirl_variants + return generate_pauli_twirl_variants except ImportError: raise ImportError( @@ -34,6 +35,7 @@ def _require_mitiq_pt(): def _require_mitiq_zne(): try: from mitiq import zne + return zne except ImportError: raise ImportError( @@ -110,7 +112,12 @@ def executor(circuit): if not isinstance(transpiled, list): transpiled = [transpiled] sampler = SamplerV2(mode=backend) - counts = sampler.run(transpiled, shots=shots).result()[0].join_data().get_counts() + counts = ( + sampler.run(transpiled, shots=shots) + .result()[0] + .join_data() + .get_counts() + ) # Normalize multi-register keys (e.g. "0 0" → "00") counts = {"".join(k.split()): v for k, v in counts.items()} n = circuit.num_qubits @@ -258,5 +265,7 @@ def run_variants(self, circuit, rem=None, qubits=None) -> list[float]: """ generate_pauli_twirl_variants = _require_mitiq_pt() base_executor = self._make_base_executor(rem=rem, qubits=qubits) - variants = generate_pauli_twirl_variants(circuit, num_circuits=self.num_variants) + variants = generate_pauli_twirl_variants( + circuit, num_circuits=self.num_variants + ) return [base_executor(v) for v in variants] diff --git a/qiskit_calculquebec/mitigation/readout.py b/qiskit_calculquebec/mitigation/readout.py index 8866bfa..3aede80 100644 --- a/qiskit_calculquebec/mitigation/readout.py +++ b/qiskit_calculquebec/mitigation/readout.py @@ -39,11 +39,13 @@ # ── optional imports ─────────────────────────────────────────────────────── + def _require_mitiq(): try: import mitiq # noqa: F401 from mitiq import MeasurementResult from mitiq.rem.inverse_confusion_matrix import mitigate_measurements + return MeasurementResult, mitigate_measurements except ImportError: raise ImportError( @@ -61,7 +63,15 @@ def _require_mthree(): from mthree.classes import QuasiCollection from mthree.exceptions import M3Error import psutil - return _direct_solve, _cal_matrix, _iterative_solver, QuasiCollection, M3Error, psutil + + return ( + _direct_solve, + _cal_matrix, + _iterative_solver, + QuasiCollection, + M3Error, + psutil, + ) except ImportError: raise ImportError( "mthree and psutil are required for method='m3'.\n" @@ -147,8 +157,7 @@ def cals_from_system(self, qubits: list[int] | None = None): # col 0 → prepared |0⟩: [P(0|0), P(1|0)] # col 1 → prepared |1⟩: [P(0|1), P(1|1)] self.single_qubit_cals[q] = np.array( - [[p0, 1.0 - p1], - [1.0 - p0, p1 ]], + [[p0, 1.0 - p1], [1.0 - p0, p1]], dtype=np.float64, ) @@ -163,7 +172,8 @@ def cals_from_system(self, qubits: list[int] | None = None): logger.info( "Calibration loaded for %d qubits from the Anyon benchmark (%s).", - len(qubits), machine_name, + len(qubits), + machine_name, ) def cals_from_matrices(self, matrices: list): @@ -179,8 +189,7 @@ def cals_from_matrices(self, matrices: list): f"List length ({len(matrices)}) != num_qubits ({self.num_qubits})." ) self.single_qubit_cals = [ - np.asarray(m, dtype=np.float64) if m is not None else None - for m in matrices + np.asarray(m, dtype=np.float64) if m is not None else None for m in matrices ] self.faulty_qubits = _faulty_qubit_checker(self.single_qubit_cals) @@ -199,7 +208,9 @@ def readout_fidelity(self, qubits: list[int] | None = None) -> list: RuntimeError: If calibration has not been loaded yet. """ if self.single_qubit_cals is None: - raise RuntimeError("Mitigator not calibrated. Call cals_from_system() first.") + raise RuntimeError( + "Mitigator not calibrated. Call cals_from_system() first." + ) if qubits is None: qubits = range(self.num_qubits) result = [] @@ -247,7 +258,9 @@ def apply_correction(self, counts, qubits: list[int], **kwargs): requested qubit is not calibrated. """ if self.single_qubit_cals is None: - raise RuntimeError("Mitigator not calibrated. Call cals_from_system() first.") + raise RuntimeError( + "Mitigator not calibrated. Call cals_from_system() first." + ) missing = [q for q in qubits if self.single_qubit_cals[q] is None] if missing: @@ -392,9 +405,14 @@ def _apply_m3( """ from time import perf_counter - _direct_solve, _cal_matrix, _iterative_solver, QuasiCollection, M3Error, psutil = ( - _require_mthree() - ) + ( + _direct_solve, + _cal_matrix, + _iterative_solver, + QuasiCollection, + M3Error, + psutil, + ) = _require_mthree() counts = dict(counts) shots = sum(counts.values()) @@ -434,8 +452,10 @@ def _apply_m3( mit_counts.mitigation_overhead = gamma * gamma if details: return mit_counts, { - "method": "direct", "time": dur, - "dimension": num_elems, "col_norms": col_norms, + "method": "direct", + "time": dur, + "dimension": num_elems, + "col_norms": col_norms, } return mit_counts @@ -448,7 +468,14 @@ def _cb(_): if details: st = perf_counter() mit_counts, col_norms, gamma = _iterative_solver( - self, counts, qubits, distance, tol, max_iter, 1, _cb, + self, + counts, + qubits, + distance, + tol, + max_iter, + 1, + _cb, return_mitigation_overhead, ) dur = perf_counter() - st @@ -456,12 +483,21 @@ def _cb(_): if gamma is not None: mit_counts.mitigation_overhead = gamma * gamma return mit_counts, { - "method": "iterative", "time": dur, - "dimension": num_elems, "iterations": iter_count[0], + "method": "iterative", + "time": dur, + "dimension": num_elems, + "iterations": iter_count[0], "col_norms": col_norms, } mit_counts, gamma = _iterative_solver( - self, counts, qubits, distance, tol, max_iter, 0, _cb, + self, + counts, + qubits, + distance, + tol, + max_iter, + 0, + _cb, return_mitigation_overhead, ) mit_counts.shots = shots @@ -470,7 +506,9 @@ def _cb(_): return mit_counts else: - raise ValueError(f"Invalid solver: {solver!r}. Choose 'auto', 'direct', or 'iterative'.") + raise ValueError( + f"Invalid solver: {solver!r}. Choose 'auto', 'direct', or 'iterative'." + ) # ───────────────────────────────────────────────────────────────────── # Internal interface used by mthree solvers @@ -485,7 +523,9 @@ def _form_cals(self, qubits) -> np.ndarray: qubits = np.asarray(qubits, dtype=int) cals = np.zeros(4 * len(qubits), dtype=np.float32) for kk, qubit in enumerate(qubits[::-1]): - cals[4 * kk: 4 * kk + 4] = self.single_qubit_cals[qubit].astype(np.float32).ravel() + cals[4 * kk : 4 * kk + 4] = ( + self.single_qubit_cals[qubit].astype(np.float32).ravel() + ) return cals def reduced_cal_matrix(self, counts, qubits, distance=None): @@ -509,6 +549,7 @@ def reduced_cal_matrix(self, counts, qubits, distance=None): # Internal utility # ───────────────────────────────────────────────────────────────────────── + def _faulty_qubit_checker(cals: list) -> list: """Return indices of qubits with inverted calibration (P(0|1) >= P(0|0)). @@ -517,6 +558,7 @@ def _faulty_qubit_checker(cals: list) -> list: it was prepared in |0⟩ — indicating the readout is unreliable. """ return [ - idx for idx, cal in enumerate(cals) + idx + for idx, cal in enumerate(cals) if cal is not None and cal[0, 1] >= cal[0, 0] ] diff --git a/qiskit_calculquebec/mitigation/zne.py b/qiskit_calculquebec/mitigation/zne.py index 9fd49cd..441406f 100644 --- a/qiskit_calculquebec/mitigation/zne.py +++ b/qiskit_calculquebec/mitigation/zne.py @@ -7,7 +7,6 @@ modify the circuit after noise folding applied by mitiq. """ - try: from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager from qiskit_ibm_runtime import SamplerV2 @@ -19,6 +18,7 @@ def _require_mitiq_zne(): try: from mitiq import zne + return zne except ImportError: raise ImportError( @@ -140,7 +140,12 @@ def executor(circuit) -> MeasurementResult: if not isinstance(transpiled, list): transpiled = [transpiled] sampler = SamplerV2(mode=backend) - counts = sampler.run(transpiled, shots=shots).result()[0].join_data().get_counts() + counts = ( + sampler.run(transpiled, shots=shots) + .result()[0] + .join_data() + .get_counts() + ) # Normalize multi-register keys (e.g. "0 0" → "00") counts = {"".join(k.split()): v for k, v in counts.items()} @@ -164,6 +169,7 @@ def executor(circuit) -> MeasurementResult: return MeasurementResult(np.array(bitstrings, dtype=int)) else: + def executor(circuit): # mitiq strips measurements before folding — re-add them if needed circ = circuit.copy() @@ -175,7 +181,12 @@ def executor(circuit): if not isinstance(transpiled, list): transpiled = [transpiled] sampler = SamplerV2(mode=backend) - counts = sampler.run(transpiled, shots=shots).result()[0].join_data().get_counts() + counts = ( + sampler.run(transpiled, shots=shots) + .result()[0] + .join_data() + .get_counts() + ) # Normalize multi-register keys (e.g. "0 0" → "00") counts = {"".join(k.split()): v for k, v in counts.items()} n = circuit.num_qubits @@ -285,9 +296,11 @@ def run_scaled(self, circuit, observable=None) -> dict[float, float]: executor = self._make_executor(observable=observable) folded = [ - zne.scaling.fold_gates_at_random(circuit, s) - if self.scale_noise is None - else self.scale_noise(circuit, s) + ( + zne.scaling.fold_gates_at_random(circuit, s) + if self.scale_noise is None + else self.scale_noise(circuit, s) + ) for s in self.scale_factors ] diff --git a/tests/mitigation/test_ddd.py b/tests/mitigation/test_ddd.py index e55cd27..96fa271 100644 --- a/tests/mitigation/test_ddd.py +++ b/tests/mitigation/test_ddd.py @@ -6,9 +6,9 @@ from qiskit_calculquebec.mitigation.ddd import DDDMitigation - # ── Fixtures ────────────────────────────────────────────────────────────────── + @pytest.fixture def backend(): return MagicMock() @@ -38,14 +38,18 @@ def mock_sampler_counts(): sampler_mock = MagicMock() sampler_mock.run.return_value = job_mock - with patch("qiskit_calculquebec.mitigation.ddd.SamplerV2", return_value=sampler_mock), \ - patch("qiskit_calculquebec.mitigation.ddd.generate_preset_pass_manager") as pm_mock: + with patch( + "qiskit_calculquebec.mitigation.ddd.SamplerV2", return_value=sampler_mock + ), patch( + "qiskit_calculquebec.mitigation.ddd.generate_preset_pass_manager" + ) as pm_mock: pm_mock.return_value.run.side_effect = lambda c: c yield sampler_mock # ── Constructor ─────────────────────────────────────────────────────────────── + def test_invalid_rule(backend): with pytest.raises(ValueError, match="rule must be one of"): DDDMitigation(backend, rule="invalid") @@ -69,9 +73,11 @@ def test_default_num_trials(backend): # ── Executor type dispatch ──────────────────────────────────────────────────── + def test_executor_float_no_annotation(backend): """Float executor must have no return annotation.""" import inspect + ddd = DDDMitigation(backend) executor = ddd._make_executor() ann = inspect.getfullargspec(executor).annotations @@ -82,6 +88,7 @@ def test_executor_measurement_result_annotation(backend): """MeasurementResult executor must be correctly annotated.""" import inspect from mitiq import MeasurementResult, Observable, PauliString + obs = Observable(PauliString("ZZ", support=[0, 1])) ddd = DDDMitigation(backend) executor = ddd._make_executor(observable=obs) @@ -91,6 +98,7 @@ def test_executor_measurement_result_annotation(backend): # ── run_unmitigated ─────────────────────────────────────────────────────────── + def test_run_unmitigated_returns_float(backend, idle_circuit, mock_sampler_counts): ddd = DDDMitigation(backend) result = ddd.run_unmitigated(idle_circuit) @@ -107,17 +115,20 @@ def test_run_unmitigated_p00(backend, idle_circuit, mock_sampler_counts): # ── run ─────────────────────────────────────────────────────────────────────── + def test_run_calls_execute_with_ddd(backend, idle_circuit): captured = {} def fake_execute_with_ddd(circuit, executor, rule, num_trials, **kwargs): - captured["rule_name"] = rule.__name__ if hasattr(rule, "__name__") else str(rule) + captured["rule_name"] = ( + rule.__name__ if hasattr(rule, "__name__") else str(rule) + ) captured["num_trials"] = num_trials return 0.85 - with patch("mitiq.ddd.execute_with_ddd", side_effect=fake_execute_with_ddd), \ - patch("qiskit_calculquebec.mitigation.ddd.generate_preset_pass_manager"), \ - patch("qiskit_calculquebec.mitigation.ddd.SamplerV2"): + with patch("mitiq.ddd.execute_with_ddd", side_effect=fake_execute_with_ddd), patch( + "qiskit_calculquebec.mitigation.ddd.generate_preset_pass_manager" + ), patch("qiskit_calculquebec.mitigation.ddd.SamplerV2"): ddd = DDDMitigation(backend, rule="xyxy", num_trials=5) result = ddd.run(idle_circuit) @@ -127,6 +138,7 @@ def fake_execute_with_ddd(circuit, executor, rule, num_trials, **kwargs): def test_run_strips_measurements_with_observable(backend, idle_circuit): from mitiq import Observable, PauliString + obs = Observable(PauliString("ZZ", support=[0, 1])) captured = {} @@ -134,9 +146,9 @@ def fake_execute_with_ddd(circuit, executor, rule, num_trials, **kwargs): captured["circuit"] = circuit return 0.85 - with patch("mitiq.ddd.execute_with_ddd", side_effect=fake_execute_with_ddd), \ - patch("qiskit_calculquebec.mitigation.ddd.generate_preset_pass_manager"), \ - patch("qiskit_calculquebec.mitigation.ddd.SamplerV2"): + with patch("mitiq.ddd.execute_with_ddd", side_effect=fake_execute_with_ddd), patch( + "qiskit_calculquebec.mitigation.ddd.generate_preset_pass_manager" + ), patch("qiskit_calculquebec.mitigation.ddd.SamplerV2"): ddd = DDDMitigation(backend) ddd.run(idle_circuit, observable=obs) @@ -144,9 +156,9 @@ def fake_execute_with_ddd(circuit, executor, rule, num_trials, **kwargs): def test_run_returns_real_float(backend, idle_circuit): - with patch("mitiq.ddd.execute_with_ddd", return_value=complex(0.75, -1e-18)), \ - patch("qiskit_calculquebec.mitigation.ddd.generate_preset_pass_manager"), \ - patch("qiskit_calculquebec.mitigation.ddd.SamplerV2"): + with patch("mitiq.ddd.execute_with_ddd", return_value=complex(0.75, -1e-18)), patch( + "qiskit_calculquebec.mitigation.ddd.generate_preset_pass_manager" + ), patch("qiskit_calculquebec.mitigation.ddd.SamplerV2"): ddd = DDDMitigation(backend) result = ddd.run(idle_circuit) @@ -156,7 +168,10 @@ def test_run_returns_real_float(backend, idle_circuit): # ── REM integration ─────────────────────────────────────────────────────────── -def test_run_unmitigated_raises_rem_without_qubits(backend, idle_circuit, mock_sampler_counts): + +def test_run_unmitigated_raises_rem_without_qubits( + backend, idle_circuit, mock_sampler_counts +): rem = MagicMock() rem.method = "m3" ddd = DDDMitigation(backend) @@ -166,6 +181,7 @@ def test_run_unmitigated_raises_rem_without_qubits(backend, idle_circuit, mock_s # ── Count key normalization ─────────────────────────────────────────────────── + def test_count_key_normalization(backend): """Multi-register counts with spaces ('0 0') should be normalized.""" counts = {"0 0": 900, "0 1": 50, "1 0": 30, "1 1": 20} @@ -180,8 +196,11 @@ def test_count_key_normalization(backend): qc.h(0) qc.measure_all() - with patch("qiskit_calculquebec.mitigation.ddd.SamplerV2", return_value=sampler_mock), \ - patch("qiskit_calculquebec.mitigation.ddd.generate_preset_pass_manager") as pm_mock: + with patch( + "qiskit_calculquebec.mitigation.ddd.SamplerV2", return_value=sampler_mock + ), patch( + "qiskit_calculquebec.mitigation.ddd.generate_preset_pass_manager" + ) as pm_mock: pm_mock.return_value.run.side_effect = lambda c: c ddd = DDDMitigation(backend, shots=1000) executor = ddd._make_executor() diff --git a/tests/mitigation/test_pauli_twirling.py b/tests/mitigation/test_pauli_twirling.py index e7f882d..c78f80b 100644 --- a/tests/mitigation/test_pauli_twirling.py +++ b/tests/mitigation/test_pauli_twirling.py @@ -7,9 +7,9 @@ from qiskit_calculquebec.mitigation.pauli_twirling import PauliTwirlingMitigation - # ── Fixtures ────────────────────────────────────────────────────────────────── + @pytest.fixture def backend(): return MagicMock() @@ -34,14 +34,19 @@ def mock_sampler_counts(): sampler_mock = MagicMock() sampler_mock.run.return_value = job_mock - with patch("qiskit_calculquebec.mitigation.pauli_twirling.SamplerV2", return_value=sampler_mock), \ - patch("qiskit_calculquebec.mitigation.pauli_twirling.generate_preset_pass_manager") as pm_mock: + with patch( + "qiskit_calculquebec.mitigation.pauli_twirling.SamplerV2", + return_value=sampler_mock, + ), patch( + "qiskit_calculquebec.mitigation.pauli_twirling.generate_preset_pass_manager" + ) as pm_mock: pm_mock.return_value.run.side_effect = lambda c: c yield sampler_mock # ── Constructor ─────────────────────────────────────────────────────────────── + def test_defaults(backend): pt = PauliTwirlingMitigation(backend) assert pt.num_variants == 10 @@ -56,9 +61,11 @@ def test_custom_params(backend): # ── Executor type dispatch ──────────────────────────────────────────────────── + def test_base_executor_no_annotation(backend): """Base executor must have no return annotation (FloatLike for mitiq).""" import inspect + pt = PauliTwirlingMitigation(backend) executor = pt._make_base_executor() ann = inspect.getfullargspec(executor).annotations @@ -68,6 +75,7 @@ def test_base_executor_no_annotation(backend): def test_pt_executor_no_annotation(backend): """PT executor must have no return annotation.""" import inspect + pt = PauliTwirlingMitigation(backend) executor = pt._make_pt_executor() ann = inspect.getfullargspec(executor).annotations @@ -76,6 +84,7 @@ def test_pt_executor_no_annotation(backend): # ── run_unmitigated ─────────────────────────────────────────────────────────── + def test_run_unmitigated_returns_float(backend, circuit, mock_sampler_counts): pt = PauliTwirlingMitigation(backend, shots=1000) result = pt.run_unmitigated(circuit) @@ -85,6 +94,7 @@ def test_run_unmitigated_returns_float(backend, circuit, mock_sampler_counts): # ── run ─────────────────────────────────────────────────────────────────────── + def test_run_averages_variants(backend, circuit): """run() should average over num_variants executions.""" call_count = {"n": 0} @@ -93,8 +103,9 @@ def fake_base_executor(c): call_count["n"] += 1 return 0.9 - with patch.object(PauliTwirlingMitigation, "_make_base_executor", return_value=fake_base_executor), \ - patch("mitiq.pt.generate_pauli_twirl_variants") as mock_variants: + with patch.object( + PauliTwirlingMitigation, "_make_base_executor", return_value=fake_base_executor + ), patch("mitiq.pt.generate_pauli_twirl_variants") as mock_variants: mock_variants.return_value = [circuit] * 3 pt = PauliTwirlingMitigation(backend, num_variants=3) result = pt.run(circuit) @@ -114,8 +125,9 @@ def fake_base_executor(c): idx["i"] += 1 return val - with patch.object(PauliTwirlingMitigation, "_make_base_executor", return_value=fake_base_executor), \ - patch("mitiq.pt.generate_pauli_twirl_variants") as mock_variants: + with patch.object( + PauliTwirlingMitigation, "_make_base_executor", return_value=fake_base_executor + ), patch("mitiq.pt.generate_pauli_twirl_variants") as mock_variants: mock_variants.return_value = [circuit] * 3 pt = PauliTwirlingMitigation(backend, num_variants=3) result = pt.run(circuit) @@ -125,17 +137,19 @@ def fake_base_executor(c): # ── run_with_zne ────────────────────────────────────────────────────────────── + def test_run_with_zne_uses_linear_factory_by_default(backend, circuit): from mitiq.zne.inference import LinearFactory + captured = {} def fake_execute_with_zne(c, executor, **kwargs): captured["factory"] = kwargs.get("factory") return 0.9 - with patch("mitiq.zne.execute_with_zne", side_effect=fake_execute_with_zne), \ - patch("qiskit_calculquebec.mitigation.pauli_twirling.generate_preset_pass_manager"), \ - patch("qiskit_calculquebec.mitigation.pauli_twirling.SamplerV2"): + with patch("mitiq.zne.execute_with_zne", side_effect=fake_execute_with_zne), patch( + "qiskit_calculquebec.mitigation.pauli_twirling.generate_preset_pass_manager" + ), patch("qiskit_calculquebec.mitigation.pauli_twirling.SamplerV2"): pt = PauliTwirlingMitigation(backend) pt.run_with_zne(circuit) @@ -144,6 +158,7 @@ def fake_execute_with_zne(c, executor, **kwargs): def test_run_with_zne_uses_custom_factory(backend, circuit): from mitiq.zne.inference import RichardsonFactory + factory = RichardsonFactory([1.0, 2.0, 3.0]) captured = {} @@ -151,9 +166,9 @@ def fake_execute_with_zne(c, executor, **kwargs): captured["factory"] = kwargs.get("factory") return 0.9 - with patch("mitiq.zne.execute_with_zne", side_effect=fake_execute_with_zne), \ - patch("qiskit_calculquebec.mitigation.pauli_twirling.generate_preset_pass_manager"), \ - patch("qiskit_calculquebec.mitigation.pauli_twirling.SamplerV2"): + with patch("mitiq.zne.execute_with_zne", side_effect=fake_execute_with_zne), patch( + "qiskit_calculquebec.mitigation.pauli_twirling.generate_preset_pass_manager" + ), patch("qiskit_calculquebec.mitigation.pauli_twirling.SamplerV2"): pt = PauliTwirlingMitigation(backend) pt.run_with_zne(circuit, factory=factory) @@ -168,9 +183,9 @@ def fake_execute_with_zne(c, executor, **kwargs): captured["circuit"] = c return 0.9 - with patch("mitiq.zne.execute_with_zne", side_effect=fake_execute_with_zne), \ - patch("qiskit_calculquebec.mitigation.pauli_twirling.generate_preset_pass_manager"), \ - patch("qiskit_calculquebec.mitigation.pauli_twirling.SamplerV2"): + with patch("mitiq.zne.execute_with_zne", side_effect=fake_execute_with_zne), patch( + "qiskit_calculquebec.mitigation.pauli_twirling.generate_preset_pass_manager" + ), patch("qiskit_calculquebec.mitigation.pauli_twirling.SamplerV2"): pt = PauliTwirlingMitigation(backend) pt.run_with_zne(circuit) @@ -178,9 +193,9 @@ def fake_execute_with_zne(c, executor, **kwargs): def test_run_with_zne_returns_real_float(backend, circuit): - with patch("mitiq.zne.execute_with_zne", return_value=complex(0.88, -1e-17)), \ - patch("qiskit_calculquebec.mitigation.pauli_twirling.generate_preset_pass_manager"), \ - patch("qiskit_calculquebec.mitigation.pauli_twirling.SamplerV2"): + with patch("mitiq.zne.execute_with_zne", return_value=complex(0.88, -1e-17)), patch( + "qiskit_calculquebec.mitigation.pauli_twirling.generate_preset_pass_manager" + ), patch("qiskit_calculquebec.mitigation.pauli_twirling.SamplerV2"): pt = PauliTwirlingMitigation(backend) result = pt.run_with_zne(circuit) @@ -190,6 +205,7 @@ def test_run_with_zne_returns_real_float(backend, circuit): # ── run_variants ────────────────────────────────────────────────────────────── + def test_run_variants_length(backend, circuit, mock_sampler_counts): with patch("mitiq.pt.generate_pauli_twirl_variants") as mock_variants: mock_variants.return_value = [circuit] * 5 @@ -202,6 +218,7 @@ def test_run_variants_length(backend, circuit, mock_sampler_counts): # ── REM integration ─────────────────────────────────────────────────────────── + def test_run_raises_rem_without_qubits(backend, circuit, mock_sampler_counts): rem = MagicMock() rem.method = "m3" @@ -214,6 +231,7 @@ def test_run_raises_rem_without_qubits(backend, circuit, mock_sampler_counts): # ── Count key normalization ─────────────────────────────────────────────────── + def test_count_key_normalization(backend): """Keys with spaces ('0 0') should be normalized to '00'.""" counts = {"0 0": 900, "1 1": 100} @@ -228,8 +246,12 @@ def test_count_key_normalization(backend): qc.h(0) qc.measure_all() - with patch("qiskit_calculquebec.mitigation.pauli_twirling.SamplerV2", return_value=sampler_mock), \ - patch("qiskit_calculquebec.mitigation.pauli_twirling.generate_preset_pass_manager") as pm_mock: + with patch( + "qiskit_calculquebec.mitigation.pauli_twirling.SamplerV2", + return_value=sampler_mock, + ), patch( + "qiskit_calculquebec.mitigation.pauli_twirling.generate_preset_pass_manager" + ) as pm_mock: pm_mock.return_value.run.side_effect = lambda c: c pt = PauliTwirlingMitigation(backend, shots=1000) executor = pt._make_base_executor() diff --git a/tests/mitigation/test_readout.py b/tests/mitigation/test_readout.py index 7c89b09..57da0f5 100644 --- a/tests/mitigation/test_readout.py +++ b/tests/mitigation/test_readout.py @@ -4,11 +4,14 @@ import pytest from unittest.mock import MagicMock, patch -from qiskit_calculquebec.mitigation.readout import ReadoutMitigation, _faulty_qubit_checker - +from qiskit_calculquebec.mitigation.readout import ( + ReadoutMitigation, + _faulty_qubit_checker, +) # ── Fixtures ────────────────────────────────────────────────────────────────── + @pytest.fixture def backend(): mock = MagicMock() @@ -20,27 +23,32 @@ def backend(): @pytest.fixture def rem_matrix(backend): rem = ReadoutMitigation(backend, method="matrix") - rem.cals_from_matrices([ - np.array([[0.97, 0.05], [0.03, 0.95]]), - np.array([[0.96, 0.06], [0.04, 0.94]]), - np.array([[0.98, 0.04], [0.02, 0.96]]), - ]) + rem.cals_from_matrices( + [ + np.array([[0.97, 0.05], [0.03, 0.95]]), + np.array([[0.96, 0.06], [0.04, 0.94]]), + np.array([[0.98, 0.04], [0.02, 0.96]]), + ] + ) return rem @pytest.fixture def rem_m3(backend): rem = ReadoutMitigation(backend, method="m3") - rem.cals_from_matrices([ - np.array([[0.97, 0.05], [0.03, 0.95]]), - np.array([[0.96, 0.06], [0.04, 0.94]]), - np.array([[0.98, 0.04], [0.02, 0.96]]), - ]) + rem.cals_from_matrices( + [ + np.array([[0.97, 0.05], [0.03, 0.95]]), + np.array([[0.96, 0.06], [0.04, 0.94]]), + np.array([[0.98, 0.04], [0.02, 0.96]]), + ] + ) return rem # ── Constructor ─────────────────────────────────────────────────────────────── + def test_invalid_method(backend): with pytest.raises(ValueError, match="method must be"): ReadoutMitigation(backend, method="invalid") @@ -53,6 +61,7 @@ def test_valid_methods(backend): # ── Calibration ─────────────────────────────────────────────────────────────── + def test_cals_from_matrices_wrong_length(backend): rem = ReadoutMitigation(backend, method="matrix") with pytest.raises(ValueError, match=r"List length"): @@ -70,13 +79,25 @@ def test_cals_from_system(backend): benchmark_data = { "resultsPerDevice": { "qubits": { - "0": {"parallelReadoutState0Fidelity": 0.97, "parallelReadoutState1Fidelity": 0.95}, - "1": {"parallelReadoutState0Fidelity": 0.96, "parallelReadoutState1Fidelity": 0.94}, - "2": {"parallelReadoutState0Fidelity": 0.98, "parallelReadoutState1Fidelity": 0.96}, + "0": { + "parallelReadoutState0Fidelity": 0.97, + "parallelReadoutState1Fidelity": 0.95, + }, + "1": { + "parallelReadoutState0Fidelity": 0.96, + "parallelReadoutState1Fidelity": 0.94, + }, + "2": { + "parallelReadoutState0Fidelity": 0.98, + "parallelReadoutState1Fidelity": 0.96, + }, } } } - with patch("qiskit_calculquebec.API.adapter.ApiAdapter.get_benchmark", return_value=benchmark_data): + with patch( + "qiskit_calculquebec.API.adapter.ApiAdapter.get_benchmark", + return_value=benchmark_data, + ): rem.cals_from_system() assert rem.single_qubit_cals is not None assert rem.cal_timestamp is not None @@ -86,6 +107,7 @@ def test_cals_from_system(backend): # ── Readout fidelity ────────────────────────────────────────────────────────── + def test_readout_fidelity_not_calibrated(backend): rem = ReadoutMitigation(backend, method="matrix") with pytest.raises(RuntimeError, match="not calibrated"): @@ -103,6 +125,7 @@ def test_readout_fidelity_values(rem_matrix): # ── apply_correction (matrix) ───────────────────────────────────────────────── + def test_apply_correction_not_calibrated(backend): rem = ReadoutMitigation(backend, method="matrix") with pytest.raises(RuntimeError, match="not calibrated"): @@ -111,11 +134,13 @@ def test_apply_correction_not_calibrated(backend): def test_apply_correction_missing_qubit(backend): rem = ReadoutMitigation(backend, method="matrix") - rem.cals_from_matrices([ - np.array([[0.97, 0.05], [0.03, 0.95]]), - None, # qubit 1 not calibrated - np.array([[0.98, 0.04], [0.02, 0.96]]), - ]) + rem.cals_from_matrices( + [ + np.array([[0.97, 0.05], [0.03, 0.95]]), + None, # qubit 1 not calibrated + np.array([[0.98, 0.04], [0.02, 0.96]]), + ] + ) with pytest.raises(RuntimeError, match="Uncalibrated qubits"): rem.apply_correction({"000": 500}, qubits=[0, 1, 2]) @@ -130,7 +155,15 @@ def test_apply_correction_matrix_returns_dict(rem_matrix): def test_apply_correction_matrix_improves_ghz(rem_matrix): """Correction should push 000 and 111 closer to equal counts.""" - counts = {"000": 420, "111": 420, "001": 40, "010": 30, "100": 40, "011": 30, "101": 20} + counts = { + "000": 420, + "111": 420, + "001": 40, + "010": 30, + "100": 40, + "011": 30, + "101": 20, + } corrected = rem_matrix.apply_correction(counts, qubits=[0, 1, 2]) total = sum(corrected.values()) assert total > 0 @@ -143,6 +176,7 @@ def test_apply_correction_matrix_improves_ghz(rem_matrix): # ── confusion matrix helpers ────────────────────────────────────────────────── + def test_get_confusion_matrix_shape(rem_matrix): mat = rem_matrix.get_confusion_matrix([0, 1, 2]) assert mat.shape == (8, 8) @@ -161,6 +195,7 @@ def test_get_confusion_matrix_not_calibrated(backend): # ── faulty qubit detection ──────────────────────────────────────────────────── + def test_faulty_qubit_checker_normal(): cals = [ np.array([[0.97, 0.05], [0.03, 0.95]]), # P(0|1)=0.05 < P(0|0)=0.97 → OK diff --git a/tests/mitigation/test_zne.py b/tests/mitigation/test_zne.py index b96c6e3..22a9c79 100644 --- a/tests/mitigation/test_zne.py +++ b/tests/mitigation/test_zne.py @@ -7,9 +7,9 @@ from qiskit_calculquebec.mitigation.zne import ZNEMitigation - # ── Fixtures ────────────────────────────────────────────────────────────────── + @pytest.fixture def backend(): mock = MagicMock() @@ -41,14 +41,18 @@ def mock_sampler_counts(): sampler_mock = MagicMock() sampler_mock.run.return_value = job_mock - with patch("qiskit_calculquebec.mitigation.zne.SamplerV2", return_value=sampler_mock), \ - patch("qiskit_calculquebec.mitigation.zne.generate_preset_pass_manager") as pm_mock: + with patch( + "qiskit_calculquebec.mitigation.zne.SamplerV2", return_value=sampler_mock + ), patch( + "qiskit_calculquebec.mitigation.zne.generate_preset_pass_manager" + ) as pm_mock: pm_mock.return_value.run.side_effect = lambda c: c yield sampler_mock # ── Constructor defaults ────────────────────────────────────────────────────── + def test_default_scale_factors(backend): zne = ZNEMitigation(backend) assert zne.scale_factors == [1.0, 1.5, 2.0, 2.5, 3.0] @@ -66,9 +70,11 @@ def test_default_shots(backend): # ── Executor type dispatch ──────────────────────────────────────────────────── + def test_executor_float_mode_no_annotation(backend): """Float executor must have no return annotation so mitiq treats it as FloatLike.""" import inspect + zne = ZNEMitigation(backend) executor = zne._make_executor() ann = inspect.getfullargspec(executor).annotations @@ -80,6 +86,7 @@ def test_executor_measurement_result_mode_annotation(backend): import inspect from mitiq import MeasurementResult from mitiq import Observable, PauliString + obs = Observable(PauliString("ZZ", support=[0, 1])) zne = ZNEMitigation(backend) executor = zne._make_executor(observable=obs) @@ -89,6 +96,7 @@ def test_executor_measurement_result_mode_annotation(backend): # ── run_unmitigated ─────────────────────────────────────────────────────────── + def test_run_unmitigated_returns_float(backend, ghz, mock_sampler_counts): zne = ZNEMitigation(backend, shots=1000) result = zne.run_unmitigated(ghz) @@ -105,9 +113,11 @@ def test_run_unmitigated_p000(backend, ghz, mock_sampler_counts): # ── run ─────────────────────────────────────────────────────────────────────── + def test_run_uses_linear_factory_by_default(backend, ghz): """Default factory should be LinearFactory, not Richardson.""" from mitiq.zne.inference import LinearFactory + zne = ZNEMitigation(backend, scale_factors=[1.0, 2.0, 3.0]) captured = {} @@ -115,9 +125,9 @@ def fake_execute_with_zne(circuit, executor, **kwargs): captured["factory"] = kwargs.get("factory") return 0.5 - with patch("mitiq.zne.execute_with_zne", side_effect=fake_execute_with_zne), \ - patch("qiskit_calculquebec.mitigation.zne.generate_preset_pass_manager"), \ - patch("qiskit_calculquebec.mitigation.zne.SamplerV2"): + with patch("mitiq.zne.execute_with_zne", side_effect=fake_execute_with_zne), patch( + "qiskit_calculquebec.mitigation.zne.generate_preset_pass_manager" + ), patch("qiskit_calculquebec.mitigation.zne.SamplerV2"): zne.run(ghz) assert isinstance(captured["factory"], LinearFactory) @@ -125,6 +135,7 @@ def fake_execute_with_zne(circuit, executor, **kwargs): def test_run_uses_custom_factory(backend, ghz): from mitiq.zne.inference import RichardsonFactory + factory = RichardsonFactory([1.0, 2.0, 3.0]) zne = ZNEMitigation(backend, factory=factory) captured = {} @@ -133,9 +144,9 @@ def fake_execute_with_zne(circuit, executor, **kwargs): captured["factory"] = kwargs.get("factory") return 0.5 - with patch("mitiq.zne.execute_with_zne", side_effect=fake_execute_with_zne), \ - patch("qiskit_calculquebec.mitigation.zne.generate_preset_pass_manager"), \ - patch("qiskit_calculquebec.mitigation.zne.SamplerV2"): + with patch("mitiq.zne.execute_with_zne", side_effect=fake_execute_with_zne), patch( + "qiskit_calculquebec.mitigation.zne.generate_preset_pass_manager" + ), patch("qiskit_calculquebec.mitigation.zne.SamplerV2"): zne.run(ghz) assert captured["factory"] is factory @@ -149,9 +160,9 @@ def fake_execute_with_zne(circuit, executor, **kwargs): captured["circuit"] = circuit return 0.5 - with patch("mitiq.zne.execute_with_zne", side_effect=fake_execute_with_zne), \ - patch("qiskit_calculquebec.mitigation.zne.generate_preset_pass_manager"), \ - patch("qiskit_calculquebec.mitigation.zne.SamplerV2"): + with patch("mitiq.zne.execute_with_zne", side_effect=fake_execute_with_zne), patch( + "qiskit_calculquebec.mitigation.zne.generate_preset_pass_manager" + ), patch("qiskit_calculquebec.mitigation.zne.SamplerV2"): zne = ZNEMitigation(backend) zne.run(ghz) @@ -159,9 +170,9 @@ def fake_execute_with_zne(circuit, executor, **kwargs): def test_run_returns_real_float(backend, ghz): - with patch("mitiq.zne.execute_with_zne", return_value=complex(0.85, -1e-17)), \ - patch("qiskit_calculquebec.mitigation.zne.generate_preset_pass_manager"), \ - patch("qiskit_calculquebec.mitigation.zne.SamplerV2"): + with patch("mitiq.zne.execute_with_zne", return_value=complex(0.85, -1e-17)), patch( + "qiskit_calculquebec.mitigation.zne.generate_preset_pass_manager" + ), patch("qiskit_calculquebec.mitigation.zne.SamplerV2"): zne = ZNEMitigation(backend) result = zne.run(ghz) @@ -171,7 +182,10 @@ def test_run_returns_real_float(backend, ghz): # ── REM integration ─────────────────────────────────────────────────────────── -def test_run_unmitigated_raises_if_rem_without_qubits(backend, ghz, mock_sampler_counts): + +def test_run_unmitigated_raises_if_rem_without_qubits( + backend, ghz, mock_sampler_counts +): rem = MagicMock() rem.method = "m3" zne = ZNEMitigation(backend) @@ -195,13 +209,14 @@ def test_run_unmitigated_applies_rem_matrix(backend, ghz): sampler_mock = MagicMock() sampler_mock.run.return_value = job_mock - with patch("qiskit_calculquebec.mitigation.zne.SamplerV2", return_value=sampler_mock), \ - patch("qiskit_calculquebec.mitigation.zne.generate_preset_pass_manager") as pm_mock: + with patch( + "qiskit_calculquebec.mitigation.zne.SamplerV2", return_value=sampler_mock + ), patch( + "qiskit_calculquebec.mitigation.zne.generate_preset_pass_manager" + ) as pm_mock: pm_mock.return_value.run.side_effect = lambda c: c zne = ZNEMitigation(backend, shots=1024) result = zne.run_unmitigated(ghz, rem=rem, qubits=[0, 1, 2]) rem.apply_correction.assert_called_once() assert isinstance(result, float) - -