{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<style>\n",
       "    .jp-Cell {max-width: 1024px !important; margin: auto}\n",
       "    .jp-Cell-inputWrapper {max-width: 1024px !important; margin: auto}\n",
       "    .jp-MarkdownOutput p {text-align: justify;}\n",
       "    .jupyter-matplotlib-figure {margin: auto;}\n",
       "</style>\n"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "%%html\n",
    "<style>\n",
    "    .jp-Cell {max-width: 1024px !important; margin: auto}\n",
    "    .jp-Cell-inputWrapper {max-width: 1024px !important; margin: auto}\n",
    "    .jp-MarkdownOutput p {text-align: justify;}\n",
    "    .jupyter-matplotlib-figure {margin: auto;}\n",
    "</style>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "%matplotlib widget\n",
    "from pyvisco import inter\n",
    "GUI = inter.Control()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# PYVISCO - Prony series identification for linear viscoelastic material models\n",
    "Author: Martin Springer | martin.springer@nrel.gov\n",
    "***\n",
    "\n",
    "## Overview\n",
    "The mechanical response of linear viscoelastic materials is often described with Generalized Maxwell models. The necessary material model parameters are typically identified by fitting a Prony series to the experimental measurement data in either the frequency-domain (via Dynamic Mechanical Thermal Analysis) or time-domain (via relaxation measurements). Pyvisco performs the necessary data processing of the experimental measurements, mathematical operations, and curve-fitting routines to identify the Prony series parameters. The experimental data can be provided as raw measurement sets at different temperatures or as pre-processed master curves.\n",
    "* If raw measurement data are provided, the time-temperature superposition principle is applied to create a master curve and obtain the shift functions prior to the Prony series parameters identification. \n",
    "* If master curves are provided, the shift procedure can be skipped, and the Prony series parameters identified directly. \n",
    "An optional minimization routine is provided to reduce the number of Prony elements. This routine is helpful for Finite Element simulations where reducing the computational complexity of the linear viscoelastic material models can shorten the simulation time."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "scrolled": false
   },
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "fbdeb0f358724a0faf0c5d9f113699a3",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "ToggleButton(value=False, description='Click here for more details!', layout=Layout(width='200px'))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "29e39b954f204a9a84c65bef18739144",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HTMLMath(value='')"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "display(GUI.b_theory)\n",
    "display(GUI.out_theory)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "***\n",
    "## Parameter identification\n",
    "\n",
    "### Specify measurement type and upload input data\n",
    "\n",
    "In this section the measurement type of the input data is specified and the experimental data are uploaded. A set of example input files can be downloaded here: [Example input files](https://github.com/NREL/pyvisco/raw/main/examples/examples.zip)\n",
    "\n",
    "#### Conventions \n",
    "Each input file needs to consist of two header rows. The first row indicates the column variables and the second row the corresponding units. The conventions used in this notebook are summarized below. Tensile moduli are denoted as $E$ and shear moduli  are denoted as $G$. Only the tensile moduli are summarized in the table below. For shear modulus data, replace `E` with `G`, e.g. `E_relax` -> `G_relax`.\n",
    "\n",
    "| Physical quantity        | Symbol               | Variable   | Unit   |\n",
    "| :---------------------   | :-------------:      | :--------- | :----: |\n",
    "| Relaxation modulus:      | $E(t)$               | `E_relax`  | `[Pa, kPa, MPa, GPa]`    |\n",
    "| Storage modulus:         | $E'(\\omega)$         | `E_stor`   | `[Pa, kPa, MPa, GPa]`    |\n",
    "| Loss modulus:            | $E''(\\omega)$        | `E_loss`   | `[Pa, kPa, MPa, GPa]`    |\n",
    "| Complex modulus:         | $\\lvert E^{*}\\rvert$ | `E_comp`   | `[Pa, kPa, MPa, GPa]`    |\n",
    "| Loss factor:             | $\\tan(\\delta)$       | `tan_del`  | `-`                      |\n",
    "| Instantaneous modulus:   | $E_0$                | `E_0`      | `[Pa, kPa, MPa, GPa]`    |\n",
    "| Equilibrium modulus:     | $E_{inf}$            | `E_inf`    | `[Pa, kPa, MPa, GPa]`    |\n",
    "| Angular frequency:       | $\\omega$             | `omega`    | `rad/s`                  |\n",
    "| Frequency:               | $f$                  | `f`        | `Hz`                     |\n",
    "| Time:                    | $t$                  | `t`        | `s`                      |\n",
    "| Temperature:             | $\\theta$             | `T`        | `°C`                     |\n",
    "| Relaxation times:        | $\\tau_i$             | `tau_i`    | `s`                      |\n",
    "| Relaxation moduli:       | $E_i$                | `E_i`      | `[Pa, kPa, MPa, GPa]`    |\n",
    "| Norm. relaxation moduli: | $\\alpha_i$           | `alpha_i`  | `-`                      |\n",
    "| Shift factor:            | $$\\log (a_{T})  $$   | `log_aT`   | `-`                      |\n",
    "\n",
    "***\n",
    "#### Domain\n",
    "The Prony series parameters can be either fitted from measurement data of Dynamic Mechanical Thermal Analysis (DMTA) in the frequency domain (freq) or from relaxation experiments in the time domain (time). \n",
    "\n",
    "#### Loading\n",
    "Measurement data can be provided from either tensile or shear experiments. Tensile moduli are denoted as $E$ and shear moduli  are denoted as $G$.\n",
    "\n",
    "#### Instrument\n",
    "* **Eplexor:** DMTA measurements conducted with a Netzsch Gabo DMA EPLEXOR can be directly uploaded as Excel files. Use the `Excel Export!` feature of the Eplexor software with the default template to create the input files. \n",
    "* **user:** Comma-separated values (csv) files are used for measurements conducted with other instruments. Prepare the input files in accordance to the performed measurement. The table below shows example headers, where the first row indicates the column quanitity and the second row the corresponding unit.\n",
    "\n",
    "| Domain               | Tensile              | shear               | \n",
    "| :-----------         | :-------------       | :---------          |\n",
    "|**Frequency domain:** | `f, E_stor, E_loss`  | `f, G_stor, G_loss` |\n",
    "|                      | `Hz, MPa, MPa`       | `Hz, GPa, GPa`      |\n",
    "|**Time domain:**      | `t, E_relax`         | `t, G_relax`        |\n",
    "|                      | `s, MPa`             | `s, GPa`            |\n",
    "\n",
    "\n",
    "#### Type\n",
    "\n",
    "Either **raw** data containing measurements at different temperatures or ready to fit **master** curves can be uploaded. \n",
    "\n",
    "Input files of **raw** measurement data need to specify the individual temperature sets of the performed experimental characterization:\n",
    "* **Eplexor:** The notebook identifies the corresponding temperature sets automatically (only available in the frequency domain).\n",
    "* **user:** Two additional columns need to be included in the input file. One column describing the temperature `T` of the measurement point and a second column `Set` to identify the corresponding measurement set of the data point (e.g. `f, E_stor, E_loss, T, Set`). All measurement points at the same temperature level are marked with the same number, e.g. 0 for the first measurement set. The first measurement set (0) represents the coldest temperature followed by the second set (1) at the next higher temperature level and so forth (see the provided [example input file](https://github.com/NREL/pyvisco/blob/main/examples/time_user_raw.csv) for further details)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "scrolled": false
   },
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "aebf64369379453e99cb9d759d0bd681",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "VBox(children=(HBox(children=(RadioButtons(description='Domain:', layout=Layout(height='auto', width='auto'), …"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "display(GUI.w_inp_gen)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "***\n",
    "#### Optional shift factor upload\n",
    "\n",
    "Already existing shift factors can be uploaded as a csv file with the header=`T, log_aT` and units=`C, -`, where `T` is the temperature level of the measurement set in Celsius and `log_aT` is the base 10 logarithm of the shift factor, $\\log(a_T)$.\n",
    "\n",
    "* **master:** Uploading the shift factors allows for the calculation of polynomial (D1 to D4) shift functions and the Williams–Landel–Ferry (WLF) shift function, but is not required for the Prony series estimation. \n",
    "> _**Note**_: If a master curve from the Eplexor software is provided, the default behavior of the notebook is to use the WLF shift function from the Eplexor software. However, in the time-temperature superpostion section, a checkbox is provided to overwrite the WLF fit of the Eplexor software and conduct another WLF fit with the algorithm in this notebook.\n",
    "\n",
    "* **raw:** The shift factors can either be directly determined for the desired reference temperature in the time-temperature superposition section (no upload necessary) or user-specified shift factors can be uploaded to be used in the creation of the master curve. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "cbcc316fdf63419dab70dcd3a07e64f2",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "VBox(children=(HBox(children=(Checkbox(value=False, description='user shift factors', layout=Layout(height='au…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "display(GUI.w_inp_shift)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "***\n",
    "#### Reference temperature \n",
    "\n",
    "Temperature chosen to contruct the master curve through application of the time-temperature superposition principle $[\\log(a_T) = 0]$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "e58aba22733b4325a604224645d42456",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(FloatText(value=0.0, description='Reference temperature (°C):', disabled=True, layout=Layout(he…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "display(GUI.w_RefT)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "***\n",
    "#### Check uploaded data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "d17ea500c93c4dfbb7f85f32f7d76c4f",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "VBox(children=(HBox(children=(Valid(value=False, description='Modul', layout=Layout(height='auto', width='200p…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "display(GUI.w_check_inp)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "***\n",
    "### Time-temperature superposition (shift functions)\n",
    "\n",
    "This section allows the calculation of shift factors from raw input data to create a master curve. The shift factors are then used to fit polynomial and WLF shift functions.\n",
    "\n",
    "#### Shift factors $\\log(a_{T})$ - Create master curve from raw input\n",
    "\n",
    "The time-temperature superposition principle is applied to create a master curve from the individual  measurement sets at different temperature levels. \n",
    "\n",
    "> _**Note**_:This subsection only applies to raw measurement data. If a master curve was uploaded, procede to the next step. \n",
    "\n",
    "  * **user shift factors uploaded:** The provided shift factors will be used to create the master curve from the raw measurement sets (_**Note**_: the `fit and overwrite provided shift factors` checkbox allows you to overwrite the uploaded user shift factors and fit new ones). \n",
    "  * **No user shift factors uploaded:** The measurement sets from the raw input file are used to estimate the shift factors and create a master curve. Measurement sets below the desired reference temperatures are shifted to lower frequencies (longer time periods), whereas measurement sets at temperatures higher than the reference temperature are shifted to higher frequencies (shorter time periods). The `manually adjust shift factors` checkbox allows you to modify the obtained shift factors manually. (_**Note**_: In the frequency domain, only the storage modulus input data are considered to create the master curve from the raw input data. The shift factors obtained from the storage modulus master curve are then used to create the loss modulus master curve.)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "scrolled": false
   },
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "558a0ca4c2794b99a70c7e39d34f245e",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "VBox(children=(HBox(children=(Checkbox(value=False, description='fit and overwrite provided shift factors', di…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "display(GUI.w_aT)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "***\n",
    "#### Shift functions (WLF & Polynomial degree 1 to 4)\n",
    "\n",
    "If shift factors are available, the WLF shift function and polynomial functions of degree 1 to 4 can be fitted and plotted below. (_**Note**_: If the WLF shift functions was already provided by the Eplexor software, the checkbox below let's you overwrite the WLF fit of the Eplexor software with a WLF fit of this notebook.) \n",
    "\n",
    "> _**Note**_:This subsection only provides shift functions and is not required to perform the parameter identification of the Prony series."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "d92e5011badb4811a5ca735f984f9d15",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "VBox(children=(HBox(children=(Checkbox(value=False, description='fit and overwrite provided WLF', disabled=Tru…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "display(GUI.w_shift)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "***\n",
    "### Estimate Prony series parameters\n",
    "\n",
    "#### Pre-process (smooth) master curve\n",
    "\n",
    "A moving median filter to remove outliers in the measurement data can be applied before the Prony series parameters are identified. The window size can be adjusted through the slider above the figure. A window size of 1 means that no filtering procedure is performed and the input data are fitted directly."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "8df04ce2cb4a499a9d409031c57549b1",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "VBox(children=(HBox(children=(Button(button_style='info', description='smooth master curve', layout=Layout(hei…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "display(GUI.w_smooth)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "***\n",
    "#### Define the number and discretization of the Prony series\n",
    "\n",
    "The number of Prony terms, $N$, needs to be defined before the parameter $\\tau_i$ and $\\alpha_i$ can be identified. The `default` behavior is to equally space one Prony term per decade along the logarithmic time axis, e.g., $\\tau_i$ = [1E-1, 1E0, 1E1,...] (s). This discretization typically delivers accurate results for engineering applications. \n",
    "> _**Note:**_ The fine discretization can be computationally heavy for using the viscoelastic material models in Finite Element simulations. Hence, the default discretization can be modified by either using the optimization routine provided below or by manually defining the number of Prony terms (`manual`). Here, the user can decide whether to round the lowest and highest relaxation times, $\\tau_i$, to the nearest base 10 number within the measurement window `round` or to use the exact minimum and maximum values of the experimental data for the relaxation times `exact`. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "e5c4ce3d191e411db332ffbf81ed4a98",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "VBox(children=(HBox(children=(RadioButtons(description='Discretization:', layout=Layout(height='auto', width='…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "display(GUI.w_dis)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "***\n",
    "#### Curve fitting\n",
    "\n",
    "Two different curve fitting routines for the Prony series parameters are employed and are dependent on the domain of the input data:\n",
    "\n",
    "* **Frequency domain**: A generalized collocation method using stiffness matrices is used as described in [Kraus, M. A., and M. Niederwald. Eur J Eng Mech 37.1 (2017): 82-106](https://journals.ub.ovgu.de/index.php/techmech/article/view/600). This methods utilizes both the storage and loss modulus master curves to estimate the Prony series parameters.\n",
    "   \n",
    "   \n",
    "* **Time domain**: A least-squares minimization is performed using the L-BFGS-B method from the scipy package. The implementation is similar to the optimization problem described by [Barrientos, E., Pelayo, F., Noriega, Á. et al. Mech Time-Depend Mater 23, 193–206 (2019)](https://doi.org/10.1007/s11043-018-9394-z) for a homogenous distribution of discrete times. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "8fd50544b2674724b2c275ad389e47ee",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "VBox(children=(HBox(children=(Button(button_style='info', description='fit Prony series', layout=Layout(height…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "display(GUI.w_out_fit_prony)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "***\n",
    "#### Generalized Maxwell model\n",
    "\n",
    "The fitted Prony series parameters in combination with the Generalized Maxwell model can be used to calculate the linear viscoelastic material parameters in both the time and frequency domain. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "4950d9096e454a97a2524508b69822fb",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "VBox(children=(HBox(children=(Button(button_style='info', description='plot Generalized Maxwell', layout=Layou…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "display(GUI.w_out_GMaxw)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "***\n",
    "### Optional: Minimize number of Prony elements (for Finite Element simulations)\n",
    "\n",
    "The Generalized Maxwell model with a high number of Prony terms can be computationally expensive. Especially, when used in combination with numerical frameworks as the Finite Element Method. Reducing the number of Prony elements will decrease the accuracy of the linear viscoelastic material model, but can help to speed up subsequent numerical simulations.\n",
    "\n",
    "We provide a simple routine to create an additional Generalized Maxwell model with a reduced number of Prony elements. The routine starts with the number of Prony terms specified above and subsequently reduces the number of terms. A least squares minimization is performed to fit the reduced term Prony parameters. The least squares residual is used to suggest an optimal number of Prony terms for subsequent FEM simulations ($R_{opt}^2 \\approx$ $1.5 R_0^2$). However, the user can change this default setting by selecting a different number of Prony terms below.\n",
    "\n",
    "> **_Note:_** This routine is computationally more demanding and can take a few minutes to complete. The runtime depends on the initial number of Prony elements and the number of data points in the measurement sets."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "51bda03ee58f40f1b41a000406266b05",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "VBox(children=(HBox(children=(Button(button_style='warning', description='minimize Prony terms', layout=Layout…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "display(GUI.w_out_fit_min)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "***\n",
    "## Download results\n",
    "\n",
    "A zip archive including the identified Prony series parameters, (shift factors and shift functions), results of the Generalized Maxwell model, and figures can be dowloaded below."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "c2cf260ddde64f37b4d92d71edac9a7d",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "VBox(children=(HBox(children=(Button(button_style='success', description='Download zip', layout=Layout(height=…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "#display(GUI.db_zip)\n",
    "display(GUI.w_out_down)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "***\n",
    "## Start over!\n",
    "Clear all input data and reload an empty notebook."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "3709c7078a08489a87614a8c279f699a",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(Button(button_style='danger', description='Clear notebook!', layout=Layout(height='auto', width…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "display(GUI.w_reload)"
   ]
  }
 ],
 "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.9.7"
  },
  "voila": {
   "template": "lab"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
