{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Getting started with prtecan" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%load_ext autoreload\n", "%autoreload 2\n", "\n", "import os\n", "import warnings\n", "from pathlib import Path\n", "\n", "import arviz as az\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", "import pandas as pd\n", "import seaborn as sb\n", "\n", "from clophfit import prtecan\n", "from clophfit.binding import fitting, plotting\n", "from clophfit.prtecan import Titration\n", "\n", "data_tests = (Path(\"..\") / \"..\" / \"tests\" / \"Tecan\").resolve().absolute()\n", "\n", "plt.show()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "os.chdir(data_tests / \"L1\")\n", "warnings.filterwarnings(\"ignore\", category=UserWarning, module=\"clophfit.prtecan\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Parsing a Single Tecan Files\n", "\n", "A Tecan file comprises of multiple label blocks, each with its unique metadata. This metadata provides critical details and context for the associated label block. In addition, the Tecan file itself also has its overarching metadata that describes its overall content.\n", "\n", "When the KEYS for label blocks are identical, it indicates that these label blocks are equivalent - meaning, they contain the same measurements. The equality of KEYS plays a significant role in parsing and analyzing Tecan files, as it assists in identifying and grouping similar measurement sets together. This understanding of label block equivalence based on KEY similarity is critical when working with Tecan files." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tf = prtecan.Tecanfile(\"290513_8.8.xls\")\n", "lb1 = tf.labelblocks[1]\n", "lb2 = tf.labelblocks[2]\n", "tf.metadata" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(\"Metadata:\\n\", lb1.metadata, \"\\n\")\n", "print(\"Data:\\n\", lb1.data)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tf1 = prtecan.Tecanfile(\"290513_8.2.xls\")\n", "\n", "tf1.labelblocks[1].__almost_eq__(lb1), tf1.labelblocks[1] == lb1" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Titration inherits TecanfilesGroup" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tfg = prtecan.TecanfilesGroup([tf, tf1])\n", "lbg1 = tfg.labelblocksgroups[1]\n", "\n", "print(lbg1.data[\"A01\"])\n", "\n", "lbg1.data_nrm[\"A01\"]" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tit = prtecan.Titration([tf, tf1], x=np.array([8.8, 8.2]), is_ph=True)\n", "print(tit)\n", "tit.labelblocksgroups[1].data_nrm[\"A01\"]" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tit.labelblocksgroups == tfg.labelblocksgroups" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tit.additions = [100, 1]\n", "\n", "tit.params.nrm = True\n", "tit.params.dil = True\n", "tit.params.bg = True\n", "tit.params" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tit.buffer.wells = [\"B02\"]\n", "\n", "tit.buffer.dataframes" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tit.bg, tit.bg_err" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tit.labelblocksgroups[1].data_nrm[\"A01\"]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Group a list of tecan files into a titration\n", "\n", "The command Titration.fromlistfile(\"../listfile\") reads a list of Tecan files, identifies unique measurements in each file, groups matching ones, and combines them into a titration set for further analysis." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tit = Titration.fromlistfile(\"./list.pH.csv\", is_ph=True)\n", "print(tit.x)\n", "lbg1 = tit.labelblocksgroups[1]\n", "lbg2 = tit.labelblocksgroups[2]\n", "print(lbg2.labelblocks[6].metadata[\"Temperature\"])\n", "lbg1.metadata, lbg2.metadata" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Within each labelblockgroups `data_norm` is immediately calculated." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "(lbg1.data[\"H03\"], lbg2.data, lbg1.data_nrm[\"H03\"], lbg2.data_nrm[\"H03\"])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Start with platescheme loading to set buffer wells (and consequently buffer values).\n", "\n", "Labelblocks group will be populated with data buffer subtracted with/out normalization." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tit.load_scheme(\"./scheme.txt\")\n", "print(f\"Buffer wells : {tit.scheme.buffer}\")\n", "print(f\"Ctrl wells : {tit.scheme.ctrl}\")\n", "print(f\"CTR name:wells {tit.scheme.names}\")\n", "\n", "tit.scheme" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "(lbg1.data[\"H12\"], lbg2.data_nrm[\"H12\"])" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tit.load_additions(\"./additions.pH\")\n", "tit.additions" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "(lbg1.data[\"H12\"], tit.data[1][\"H12\"], lbg1.data_nrm[\"H12\"], tit.bg[1])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The order in which you apply dilution correction and plate scheme can impact your intermediate results, even though the final results might be the same.\n", "\n", " Dilution correction adjusts the measured data to account for any dilutions made during sample preparation. This typically involves multiplying the measured values by the dilution factor to estimate the true concentration of the sample.\n", "\n", " A plate scheme describes the layout of the samples on a plate (common in laboratory experiments, such as those involving microtiter plates). The plate scheme may involve rearranging or grouping the data in some way based on the physical location of the samples on the plate." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Reassign Buffer Wells\n", "\n", "You can reassess buffer wells, updating the data to account for any dilution (additions) and subtracting the updated buffer value. This is a handy feature that gives you more control over your analysis.\n", "\n", "For instance, consider the following data for a particular well:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(tit.labelblocksgroups[2].data_nrm[\"D01\"])\n", "tit.data[2].get(\"D01\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tit.params.bg = False\n", "tit.params.dil = False\n", "print(tit.data[2][\"D02\"])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can reassign buffer wells using the `buffer_wells` attribute:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tit.params.bg = True\n", "tit.buffer.wells = [\"D01\", \"E01\"]" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tit.bg" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This updates the data for the specified wells, correcting for dilution and subtracting the buffer value:" ] }, { "cell_type": "markdown", "metadata": { "jp-MarkdownHeadingCollapsed": true }, "source": [ "
\n", "

🚨 The data remain: 🚨

\n", "

- unchanged in labelblocksgroups[:].data

\n", "

- buffer subtracted in labelblocksgroups[:].data_buffersubtracted

\n", "

- buffer subtracted and dilution corrected in data

\n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Fitting" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "test:\n", "\n", "- E10\n", "- F10\n", "- G09\n", "\n", "TODO:\n", "\n", "- Remove datapoint ini fin outlier" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "os.chdir(data_tests / \"L1\")\n", "tit = Titration.fromlistfile(\"./list.pH.csv\", is_ph=True)\n", "tit.load_scheme(\"./scheme.0.txt\")\n", "tit.load_additions(\"additions.pH\")\n", "tit" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tit.results[1].dataframe.head()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "rg = tit.result_global[\"D10\"]\n", "rg.figure" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%time\n", "rro = fitting.fit_binding_pymc_odr(rg, n_sd=0.5)\n", "\n", "rro" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df = az.summary(rro)\n", "df" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "az.plot_trace(rro)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "rp = tit.result_mcmc[\"H11\"]" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "rp.figure" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "rp.dataset" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "az.plot_trace(\n", " rp.mini, var_names=[\"x_true\", \"K\", \"x_diff\"], divergences=False, combined=True\n", ")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "type(tit.result_global[\"H11\"].result.params.keys())" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tit.result_global.all_computed()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Choose buffer value to be subtracted between mean values and ODR fitted values." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "lb = 2\n", "\n", "x = tit.buffer.dataframes_nrm[lb][\"fit\"]\n", "y = tit.buffer.dataframes_nrm[lb][\"mean\"]\n", "x_err = tit.buffer.dataframes_nrm[lb][\"fit_err\"] / 10\n", "y_err = tit.buffer.dataframes_nrm[lb][\"sem\"] / 10\n", "\n", "plt.errorbar(\n", " x,\n", " y,\n", " xerr=x_err,\n", " yerr=y_err,\n", " fmt=\"o\",\n", " color=\"blue\",\n", " ecolor=\"lightgray\",\n", " elinewidth=2,\n", " capsize=4,\n", ")\n", "plt.xlabel(\"ODR Fit\")\n", "plt.ylabel(\"Buffer wells Mean\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tit.buffer.plot(1).fig" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tit.buffer.fit_results_nrm" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tit.plot_temperature()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tit.bg_err" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "k = \"F10\" # \"G09\"\n", "tit.result_global[k].figure" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "r = tit.result_global[k]\n", "r.result.params" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tit.result_odr[k].figure" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ro = fitting.fit_binding_odr(r)\n", "ro.figure\n", "# ro.result.params" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tit._dil_corr" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tit.params.nrm = False\n", "tit.params" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "os.chdir(\"../../Tecan/140220/\")\n", "tit = Titration.fromlistfile(\"./list.pH.csv\", is_ph=True)\n", "tit.load_scheme(\"./scheme.txt\")\n", "tit.load_additions(\"additions.pH\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tit.data[1][\"H03\"]" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tit.params.bg_adj = True\n", "tit.params.bg_mth = \"mean\"\n", "\n", "tit.params" ] }, { "cell_type": "raw", "metadata": {}, "source": [ "tit.params.mcmc = True\n", "tit.result_mcmc" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df1 = pd.read_csv(\"../140220/fit1-1.csv\", index_col=0)\n", "# merged_df = tit.result_dfs[1][[\"K\", \"sK\"]].merge(df1, left_index=True, right_index=True)\n", "merged_df = (\n", " tit.results[2].dataframe[[\"K\", \"sK\"]].merge(df1, left_index=True, right_index=True)\n", ")\n", "\n", "sb.jointplot(merged_df, x=\"K_y\", y=\"K_x\", ratio=3, space=0.4)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tit.result_global[\"A01\"].figure" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tit.data[1][\"A01\"]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If a fit fails in a well, the well key will be anyway present in results list of dict." ] }, { "cell_type": "raw", "metadata": {}, "source": [ "tit.results[1].compute_all()\n", "\n", "conf = prtecan.TecanConfig(Path(\"jjj\"), False, (), \"\", True, True)\n", "\n", "tit.export_data_fit(conf)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(tit.data[1][\"H02\"])\n", "tit.results[2][\"H02\"].figure" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(tit.results[2].results.keys() - tit.results[2].results.keys())\n", "print(tit.result_global.results.keys() - tit.results[1].results.keys())\n", "print(tit.result_odr.results.keys() - tit.results[1].results.keys())" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tit.params.nrm = False" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tit.results[1][\"H01\"].figure" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tit.result_global[\"H01\"].figure" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And in the global fit (i.e. fitting 2 labelblocks) dataset with insufficient data points are removed." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tit.params.nrm = True\n", "well = \"H02\"\n", "y1 = np.array(tit.data[1][well])\n", "y2 = np.array(tit.data[2][well])\n", "\n", "x = np.array(tit.x)\n", "ds = fitting.Dataset(\n", " {\"y1\": fitting.DataArray(x, y1), \"y2\": fitting.DataArray(x, y2)}, is_ph=True\n", ")\n", "rfit = fitting.fit_binding_glob(ds)\n", "\n", "rfit.result.params" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "rfit.figure" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "fitting.fit_binding_odr(rfit).figure" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tit.results[2].dataframe.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can decide how to pre-process data with datafit_params:\n", "- [bg] subtract background\n", "- [dil] apply correction for dilution (when e.g. during a titration you add titrant without protein)\n", "- [nrm] normalize for gain, number of flashes and integration time. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tit.params.nrm = False\n", "tit.params.bg = True\n", "tit.params.bg_adj = False\n", "tit.data[1][\"E06\"]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Posterior analysis with emcee\n", "\n", "To explore the posterior of parameters you can use the Minimizer object returned in FitResult." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "np.random.seed(0) # noqa: NPY002\n", "remcee = rfit.mini.emcee(\n", " burn=50,\n", " steps=2000,\n", " workers=8,\n", " thin=10,\n", " nwalkers=30,\n", " progress=False,\n", " is_weighted=False,\n", ")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "f = plotting.plot_emcee(remcee.flatchain)\n", "print(remcee.flatchain.quantile([0.03, 0.97])[\"K\"].to_list())" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "samples = remcee.flatchain[[\"K\"]]\n", "# Convert the dictionary of flatchains to an ArviZ InferenceData object\n", "samples_dict = {key: np.array(val) for key, val in samples.items()}\n", "idata = az.from_dict(posterior=samples_dict)\n", "k_samples = idata.posterior[\"K\"].to_numpy()\n", "percentile_value = np.percentile(k_samples, 3)\n", "print(f\"Value at which the probability of being higher is 99%: {percentile_value}\")\n", "\n", "az.plot_forest(k_samples)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Cl titration analysis" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "os.chdir(\"../140220/\")\n", "cl_an = prtecan.Titration.fromlistfile(\"list.cl.csv\", is_ph=False)\n", "cl_an.load_scheme(\"scheme.txt\")\n", "cl_an.scheme" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "cl_an.load_additions(\"additions.cl\")\n", "print(cl_an.x)\n", "cl_an.x = prtecan.calculate_conc(cl_an.additions, 1000)\n", "cl_an.x" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "fres = cl_an.result_global[well]\n", "print(fres.is_valid(), fres.result.bic, fres.result.redchi)\n", "fres.figure" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Plotting" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tit.results[2].plot_k(title=\"2014-12-23\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### selection" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tit.params.nrm = True\n", "tit.params.dil = True\n", "tit.params.bg_mth = \"fit\"\n", "tit" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df_ctr = tit.results[1].dataframe\n", "for name, wells in tit.scheme.names.items():\n", " for well in wells:\n", " df_ctr.loc[well, \"ctrl\"] = name\n", "\n", "df_ctr.loc[df_ctr[\"ctrl\"].isna(), \"ctrl\"] = \"U\"\n", "\n", "sb.set_style(\"whitegrid\")\n", "g = sb.PairGrid(\n", " df_ctr,\n", " x_vars=[\"K\", \"S1_default\", \"S0_default\"],\n", " y_vars=[\"K\", \"S1_default\", \"S0_default\"],\n", " hue=\"ctrl\",\n", " palette=\"Set1\",\n", " diag_sharey=False,\n", ")\n", "g.map_lower(plt.scatter)\n", "g.map_upper(sb.kdeplot, fill=True)\n", "g.map_diag(sb.kdeplot)\n", "g.add_legend()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with sb.axes_style(\"darkgrid\"):\n", " g = sb.pairplot(\n", " tit.result_global.dataframe[[\"S1_y2\", \"S0_y2\", \"K\", \"S1_y1\", \"S0_y1\"]],\n", " hue=\"S1_y1\",\n", " palette=\"Reds\",\n", " corner=True,\n", " diag_kind=\"kde\",\n", " )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### combining" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "keys_unk = tit.fit_keys - set(tit.scheme.ctrl)\n", "res_unk = tit.results[1].dataframe.loc[list(keys_unk)].sort_index()\n", "res_unk[\"well\"] = res_unk.index" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "f = plt.figure(figsize=(24, 14))\n", "# Make the PairGrid\n", "g = sb.PairGrid(\n", " res_unk,\n", " x_vars=[\"K\", \"S1_default\", \"S0_default\"],\n", " y_vars=\"well\",\n", " height=12,\n", " aspect=0.4,\n", ")\n", "# Draw a dot plot using the stripplot function\n", "g.map(sb.stripplot, size=14, orient=\"h\", palette=\"Set2\", edgecolor=\"auto\")\n", "\n", "# Use the same x axis limits on all columns and add better labels\n", "# g.set(xlim=(0, 25), xlabel=\"Crashes\", ylabel=\"\")\n", "\n", "# Use semantically meaningful titles for the columns\n", "titles = [\"$pK_a$\", \"B$_{neutral}$\", \"B$_{anionic}$\"]\n", "\n", "for ax, title in zip(g.axes.flat, titles, strict=False):\n", " # Set a different title for each axes\n", " ax.set(title=title)\n", "\n", " # Make the grid horizontal instead of vertical\n", " ax.xaxis.grid(False)\n", " ax.yaxis.grid(True)\n", "\n", "sb.despine(left=True, bottom=True)" ] } ], "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.12.7" } }, "nbformat": 4, "nbformat_minor": 4 }