{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "b6d1e3ec",
      "metadata": {},
      "source": [
        "---\n",
        "title: \"Resuelve el modelo de Sherrington-Kirkpatrick con el optimizador «Parity Twine» de ParityQC\"\n",
        "description: \"Tutorial para resolver el modelo de Sherrington-Kirkpatrick con el optimizador «Parity Twine»\"\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",
        "# Resuelve el modelo de Sherrington-Kirkpatrick con el optimizador «Parity Twine» de ParityQC\n",
        "\n"
      ]
    },
    {
      "attachments": {},
      "cell_type": "markdown",
      "id": "a6f69b77",
      "metadata": {},
      "source": [
        "Estimación *de tiempo de ejecución: 10 segundos en un procesador Nighthawk r2. (NOTA: Se trata únicamente de una estimación. (El tiempo de ejecución puede variar.)*\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "21156b6f",
      "metadata": {},
      "source": [
        "<span id=\"learning-outcomes\" />\n",
        "\n",
        "## Resultados del aprendizaje\n",
        "\n",
        "* Utiliza el optimizador Parity Twine para resolver el modelo de Sherrington-Kirkpatrick.\n",
        "* Descubre qué opciones están disponibles en el Parity Twine Optimizer y qué resultados se obtienen.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "d185259f257c1618",
      "metadata": {},
      "source": [
        "<span id=\"background\" />\n",
        "\n",
        "## En segundo plano\n",
        "\n",
        "Este tutorial muestra cómo resolver el modelo de Sherrington-Kirkpatrick utilizando el optimizador «Parity Twine» de ParityQC.\n",
        "\n",
        "Proporciona código para formular el problema de forma totalmente local, en un formato compatible con el optimizador Parity Twine.\n",
        "\n",
        "<span id=\"the-sherrington-kirkpatrick-model\" />\n",
        "\n",
        "### El modelo de Sherrington-Kirkpatrick\n",
        "\n",
        "El modelo de Sherrington-Kirkpatrick (SK) es un modelo fundamental de la mecánica estadística, concretamente en el ámbito del estudio de los vidrios de espín. A diferencia del modelo de Ising estándar, en el que las interacciones suelen\n",
        "limitarse a los vecinos más cercanos, el modelo SK es un modelo de alcance infinito, lo que significa que cada espín interactúa con todos los demás espines del sistema. Esto da lugar a un paisaje energético muy complejo y «accidentado», caracterizado por\n",
        "numerosos mínimos locales, lo cual es el rasgo distintivo del comportamiento vítreo.\n",
        "\n",
        "La característica fundamental del modelo SK radica en la frustración. En el modelo, las intensidades de interacción $J_ij$ entre los espines se distribuyen aleatoriamente entre valores positivos y negativos. Esto da lugar a situaciones (por\n",
        "ejemplo, en disposiciones triangulares) en las que no es posible organizar los espines de forma que se minimicen todas las interacciones al mismo tiempo. En el modelo SK, dado que cada espín interactúa con todos los demás espines, esta frustración se agrava\n",
        "a nivel global, lo que da lugar a una red de restricciones contradictorias.\n",
        "\n",
        "<span id=\"mathematical-formulation\" />\n",
        "\n",
        "### Formulación matemática\n",
        "\n",
        "El estado del sistema viene definido por un conjunto de $N$ espines de Ising, $s_i \\in \\{+1, -1 \\} $. La energía de una configuración concreta viene dada por el hamiltoniano:\n",
        "\n",
        "$H = - \\sum_{1 \\leq i \\leq j \\leq N} J_{ij} s_i s_j$\n",
        "\n",
        "donde $J_{ij}$ es la intensidad del acoplamiento entre el espín $i$ y el espín $j$.\n",
        "\n",
        "En el modelo SK, los acoplamientos $J_{ij}$ son variables aleatorias independientes e idénticamente distribuidas. Para garantizar que la energía siga siendo extensiva (proporcional a $N$ ) a medida que\n",
        "$N \\rightarrow \\infty$, la varianza de los acoplamientos debe escalar con el número de partículas:\n",
        "\n",
        "$J_{ij} \\sim \\mathcal{N} \\left( 0, \\frac{J^2}{N} \\right) $\n",
        "\n",
        "El estado fundamental de un sistema dado $H$ es la configuración específica de los espines ( $s_1, s_2, ...s_n$ ) que minimiza la energía. Encontrar el estado fundamental es un problema de optimización NP-difícil.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "55b94021",
      "metadata": {},
      "source": [
        "<span id=\"requirements\" />\n",
        "\n",
        "## Requisitos\n",
        "\n",
        "Antes de comenzar este tutorial, asegúrate de que tengas instalados los siguientes elementos:\n",
        "\n",
        "* Qiskit Functions Catalog IBM Cliente (`pip install qiskit-ibm-catalog`)\n",
        "* Complemento de Qiskit «Optimization Mapper» (`pip install qiskit_addon_opt_mapper`)\n",
        "* NumPy (`pip install numpy`)\n",
        "\n",
        "También necesitas permiso para acceder a la función « ParityQC » de Twine Optimizer. Para solicitar acceso, rellena este [formulario](https://parityqc.com/products/parity-twine-optimizer/free-trial).\n",
        "\n"
      ]
    },
    {
      "attachments": {},
      "cell_type": "markdown",
      "id": "7db2e559",
      "metadata": {},
      "source": [
        "<span id=\"setup\" />\n",
        "\n",
        "## Configuración\n",
        "\n",
        "(Este código da por hecho que ya has [guardado tu cuenta](/docs/guides/functions-get-started#install-qiskit-functions-catalog-client) en tu entorno local.)\n",
        "\n",
        "En primer lugar, importa todos los paquetes necesarios para este 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": [
        "Carga el «Parity Twine Optimizer» del catálogo « 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",
        "### Paso 1: Definir el problema como una función objetivo\n",
        "\n",
        "En lugar de obtener el problema SK de una biblioteca, como hacemos con el [problema «Market Split»](/docs/tutorials/parity-twine-optimizer-ms), lo formulas directamente.\n",
        "\n",
        "La función `generate_sk_problem` formula el problema SK directamente en el formato de diccionario requerido. El único dato `n`que hay que introducir es el número de giros del modelo.\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 :func:`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",
        "        A ``ProblemRepresentation`` 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": [
        "Utiliza la función `_generate_couplings_sk_model` para calcular los términos de acoplamiento aleatorios en el modelo SK. Para tener un mayor control sobre los acoplamientos, puedes utilizar argumentos opcionales, que se explican en la cadena de documentación de la función.\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",
        "### Paso 2: Resuelve el problema utilizando el «Parity Twine Optimizer»\n",
        "\n",
        "Con las funciones anteriores, puedes plantear el problema SK y encontrar una solución utilizando el optimizador de Twine y el backend de IBM Quantum® que elijas.\n",
        "\n",
        "Para ejecutar la función, elige un backend adecuado; por ejemplo, ibm\\_phoenix.\n",
        "\n",
        "Si lo deseas, puedes utilizar las opciones para tener un mayor control sobre el envío:\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": [
        "donde es `shots` un número entero que especifica el número de ejecuciones del circuito, `postprocessing_level` determina si se aplica un posprocesamiento al resultado,\n",
        "`transpile_only` determina si el problema solo se transpila a un circuito (y no se resuelve), y `job_tags` es una etiqueta para identificar el trabajo en IBM Quantum Platform.\n",
        "\n",
        "El tamaño del modelo SK viene definido por $N$, el número de espines que interactúan. Una vez que elijas « $N$ », el código anterior genera el problema para `n_spins`.\n",
        "\n",
        "Ejecuta el optimizador:\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": [
        "Comprueba el estado del trabajo:\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": [
        "Obtener resultados:\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": [
        "El resultado tiene la siguiente forma:\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",
        "donde el diccionario `solution` se corresponde con los qubits definidos en el problema y proporciona sus valores de espín optimizados para el hamiltoniano del modelo SK.\n",
        "Esta secuencia concreta de giros en la solución óptima representa la configuración que minimiza la energía total del sistema en función de las intensidades de interacción aleatorias dadas.\n",
        "En el modelo SK, esto puede considerarse como el estado de menor energía de un sistema magnético desordenado.\n",
        "\n",
        "`metadata` ofrece información sobre la transpilación (número de puertas de dos qubits/profundidad, puertas utilizadas, qubits activos) y diversos tiempos de ejecución.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "2cb5785c",
      "metadata": {},
      "source": [
        "<span id=\"next-steps\" />\n",
        "\n",
        "## Próximos pasos\n",
        "\n",
        "<Admonition type=\"tip\" title=\"Recomendaciones\">\n",
        "  * Solicita acceso a la función rellenando este [formulario](https://parityqc.com/products/parity-twine-optimizer/free-trial).\n",
        "  * Consulta la [referencia](/docs/api/functions/parity-twine-optimizer) de la API de esta función de Qiskit.\n",
        "  * Lee la [guía](/docs/guides/parity-twine-optimizer).\n",
        "  * Prueba el [tutorial](/docs/tutorials/parity-twine-optimizer-ms) sobre cómo aplicar el «Parity Twine Optimizer» al problema de la división del mercado.\n",
        "  * Consulta el artículo [«Connectivity-aware Synthesis of Quantum Algorithms», de Drier et al. (2025), disponible como](https://arxiv.org/abs/2501.14020) preimpresión en 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
}