{
  "cells": [
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "%matplotlib inline"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "\n# Downsampling\n\nIn this tutorial we demonstrate how to configure the digital estimator\nfor downsampling.\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "import scipy.signal\nimport numpy as np\nimport cbadc as cbc\nimport matplotlib.pyplot as plt"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Setting up the Analog System and Digital Control\n\nIn this example, we assume that we have access to a control signal\ns[k] generated by the interactions of an analog system and digital control.\nFurthermore, we a chain-of-integrators converter with corresponding\nanalog system and digital control.\n\n<img src=\"file://images/chainOfIntegratorsGeneral.svg\" width=\"500\" align=\"center\" alt=\"The chain of integrators ADC.\">\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "# Setup analog system and digital control\n\n# We fix the number of analog states.\nN = 6\nM = N\n# Set the amplification factor.\nbeta = 6250.\nrho = - 1e-2\nkappa = - 1.0\n# In this example, each nodes amplification and local feedback will be set\n# identically.\nbetaVec = beta * np.ones(N)\nrhoVec = betaVec * rho\nkappaVec = kappa * beta * np.eye(N)\n\n# Instantiate a chain-of-integrators analog system.\nanalog_system = cbc.analog_system.ChainOfIntegrators(betaVec, rhoVec, kappaVec)\n\n\nT = 1/(2 * beta)\ndigital_control = cbc.digital_control.DigitalControl(T, M)\n\n\n# Summarize the analog system, digital control, and digital estimator.\nprint(analog_system, \"\\n\")\nprint(digital_control)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Loading Control Signal from File\n\nNext, we will load an actual control signal to demonstrate the digital\nestimator's capabilities. To this end, we will use the\n`sinusodial_simulation.adcs` file that was produced in\n:doc:`./plot_b_simulate_a_control_bounded_adc`.\n\nThe control signal file is encoded as raw binary data so to unpack it\ncorrectly we will use the :func:`cbadc.utilities.read_byte_stream_from_file`\nand :func:`cbadc.utilities.byte_stream_2_control_signal` functions.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "byte_stream = cbc.utilities.read_byte_stream_from_file(\n    '../a_getting_started/sinusodial_simulation.adcs', M)\ncontrol_signal_sequences1 = cbc.utilities.byte_stream_2_control_signal(\n    byte_stream, M)\n\nbyte_stream = cbc.utilities.read_byte_stream_from_file(\n    '../a_getting_started/sinusodial_simulation.adcs', M)\ncontrol_signal_sequences2 = cbc.utilities.byte_stream_2_control_signal(\n    byte_stream, M)\n\nbyte_stream = cbc.utilities.read_byte_stream_from_file(\n    '../a_getting_started/sinusodial_simulation.adcs', M)\ncontrol_signal_sequences3 = cbc.utilities.byte_stream_2_control_signal(\n    byte_stream, M)\n\n\nbyte_stream = cbc.utilities.read_byte_stream_from_file(\n    '../a_getting_started/sinusodial_simulation.adcs', M)\ncontrol_signal_sequences4 = cbc.utilities.byte_stream_2_control_signal(\n    byte_stream, M)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Oversampling\n\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "OSR = 16\n\nomega_3dB = 2 * np.pi / (T * OSR)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Oversampling = 1\n\nFirst we initialize our default estimator without a downsampling parameter\nwhich then defaults to 1, i.e., no downsampling.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "# Set the bandwidth of the estimator\nG_at_omega = np.linalg.norm(\n    analog_system.transfer_function_matrix(np.array([omega_3dB / 2])))\neta2 = G_at_omega**2\n# eta2 = 1.0\nprint(f\"eta2 = {eta2}, {10 * np.log10(eta2)} [dB]\")\n\n# Set the filter size\nL1 = 1 << 12\nL2 = L1\n\n# Instantiate the digital estimator.\ndigital_estimator_ref = cbc.digital_estimator.FIRFilter(\n    analog_system, digital_control, eta2, L1, L2)\ndigital_estimator_ref(control_signal_sequences1)\n\nprint(digital_estimator_ref, \"\\n\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Visualize Estimator's Transfer Function\n\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "# Logspace frequencies\nfrequencies = np.logspace(-3, 0, 100)\nomega = 4 * np.pi * beta * frequencies\n\n# Compute NTF\nntf = digital_estimator_ref.noise_transfer_function(omega)\nntf_dB = 20 * np.log10(np.abs(ntf))\n\n# Compute STF\nstf = digital_estimator_ref.signal_transfer_function(omega)\nstf_dB = 20 * np.log10(np.abs(stf.flatten()))\n\n# Signal attenuation at the input signal frequency\nstf_at_omega = digital_estimator_ref.signal_transfer_function(\n    np.array([omega_3dB]))[0]\n\n# Plot\nplt.figure()\nplt.semilogx(frequencies, stf_dB, label='$STF(\\omega)$')\nfor n in range(N):\n    plt.semilogx(frequencies, ntf_dB[0, n, :], label=f\"$|NTF_{n+1}(\\omega)|$\")\nplt.semilogx(frequencies, 20 * np.log10(np.linalg.norm(\n    ntf[:, 0, :], axis=0)), '--', label=\"$ || NTF(\\omega) ||_2 $\")\n\n# Add labels and legends to figure\nplt.legend()\nplt.grid(which='both')\nplt.title(\"Signal and noise transfer functions\")\nplt.xlabel(\"$\\omega / (4 \\pi \\\\beta ) $\")\nplt.ylabel(\"dB\")\nplt.xlim((frequencies[5], frequencies[-1]))\nplt.gcf().tight_layout()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## FIR Filter With Downsampling\n\nNext we repeat the initialization steps above but for a downsampled estimator\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "digital_estimator_dow = cbc.digital_estimator.FIRFilter(\n    analog_system,\n    digital_control,\n    eta2,\n    L1,\n    L2,\n    downsample=OSR)\ndigital_estimator_dow(control_signal_sequences2)\n\nprint(digital_estimator_dow, \"\\n\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Estimating (Filtering)\n\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "# Set simulation length\nsize = 1 << 17\nu_hat_ref = np.zeros(size)\nu_hat_dow = np.zeros(size // OSR)\nfor index in range(size):\n    u_hat_ref[index] = next(digital_estimator_ref)\nfor index in range(size // OSR):\n    u_hat_dow[index] = next(digital_estimator_dow)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "### Aliasing\n\nWe compare the difference between the downsampled estimate and the default.\nClearly, we are suffering from aliasing as is also explained by considering\nthe PSD plot.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "# compensate the built in L1 delay of FIR filter.\nt = np.arange(-L1 + 1, size - L1 + 1)\nt_down = np.arange(-(L1) // OSR, (size - L1) // OSR) * OSR + 1\nplt.plot(t, u_hat_ref, label=\"$\\hat{u}(t)$ Reference\")\nplt.plot(t_down, u_hat_dow, label=\"$\\hat{u}(t)$ Downsampled\")\nplt.xlabel('$t / T$')\nplt.legend()\nplt.title(\"Estimated input signal\")\nplt.grid(which='both')\nplt.xlim((-50, 1000))\nplt.tight_layout()\n\nplt.figure()\nu_hat_ref_clipped = u_hat_ref[(L1 + L2):]\nu_hat_dow_clipped = u_hat_dow[(L1 + L2) // OSR:]\nf_ref, psd_ref = cbc.utilities.compute_power_spectral_density(\n    u_hat_ref_clipped, fs=1.0/T)\nf_dow, psd_dow = cbc.utilities.compute_power_spectral_density(\n    u_hat_dow_clipped, fs=1.0/(T * OSR))\nplt.semilogx(f_ref, 10 * np.log10(psd_ref), label=\"$\\hat{U}(f)$ Referefence\")\nplt.semilogx(f_dow, 10 * np.log10(psd_dow), label=\"$\\hat{U}(f)$ Downsampled\")\nplt.legend()\nplt.ylim((-300, 50))\nplt.xlim((f_ref[1], f_ref[-1]))\nplt.xlabel('$f$ [Hz]')\nplt.ylabel('$ \\mathrm{V}^2 \\, / \\, (1 \\mathrm{Hz})$')\nplt.grid(which='both')\nplt.show()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Prepending a Virtual Bandlimiting Filter\n\nTo battle the aliasing we extend the current estimator by placing a\nbandlimiting filter in front of the system. Note that this filter is a\nconceptual addition and not actually part of the physical analog system.\nRegardless, this effectively suppresses aliasing since we now reconstruct\na signal shaped by both the STF of the system in addition\nto a bandlimiting filter.\n\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "wp = omega_3dB / 2.0\nws = omega_3dB\ngpass = 0.1\ngstop = 80\n\nfilter = cbc.analog_system.IIRDesign(wp, ws, gpass, gstop, ftype=\"ellip\")\n\n# Compute transfer functions for each frequency in frequencies\ntransfer_function_filter = filter.transfer_function_matrix(omega)\n\nplt.semilogx(\n    omega/(2 * np.pi),\n    20 * np.log10(np.linalg.norm(\n        transfer_function_filter[:, 0, :],\n        axis=0)),\n    label=\"Cauer\")\n# Add labels and legends to figure\n# plt.legend()\nplt.grid(which='both')\nplt.title(\"Filter Transfer Functions\")\nplt.xlabel(\"$f$ [Hz]\")\nplt.ylabel(\"dB\")\nplt.xlim((5e1, 1e4))\nplt.gcf().tight_layout()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## New Analog System\n\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "new_analog_system = cbc.analog_system.chain([filter, analog_system])\nprint(new_analog_system)\n\ntransfer_function_analog_system = analog_system.transfer_function_matrix(omega)\n\ntransfer_function_new_analog_system = new_analog_system.transfer_function_matrix(\n    omega)\n\nplt.semilogx(\n    omega/(2 * np.pi),\n    20 * np.log10(np.linalg.norm(\n        transfer_function_analog_system[:, 0, :],\n        axis=0)),\n    label=\"Default Analog System\")\nplt.semilogx(\n    omega/(2 * np.pi),\n    20 * np.log10(np.linalg.norm(\n        transfer_function_new_analog_system[:, 0, :],\n        axis=0)),\n    label=\"Combined Analog System\")\n\n# Add labels and legends to figure\nplt.legend()\nplt.grid(which='both')\nplt.title(\"Analog System Transfer Function\")\nplt.xlabel(\"$f$ [Hz]\")\nplt.ylabel(\"$||\\mathbf{G}(\\omega)||_2$ dB\")\n# plt.xlim((frequencies[0], frequencies[-1]))\nplt.gcf().tight_layout()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## New Digital Estimator\n\nCombining the virtual pre filter together with the default analog system\nresults in the following system.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "digital_estimator_dow_and_pre_filt = cbc.digital_estimator.FIRFilter(\n    new_analog_system,\n    digital_control,\n    eta2,\n    L1,\n    L2,\n    downsample=OSR)\ndigital_estimator_dow_and_pre_filt(control_signal_sequences3)\nprint(digital_estimator_dow_and_pre_filt)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Post filtering the FIR filter coefficients\n\nYet another approach is to, instead of pre-filtering, post filter\nthe resulting FIR filter coefficients with another lowpass FIR filter.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "numtaps = 1 << 10\nf_cutoff = 1.0 / OSR\nfir_filter = scipy.signal.firwin(numtaps, f_cutoff)\n\ndigital_estimator_dow_and_post_filt = cbc.digital_estimator.FIRFilter(\n    analog_system,\n    digital_control,\n    eta2,\n    L1,\n    L2,\n    downsample=OSR)\ndigital_estimator_dow_and_post_filt(control_signal_sequences4)\n\n# Apply the FIR post filter\ndigital_estimator_dow_and_post_filt.convolve(fir_filter)\n\nprint(digital_estimator_dow_and_post_filt, \"\\n\")\n\nFIR_frequency_response = np.fft.rfft(fir_filter)\nf_FIR = np.fft.rfftfreq(numtaps, d=T)\nplt.figure()\nplt.semilogx(f_FIR, 20 * np.log10(np.abs(FIR_frequency_response)))\nplt.xlabel('$f$ [Hz]')\nplt.ylabel('$|h|$ dB')\nplt.grid(which='both')\n\nimpulse_response_dB_dow = 20 * \\\n    np.log10(np.linalg.norm(\n        np.array(digital_estimator_dow.h[0, :, :]), axis=1))\n\nimpulse_response_dB_dow_and_post_filt = 20 * \\\n    np.log10(np.linalg.norm(\n        np.array(digital_estimator_dow_and_post_filt.h[0, :, :]), axis=1))\n\nimpulse_response_dB_FIR_filter = 20 * np.log10(np.abs(fir_filter[numtaps//2:]))\n\nplt.figure()\nplt.plot(np.arange(0, L1),\n         impulse_response_dB_dow[L1:],\n         label=\"Ref\")\nplt.plot(np.arange(0, numtaps//2),\n         impulse_response_dB_FIR_filter,\n         label=\"Post FIR Filter\")\nplt.plot(np.arange(0, L1),\n         impulse_response_dB_dow_and_post_filt[L1:],\n         label=\"Combined Post Filtered\")\n\nplt.legend()\nplt.xlabel(\"filter tap k\")\nplt.ylabel(\"$|| \\mathbf{h} [k]||_2$ [dB]\")\nplt.xlim((0, 1024))\nplt.ylim((-160, 0))\nplt.grid(which='both')"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Plotting the Estimator's Signal and Noise Transfer Function\n\nNext we visualize the resulting STF and NTF of the new digital estimator\nfilters.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "# Compute NTF\nntf_pre = digital_estimator_dow_and_pre_filt.noise_transfer_function(omega)\nntf_post = digital_estimator_dow_and_post_filt.noise_transfer_function(\n    2 * np.pi * f_FIR) * FIR_frequency_response\nntf_dow = digital_estimator_dow.noise_transfer_function(omega)\n\n# Compute STF\nstf_pre = digital_estimator_dow_and_pre_filt.signal_transfer_function(omega)\nstf_dB_pre = 20 * np.log10(np.abs(stf_pre.flatten()))\nstf_post = digital_estimator_dow_and_post_filt.signal_transfer_function(\n    2 * np.pi * f_FIR) * FIR_frequency_response\nstf_dB_post = 20 * np.log10(np.abs(stf_post.flatten()))\nstf_dow = digital_estimator_dow.signal_transfer_function(omega)\nstf_dow_dB = 20 * np.log10(np.abs(stf_dow.flatten()))\n\n# Plot\nplt.figure()\nplt.semilogx(omega/(2 * np.pi), stf_dB_pre, label='$STF(\\omega)$ pre-filter')\nplt.semilogx(f_FIR, stf_dB_post, label='$STF(\\omega)$ post-filter')\nplt.semilogx(omega/(2 * np.pi), stf_dow_dB,\n             label='$STF(\\omega)$ ref',  color='black')\nplt.semilogx(omega/(2 * np.pi), 20 * np.log10(np.linalg.norm(\n    ntf_pre[:, 0, :], axis=0)), '--', label=\"$ || NTF(\\omega) ||_2 $ pre-filter\")\nplt.semilogx(f_FIR, 20 * np.log10(np.linalg.norm(\n    ntf_post[:, 0, :], axis=0)), '--', label=\"$ || NTF(\\omega) ||_2 $ post-filter\")\nplt.semilogx(omega/(2 * np.pi), 20 * np.log10(np.linalg.norm(\n    ntf_dow[:, 0, :], axis=0)), '--', label=\"$ || NTF(\\omega) ||_2 $ ref\", color='black')\n\n# Add labels and legends to figure\nplt.legend()\nplt.grid(which='both')\nplt.title(\"Signal and noise transfer functions\")\nplt.xlabel(\"$f$ [Hz]\")\nplt.ylabel(\"dB\")\nplt.xlim((1e2, 5e3))\nplt.gcf().tight_layout()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Filtering Estimate\n\nFinally, we plot the resulting input estimate PSD for each estimator.\nClearly, both the pre and post filter effectively suppresses the aliasing\neffect.\n\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "u_hat_dow_and_pre_filt = np.zeros(size // OSR)\nu_hat_dow_and_post_filt = np.zeros(size // OSR)\nfor index in cbc.utilities.show_status(range(size // OSR)):\n    u_hat_dow_and_pre_filt[index] = next(digital_estimator_dow_and_pre_filt)\n    u_hat_dow_and_post_filt[index] = next(digital_estimator_dow_and_post_filt)\n\nplt.figure()\nu_hat_dow_and_pre_filt_clipped = u_hat_dow_and_pre_filt[(L1 + L2) // OSR:]\nu_hat_dow_and_post_filt_clipped = u_hat_dow_and_post_filt[(L1 + L2) // OSR:]\n_, psd_dow_and_pre_filt = cbc.utilities.compute_power_spectral_density(\n    u_hat_dow_and_pre_filt_clipped, fs=1.0/(T * OSR))\n_, psd_dow_and_post_filt = cbc.utilities.compute_power_spectral_density(\n    u_hat_dow_and_post_filt_clipped, fs=1.0/(T * OSR))\nplt.semilogx(f_ref, 10 * np.log10(psd_ref), label=\"$\\hat{U}(f)$ Referefence\")\nplt.semilogx(f_dow, 10 * np.log10(psd_dow), label=\"$\\hat{U}(f)$ Downsampled\")\nplt.semilogx(f_dow, 10 * np.log10(psd_dow_and_pre_filt),\n             label=\"$\\hat{U}(f)$ Downsampled & Pre Filtered\")\nplt.semilogx(f_dow, 10 * np.log10(psd_dow_and_post_filt),\n             label=\"$\\hat{U}(f)$ Downsampled & Post Filtered\")\nplt.legend()\nplt.ylim((-300, 50))\nplt.xlim((f_ref[1], f_ref[-1]))\nplt.xlabel('$f$ [Hz]')\nplt.ylabel('$ \\mathrm{V}^2 \\, / \\, (1 \\mathrm{Hz})$')\nplt.grid(which='both')\nplt.show()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## In Time Domain\n\nThe corresponding estimate samples are plotted. As is evident from the plots\nthe different filter realization all result in different filter lags.\nNaturally, the filter lag follows from the choice of K1, K2, and the pre or\npost filter design and is therefore a known parameter.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "t = np.arange(size)\nt_down = np.arange(size // OSR) * OSR\nplt.plot(t, u_hat_ref, label=\"$\\hat{u}(t)$ Reference\")\nplt.plot(t_down, u_hat_dow, label=\"$\\hat{u}(t)$ Downsampled\")\nplt.plot(t_down, u_hat_dow_and_pre_filt,\n         label=\"$\\hat{u}(t)$ Downsampled and Pre Filtered\")\nplt.plot(t_down, u_hat_dow_and_post_filt,\n         label=\"$\\hat{u}(t)$ Downsampled and Post Filtered\")\nplt.xlabel('$t / T$')\nplt.legend()\nplt.title(\"Estimated input signal\")\nplt.grid(which='both')\noffset = (L1 + L2) * 4\nplt.xlim((offset, offset + 1000))\nplt.ylim((-0.6, 0.6))\nplt.tight_layout()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Compare Filter Coefficients\n\nFuthermore, the filter coefficient's magnitude decay varies for the different\nimplementations. Keep in mind that the for this example the pre and post\nfilter are parametrized such that the formed slightly outperforms the latter\nin terms of precision (see the PSD plot above).\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "impulse_response_dB_dow_and_pre_filt = 20 * \\\n    np.log10(np.linalg.norm(\n        np.array(digital_estimator_dow_and_pre_filt.h[0, :, :]), axis=1))\n\nplt.plot(np.arange(0, L1),\n         impulse_response_dB_dow[L1:],\n         label=\"Ref\")\n\nplt.plot(np.arange(0, L1),\n         impulse_response_dB_dow_and_pre_filt[L1:],\n         label=\"Pre Filtered\")\nplt.plot(np.arange(0, L1),\n         impulse_response_dB_dow_and_post_filt[L1:],\n         label=\"Post Filtered\")\nplt.legend()\nplt.xlabel(\"filter tap k\")\nplt.ylabel(\"$|| \\mathbf{h} [k]||_2$ [dB]\")\nplt.xlim((0, 1024))\nplt.ylim((-160, -20))\nplt.grid(which='both')"
      ]
    }
  ],
  "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.5"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 0
}