{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "\n", "\n", "# `MaxwellVacuumID`: An Einstein Toolkit thorn for generating initial data for Maxwell's equations\n", "\n", "## Authors: Terrence Pierre Jacques, Patrick Nelson, & Zach Etienne\n", "### Formatting improvements courtesy Brandon Clark\n", "\n", "### NRPy+ Source Code for this module: [Maxwell/InitialData.py](../edit/Maxwell/InitialData.py) [\\[**tutorial**\\]](Tutorial-VacuumMaxwell_InitialData.ipynb) Contructs the SymPy expressions for toroidal dipole field initial data\n", "\n", "## Introduction:\n", "In this part of the tutorial, we will construct an Einstein Toolkit (ETK) thorn (module) that will set up *initial data* for two formulations Maxwell's equations. In a [previous tutorial notebook](Tutorial-VacuumMaxwell_InitialData.ipynb), we used NRPy+ to contruct the SymPy expressions for toroidal dipole initial data. This thorn is largely based on and should function similarly to the NRPy+ generated [`IDScalarWaveNRPy`](Tutorial-ETK_thorn-IDScalarWaveNRPy.ipynb) thorn.\n", "\n", "We will construct this thorn in two steps.\n", "\n", "1. Call on NRPy+ to convert the SymPy expressions for the initial data into one C-code kernel.\n", "1. Write the C code and linkages to the Einstein Toolkit infrastructure (i.e., the .ccl files) to complete this Einstein Toolkit module." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "\n", "# Table of Contents\n", "$$\\label{toc}$$\n", "\n", "This notebook is organized as follows\n", "\n", "1. [Step 1](#initializenrpy): Initialize needed Python/NRPy+ modules\n", "1. [Step 2](#toroidal_id): NRPy+-generated C code kernels for toroidal dipole field initial data\n", "1. [Step 3](#cclfiles): CCL files - Define how this module interacts and interfaces with the wider Einstein Toolkit infrastructure\n", " 1. [Step 3.a](#paramccl): `param.ccl`: specify free parameters within `MaxwellVacuumID`\n", " 1. [Step 3.b](#interfaceccl): `interface.ccl`: define needed gridfunctions; provide keywords denoting what this thorn provides and what it should inherit from other thorns\n", " 1. [Step 3.c](#scheduleccl): `schedule.ccl`:schedule all functions used within `MaxwellVacuumID`, specify data dependencies within said functions, and allocate memory for gridfunctions\n", "1. [Step 4](#cdrivers): C driver functions for ETK registration & NRPy+-generated kernels\n", " 1. [Step 4.a](#etkfunctions): Initial data function\n", " 1. [Step 4.b](#makecodedefn): `make.code.defn`: List of all C driver functions needed to compile `MaxwellVacuumID`\n", "1. [Step 5](#latex_pdf_output): Output this notebook to $\\LaTeX$-formatted PDF file" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "\n", "# Step 1: Initialize needed Python/NRPy+ modules \\[Back to [top](#toc)\\]\n", "\n", "$$\\label{initializenrpy}$$" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "execution": { "iopub.execute_input": "2021-03-07T17:08:21.371230Z", "iopub.status.busy": "2021-03-07T17:08:21.370406Z", "iopub.status.idle": "2021-03-07T17:08:21.692722Z", "shell.execute_reply": "2021-03-07T17:08:21.693317Z" } }, "outputs": [], "source": [ "# Step 1: Import needed core NRPy+ modules\n", "from outputC import lhrh # NRPy+: Core C code output module\n", "import finite_difference as fin # NRPy+: Finite difference C code generation module\n", "import NRPy_param_funcs as par # NRPy+: Parameter interface\n", "import grid as gri # NRPy+: Functions having to do with numerical grids\n", "import loop as lp # NRPy+: Generate C code loops\n", "import indexedexp as ixp # NRPy+: Symbolic indexed expression (e.g., tensors, vectors, etc.) support\n", "import cmdline_helper as cmd # NRPy+: Multi-platform Python command-line interface\n", "import os, sys # Standard Python modules for multiplatform OS-level functions\n", "\n", "# Step 1a: Create directories for the thorn if they don't exist.\n", "# Create directory for MaxwellVacuumID thorn & subdirectories in case they don't exist.\n", "outrootdir = \"MaxwellVacuumID/\"\n", "cmd.mkdir(os.path.join(outrootdir))\n", "outdir = os.path.join(outrootdir,\"src\") # Main C code output directory\n", "cmd.mkdir(outdir)\n", "\n", "# Step 1b: This is an Einstein Toolkit (ETK) thorn. Here we\n", "# tell NRPy+ that gridfunction memory access will\n", "# therefore be in the \"ETK\" style.\n", "par.set_parval_from_str(\"grid::GridFuncMemAccess\",\"ETK\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "\n", "# Step 2: Constructing the Einstein Toolkit C-code calling functions that include the C code kernels \\[Back to [top](#toc)\\]\n", "$$\\label{toroidal_id}$$\n", "\n", "Using sympy, we construct the exact expressions for toroidal dipole field initial data currently supported in NRPy, documented in [Tutorial-VacuumMaxwell_InitialData.ipynb](Tutorial-VacuumMaxwell_InitialData.ipynb). We write the generated C codes into different C files, corresponding to the type of initial data the may want to choose at run time. Note that the code below can be easily extensible to include other types of initial data." ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "execution": { "iopub.execute_input": "2021-03-07T17:08:21.709295Z", "iopub.status.busy": "2021-03-07T17:08:21.708493Z", "iopub.status.idle": "2021-03-07T17:08:26.538403Z", "shell.execute_reply": "2021-03-07T17:08:26.537652Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Currently using System_I initial data\n", "Currently using System_II initial data\n" ] } ], "source": [ "import Maxwell.InitialData as mwid\n", "\n", "# Set coordinate system. ETK only supports cartesian coordinates\n", "CoordSystem = \"Cartesian\"\n", "par.set_parval_from_str(\"reference_metric::CoordSystem\",CoordSystem)\n", "\n", "# set up ID sympy expressions - System I\n", "mwid.InitialData()\n", "\n", "# x,y,z = gri.register_gridfunctions(\"AUX\",[\"x\",\"y\",\"z\"])\n", "\n", "AIU = ixp.register_gridfunctions_for_single_rank1(\"EVOL\",\"AIU\")\n", "EIU = ixp.register_gridfunctions_for_single_rank1(\"EVOL\",\"EIU\")\n", "psiI = gri.register_gridfunctions(\"EVOL\",\"psiI\")\n", "\n", "# Set which system to use, which are defined in Maxwell/VacuumMaxwell_Flat_Cartesian_ID.py\n", "par.set_parval_from_str(\"Maxwell.InitialData::System_to_use\",\"System_II\")\n", "\n", "# set up ID sympy expressions - System II\n", "mwid.InitialData()\n", "\n", "AIIU = ixp.register_gridfunctions_for_single_rank1(\"EVOL\",\"AIIU\")\n", "EIIU = ixp.register_gridfunctions_for_single_rank1(\"EVOL\",\"EIIU\")\n", "psiII = gri.register_gridfunctions(\"EVOL\",\"psiII\")\n", "GammaII = gri.register_gridfunctions(\"EVOL\",\"GammaII\")\n", "\n", "Maxwell_ID_SymbExpressions = [\\\n", " lhrh(lhs=gri.gfaccess(\"out_gfs\",\"AIU0\"),rhs=mwid.AidU[0]),\\\n", " lhrh(lhs=gri.gfaccess(\"out_gfs\",\"AIU1\"),rhs=mwid.AidU[1]),\\\n", " lhrh(lhs=gri.gfaccess(\"out_gfs\",\"AIU2\"),rhs=mwid.AidU[2]),\\\n", " lhrh(lhs=gri.gfaccess(\"out_gfs\",\"EIU0\"),rhs=mwid.EidU[0]),\\\n", " lhrh(lhs=gri.gfaccess(\"out_gfs\",\"EIU1\"),rhs=mwid.EidU[1]),\\\n", " lhrh(lhs=gri.gfaccess(\"out_gfs\",\"EIU2\"),rhs=mwid.EidU[2]),\\\n", " lhrh(lhs=gri.gfaccess(\"out_gfs\",\"psiI\"),rhs=mwid.psi_ID),\\\n", " lhrh(lhs=gri.gfaccess(\"out_gfs\",\"AIIU0\"),rhs=mwid.AidU[0]),\\\n", " lhrh(lhs=gri.gfaccess(\"out_gfs\",\"AIIU1\"),rhs=mwid.AidU[1]),\\\n", " lhrh(lhs=gri.gfaccess(\"out_gfs\",\"AIIU2\"),rhs=mwid.AidU[2]),\\\n", " lhrh(lhs=gri.gfaccess(\"out_gfs\",\"EIIU0\"),rhs=mwid.EidU[0]),\\\n", " lhrh(lhs=gri.gfaccess(\"out_gfs\",\"EIIU1\"),rhs=mwid.EidU[1]),\\\n", " lhrh(lhs=gri.gfaccess(\"out_gfs\",\"EIIU2\"),rhs=mwid.EidU[2]),\\\n", " lhrh(lhs=gri.gfaccess(\"out_gfs\",\"psiII\"),rhs=mwid.psi_ID),\\\n", " lhrh(lhs=gri.gfaccess(\"out_gfs\",\"GammaII\"),rhs=mwid.Gamma_ID)]\n", "declare_string = \"\"\"\n", "const double x = xGF[CCTK_GFINDEX3D(cctkGH, i0,i1,i2)];\n", "const double y = yGF[CCTK_GFINDEX3D(cctkGH, i0,i1,i2)];\n", "const double z = zGF[CCTK_GFINDEX3D(cctkGH, i0,i1,i2)];\n", "\n", "\"\"\"\n", "Maxwell_ID_CcodeKernel = fin.FD_outputC(\"returnstring\",\n", " Maxwell_ID_SymbExpressions,\\\n", " params=\"outCverbose=True\")\n", "\n", "Maxwell_ID_looped = lp.loop([\"i2\",\"i1\",\"i0\"],[\"0\",\"0\",\"0\"],[\"cctk_lsh[2]\",\"cctk_lsh[1]\",\"cctk_lsh[0]\"],\\\n", " [\"1\",\"1\",\"1\"],[\"#pragma omp parallel for\",\"\",\"\"],\"\",\\\n", " declare_string+Maxwell_ID_CcodeKernel).replace(\"time\",\"cctk_time\")\\\n", " .replace(\"xx0\", \"x\")\\\n", " .replace(\"xx1\", \"y\")\\\n", " .replace(\"xx2\", \"z\")\n", "\n", "\n", "# Step 4: Write the C code kernel to file.\n", "with open(os.path.join(outdir,\"Maxwell_ID.h\"), \"w\") as file:\n", " file.write(str(Maxwell_ID_looped))\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "\n", "# Step 3: ETK `ccl` file generation \\[Back to [top](#toc)\\]\n", "$$\\label{cclfiles}$$\n", "\n", "\n", "\n", "## Step 3.a: `param.ccl`: specify free parameters within `MaxwellVacuumID` \\[Back to [top](#toc)\\]\n", "$$\\label{paramccl}$$\n", "\n", "All parameters necessary for the computation of the initial data expressions are registered within NRPy+; we use this information to automatically generate `param.ccl`. NRPy+ also specifies default values for each parameter. \n", "\n", "More information on `param.ccl` syntax can be found in the [official Einstein Toolkit documentation](https://einsteintoolkit.org/usersguide/UsersGuide.html#x1-184000D2.3)." ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "execution": { "iopub.execute_input": "2021-03-07T17:08:26.549504Z", "iopub.status.busy": "2021-03-07T17:08:26.546431Z", "iopub.status.idle": "2021-03-07T17:08:26.551971Z", "shell.execute_reply": "2021-03-07T17:08:26.552440Z" } }, "outputs": [], "source": [ "def keep_param__return_type(paramtuple):\n", " keep_param = True # We'll not set some parameters in param.ccl;\n", " # e.g., those that should be #define'd like M_PI.\n", " typestring = \"\"\n", " # Separate thorns within the ETK take care of grid/coordinate parameters;\n", " # thus we ignore NRPy+ grid/coordinate parameters:\n", " if paramtuple.module == \"grid\" or paramtuple.module == \"reference_metric\":\n", " keep_param = False\n", "\n", " partype = paramtuple.type\n", " if partype == \"bool\":\n", " typestring += \"BOOLEAN \"\n", " elif partype == \"REAL\":\n", " if paramtuple.defaultval != 1e300: # 1e300 is a magic value indicating that the C parameter should be mutable\n", " typestring += \"CCTK_REAL \"\n", " else:\n", " keep_param = False\n", " elif partype == \"int\":\n", " typestring += \"CCTK_INT \"\n", " elif partype == \"#define\":\n", " keep_param = False\n", " elif partype == \"char\":\n", " print(\"Error: parameter \"+paramtuple.module+\"::\"+paramtuple.parname+\n", " \" has unsupported type: \\\"\"+ paramtuple.type + \"\\\"\")\n", " sys.exit(1)\n", " else:\n", " print(\"Error: parameter \"+paramtuple.module+\"::\"+paramtuple.parname+\n", " \" has unsupported type: \\\"\"+ paramtuple.type + \"\\\"\")\n", " sys.exit(1)\n", " return keep_param, typestring\n", "\n", "\n", "with open(os.path.join(outrootdir,\"param.ccl\"), \"w\") as file:\n", " file.write(\"\"\"\n", "# This param.ccl file was automatically generated by NRPy+.\n", "# You are advised against modifying it directly; instead\n", "# modify the Python code that generates it.\n", "\n", "shares: grid\n", "\n", "USES KEYWORD type\n", "\n", "CCTK_KEYWORD initial_data \"Type of initial data\"\n", "{\n", " \"toroid\" :: \"Toroidal Dipole field\"\n", "} \"toroid\"\n", "\n", "restricted:\n", "\n", "\"\"\")\n", "\n", " paramccl_str = \"\"\n", " for i in range(len(par.glb_Cparams_list)):\n", " # keep_param is a boolean indicating whether we should accept or reject\n", " # the parameter. singleparstring will contain the string indicating\n", " # the variable type.\n", " keep_param, singleparstring = keep_param__return_type(par.glb_Cparams_list[i])\n", "\n", " if keep_param:\n", " parname = par.glb_Cparams_list[i].parname\n", " partype = par.glb_Cparams_list[i].type\n", " singleparstring += parname + \" \\\"\"+ parname +\" (see NRPy+ for parameter definition)\\\"\\n\"\n", " singleparstring += \"{\\n\"\n", " if partype != \"bool\":\n", " singleparstring += \" *:* :: \\\"All values accepted. NRPy+ does not restrict the allowed ranges of parameters yet.\\\"\\n\"\n", " singleparstring += \"} \"+str(par.glb_Cparams_list[i].defaultval)+\"\\n\\n\"\n", "\n", " paramccl_str += singleparstring\n", " file.write(paramccl_str)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "\n", "## Step 3.b: `interface.ccl`: define needed gridfunctions; provide keywords denoting what this thorn provides and what it should inherit from other thorns \\[Back to [top](#toc)\\]\n", "$$\\label{interfaceccl}$$\n", "\n", "`interface.ccl` declares all gridfunctions and determines how `MaxwellVacuumID` interacts with other Einstein Toolkit thorns.\n", "\n", "The [official Einstein Toolkit documentation](https://einsteintoolkit.org/usersguide/UsersGuide.html#x1-179000D2.2) defines what must/should be included in an `interface.ccl` file. " ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "execution": { "iopub.execute_input": "2021-03-07T17:08:26.559151Z", "iopub.status.busy": "2021-03-07T17:08:26.558257Z", "iopub.status.idle": "2021-03-07T17:08:26.560804Z", "shell.execute_reply": "2021-03-07T17:08:26.561328Z" } }, "outputs": [], "source": [ "evol_gfs_list = []\n", "for i in range(len(gri.glb_gridfcs_list)):\n", " if gri.glb_gridfcs_list[i].gftype == \"EVOL\":\n", " evol_gfs_list.append( gri.glb_gridfcs_list[i].name+\"GF\")\n", "\n", "# NRPy+'s finite-difference code generator assumes gridfunctions\n", "# are alphabetized; not sorting may result in unnecessary\n", "# cache misses.\n", "evol_gfs_list.sort()\n", "\n", "with open(os.path.join(outrootdir,\"interface.ccl\"), \"w\") as file:\n", " file.write(\"\"\"\n", "# With \"implements\", we give our thorn its unique name.\n", "implements: MaxwellVacuumID\n", "\n", "# By \"inheriting\" other thorns, we tell the Toolkit that we\n", "# will rely on variables/function that exist within those\n", "# functions.\n", "inherits: MaxwellVacuum grid\n", "\"\"\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "\n", "## Step 3.c: `schedule.ccl`: schedule all functions used within `MaxwellVacuumID`, specify data dependencies within said functions, and allocate memory for gridfunctions \\[Back to [top](#toc)\\]\n", "$$\\label{scheduleccl}$$\n", "\n", "Official documentation on constructing ETK `schedule.ccl` files is found [here](https://einsteintoolkit.org/usersguide/UsersGuide.html#x1-187000D2.4). " ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "execution": { "iopub.execute_input": "2021-03-07T17:08:26.566452Z", "iopub.status.busy": "2021-03-07T17:08:26.565790Z", "iopub.status.idle": "2021-03-07T17:08:26.567976Z", "shell.execute_reply": "2021-03-07T17:08:26.568467Z" } }, "outputs": [], "source": [ "with open(os.path.join(outrootdir,\"schedule.ccl\"), \"w\") as file:\n", " file.write(\"\"\"\n", "# This schedule.ccl file was automatically generated by NRPy+.\n", "# You are advised against modifying it directly; instead\n", "# modify the Python code that generates it.\n", "\n", "schedule Maxwell_InitialData at CCTK_INITIAL as Maxwell_InitialData\n", "{\n", " STORAGE: MaxwellVacuum::evol_variables[3]\n", " LANG: C\n", "} \"Initial data for Maxwell's equations\"\n", "\n", "\"\"\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "\n", "# Step 4: C driver functions for ETK registration & NRPy+-generated kernels \\[Back to [top](#toc)\\]\n", "$$\\label{cdrivers}$$\n", "\n", "Now that we have constructed the basic C code kernels and the needed Einstein Toolkit `ccl` files, we next write the driver functions for registering `MaxwellVacuumID` within the Toolkit and the C code kernels. Each of these driver functions is called directly from [`schedule.ccl`](#scheduleccl)." ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "execution": { "iopub.execute_input": "2021-03-07T17:08:26.573933Z", "iopub.status.busy": "2021-03-07T17:08:26.573246Z", "iopub.status.idle": "2021-03-07T17:08:26.575283Z", "shell.execute_reply": "2021-03-07T17:08:26.575779Z" } }, "outputs": [], "source": [ "make_code_defn_list = []\n", "def append_to_make_code_defn_list(filename):\n", " if filename not in make_code_defn_list:\n", " make_code_defn_list.append(filename)\n", " return os.path.join(outdir,filename)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "\n", "## Step 4.a: Initial data function \\[Back to [top](#toc)\\]\n", "$$\\label{etkfunctions}$$\n", "\n", "Here we define the initial data function, and how it's to be called in the `schedule.ccl` file by ETK." ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "execution": { "iopub.execute_input": "2021-03-07T17:08:26.580759Z", "iopub.status.busy": "2021-03-07T17:08:26.579890Z", "iopub.status.idle": "2021-03-07T17:08:26.583274Z", "shell.execute_reply": "2021-03-07T17:08:26.582687Z" } }, "outputs": [], "source": [ "with open(append_to_make_code_defn_list(\"InitialData.c\"),\"w\") as file:\n", " file.write(\"\"\"\n", "\n", "#include \n", "#include \n", "\n", "#include \"cctk.h\"\n", "#include \"cctk_Parameters.h\"\n", "#include \"cctk_Arguments.h\"\n", "\n", "void Maxwell_InitialData(CCTK_ARGUMENTS)\n", "{\n", " DECLARE_CCTK_ARGUMENTS\n", " DECLARE_CCTK_PARAMETERS\n", "\n", " const CCTK_REAL *xGF = x;\n", " const CCTK_REAL *yGF = y;\n", " const CCTK_REAL *zGF = z;\n", "#include \"Maxwell_ID.h\"\n", "}\n", "\"\"\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "\n", "## Step 4.b: `make.code.defn`: List of all C driver functions needed to compile `MaxwellVacuumID` \\[Back to [top](#toc)\\]\n", "$$\\label{makecodedefn}$$\n", "\n", "When constructing each C code driver function above, we called the `append_to_make_code_defn_list()` function, which built a list of each C code driver file. We'll now add each of those files to the `make.code.defn` file, used by the Einstein Toolkit's build system." ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "execution": { "iopub.execute_input": "2021-03-07T17:08:26.590077Z", "iopub.status.busy": "2021-03-07T17:08:26.588799Z", "iopub.status.idle": "2021-03-07T17:08:26.594057Z", "shell.execute_reply": "2021-03-07T17:08:26.593445Z" } }, "outputs": [], "source": [ "with open(os.path.join(outdir,\"make.code.defn\"), \"w\") as file:\n", " file.write(\"\"\"\n", "# Main make.code.defn file for thorn MaxwellVacuumID\n", "\n", "# Source files in this directory\n", "SRCS =\"\"\")\n", " filestring = \"\"\n", " for i in range(len(make_code_defn_list)):\n", " filestring += \" \"+make_code_defn_list[i]\n", " if i != len(make_code_defn_list)-1:\n", " filestring += \" \\\\\\n\"\n", " else:\n", " filestring += \"\\n\"\n", " file.write(filestring)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "\n", "# Step 5: Output this notebook to $\\LaTeX$-formatted PDF file \\[Back to [top](#toc)\\]\n", "$$\\label{latex_pdf_output}$$\n", "\n", "The following code cell converts this Jupyter notebook into a proper, clickable $\\LaTeX$-formatted PDF file. After the cell is successfully run, the generated PDF may be found in the root NRPy+ tutorial directory, with filename\n", "[Tutorial-ETK_thorn-MaxwellVacuumID.pdf](Tutorial-ETK_thorn-MaxwellVacuumID.pdf) (Note that clicking on this link may not work; you may need to open the PDF file through another means.)" ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "execution": { "iopub.execute_input": "2021-03-07T17:08:26.601747Z", "iopub.status.busy": "2021-03-07T17:08:26.598966Z", "iopub.status.idle": "2021-03-07T17:08:30.089819Z", "shell.execute_reply": "2021-03-07T17:08:30.090447Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Created Tutorial-ETK_thorn-MaxwellVacuumID.tex, and compiled LaTeX file to\n", " PDF file Tutorial-ETK_thorn-MaxwellVacuumID.pdf\n" ] } ], "source": [ "import cmdline_helper as cmd # NRPy+: Multi-platform Python command-line interface\n", "cmd.output_Jupyter_notebook_to_LaTeXed_PDF(\"Tutorial-ETK_thorn-MaxwellVacuumID\")" ] } ], "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.8.2" } }, "nbformat": 4, "nbformat_minor": 2 }