{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "b6d1e3ec",
      "metadata": {},
      "source": [
        "---\n",
        "title: \"Risolvi il modello di Sherrington-Kirkpatrick con l'ottimizzatore Parity Twine di ParityQC\"\n",
        "description: \"Tutorial per risolvere il modello di Sherrington-Kirkpatrick con il Parity Twine Optimizer\"\n",
        "---\n",
        "\n",
        "{/* cspell:ignore parityqc QOBLIB marketsplit independentset */}\n",
        "\n",
        "<span id=\"solve-the-sherrington-kirkpatrick-model-with-the-parityqc-parity-twine-optimizer\" />\n",
        "\n",
        "# Risolvi il modello di Sherrington-Kirkpatrick con l'ottimizzatore Parity Twine di ParityQC\n",
        "\n"
      ]
    },
    {
      "attachments": {},
      "cell_type": "markdown",
      "id": "a6f69b77",
      "metadata": {},
      "source": [
        "Stima dei tempi di *esecuzione: 10 secondi su un processore Nighthawk r2. (NOTA: Si tratta solo di una stima. (La durata potrebbe variare.)*\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "21156b6f",
      "metadata": {},
      "source": [
        "<span id=\"learning-outcomes\" />\n",
        "\n",
        "## Risultati di apprendimento\n",
        "\n",
        "* Utilizza il Parity Twine Optimizer per risolvere il modello di Sherrington-Kirkpatrick.\n",
        "* Scopri quali opzioni sono disponibili per il Parity Twine Optimizer e quali risultati vengono generati.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "d185259f257c1618",
      "metadata": {},
      "source": [
        "<span id=\"background\" />\n",
        "\n",
        "## Sfondo\n",
        "\n",
        "Questo tutorial illustra come risolvere il modello di Sherrington-Kirkpatrick utilizzando l'ottimizzatore Parity Twine di ParityQC.\n",
        "\n",
        "Fornisce il codice necessario per formulare il problema interamente a livello locale in una forma che possa interfacciarsi con il Parity Twine Optimizer.\n",
        "\n",
        "<span id=\"the-sherrington-kirkpatrick-model\" />\n",
        "\n",
        "### Il modello di Sherrington-Kirkpatrick\n",
        "\n",
        "Il modello di Sherrington-Kirkpatrick (SK) è un modello fondamentale della meccanica statistica, in particolare nell'ambito dello studio dei vetri di spin. A differenza del modello di Ising standard, in cui le interazioni sono solitamente\n",
        "limitate ai vicini più prossimi, il modello SK è un modello a raggio infinito, il che significa che ogni spin interagisce con ogni altro spin del sistema. Ciò porta a un panorama energetico estremamente complesso e “irregolare”, caratterizzato da\n",
        "numerosi minimi locali, che costituiscono il tratto distintivo del comportamento vetroso.\n",
        "\n",
        "La caratteristica fondamentale del modello SK risiede nella frustrazione. Nel modello, le intensità di interazione $J_{ij}$ tra gli spin sono distribuite in modo casuale tra valori positivi e negativi. Ciò porta a situazioni (ad\n",
        "esempio, negli schemi triangolari) in cui non è possibile disporre gli spin in modo da minimizzare contemporaneamente tutte le interazioni. Nel modello SK, poiché ogni spin interagisce con tutti gli altri spin, questa frustrazione si aggrava\n",
        "a livello globale, dando origine a una rete di vincoli contrastanti.\n",
        "\n",
        "<span id=\"mathematical-formulation\" />\n",
        "\n",
        "### Formulazione matematica\n",
        "\n",
        "Lo stato del sistema è definito da un insieme di $N$ spin di Ising, $s_i \\in \\{+1, -1 \\} $. L'energia di una specifica configurazione è data dall'hamiltoniano:\n",
        "\n",
        "$H = - \\sum_{1 \\leq i \\leq j \\leq N} J_{ij} s_i s_j$\n",
        "\n",
        "dove $J_{ij}$ è l'intensità di accoppiamento tra lo spin $i$ e lo spin $j$.\n",
        "\n",
        "Nel modello SK, i coefficienti di accoppiamento $J_{ij}$ sono variabili casuali indipendenti e identicamente distribuite. Per garantire che l'energia rimanga estensiva (proporzionale a $N$ ) come\n",
        "$N \\rightarrow \\infty$, la varianza degli accoppiamenti deve essere proporzionale al numero di particelle:\n",
        "\n",
        "$J_{ij} \\sim \\mathcal{N} \\left( 0, \\frac{J^2}{N} \\right) $\n",
        "\n",
        "Lo stato fondamentale di una data $H$ è la configurazione specifica degli spin ( $s_1, s_2, ...s_n$ ) che minimizza l'energia. Trovare lo stato fondamentale è un problema di ottimizzazione NP-difficile.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "55b94021",
      "metadata": {},
      "source": [
        "<span id=\"requirements\" />\n",
        "\n",
        "## Requisiti\n",
        "\n",
        "Prima di iniziare questo tutorial, assicurati che siano installati i seguenti componenti:\n",
        "\n",
        "* Qiskit Functions Catalog IBM Cliente (`pip install qiskit-ibm-catalog`)\n",
        "* Componente aggiuntivo Qiskit Optimization Mapper (`pip install qiskit_addon_opt_mapper`)\n",
        "* NumPy (`pip install numpy`)\n",
        "\n",
        "È inoltre necessario disporre dei permessi necessari per accedere alla funzione \" ParityQC \" di Twine Optimizer. Per richiedere l'accesso, compila questo [modulo](https://parityqc.com/products/parity-twine-optimizer/free-trial).\n",
        "\n"
      ]
    },
    {
      "attachments": {},
      "cell_type": "markdown",
      "id": "7db2e559",
      "metadata": {},
      "source": [
        "<span id=\"setup\" />\n",
        "\n",
        "## Configura\n",
        "\n",
        "(Questo codice presuppone che tu abbia già [salvato il tuo account](/docs/guides/functions-get-started#install-qiskit-functions-catalog-client) nell'ambiente locale.)\n",
        "\n",
        "Per prima cosa, importa tutti i pacchetti necessari per questo tutorial.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "bc380c46",
      "metadata": {},
      "outputs": [],
      "source": [
        "import numpy as np\n",
        "\n",
        "from qiskit_ibm_catalog import QiskitFunctionsCatalog"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "6d808e11c0992c07",
      "metadata": {},
      "source": [
        "Carica il “Parity Twine Optimizer” dal catalogo “ Qiskit Functions ”:\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "c4a7dc1cbb3d35ff",
      "metadata": {},
      "outputs": [],
      "source": [
        "catalog = QiskitFunctionsCatalog(channel=\"ibm_quantum_platform\")\n",
        "function = catalog.load(\"parityqc/parity-twine-optimizer\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "3071bc21ac4f3484",
      "metadata": {},
      "source": [
        "<span id=\"step-1-define-the-problem-as-an-objective-function\" />\n",
        "\n",
        "### Fase 1: Definire il problema come funzione obiettivo\n",
        "\n",
        "Anziché ricavare il problema SK da una libreria, come facciamo per il [problema della suddivisione del mercato](/docs/tutorials/parity-twine-optimizer-ms), lo si formula direttamente.\n",
        "\n",
        "La funzione `generate_sk_problem` formula il problema SK direttamente nel formato dizionario richiesto. L'unico dato da inserire è il `n` numero di giri previsti dal modello.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "d4c3b37d482e4f56",
      "metadata": {},
      "outputs": [],
      "source": [
        "def generate_sk_problem(\n",
        "    n: int,\n",
        "    coupling_mean: float = 0.0,\n",
        "    coupling_std: float = 1.0,\n",
        "    local_fields_mean: float = 0.0,\n",
        "    local_fields_std: float = 0.0,\n",
        "    edge_density: float = 1.0,\n",
        "    ensure_extensivity: bool = False,\n",
        "    seed: int | None = None,\n",
        ") -> dict:\n",
        "    \"\"\"Generate the Sherrington-Kirkpatrick (SK) model with varying\n",
        "     edge density.\n",
        "\n",
        "    Samples couplings and local fields via ``generate_couplings_sk_model``\n",
        "    and assembles the corresponding Ising Hamiltonian\n",
        "\n",
        "        H = -∑_{i<j} J_ij z_i z_j - ∑_i h_i z_i,\n",
        "\n",
        "    where z_i ∈ {-1, +1}.\n",
        "\n",
        "    Args:\n",
        "        n: Number of spins (>= 2).\n",
        "        coupling_mean: Mean coupling before optional SK scaling.\n",
        "        coupling_std: Coupling std before optional SK scaling.\n",
        "        local_fields_mean: Mean longitudinal field.\n",
        "        local_fields_std: Std of the longitudinal fields.\n",
        "        edge_density: Fraction of non-zero couplings, in ``[2/n, 1]``.\n",
        "        ensure_extensivity: Whether to apply the SK 1/n scaling.\n",
        "        seed: random number generator seed.\n",
        "\n",
        "    Returns:\n",
        "        An encoding the SK Hamiltonian.\n",
        "\n",
        "    Raises:\n",
        "        ValueError: If ``n < 2``, ``coupling_std < 0``, ``local_fields_std < 0``,\n",
        "            or ``edge_density`` is outside ``[2/n, 1]``.\n",
        "    \"\"\"\n",
        "    couplings, local_fields = _generate_couplings_sk_model(\n",
        "        n=n,\n",
        "        coupling_mean=coupling_mean,\n",
        "        coupling_std=coupling_std,\n",
        "        local_fields_mean=local_fields_mean,\n",
        "        local_fields_std=local_fields_std,\n",
        "        edge_density=edge_density,\n",
        "        ensure_extensivity=ensure_extensivity,\n",
        "        seed=seed,\n",
        "    )\n",
        "\n",
        "    # Handle quadratic terms: coupling[i, j] * zj[i] * zj[j]\n",
        "    # Only iterate over the upper triangle (i < j)\n",
        "    sk_problem = {\n",
        "        str((i, j)): float(couplings[i, j])\n",
        "        for i in range(n)\n",
        "        for j in range(i + 1, n)\n",
        "        if couplings[i, j] != 0\n",
        "    }\n",
        "\n",
        "    # Handle linear terms: local_fields[i] * zj[i]\n",
        "    sk_problem.update(\n",
        "        {\n",
        "            str((i,)): float(local_fields[i])\n",
        "            for i in range(n)\n",
        "            if local_fields[i] != 0\n",
        "        }\n",
        "    )\n",
        "\n",
        "    return sk_problem"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c0caec311cb77bf6",
      "metadata": {},
      "source": [
        "Utilizzare la funzione `_generate_couplings_sk_model` per calcolare i termini di accoppiamento casuali nel modello SK. Per una maggiore controllo sugli accoppiamenti, è possibile utilizzare argomenti opzionali, descritti nella stringa di documentazione della funzione.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "cb32954d63628202",
      "metadata": {},
      "outputs": [],
      "source": [
        "def _generate_couplings_sk_model(\n",
        "    n: int,\n",
        "    coupling_mean: float = 0.0,\n",
        "    coupling_std: float = 1.0,\n",
        "    local_fields_mean: float = 0.0,\n",
        "    local_fields_std: float = 0.0,\n",
        "    edge_density: float = 1.0,\n",
        "    ensure_extensivity: bool = False,\n",
        "    seed: int | None = None,\n",
        ") -> tuple[np.ndarray, np.ndarray]:\n",
        "    \"\"\"Generate random couplings and local fields for an Ising / SK model.\n",
        "\n",
        "    Couplings are Gaussian. With ``ensure_extensivity=True`` they follow the\n",
        "    Sherrington-Kirkpatrick scaling ``J_ij ~ N(coupling_mean/n, coupling_std^2/n)``\n",
        "    (extensive energy, O(n)); otherwise ``J_ij ~ N(coupling_mean, coupling_std^2)``\n",
        "    (energy O(n^2)). Fields are ``h_i ~ N(local_fields_mean, local_fields_std^2)``.\n",
        "\n",
        "    ``edge_density`` sets the fraction of the ``n*(n-1)/2`` possible couplings that\n",
        "    are non-zero (1 = fully dense). The kept edges always include a random spanning\n",
        "    tree, so the interaction graph is guaranteed connected. This requires at least\n",
        "    ``n-1`` edges, so ``edge_density`` must be at least ``2/n``.\n",
        "\n",
        "    Args:\n",
        "        n: Number of spins (>= 2).\n",
        "        coupling_mean: Mean coupling before optional SK scaling.\n",
        "        coupling_std: Coupling std before optional SK scaling.\n",
        "        local_fields_mean: Mean longitudinal field.\n",
        "        local_fields_std: Std of the longitudinal fields.\n",
        "        edge_density: Fraction of non-zero couplings, in ``[2/n, 1]``.\n",
        "        ensure_extensivity: Whether to apply the SK 1/n scaling.\n",
        "        seed: random number generator seed.\n",
        "\n",
        "    Returns:\n",
        "        Tuple ``(couplings, fields)``: a symmetric ``(n, n)`` matrix with zero\n",
        "        diagonal, and an ``(n,)`` field vector.\n",
        "\n",
        "    Raises:\n",
        "        ValueError: If ``n < 2``, ``coupling_std < 0``, ``local_fields_std < 0``,\n",
        "            or ``edge_density`` is outside ``[2/n, 1]``.\n",
        "    \"\"\"\n",
        "    if n < 2:\n",
        "        raise ValueError(f\"n must be >= 2, got {n}\")\n",
        "    if coupling_std < 0 or local_fields_std < 0:\n",
        "        raise ValueError(\n",
        "            \"coupling_std and local_fields_std must be non-negative\"\n",
        "        )\n",
        "\n",
        "    # A connected graph on n nodes needs at least n-1 of the n*(n-1)/2 possible\n",
        "    # edges, so edge_density has a hard lower bound of 2/n.\n",
        "    min_edge_density = 2.0 / n\n",
        "    if not min_edge_density <= edge_density <= 1.0:\n",
        "        raise ValueError(\n",
        "            f\"edge_density must be in [{min_edge_density:.4g}, 1] for n={n} \"\n",
        "            f\"(at least n-1 edges are needed to keep the graph connected), \"\n",
        "            f\"got {edge_density}\"\n",
        "        )\n",
        "\n",
        "    rng = np.random.default_rng(seed)\n",
        "\n",
        "    j_loc, j_scale = (\n",
        "        (coupling_mean / n, coupling_std / np.sqrt(n))\n",
        "        if ensure_extensivity\n",
        "        else (coupling_mean, coupling_std)\n",
        "    )\n",
        "\n",
        "    upper_idx = np.triu_indices(n, k=1)\n",
        "    n_edges = len(upper_idx[0])\n",
        "\n",
        "    # Select which edges are present.\n",
        "    if edge_density < 1.0:\n",
        "        n_keep = int(round(edge_density * n_edges))\n",
        "        # Map each (i, j) node pair to its position in the flat upper-triangle list.\n",
        "        pair_to_flat = {\n",
        "            (int(i), int(j)): idx\n",
        "            for idx, (i, j) in enumerate(\n",
        "                zip(upper_idx[0], upper_idx[1], strict=False)\n",
        "            )\n",
        "        }\n",
        "\n",
        "        # Random spanning tree: node perm[k] links to a random earlier node.\n",
        "        perm = rng.permutation(n)\n",
        "        keep = np.zeros(n_edges, dtype=bool)\n",
        "        for k in range(1, n):\n",
        "            child, parent = perm[k], perm[rng.integers(0, k)]\n",
        "            i, j = min(child, parent), max(child, parent)\n",
        "            keep[pair_to_flat[(int(i), int(j))]] = True\n",
        "\n",
        "        # Fill the remaining budget with random non-tree edges.\n",
        "        remaining = n_keep - (n - 1)\n",
        "        if remaining > 0:\n",
        "            keep[\n",
        "                rng.choice(\n",
        "                    np.flatnonzero(~keep), size=remaining, replace=False\n",
        "                )\n",
        "            ] = True\n",
        "    else:\n",
        "        keep = np.ones(n_edges, dtype=bool)\n",
        "\n",
        "    n_present = int(keep.sum())\n",
        "    if j_scale == 0.0:\n",
        "        vals = np.full(n_present, j_loc)\n",
        "    else:\n",
        "        vals = rng.normal(loc=j_loc, scale=j_scale, size=n_present)\n",
        "\n",
        "    couplings = np.zeros((n, n))\n",
        "    couplings[upper_idx[0][keep], upper_idx[1][keep]] = vals\n",
        "    couplings += couplings.T  # symmetrize; diagonal stays zero\n",
        "\n",
        "    fields = (\n",
        "        np.full(n, local_fields_mean)\n",
        "        if local_fields_std == 0.0\n",
        "        else rng.normal(loc=local_fields_mean, scale=local_fields_std, size=n)\n",
        "    )\n",
        "\n",
        "    return couplings, fields"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "cbc412ce550b1e6c",
      "metadata": {},
      "source": [
        "<span id=\"step-2-solve-the-problem-using-the-parity-twine-optimizer\" />\n",
        "\n",
        "### Fase 2: Risolvere il problema utilizzando il Parity Twine Optimizer\n",
        "\n",
        "Grazie alle funzioni sopra descritte, è possibile impostare il problema SK e trovare una soluzione utilizzando Twine Optimizer e un backend IBM Quantum® a scelta.\n",
        "\n",
        "Per eseguire la funzione, scegliere un backend adeguato; ad esempio, ibm\\_phoenix.\n",
        "\n",
        "Se lo si desidera, è possibile utilizzare le opzioni per un maggiore controllo sull'invio:\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "9dcece43ce7ad415",
      "metadata": {},
      "outputs": [],
      "source": [
        "options = {\n",
        "    \"shots\": 100000,\n",
        "    \"postprocessing_level\": 1,\n",
        "    \"transpile_only\": False,\n",
        "    \"job_tags\": [\"sk\"],\n",
        "}"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "f5f18f4df42cb260",
      "metadata": {},
      "source": [
        "dove `shots` è un numero intero che specifica il numero di esecuzioni del circuito, `postprocessing_level` determina se al risultato viene applicata una post-elaborazione,\n",
        "`transpile_only` sceglie se il problema viene solo transpilato in un circuito (senza essere risolto) e `job_tags` è un’etichetta che identifica il lavoro su IBM Quantum Platform.\n",
        "\n",
        "La dimensione del modello SK è definita d $N$ e, ovvero il numero di spin interagenti. Una volta scelto $N$, il codice sopra riportato genera il problema per `n_spins`.\n",
        "\n",
        "Esegui l'ottimizzatore:\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "d36e4ab4ca0a4a08",
      "metadata": {},
      "outputs": [],
      "source": [
        "n_spins = 50\n",
        "sk_problem = generate_sk_problem(n_spins)\n",
        "\n",
        "function_job = function.run(\n",
        "    problem=sk_problem,\n",
        "    variable_type=\"spin\",\n",
        "    backend_name=\"ibm_phoenix\",\n",
        "    options=options,\n",
        ")\n",
        "print(f\"Job ID: {function_job.job_id}\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "8d4d7fd9e9fa2448",
      "metadata": {},
      "source": [
        "Verifica lo stato del lavoro:\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "c42621d5cdc6b25",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Monitor the job status\n",
        "function_job.status()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "a9ec094ef5bcdac2",
      "metadata": {},
      "source": [
        "Recupera i risultati:\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "c10e7dd2b49e7e36",
      "metadata": {},
      "outputs": [],
      "source": [
        "result = function_job.result()\n",
        "\n",
        "result"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "d993123e6787e5af",
      "metadata": {},
      "source": [
        "Il risultato ha la forma seguente:\n",
        "\n",
        "```\n",
        "{\n",
        "    'solution': {'0': 1, '1': 1, '10': 1, '11': 1, ... },\n",
        "    'objective_value':  -240.5425312543882,\n",
        "    'solution_bitstring': '00001101110100100111001110101001101111111011001110',\n",
        "    'metadata': {\n",
        "        'circuit_metrics': {\n",
        "            'depth': 523,\n",
        "            'gate_count': 10118,\n",
        "            'two_qubit_gate_depth': 196,\n",
        "            'two_qubit_gate_count': 2499,\n",
        "            'num_qubits': 50,\n",
        "            'operations': {'sx': 3353, 'rz': 3320, 'cz': 2499, 'delay': 894, 'measure': 50, 'x': 2},\n",
        "        },\n",
        "        'solver_info': {\n",
        "            'variable_mapping': {'0': 0, '1': 1, '10': 2, '11': 3, ... },\n",
        "            'bitstring_distributions': {\n",
        "                'before_postprocessing': {'011101110010110111001110011000': 1, ... },\n",
        "                'after_postprocessing': {'011011110000110101001111011000': 1, ... }\n",
        "            },\n",
        "            'best_parameters': {\n",
        "                'beta': [-0.4602084830507902],\n",
        "                'gamma': [1.8500357096574955]\n",
        "            }\n",
        "        },\n",
        "        'resource_usage': {\n",
        "            'RUNNING: MAPPING': {'CPU_TIME': 290.272},\n",
        "            'RUNNING: OPTIMIZING_FOR_HARDWARE': {'CPU_TIME': 0.494},\n",
        "            'RUNNING: WAITING_FOR_QPU': {'CPU_TIME': 8.775},\n",
        "            'RUNNING: EXECUTING_QPU': {'QPU_TIME': 31.0},\n",
        "            'RUNNING: POST_PROCESSING': {'CPU_TIME': 162.96},\n",
        "        },\n",
        "    }\n",
        "}\n",
        "```\n",
        "\n",
        "dove il dizionario `solution` corrisponde ai qubit definiti nel problema e fornisce i loro valori di spin ottimizzati per l'Hamiltoniano del modello SK.\n",
        "Questa specifica sequenza di spin nella soluzione ottimale rappresenta la configurazione che minimizza l'energia totale del sistema in base alle intensità di interazione casuali date.\n",
        "Nel modello SK, questo può essere considerato lo stato di minima energia di un sistema magnetico disordinato.\n",
        "\n",
        "`metadata` fornisce informazioni sulla transpilazione (numero di gate per due qubit/profondità, gate utilizzati, qubit attivi) e sui vari tempi di esecuzione.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "2cb5785c",
      "metadata": {},
      "source": [
        "<span id=\"next-steps\" />\n",
        "\n",
        "## Passi successivi\n",
        "\n",
        "<Admonition type=\"tip\" title=\"Suggerimenti\">\n",
        "  * Richiedi l'accesso alla funzione compilando questo [modulo](https://parityqc.com/products/parity-twine-optimizer/free-trial).\n",
        "  * Consulta la documentazione [di riferimento dell'API](/docs/api/functions/parity-twine-optimizer) relativa a questa funzione di Qiskit.\n",
        "  * Leggi la [guida](/docs/guides/parity-twine-optimizer).\n",
        "  * Prova il [tutorial](/docs/tutorials/parity-twine-optimizer-ms) sull’applicazione del Parity Twine Optimizer al problema della suddivisione del mercato.\n",
        "  * Si veda il preprint [“Connectivity-aware Synthesis of Quantum Algorithms”, Drier et al. (2025)](https://arxiv.org/abs/2501.14020) ArXiv.\n",
        "</Admonition>\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "id": "a1b8767d",
      "source": "© IBM Corp., 2017-2026"
    }
  ],
  "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"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 5
}