{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "b6d1e3ec",
      "metadata": {},
      "source": [
        "---\n",
        "title: \"Resolva o modelo de Sherrington-Kirkpatrick com o Otimizador de Cordões de Paridade da ParityQC\"\n",
        "description: \"Tutorial para resolver o modelo de Sherrington-Kirkpatrick com o 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",
        "# Resolva o modelo de Sherrington-Kirkpatrick com o Otimizador de Cordões de Paridade da ParityQC\n",
        "\n"
      ]
    },
    {
      "attachments": {},
      "cell_type": "markdown",
      "id": "a6f69b77",
      "metadata": {},
      "source": [
        "Estimativa de *tempo de execução: 10 segundos em um processador Nighthawk r2. (OBSERVAÇÃO: Trata-se apenas de uma estimativa. (O tempo de execução pode variar.)*\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "21156b6f",
      "metadata": {},
      "source": [
        "<span id=\"learning-outcomes\" />\n",
        "\n",
        "## Resultados do aprendizado\n",
        "\n",
        "* Utilize o Parity Twine Optimizer para resolver o modelo de Sherrington-Kirkpatrick.\n",
        "* Saiba quais opções do Parity Twine Optimizer estão disponíveis e quais resultados são gerados.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "d185259f257c1618",
      "metadata": {},
      "source": [
        "<span id=\"background\" />\n",
        "\n",
        "## Segundo plano\n",
        "\n",
        "Este tutorial mostra como resolver o modelo de Sherrington-Kirkpatrick utilizando o Parity Twine Optimizer da ParityQC.\n",
        "\n",
        "Ele fornece código para formular o problema de forma totalmente local, de maneira que possa interagir com o Otimizador Parity Twine.\n",
        "\n",
        "<span id=\"the-sherrington-kirkpatrick-model\" />\n",
        "\n",
        "### O modelo de Sherrington-Kirkpatrick\n",
        "\n",
        "O modelo de Sherrington-Kirkpatrick (SK) é um modelo fundamental na mecânica estatística, especificamente no âmbito do estudo dos vidros de spin. Ao contrário do modelo de Ising padrão, no qual as interações geralmente\n",
        "se limitam aos vizinhos mais próximos, o modelo SK é um modelo de alcance infinito, o que significa que cada spin interage com todos os outros spins do sistema. Isso leva a um cenário energético altamente complexo e “irregular”, caracterizado por\n",
        "muitos mínimos locais, o que é a marca registrada do comportamento vítreo.\n",
        "\n",
        "A característica central do modelo SK reside na frustração. No modelo, as intensidades de interação $J_ij$ entre os spins são distribuídas aleatoriamente entre valores positivos e negativos. Isso leva a situações (por\n",
        "exemplo, em arranjos triangulares) em que os spins não podem ser dispostos de forma a minimizar todas as interações simultaneamente. No modelo SK, como cada spin interage com todos os outros spins, essa frustração se agrava\n",
        "globalmente, levando a uma rede de restrições conflitantes.\n",
        "\n",
        "<span id=\"mathematical-formulation\" />\n",
        "\n",
        "### Formulação matemática\n",
        "\n",
        "O estado do sistema é definido por um conjunto de $N$ espins de Ising, $s_i \\in \\{+1, -1 \\} $. A energia de uma configuração específica é dada pelo hamiltoniano:\n",
        "\n",
        "$H = - \\sum_{1 \\leq i \\leq j \\leq N} J_{ij} s_i s_j$\n",
        "\n",
        "onde $J_{ij}$ é a intensidade do acoplamento entre o spin $i$ e o spin $j$.\n",
        "\n",
        "No modelo SK, os acoplamentos $J_{ij}$ são variáveis aleatórias independentes e com distribuição idêntica. Para garantir que a energia permaneça extensiva (proporcional a $N$ ) à medida que\n",
        "$N \\rightarrow \\infty$, a variância dos acoplamentos deve variar proporcionalmente ao número de partículas:\n",
        "\n",
        "$J_{ij} \\sim \\mathcal{N} \\left( 0, \\frac{J^2}{N} \\right) $\n",
        "\n",
        "O estado fundamental de um determinado sistema de spin-1-2 ( $H$ ) é a configuração específica dos spins ( $s_1, s_2, ...s_n$ ) que minimiza a energia. Encontrar o estado fundamental é um problema de otimização NP-difícil.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "55b94021",
      "metadata": {},
      "source": [
        "<span id=\"requirements\" />\n",
        "\n",
        "## Requisitos\n",
        "\n",
        "Antes de iniciar este tutorial, certifique-se de que os seguintes itens estejam instalados:\n",
        "\n",
        "* Qiskit Functions Catalog IBM Cliente (`pip install qiskit-ibm-catalog`)\n",
        "* Componente adicional do Qiskit: Optimization Mapper (`pip install qiskit_addon_opt_mapper`)\n",
        "* NumPy (`pip install numpy`)\n",
        "\n",
        "Você também precisa de permissão para acessar a função “ ParityQC ” do Twine Optimizer. Para solicitar acesso, preencha este [formulário](https://parityqc.com/products/parity-twine-optimizer/free-trial).\n",
        "\n"
      ]
    },
    {
      "attachments": {},
      "cell_type": "markdown",
      "id": "7db2e559",
      "metadata": {},
      "source": [
        "<span id=\"setup\" />\n",
        "\n",
        "## Instalação\n",
        "\n",
        "(Este código pressupõe que você já tenha [salvo sua conta](/docs/guides/functions-get-started#install-qiskit-functions-catalog-client) no seu ambiente local.)\n",
        "\n",
        "Primeiro, importe todos os pacotes necessários 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": [
        "Carregue o Parity Twine Optimizer do 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",
        "### Etapa 1: Definir o problema como uma função-objetivo\n",
        "\n",
        "Em vez de obter o problema SK a partir de uma biblioteca, como fazemos no caso do [problema de divisão de mercado](/docs/tutorials/parity-twine-optimizer-ms), você o formula diretamente.\n",
        "\n",
        "A função `generate_sk_problem` formula o problema SK diretamente no formato de dicionário exigido. A única informação necessária `n` é o número de giros no 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": [
        "Utilize a função `_generate_couplings_sk_model` para calcular os termos de acoplamento aleatórios no modelo SK. Para ter mais controle sobre os acoplamentos, você pode usar argumentos opcionais, que são explicados na descrição da função.\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",
        "### Etapa 2: Resolva o problema usando o Otimizador de Cordas de Paridade\n",
        "\n",
        "Com as funções acima, você pode definir o problema SK e encontrar uma solução usando o Twine Optimizer e um backend do tipo “ IBM Quantum® ” de sua escolha.\n",
        "\n",
        "Para executar a função, escolha um backend adequado; por exemplo, ibm\\_phoenix.\n",
        "\n",
        "Opcionalmente, utilize as opções para ter maior controle sobre o envio:\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": [
        "onde `shots` é um número inteiro que especifica o número de execuções do circuito, `postprocessing_level` determina se o pós-processamento é aplicado ao resultado,\n",
        "`transpile_only` determina se o problema é apenas compilado para um circuito (e não resolvido), e `job_tags` é um rótulo para identificar o trabalho em IBM Quantum Platform.\n",
        "\n",
        "O tamanho do modelo SK é definido por $N$, o número de spins que interagem. Depois de escolher $N$, o código acima gera o problema para `n_spins`.\n",
        "\n",
        "Execute o otimizador:\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": [
        "Verifique o status do trabalho:\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": [
        "Recuperar 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": [
        "O resultado tem a seguinte 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",
        "onde o dicionário `solution` corresponde aos qubits definidos no problema e fornece seus valores de spin otimizados para o hamiltoniano do modelo SK.\n",
        "Essa sequência específica de giros na solução ótima representa a configuração que minimiza a energia total do sistema com base nas intensidades de interação aleatórias fornecidas.\n",
        "No modelo SK, isso pode ser considerado como o estado de menor energia de um sistema magnético desordenado.\n",
        "\n",
        "`metadata` fornece informações sobre a transpilação (contagem de portas de dois qubits/profundidade, portas utilizadas, qubits ativos) e vários tempos de execução.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "2cb5785c",
      "metadata": {},
      "source": [
        "<span id=\"next-steps\" />\n",
        "\n",
        "## Próximas etapas\n",
        "\n",
        "<Admonition type=\"tip\" title=\"Recomendações\">\n",
        "  * Solicite acesso à função preenchendo este [formulário](https://parityqc.com/products/parity-twine-optimizer/free-trial).\n",
        "  * Acesse a [referência](/docs/api/functions/parity-twine-optimizer) da API desta função do Qiskit.\n",
        "  * Leia o [guia](/docs/guides/parity-twine-optimizer).\n",
        "  * Experimente o [tutorial](/docs/tutorials/parity-twine-optimizer-ms) sobre como aplicar o Otimizador de Parity Twine ao problema da divisão de mercado.\n",
        "  * Consulte o artigo [“Connectivity-aware Synthesis of Quantum Algorithms”, de Drier et al. (2025), disponível como](https://arxiv.org/abs/2501.14020) pré-impressão em 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
}