From 5671debf24b1a678b80c5127f4debfb736f7978c Mon Sep 17 00:00:00 2001 From: gonfeco Date: Tue, 16 Jan 2024 11:37:05 +0100 Subject: [PATCH] Finished documentation of notebooks for BTC_02_AE --- .../notebooks/02_AmplitudeEstimationBTC.ipynb | 13 +- .../04_AmpliutdeEstimationAlgorithms.ipynb | 562 ++++++++++++++++++ 2 files changed, 569 insertions(+), 6 deletions(-) create mode 100644 tnbs/BTC_02_AE/QQuantLib/notebooks/04_AmpliutdeEstimationAlgorithms.ipynb diff --git a/tnbs/BTC_02_AE/QQuantLib/notebooks/02_AmplitudeEstimationBTC.ipynb b/tnbs/BTC_02_AE/QQuantLib/notebooks/02_AmplitudeEstimationBTC.ipynb index 85e38e6..a7b278c 100644 --- a/tnbs/BTC_02_AE/QQuantLib/notebooks/02_AmplitudeEstimationBTC.ipynb +++ b/tnbs/BTC_02_AE/QQuantLib/notebooks/02_AmplitudeEstimationBTC.ipynb @@ -296,9 +296,7 @@ { "cell_type": "markdown", "id": "50411718-9c63-4705-8706-7501b9056317", - "metadata": { - "jp-MarkdownHeadingCollapsed": true - }, + "metadata": {}, "source": [ "### 2.4. Encoding function in a quantum circuit.\n", "\n", @@ -550,7 +548,10 @@ "* *oracle*: *myqlm* gate with the unitary operator $\\mathbf{A(f_{x_i})}$\n", "* *target*: the state $\\ket{\\Psi_0}$ of the unitary operator $\\mathbf{A(f_{x_i})}$\n", "* *index*: index affected by the unitary operator $\\mathbf{A(f_{x_i})}$\n", - "* *ae_dictionary*: Python dictionary with the complete configuration of the **AE** algorithm. Most important key is: *ae_type* key where the **AE** algorithm should be specified. The following strings can be provided: **CQPEAE**, **IQPEAE**, **MLAE**, **IQAE**,**RQAE**\n", + "* *ae_dictionary*: Python dictionary with the complete configuration of the **AE** algorithm. Most important key is: *ae_type* key where the **AE** algorithm should be specified. The following strings can be provided: **CQPEAE**, **IQPEAE**, **MLAE**, **IQAE**,**RQAE** and **MCAE**.\n", + "\n", + "**NOTE**\n", + "Some descriptions of the different *AE* algorithms implemented and about their configuration settings are provided in the notebook: *04_AmpliutdeEstimationAlgorithms*.\n", "\n", "The class directly builds the *Grover* operator mandatory for the **AE** algorithm. The **run** method of the class executes the **AE** algorithm. The *AE* class have the following important attributes:\n", "\n", @@ -1365,9 +1366,9 @@ ], "metadata": { "kernelspec": { - "display_name": "Python [conda env:tnbs] *", + "display_name": "Python [conda env:tnbs2c] *", "language": "python", - "name": "conda-env-tnbs-py" + "name": "conda-env-tnbs2c-py" }, "language_info": { "codemirror_mode": { diff --git a/tnbs/BTC_02_AE/QQuantLib/notebooks/04_AmpliutdeEstimationAlgorithms.ipynb b/tnbs/BTC_02_AE/QQuantLib/notebooks/04_AmpliutdeEstimationAlgorithms.ipynb new file mode 100644 index 0000000..088ff97 --- /dev/null +++ b/tnbs/BTC_02_AE/QQuantLib/notebooks/04_AmpliutdeEstimationAlgorithms.ipynb @@ -0,0 +1,562 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "5061ad02-4857-4d2f-98e4-057c1bfeae5a", + "metadata": {}, + "source": [ + "# AE Algorithms and AE class\n", + "\n", + "As explained in section 2.5 of notebook *02_AmplitudeEstimationBTC* one of the main ingredients of the **AE BTC** is the **AE** algorithm used. In the present code implementation, 6 different algorithms can be used and all of them are managed by the *AE* class of the *BTC_02_AE/QQuantLib/AE/ae_class* module. The present notebook offers insight into the different **AE** algorithms and the keys of the input dictionary of the *AE* class for configuring them.\n", + "\n", + "As remaining the **AE kernel** is given a unitary operator $\\mathbf{A(f_{x_i})}$ such that:\n", + "\n", + "$$|\\Psi\\rangle= \\mathbf{A}|0\\rangle_n = \\sqrt{a} |\\Psi_0\\rangle + \\sqrt{1-a}|\\Psi_1\\rangle \\tag{1}$$\n", + "\n", + "The **AE kernel** tries to get an estimation of the probability of obtaining the state $\\ket{\\Psi_0}$, this is an estimator of $a$.\n", + "\n", + "In the following cells we create the operator $\\mathbf{A(f_{x_i})}$ that we are going to use for testing the different algorithms." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6c03a1fd-8cc4-4807-8870-94914d9ead58", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "def sin_integral(a,b):\n", + " return np.cos(a)-np.cos(b)\n", + "\n", + "start = [0.0, np.pi]\n", + "end = [3.0*np.pi/8.0, 5.0*np.pi/4.0]\n", + "\n", + "#First integration domain\n", + "a = start[0]\n", + "b = end[0]\n", + "#For fix the number of discretization intervals\n", + "n=4\n", + "domain_x = np.linspace(a, b, 2**n+1)\n", + "\n", + "#The selected fucntion\n", + "f = np.sin\n", + "\n", + "#Discretization of the selected function\n", + "f_x = []\n", + "x_ = []\n", + "for i in range(1, len(domain_x)):\n", + " step_f = (f(domain_x[i]) + f(domain_x[i-1]))/2.0\n", + " #print(i)\n", + " f_x.append(step_f)\n", + " x_.append((domain_x[i] + domain_x[i-1])/2.0)\n", + "f_x = np.array(f_x)\n", + "x_ = np.array(x_)\n", + "\n", + "normalization = np.max(np.abs(f_x))\n", + "print(\"Normalization constant: {}\".format(normalization))\n", + "#normalization = 1.0\n", + "f_norm_x = f_x/normalization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "79835d4a-a037-4ee1-99b8-58377c6918ad", + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "sys.path.append(\"../../\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32930277-7c86-41ee-b17a-7fd0e77fe2fa", + "metadata": {}, + "outputs": [], + "source": [ + "from QQuantLib.DL.encoding_protocols import Encoding" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "be7d3291-38b8-4c72-908c-6ec1c0d6939c", + "metadata": {}, + "outputs": [], + "source": [ + "encoding_object = Encoding(\n", + " array_function=f_norm_x, \n", + " array_probability=None, \n", + " encoding=2\n", + ")\n", + "encoding_object.run()" + ] + }, + { + "cell_type": "markdown", + "id": "8807f781-5864-4f30-81c0-8387575e1f4d", + "metadata": {}, + "source": [ + "## 1. MonteCarlo Amplitude Estimation (MCAE)\n", + "\n", + "In this case only the unitary operator $\\mathbf{A(f_{x_i})}$ that encodes the function to integrate is mandatory. the idea is to execute the circuit and measure the probability of obtaining the state $\\ket{\\Psi_0}$: $a$.\n", + "\n", + "For this technique, the only mandatory input of the configuration dictionary of the **AE** class will be the number of shots for executing the circuit that is provided using the key: *shots*." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "be1586b2-8954-47e5-8c02-c2623872b8bc", + "metadata": {}, + "outputs": [], + "source": [ + "# QPU for solving the AE problem\n", + "from get_qpu import get_qpu\n", + "qpu = get_qpu(\"c\")\n", + "from QQuantLib.AE.ae_class import AE" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1b6912eb-233d-493b-8bde-ea7ae3eff83c", + "metadata": {}, + "outputs": [], + "source": [ + "mcae_dict = {\n", + " #QPU is alwways mandatory\n", + " 'qpu': qpu,\n", + " #shots\n", + " 'shots': None,\n", + " 'ae_type': 'MCAE'\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4d945aa9-30db-4ace-8026-9fede9348254", + "metadata": {}, + "outputs": [], + "source": [ + "mcae = AE(\n", + " oracle=encoding_object.oracle,\n", + " target=encoding_object.target,\n", + " index=encoding_object.index,\n", + " **mcae_dict\n", + ")\n", + "# We need to execute run method for solving the AE problem\n", + "mcae.run()\n", + "# Estimation of the a\n", + "mcae_pdf = mcae.ae_pdf" + ] + }, + { + "cell_type": "markdown", + "id": "2d79698e-8629-4f6b-ab4d-37706057cf77", + "metadata": {}, + "source": [ + "The **MCAE*^* does not provide upper and limit bounds but can be easily computed using as an error: $$\\frac{1}{\\sqrt{n_{shots}}}$$ where $n_{shots}$ is the number of shots." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6450b345-d83f-4c7f-8c2f-362916ebf032", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "mcae_pdf" + ] + }, + { + "cell_type": "markdown", + "id": "36e34542-6938-4fa0-a06b-62285bb24fa7", + "metadata": {}, + "source": [ + "## 2. Classical Quantum Phase Estimation (CQPEAE).\n", + "\n", + "This is the canonical Quantum Phase Estimation method presented in the notebook **01_AmplitudeEstimationKernel**. This algorithm needs the Grover operator of $\\mathbf{A(f_{x_i})}$, $\\mathbf{G}(\\mathbf{A(f_{x_i})})$. In this case, the configuration keys for the *AE* class are:\n", + "* *auxiliar_qbits_number*: number of auxiliary (or ancilla) qubits used for doing the QFT. This number of qubits sets the precision of the returned estimation.\n", + "* *shots*: number of shots for executing the complete circuit.\n", + "\n", + "Reference paper:\n", + "* Brassard, Gilles and Hoyer, Peter and Mosca, Michele and Tapp, Alain (2000). Quantum Amplitude Amplification and Estimation. AMS Contemporary Mathematics Series, **305**.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0467c227-66e3-4c3f-be80-23bc5cc04177", + "metadata": {}, + "outputs": [], + "source": [ + "cqpeae_dict = {\n", + " #QPU is alwways mandatory\n", + " 'qpu': qpu,\n", + " #shots\n", + " 'shots': 100,\n", + " # Number of ancilla qubits for QFT\n", + " 'auxiliar_qbits_number': 10,\n", + " 'ae_type': 'CQPEAE'\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b1352695-822b-4912-b968-f3ed6c1e1315", + "metadata": {}, + "outputs": [], + "source": [ + "cqpeae = AE(\n", + " oracle=encoding_object.oracle,\n", + " target=encoding_object.target,\n", + " index=encoding_object.index,\n", + " **cqpeae_dict\n", + ")\n", + "# We need to execute run method for solving the AE problem\n", + "cqpeae.run()\n", + "# Estimation of the a\n", + "cqpeae_pdf = cqpeae.ae_pdf" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d71e064b-a15c-48e8-8b99-a19b89aaa8fd", + "metadata": {}, + "outputs": [], + "source": [ + "cqpeae_pdf" + ] + }, + { + "cell_type": "markdown", + "id": "f3b62422-2b09-4c60-aea1-4410907ff0ba", + "metadata": {}, + "source": [ + "## 3. Iterative Quantum Phase Estimation (IQAE).\n", + "\n", + "The **IQAE** is a variant of the **CQPEAE** algorithm where the Quantum Fourier Transform is done using only an additional qubit. A lower number of qubits are used but the depth of the circuits is much higher. This algorithm needs the Grover operator of $\\mathbf{A(f_{x_i})}$, $\\mathbf{G}(\\mathbf{A(f_{x_i})})$. The configuration keys for the *AE* are:\n", + "\n", + "* *cbits_number*: number of classical bits used for computing the returned estimation.\n", + "* *shots*: number of shots for executing the complete circuit.\n", + "\n", + "Reference paper:\n", + "* Alexei Y. Kitaev (1995). Quantum measurements and the Abelian Stabilizer Problem. Electron. Colloquium Comput. Complex **TR96**." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5c11b6b1-dead-4f8d-a575-6116efe52af4", + "metadata": {}, + "outputs": [], + "source": [ + "iqpeae_dict = {\n", + " #QPU is alwways mandatory\n", + " 'qpu': qpu,\n", + " #shots\n", + " 'shots': 100,\n", + " # Number of classical qubits for QFT\n", + " 'cbits_number': 10,\n", + " 'ae_type': 'IQPEAE'\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "127d19d0-7bb1-4824-bb00-ab977f3a90e0", + "metadata": {}, + "outputs": [], + "source": [ + "iqpeae = AE(\n", + " oracle=encoding_object.oracle,\n", + " target=encoding_object.target,\n", + " index=encoding_object.index,\n", + " **iqpeae_dict\n", + ")\n", + "# We need to execute run method for solving the AE problem\n", + "iqpeae.run()\n", + "# Estimation of the a\n", + "iqpeae_pdf = iqpeae.ae_pdf" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bcfb0ec3-8798-4945-9170-248ff1a3c4cd", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "iqpeae_pdf" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a67cfcfa-8563-4aec-b2b4-2648fdbee807", + "metadata": {}, + "outputs": [], + "source": [ + "# for getting the circuit\n", + "c = iqpeae.solver_ae.iqpe_object.circuit\n", + "%qatdisplay c --svg" + ] + }, + { + "cell_type": "markdown", + "id": "e665708d-fd37-4867-a5e2-c10aa7fd10f3", + "metadata": {}, + "source": [ + "## 4. Maximum Likelihood Amplitude Estimation (MLAE)\n", + "\n", + "This algorithm needs the Grover operator of $\\mathbf{A(f_{x_i})}$, $\\mathbf{G}(\\mathbf{A(f_{x_i})})$. It is an iterative algorithm. \n", + "\n", + "In each step of the algorithm, the following operation is executed as a circuit: $$\\mathbf{G}^{m_k} \\mathbf{A(f_{x_i})} \\ket{0}_n$$ where $m_k$ is selected in advance and the circuit is measured a fixed number of shots ($n_k$) and the probability of obtaining the $\\ket{\\Psi_0}$ is obtained (h_k). \n", + "\n", + "Using the properties of the *Grover* operator it can be shown that the following equation holds:\n", + "\n", + "$$\\mathbf{G}^{m_k} \\mathbf{A(f_{x_i})} \\ket{0}_n =\\sin\\left((2m_k+1)\\theta\\right)|\\Psi_0\\rangle +\\cos\\left((2m_k+1)\\theta\\right)|\\Psi_1\\rangle,$$\n", + "\n", + "where $$\\sqrt{a} = \\sin \\theta$$.\n", + "\n", + "In each step of the algorithm, the likelihood of obtaining $h_k$ in the function of $\\theta$ is obtained: $l(\\theta|h_k)$. Then the different likelihoods are gathered into a a cost function\n", + "\n", + "$$C(\\theta) = -\\log\\left(\\prod_{k = 0}^M l_k(\\theta,h_k)\\right)$$\n", + "\n", + "Then using a classical optimisation program (by default the *AE* class will use **brute force** scipy optimization: https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.brute.html) we compute the $\\theta^*$ that minimizes the cost function. Finally, the desired amplitude is computed by: $$a=sin^2(\\theta^*)$$\n", + "\n", + "The configuration keys for the *AE* are:\n", + "* *schedule*: list of list where the different $m_k$ and $n_k$ should be provided.\n", + "* *delta*: float for configuring the *brute force* optimizer. To avoid problems in 0 and pi/2 theta limits for defining the search domain.\n", + "* *ns*: int for defining the number of grid points used by the *brute force optimizer*.\n", + "\n", + "Reference paper:\n", + "* Yohichi Suzuki and Shumpei Uno and Rudy Raymond and Tomoki Tanaka and Tamiya Onodera and Naoki Yamamoto (2020). Amplitude estimation without phase estimation **19**(2)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7ce1e78b-7bce-4895-81a2-b27f37152c4e", + "metadata": {}, + "outputs": [], + "source": [ + "mk = [1, 2, 3, 4, 5]\n", + "nk = [100] * len(mk)\n", + "schedule = [mk, nk]\n", + "\n", + "mlae_dict = {\n", + " #QPU is alwways mandatory\n", + " 'qpu': qpu,\n", + " #MLAE\n", + " #MLAE schedule\n", + " 'schedule': schedule,\n", + " 'delta': 1.0e-8,\n", + " 'ns': 100000,\n", + " 'ae_type': 'MLAE'\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3b450640-3de2-46a0-a3a0-00672aa1d3a3", + "metadata": {}, + "outputs": [], + "source": [ + "mlae = AE(\n", + " oracle=encoding_object.oracle,\n", + " target=encoding_object.target,\n", + " index=encoding_object.index,\n", + " **mlae_dict\n", + ")\n", + "# We need to execute run method for solving the AE problem\n", + "mlae.run()\n", + "# Estimation of the a\n", + "mlae_pdf = mlae.ae_pdf" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1fb669c6-fc58-4cc5-aa16-1294dacf1556", + "metadata": {}, + "outputs": [], + "source": [ + "mlae_pdf" + ] + }, + { + "cell_type": "markdown", + "id": "963d30c6-a89a-4664-b3a6-c08082cb0935", + "metadata": {}, + "source": [ + "## 5. Iterative Quantum Amplitude Estimation.\n", + "\n", + "This algorithm needs the Grover operator of $\\mathbf{A(f_{x_i})}$, $\\mathbf{G}(\\mathbf{A(f_{x_i})})$. It is an iterative algorithm.\n", + "\n", + "In each step of the algorithm the property of the *Grover* operator: $$\\mathbf{G}^{m_k} \\mathbf{A(f_{x_i})} \\ket{0}_n =\\sin\\left((2m_k+1)\\theta\\right)|\\Psi_0\\rangle +\\cos\\left((2m_k+1)\\theta\\right)|\\Psi_1\\rangle,$$ is used for obtaining some close bounds for the estimation of $a$.\n", + "\n", + "The **IQAE** needs an error $\\epsilon$ and a confident interval $\\alpha$ as input and the final mission is estimating some upper and lower bounds $(a_l, a_u)$ such $a$ satisfies that: $$P\\big[a \\in [a_l, a_u]\\big] \\gt 1-\\alpha$$ and $$\\frac{a_u-a_l}{2} \\leq \\epsilon$$\n", + "\n", + "The configuration keys for the *AE* are:\n", + "* *epsilon*: float fot the $\\epsilon$ of the **IQAE** algorithm.\n", + "* *alpha*: float confidence level $\\alpha$ of the **IQAE** algorithm.\n", + "* *shots*: number of shots\n", + "\n", + "Reference paper:\n", + "* Dmitry Grinko and Julien Gacon and Christa Zoufal and Stefan Woerner (2021). Iterative quantum amplitude estimation, npj Quantum Information **7**(1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "570d20e8-8f2e-48f4-8d73-5530d5017014", + "metadata": {}, + "outputs": [], + "source": [ + "iqae_dict = {\n", + " #QPU is alwways mandatory\n", + " 'qpu': qpu,\n", + " #MLAE\n", + " #MLAE schedule\n", + " 'epsilon' : 0.001,\n", + " 'alpha': 0.05,\n", + " 'shots': 1000,\n", + " 'ae_type': 'IQAE'\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ca7a532b-5e95-4f4b-babf-5fc6180e3bcf", + "metadata": {}, + "outputs": [], + "source": [ + "iqae = AE(\n", + " oracle=encoding_object.oracle,\n", + " target=encoding_object.target,\n", + " index=encoding_object.index,\n", + " **iqae_dict\n", + ")\n", + "# We need to execute run method for solving the AE problem\n", + "iqae.run()\n", + "# Estimation of the a\n", + "iqae_pdf = iqae.ae_pdf" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "deae58ee-2498-49a4-a888-d12251c79e75", + "metadata": {}, + "outputs": [], + "source": [ + "iqae_pdf" + ] + }, + { + "cell_type": "markdown", + "id": "e0f04faf-66aa-48c3-a547-f6b71d27b40a", + "metadata": {}, + "source": [ + "# 6. Real Quantum Amplitude Estimation (RQAE).\n", + "\n", + "This algorithm needs the Grover operator of $\\mathbf{A(f_{x_i})}$, $\\mathbf{G}(\\mathbf{A(f_{x_i})})$. It is an iterative algorithm. It computes the amplitude $a$ so it can return negative values. \n", + "\n", + "The **RQAE** needs an error $\\epsilon$ and a confident interval $\\gamma$ as input and the final mission is estimating some upper and lower bounds $(a_l, a_u)$ such $a$ satisfies that: $$P\\big[a \\in [a_l, a_u]\\big] \\gt 1-\\gamma$$ and $$\\frac{a_u-a_l}{2} \\leq \\epsilon$$\n", + "\n", + "The configuration keys for the *AE* are:\n", + "* *epsilon*: float fot the $\\epsilon$ of the **RQAE** algorithm.\n", + "* *gamma*: float confidence level $\\gamma$ of the **RQAE** algorithm.\n", + "* *ratio*: the **RQAE** allows the user to set the amplification ratio (this sets the times that the *Grover* operator is applied in each step). The recommended value is 2.\n", + "\n", + "Reference paper:\n", + "* Manzano, Alberto and Musso, Daniele and Leitao, Álvaro (2023). Real Quantum Amplitude Estimation, EPJ Quantum Technology, **10**.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "45bf4a8c-5c70-4dc6-8113-b54001bb042d", + "metadata": {}, + "outputs": [], + "source": [ + "rqae_dict = {\n", + " #QPU is alwways mandatory\n", + " 'qpu': qpu,\n", + " #MLAE\n", + " #MLAE schedule\n", + " 'epsilon' : 0.001,\n", + " 'gamma': 0.05,\n", + " 'q': 2.0,\n", + " 'ae_type': 'RQAE'\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "adc58be8-1c03-4b26-8d80-2229413fba67", + "metadata": {}, + "outputs": [], + "source": [ + "rqae = AE(\n", + " oracle=encoding_object.oracle,\n", + " target=encoding_object.target,\n", + " index=encoding_object.index,\n", + " **rqae_dict\n", + ")\n", + "# We need to execute run method for solving the AE problem\n", + "rqae.run()\n", + "# Estimation of the a\n", + "rqae_pdf = rqae.ae_pdf" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "73cbc9c1-928b-4064-942e-9cfdb95782c9", + "metadata": {}, + "outputs": [], + "source": [ + "rqae_pdf" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python [conda env:tnbs2c] *", + "language": "python", + "name": "conda-env-tnbs2c-py" + }, + "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.11.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}