{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "c490a5a6-9fc6-493a-8e98-d89178774925",
   "metadata": {
    "nbgrader": {
     "grade": false,
     "grade_id": "cell-5fc895a541c69988",
     "locked": true,
     "schema_version": 3,
     "solution": false,
     "task": false
    }
   },
   "source": [
    "# TP problème inverse avec des PINNs\n",
    "\n",
    "**Marc Buffat** Dpt mécanique, UCB Lyon 1\n",
    "\n",
    "![problème inverse](PINNS_inversepble.png)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "93db920a-98f5-494a-9d20-f8b564d3489c",
   "metadata": {
    "nbgrader": {
     "grade": false,
     "grade_id": "cell-e6949867461db4d0",
     "locked": true,
     "schema_version": 3,
     "solution": false,
     "task": false
    }
   },
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "**ERREUR:** numéro d'étudiant non spécifié!!!"
      ],
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/markdown": [
       "**Login étudiant Marc BUFFAT uid=137764122**"
      ],
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Max threads : 4 / used threads 4\n",
      "Attention: no GPU available! using CPU\n",
      "Cuda GPU device:  cpu\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/home/buffat/venvs/jupyter/lib/python3.10/site-packages/torch/cuda/__init__.py:107: UserWarning: CUDA initialization: The NVIDIA driver on your system is too old (found version 9010). Please update your GPU driver by downloading and installing a new version from the URL: http://www.nvidia.com/Download/index.aspx Alternatively, go to: https://pytorch.org to install a PyTorch version that has been compiled with your version of the CUDA driver. (Triggered internally at ../c10/cuda/CUDAFunctions.cpp:109.)\n",
      "  return torch._C._cuda_getDeviceCount() > 0\n"
     ]
    }
   ],
   "source": [
    "# initialisation: (ne pas re-executer sauf en cas de restart)\n",
    "import numpy as np\n",
    "import os, time\n",
    "from IPython.display import display,Markdown\n",
    "def printmd(text):\n",
    "    display(Markdown(text))\n",
    "from validation.validation import info_etudiant, bib_validation, test_function\n",
    "from validation.valide_markdown import test_code\n",
    "try: NUMERO_ETUDIANT\n",
    "except NameError: NUMERO_ETUDIANT = None \n",
    "if type(NUMERO_ETUDIANT) is not int :\n",
    "    printmd(\"**ERREUR:** numéro d'étudiant non spécifié!!!\")\n",
    "    NOM, PRENOM, NUMERO_ETUDIANT = info_etudiant()\n",
    "    #raise AssertionError(\"NUMERO_ETUDIANT non défini\")\n",
    "# parametres spécifiques\n",
    "_uid_    = NUMERO_ETUDIANT\n",
    "np.random.seed(_uid_)\n",
    "printmd(\"**Login étudiant {} {} uid={}**\".format(NOM,PRENOM,_uid_))\n",
    "bib_validation('cours','IntroIA')\n",
    "from Brinkman import Brinkman\n",
    "BM = Brinkman(_uid_)\n",
    "# initialisation GPU (ne pas re-executer sauf en cas de restart)\n",
    "from validation.libIA_GPU import Init_torchGPU\n",
    "try: cuda_dev\n",
    "except NameError: cuda_dev = Init_torchGPU(4, _uid_%2)\n",
    "if cuda_dev is None: cuda_dev = \"cpu\" \n",
    "print(\"Cuda GPU device: \",cuda_dev)\n",
    "_notebook_=\"TP4_PINNinvpble.ipynb\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fb06bc68-d44d-4627-b114-40c07e04aa93",
   "metadata": {
    "nbgrader": {
     "grade": false,
     "grade_id": "cell-04d1d79ff4c0c883",
     "locked": true,
     "schema_version": 3,
     "solution": false,
     "task": false
    }
   },
   "source": [
    "## Objectif\n",
    "\n",
    "L'objectif du TP est l'utilisation d'un modèle de **PINN** (Physical Induced Neural Network) pour résoudre un problème inverse en mécanique des fluides.\n",
    "\n",
    "Pour des écoulements en milieu poreux, à l'aide de quelques points de mesure et d'un modèle, on veut déterminer la valeur d'un paramètre physique difficilement mesurable : la viscosité efficace $\\nu_e$. \n",
    "\n",
    "### modèle de Brinkman: Écoulement en milieu poreux\n",
    "\n",
    "![milieu poreux](brinkman.png)\n",
    "\n",
    "Dans les milieux poreux, on utilise classiquement la loi de Darcy (1856):\n",
    "\n",
    "$$ \\frac{\\nu}{K} u =  f $$\n",
    "\n",
    "où $\\nu$ est la viscosité du fluide, $K$ sa perméabilité et $f$  le terme de force extérieure (gradient de pression et force de gravité):\n",
    "$$ f = -\\frac{1}{\\rho} \\frac{\\partial p}{\\partial x} + \\vec{g}.\\vec{e_x}$$\n",
    "\n",
    "\n",
    "Le modèle de Brinkman est une extension de cette loi de Darcy pour les milieux poreux qui tient compte du cisaillement visqueux lorsque la perméabilité est élevée et impose la condition de non-glissement sur les parois solides. Dans le cas d'un écoulement 2D plan stationnaire dans un milieu poreux entre 2 plaques distantes de H, l'équation de Brinkman pour la vitesse $u(x)$ s'écrit:\n",
    "\n",
    "$$ \\frac{\\nu}{K} u =  f +  \\frac{\\nu_e}{\\epsilon} \\frac{d^2 u}{d x^2}  \\hspace{1cm} x \\in [0,H] $$\n",
    "\n",
    "associée aux 2 conditions aux limites sur les parois: $u(0) =u(H) =0$\n",
    "\n",
    "Dans le terme supplémentaire de Brinkman, $\\epsilon$ est la porosité du milieu poreux et $\\nu_e$ la viscosité effective. Cette viscosité $\\nu_e$ est reliée à la structure poreuse du milieu et est malheureusement très difficile à mesurer.\n",
    "\n",
    "A partir de 5 points de mesure de vitesse, on va déterminer la \"meilleur approximation\" de la viscosité effective $\\nu_e$  en utilisant le modèle de Brinkman.\n",
    "\n",
    "### Principe: modèle inverse\n",
    "\n",
    "1. On se donne les valeurs expérimentales aux points de mesure: $xd_i$, $ud_i$ pour $i=1,Nd=5$\n",
    "2. Pour une valeur de $\\nu_e$ fixé, on résout numériquement le problème de Brinkman pour obtenir une solution $\\hat{u}(x)$\n",
    "3. On calcul l'erreur aux points de mesure:\n",
    "   $$Err = \\sum_{i=1}^{Nd} \\left | \\hat{u}(xd_i) - ud_i \\right |^2$$\n",
    "4. On corrige la valeur de $\\nu_e$ pour minimiser cette erreur\n",
    "5. On itère en 2 jusqu'à une précision fixée.\n",
    "\n",
    "La résolution numérique et le problème de minimisation inverse utilise une approche PINN"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "741761b8-599d-4e5c-bbdc-d700473ad1ab",
   "metadata": {
    "nbgrader": {
     "grade": false,
     "grade_id": "cell-e5e75bd2d3f3af9e",
     "locked": true,
     "schema_version": 3,
     "solution": false,
     "task": false
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "False\n",
      "CUDA device:  cpu\n",
      "Torch CUDA device:  cpu  threads: 4 4\n"
     ]
    }
   ],
   "source": [
    "# bibliothèque \n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "\n",
    "print(torch.cuda.is_available())\n",
    "device = torch.device(cuda_dev)\n",
    "print(\"CUDA device: \",cuda_dev)\n",
    "print(\"Torch CUDA device: \",device,\" threads:\",torch.get_num_threads(),torch.get_num_interop_threads())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "47371d6f-b450-481d-aed1-86e3597294ca",
   "metadata": {
    "nbgrader": {
     "grade": false,
     "grade_id": "cell-baa05cafe2d62313",
     "locked": true,
     "schema_version": 3,
     "solution": false,
     "task": false
    }
   },
   "source": [
    "## Paramètres du problème\n",
    "\n",
    "Les paramètres du problème étudié sont les suivants:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "d91eff3e-e8c6-409a-be97-17b80acdf082",
   "metadata": {
    "nbgrader": {
     "grade": false,
     "grade_id": "cell-ffff3fdad772ed92",
     "locked": true,
     "schema_version": 3,
     "solution": false,
     "task": false
    }
   },
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "**paramètres: H=1.0m  nu=0.001 m^2/s  eps=0.4  K=0.001 m^2  F=1.0 m/s^2**"
      ],
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "printmd(f\"**paramètres: H={BM.H}m  nu={BM.nu} m^2/s  eps={BM.eps}  K={BM.K} m^2  F={BM.F} m/s^2**\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e23cf408-21fb-42b8-96f6-ead72dcaecb9",
   "metadata": {
    "nbgrader": {
     "grade": false,
     "grade_id": "cell-efc63d9a1f16dbf2",
     "locked": true,
     "schema_version": 3,
     "solution": false,
     "task": false
    }
   },
   "source": [
    "## Résolution de l'équation de Brinkman à nue fixé\n",
    "\n",
    "En choisissant $\\nu_e=\\nu$ , on va résoudre le modèle de Brinkman avec une approche PINN en utilisant les valeurs des paramètres précédents.\n",
    "\n",
    "Définir les paramètres du problème dans la cellule suivante en utilisant les noms de variables ci-dessous:\n",
    "\n",
    " - H  nu  eps  K  F   nu_param"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "86754079-0ac9-42ee-8b75-7ffbc1a99bfb",
   "metadata": {
    "nbgrader": {
     "grade": false,
     "grade_id": "cell-5a7947bdb18b3acb",
     "locked": false,
     "schema_version": 3,
     "solution": true,
     "task": false
    }
   },
   "outputs": [],
   "source": [
    "# paramètres\n",
    "H = nu = eps = K = F = None\n",
    "nue_param = None\n",
    "### BEGIN SOLUTION\n",
    "### END SOLUTION"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "beac6db1-11c9-4bfd-8705-8c28cfbd9c16",
   "metadata": {
    "nbgrader": {
     "grade": false,
     "grade_id": "cell-9bb0c74f09431f7a",
     "locked": true,
     "schema_version": 3,
     "solution": false,
     "task": false
    }
   },
   "source": [
    "### création du réseau de neurones PINN\n",
    "Définir un réseau de neurones à 3 couches linéaires 1x32 32x32 32x1 avec une fonction d'activation en tanh. On créera une nouvelle classe `PINN` dérivée de la classe `nn.Module`.\n",
    "\n",
    "Créer le modèle sur le GPU en utilisant cette nouvelle classe et le mettre dans la variable `model` "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "197ee571-693c-443b-bdcf-14550394c0e3",
   "metadata": {
    "nbgrader": {
     "grade": false,
     "grade_id": "cell-verif1",
     "locked": false,
     "schema_version": 3,
     "solution": true,
     "task": false
    }
   },
   "outputs": [],
   "source": [
    "print(\"GPU \",device)\n",
    "torch.set_default_device(device)\n",
    "if torch.cuda.is_available(): torch.cuda.init()\n",
    "NRN = 32\n",
    "model = None\n",
    "## BEGIN SOLUTION\n",
    "## END SOLUTION"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "df41135a-c1cd-49c7-88aa-de398348d27a",
   "metadata": {
    "nbgrader": {
     "grade": false,
     "grade_id": "cell-c3dc4898800f7380",
     "locked": true,
     "schema_version": 3,
     "solution": false,
     "task": false
    }
   },
   "source": [
    "### variables d'entrée\n",
    "on choisit comme entrée Nc = 100 points de collocation equi-répartis entre 0 et H à l'exclusion des bornes.\n",
    "\n",
    "Créer le tenseur pytorch dans `x _phys` en indiquant que l'on veut calculer les gradients par rapport à ce tenseur (`requires_grad`) et sélectionner la bonne méthode d'optimisation dans la variable `optimiser`. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "044abeb1-f5cb-4fef-8008-b6ec964894e8",
   "metadata": {
    "nbgrader": {
     "grade": false,
     "grade_id": "cell-8ab5e9c65e91bfa5",
     "locked": false,
     "schema_version": 3,
     "solution": true,
     "task": false
    }
   },
   "outputs": [],
   "source": [
    "# points de collocation\n",
    "Nc = 100\n",
    "x_phys = None\n",
    "optimiser = None\n",
    "## BEGIN SOLUTION\n",
    "## END SOLUTION"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "46ff4b0e-a4b8-4d2b-a4b4-5912557f3fc1",
   "metadata": {
    "editable": true,
    "nbgrader": {
     "grade": false,
     "grade_id": "cell-59e44c8ee263eeca",
     "locked": true,
     "schema_version": 3,
     "solution": false,
     "task": false
    },
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "source": [
    "### calcul du résidu\n",
    "Définir la fonction `résidu(x,y,nue)` qui calcule le résidu de l'équation pour une approximation `y` de  la solution en un point `x` (qui sont tous les deux des tenseurs pytorch) pour une valeur de la viscosité effective `nue` \n",
    "\n",
    "Vérifier en calculant le résidu à partir d'une prédiction du modèle"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "9dddd9f1-c9b5-4b7f-92d5-c953c663ec9e",
   "metadata": {
    "editable": true,
    "nbgrader": {
     "grade": false,
     "grade_id": "cell-6cea3f48d8b9f40c",
     "locked": false,
     "schema_version": 3,
     "solution": true,
     "task": false
    },
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "# résidu de l'équation\n",
    "## BEGIN SOLUTION\n",
    "## END SOLUTION"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f4bf7b45-0d7c-4e93-8f01-6891476faa57",
   "metadata": {
    "nbgrader": {
     "grade": false,
     "grade_id": "cell-ad04475cdcd01743",
     "locked": true,
     "schema_version": 3,
     "solution": false,
     "task": false
    }
   },
   "source": [
    "### boucle de minimisation\n",
    "\n",
    "Ecrire la boucle de minimisation avec une variable itérative `epoch` pour minimiser\n",
    "la fonction perte et obtenir la solution approchée par PINN.\n",
    "\n",
    "La fonction loss doit prendre en compte les conditions aux limites imposées;"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "76da93dc-b6fa-4e72-8fca-7cb120582ffc",
   "metadata": {
    "nbgrader": {
     "grade": false,
     "grade_id": "cell-verif3",
     "locked": false,
     "schema_version": 3,
     "solution": true,
     "task": false
    }
   },
   "outputs": [],
   "source": [
    "# boucle de minimisation\n",
    "loss = None\n",
    "nue_param = nu\n",
    "## BEGIN SOLUTION\n",
    "## END SOLUTION"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7fa73782-ffc1-47e1-824e-b781814520d5",
   "metadata": {
    "nbgrader": {
     "grade": false,
     "grade_id": "cell-24854ac39b8f65b3",
     "locked": true,
     "schema_version": 3,
     "solution": false,
     "task": false
    }
   },
   "source": [
    "### Comparaison avec la solution de référence\n",
    "\n",
    "Comparer la solution obtenue avec la solution de référence fournit par la fonction `BM.Uex(x)`.\n",
    "On evaluera ces solutions sur 300 points equi-répartis, que l'on mettra  dans la variable `x_test` . Pour ces points, on calcule la solution de référence dans `y_exact` et la solution prédite par PINN dans `y_pred`.\n",
    "\n",
    "Tracer ces 2 solutions sur un même graphe avec des légendes pour comparer."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "b76cb0e2-a835-42a6-8dd1-997c1fcbd14d",
   "metadata": {
    "nbgrader": {
     "grade": false,
     "grade_id": "cell-verif4",
     "locked": false,
     "schema_version": 3,
     "solution": true,
     "task": false
    }
   },
   "outputs": [],
   "source": [
    "# comparaison solution exacte\n",
    "x_test = None\n",
    "y_exact = None\n",
    "y_pred = None\n",
    "## BEGIN SOLUTION\n",
    "## END SOLUTION"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "54477b1a-b21e-46ff-a81c-444fc8c23bd5",
   "metadata": {
    "nbgrader": {
     "grade": false,
     "grade_id": "cell-3d3acd93dfd9e247",
     "locked": true,
     "schema_version": 3,
     "solution": false,
     "task": false
    }
   },
   "source": [
    "## Problème inverse\n",
    "\n",
    "Lire les points de mesure dans le fichier `data.txt` qui contient sur 2 colonnes x_data et y_data.\n",
    "\n",
    "Tracer les mesures pour vérifier, puis les convertir en tenseur pytorch."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "392bfbfa-073d-4170-9bd8-bc9031ee56ad",
   "metadata": {
    "nbgrader": {
     "grade": false,
     "grade_id": "cell-verif6",
     "locked": false,
     "schema_version": 3,
     "solution": true,
     "task": false
    }
   },
   "outputs": [],
   "source": [
    "# lecture des données\n",
    "x_data = y_data = None\n",
    "## BEGIN SOLUTION\n",
    "## END SOLUTION"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "af078269-0793-478b-9d16-a99953c33593",
   "metadata": {
    "nbgrader": {
     "grade": false,
     "grade_id": "cell-9dbcfacf3af7ab12",
     "locked": true,
     "schema_version": 3,
     "solution": false,
     "task": false
    }
   },
   "source": [
    "### Construction du model PINN\n",
    "\n",
    "De la même façon que précédemment, construire le modèle PINN mais en ajoutant un paramètre supplémentaire : `nue_param` "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "43b09102-f718-419f-9c70-7caca6d5aa95",
   "metadata": {
    "nbgrader": {
     "grade": false,
     "grade_id": "cell-59cef00a2d665b6f",
     "locked": false,
     "schema_version": 3,
     "solution": true,
     "task": false
    }
   },
   "outputs": [],
   "source": [
    "model = None\n",
    "nue_param = None\n",
    "optimizer = None\n",
    "## BEGIN SOLUTION\n",
    "## END SOLUTION"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7e0ee3b0-eb98-4629-9882-a4d6399e86ed",
   "metadata": {
    "nbgrader": {
     "grade": false,
     "grade_id": "cell-60247bf4b8cc1c9f",
     "locked": true,
     "schema_version": 3,
     "solution": false,
     "task": false
    }
   },
   "source": [
    "### Boucle d'optimisation\n",
    "Ecrire la boucle d'optimisation en ajoutant dans la fonction perte l'écart avec les données mesurées.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "f251759a-9911-484e-9fa5-da561e230276",
   "metadata": {
    "nbgrader": {
     "grade": false,
     "grade_id": "cell-verif7",
     "locked": false,
     "schema_version": 3,
     "solution": true,
     "task": false
    }
   },
   "outputs": [],
   "source": [
    "# boucle de minimisation\n",
    "loss = None\n",
    "## BEGIN SOLUTION\n",
    "## END SOLUTION"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b680b2eb-be81-4707-92e4-c57381afe6b8",
   "metadata": {
    "nbgrader": {
     "grade": false,
     "grade_id": "cell-16e592a187742f59",
     "locked": true,
     "schema_version": 3,
     "solution": false,
     "task": false
    }
   },
   "source": [
    "### Analyse de la solution\n",
    "\n",
    "Analyser le résultat en traçant sur un même figure, la solution PINN et les données mesurées."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "eaafe90f-106b-452e-8712-e3a6692a1aee",
   "metadata": {
    "nbgrader": {
     "grade": false,
     "grade_id": "cell-verif8",
     "locked": false,
     "schema_version": 3,
     "solution": true,
     "task": false
    }
   },
   "outputs": [],
   "source": [
    "# comparaison donnee\n",
    "x_test = y_pred = None\n",
    "## BEGIN SOLUTION\n",
    "## END SOLUTION "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cafb5c9a-b72b-4dd3-a8e9-3c7c946d149a",
   "metadata": {
    "nbgrader": {
     "grade": false,
     "grade_id": "cell-ec25e706acac8223",
     "locked": true,
     "schema_version": 3,
     "solution": false,
     "task": false
    }
   },
   "source": [
    "## Analyse et conclusion\n",
    "\n",
    "- dans le fichier **CompteRendu.md** écrire votre analyse et vos conclusions en markdown\n",
    "\n",
    "- puis générer la version html avec la commande suivante"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1e736ce0-933d-4d05-8472-a31dcbb7a2b2",
   "metadata": {},
   "outputs": [],
   "source": [
    "# génération de la version html du CR\n",
    "!genereTPhtml CompteRendu"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "43a314d3-a367-41a5-a38f-8f85b1d78fd0",
   "metadata": {
    "editable": true,
    "nbgrader": {
     "grade": false,
     "grade_id": "cell-e86c1e3a199a86d6",
     "locked": true,
     "schema_version": 3,
     "solution": false,
     "task": false
    },
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "source": [
    "## FIN"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "85cde946-29f9-4090-9c05-3caf6490323c",
   "metadata": {
    "editable": true,
    "slideshow": {
     "slide_type": ""
    },
    "tags": []
   },
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "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.12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
