{ "cells": [ { "cell_type": "markdown", "id": "bed7bd15-21d9-4735-82c1-c27c1a5e3346", "metadata": {}, "source": [ "# Gotthard2 Offline Correction #\n", "\n", "Author: European XFEL Detector Group, Version: 1.0\n", "\n", "Offline Calibration for the Gothard2 Detector" ] }, { "cell_type": "code", "execution_count": null, "id": "570322ed-f611-4fd1-b2ec-c12c13d55843", "metadata": {}, "outputs": [], "source": [ "in_folder = \"/gpfs/exfel/exp/FXE/202221/p003225/raw\" # the folder to read data from, required\n", "out_folder = \"/gpfs/exfel/data/scratch/ahmedk/test/gotthard2\" # the folder to output to, required\n", "metadata_folder = \"\" # Directory containing calibration_metadata.yml when run by xfel-calibrate\n", "run = 50 # run to process, required\n", "sequences = [-1] # sequences to correct, set to [-1] for all, range allowed\n", "sequences_per_node = 1 # number of sequence files per node if notebook executed through xfel-calibrate, set to 0 to not run SLURM parallel\n", "\n", "# Parameters used to access raw data.\n", "karabo_id = \"FXE_XAD_G2XES\" # karabo prefix of Gotthard-II devices\n", "karabo_da = [\"GH201\"] # data aggregators\n", "receiver_template = \"RECEIVER\" # receiver template used to read INSTRUMENT keys.\n", "control_template = \"CONTROL\" # control template used to read CONTROL keys.\n", "instrument_source_template = \"{}/DET/{}:daqOutput\" # template for source name (filled with karabo_id & receiver_id). e.g. 'SPB_IRDA_JF4M/DET/JNGFR01:daqOutput'\n", "ctrl_source_template = \"{}/DET/{}\" # template for control source name (filled with karabo_id_control)\n", "karabo_id_control = \"\" # Control karabo ID. Set to empty string to use the karabo-id\n", "\n", "# Parameters for calibration database.\n", "cal_db_interface = \"tcp://max-exfl-cal001:8016#8025\" # the database interface to use.\n", "cal_db_timeout = 180000 # timeout on caldb requests.\n", "creation_time = \"\" # To overwrite the measured creation_time. Required Format: YYYY-MM-DD HR:MN:SC e.g. \"2022-06-28 13:00:00\"\n", "\n", "# Parameters affecting corrected data.\n", "constants_file = \"\" # Use constants in given constant file path. /gpfs/exfel/data/scratch/ahmedk/dont_remove/gotthard2/constants/calibration_constants_GH2.h5\n", "offset_correction = True # apply offset correction. This can be disabled to only apply LUT or apply LUT and gain correction for non-linear differential results.\n", "gain_correction = True # apply gain correction.\n", "chunks_data = 1 # HDF chunk size for pixel data in number of frames.\n", "\n", "# Parameter conditions.\n", "bias_voltage = -1 # Detector bias voltage, set to -1 to use value in raw file.\n", "exposure_time = -1. # Detector exposure time, set to -1 to use value in raw file.\n", "exposure_period = -1. # Detector exposure period, set to -1 to use value in raw file.\n", "acquisition_rate = -1. # Detector acquisition rate (1.1/4.5), set to -1 to use value in raw file.\n", "single_photon = -1 # Detector single photon mode (High/Low CDS), set to -1 to use value in raw file.\n", "\n", "# Parameters for plotting\n", "skip_plots = False # exit after writing corrected files\n", "pulse_idx_preview = 3 # pulse index to preview. The following even/odd pulse index is used for preview. # TODO: update to pulseId preview.\n", "\n", "\n", "def balance_sequences(in_folder, run, sequences, sequences_per_node, karabo_da):\n", " from xfel_calibrate.calibrate import balance_sequences as bs\n", " return bs(in_folder, run, sequences, sequences_per_node, karabo_da)" ] }, { "cell_type": "code", "execution_count": null, "id": "6e9730d8-3908-41d7-abe2-d78e046d5de2", "metadata": {}, "outputs": [], "source": [ "import warnings\n", "from logging import warning\n", "\n", "import h5py\n", "import pasha as psh\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "from IPython.display import Markdown, display\n", "from extra_data import RunDirectory, H5File\n", "from pathlib import Path\n", "\n", "import cal_tools.restful_config as rest_cfg\n", "from cal_tools.calcat_interface import CalCatError, GOTTHARD2_CalibrationData\n", "from cal_tools.files import DataFile\n", "from cal_tools.gotthard2 import gotthard2algs, gotthard2lib\n", "from cal_tools.step_timing import StepTimer\n", "from cal_tools.tools import (\n", " calcat_creation_time,\n", " write_constants_fragment,\n", ")\n", "from XFELDetAna.plotting.heatmap import heatmapPlot\n", "\n", "warnings.filterwarnings('ignore')\n", "\n", "%matplotlib inline" ] }, { "cell_type": "code", "execution_count": null, "id": "d7c02c48-4429-42ea-a42e-de45366d7fa3", "metadata": {}, "outputs": [], "source": [ "in_folder = Path(in_folder)\n", "run_folder = in_folder / f\"r{run:04d}\"\n", "out_folder = Path(out_folder)\n", "out_folder.mkdir(parents=True, exist_ok=True)\n", "\n", "if not karabo_id_control:\n", " karabo_id_control = karabo_id\n", "\n", "instrument_src = instrument_source_template.format(karabo_id, receiver_template)\n", "ctrl_src = ctrl_source_template.format(karabo_id_control, control_template)\n", "\n", "print(f\"Process modules: {karabo_da} for run {run}\")\n", "\n", "# Run's creation time:\n", "creation_time = calcat_creation_time(in_folder, run, creation_time)\n", "print(f\"Creation time: {creation_time}\")" ] }, { "cell_type": "code", "execution_count": null, "id": "b5eb816e-b5f2-44ce-9907-0273d82341b6", "metadata": {}, "outputs": [], "source": [ "# Select only sequence files to process for the selected detector.\n", "if sequences == [-1]:\n", " possible_patterns = list(f\"*{mod}*.h5\" for mod in karabo_da)\n", "else:\n", " possible_patterns = list(\n", " f\"*{mod}-S{s:05d}.h5\" for mod in karabo_da for s in sequences\n", " )\n", "\n", "run_folder = Path(in_folder / f\"r{run:04d}\")\n", "seq_files = [\n", " f for f in run_folder.glob(\"*.h5\") if any(f.match(p) for p in possible_patterns)\n", "]\n", "\n", "seq_files = sorted(seq_files)\n", "\n", "if not seq_files:\n", " raise IndexError(\"No sequence files available for the selected sequences.\")\n", "\n", "print(f\"Processing a total of {len(seq_files)} sequence files\")" ] }, { "cell_type": "code", "execution_count": null, "id": "f9a8d1eb-ce6a-4ed0-abf4-4a6029734672", "metadata": {}, "outputs": [], "source": [ "step_timer = StepTimer()" ] }, { "cell_type": "code", "execution_count": null, "id": "892172d8", "metadata": {}, "outputs": [], "source": [ "# Read slow data\n", "run_dc = RunDirectory(run_folder)\n", "g2ctrl = gotthard2lib.Gotthard2Ctrl(run_dc=run_dc, ctrl_src=ctrl_src)\n", "\n", "if bias_voltage == -1:\n", " bias_voltage = g2ctrl.get_bias_voltage()\n", "if exposure_time == -1:\n", " exposure_time = g2ctrl.get_exposure_time()\n", "if exposure_period == -1:\n", " exposure_period = g2ctrl.get_exposure_period()\n", "if acquisition_rate == -1:\n", " acquisition_rate = g2ctrl.get_acquisition_rate()\n", "if single_photon == -1:\n", " single_photon = g2ctrl.get_single_photon()\n", "\n", "print(\"Bias Voltage:\", bias_voltage)\n", "print(\"Exposure Time:\", exposure_time)\n", "print(\"Exposure Period:\", exposure_period)\n", "print(\"Acquisition Rate:\", acquisition_rate)\n", "print(\"Single Photon:\", single_photon)" ] }, { "cell_type": "markdown", "id": "8c852392-bb19-4c40-b2ce-3b787538a92d", "metadata": {}, "source": [ "### Retrieving calibration constants" ] }, { "cell_type": "code", "execution_count": null, "id": "5717d722", "metadata": {}, "outputs": [], "source": [ "da_to_pdu = {}\n", "# Used for old FXE (p003225) runs before adding Gotthard2 to CALCAT\n", "const_data = dict()\n", "\n", "g2_cal = GOTTHARD2_CalibrationData(\n", " detector_name=karabo_id,\n", " sensor_bias_voltage=bias_voltage,\n", " exposure_time=exposure_time,\n", " exposure_period=exposure_period,\n", " acquisition_rate=acquisition_rate,\n", " single_photon=single_photon,\n", " event_at=creation_time,\n", " client=rest_cfg.calibration_client(),\n", ")\n", "# Keep as long as it is essential to correct\n", "# RAW data (FXE p003225) before the data mapping was added to CALCAT.\n", "try: # in case local constants are used with old RAW data. This can be removed in the future.\n", " for mod_info in g2_cal.physical_detector_units.values():\n", " da_to_pdu[mod_info[\"karabo_da\"]] = mod_info[\"physical_name\"]\n", " db_modules = [da_to_pdu[da] for da in karabo_da]\n", "except CalCatError as e:\n", " print(e)\n", " db_modules = [None] * len(karabo_da)\n", "\n", "if constants_file:\n", " for mod in karabo_da:\n", " const_data[mod] = dict()\n", " # load constants temporarily using defined local paths.\n", " with h5py.File(constants_file, \"r\") as cfile:\n", " const_data[mod][\"LUTGotthard2\"] = cfile[\"LUT\"][()]\n", " const_data[mod][\"OffsetGotthard2\"] = cfile[\"offset_map\"][()].astype(np.float32)\n", " const_data[mod][\"RelativeGainGotthard2\"] = cfile[\"gain_map\"][()].astype(np.float32)\n", " const_data[mod][\"Mask\"] = cfile[\"bpix_ff\"][()].astype(np.uint32)\n", "else:\n", " constant_names = [\"LUTGotthard2\", \"OffsetGotthard2\", \"BadPixelsDarkGotthard2\"]\n", " if gain_correction:\n", " constant_names += [\"RelativeGainGotthard2\", \"BadPixelsFFGotthard2\"]\n", "\n", " g2_metadata = g2_cal.metadata(calibrations=constant_names)\n", "\n", " # Validate the constants availability and raise/warn correspondingly.\n", " for mod, calibrations in g2_metadata.items():\n", "\n", " dark_constants = {\"LUTGotthard2\"}\n", " if offset_correction:\n", " dark_constants |= {\"OffsetGotthard2\", \"BadPixelsDarkGotthard2\"}\n", "\n", " missing_dark_constants = dark_constants - set(calibrations)\n", " if missing_dark_constants:\n", " karabo_da.remove(mod)\n", " warning(f\"Dark constants {missing_dark_constants} are not available to correct {mod}.\") # noqa\n", "\n", " missing_gain_constants = {\n", " \"BadPixelsFFGotthard2\", \"RelativeGainGotthard2\"} - set(calibrations)\n", " if gain_correction and missing_gain_constants:\n", " warning(f\"Gain constants {missing_gain_constants} are not retrieved for mod {mod}.\")\n", "\n", "if not karabo_da:\n", " raise ValueError(\"Dark constants are not available for all modules.\")" ] }, { "cell_type": "code", "execution_count": null, "id": "ac1cdec5", "metadata": {}, "outputs": [], "source": [ "# Record constant details in YAML metadata.\n", "write_constants_fragment(\n", " out_folder=(metadata_folder or out_folder),\n", " det_metadata=g2_metadata,\n", " caldb_root=g2_cal.caldb_root)\n", "\n", "# Load constants data for all constants.\n", "const_data = g2_cal.ndarray_map(metadata=g2_metadata)\n", "\n", "# Prepare constant arrays.\n", "if not constants_file:\n", " # Create the mask array.\n", " bpix = const_data[mod].get(\"BadPixelsDarkGotthard2\")\n", " if bpix is None:\n", " bpix = np.zeros((1280, 2, 3), dtype=np.uint32)\n", " if const_data[mod].get(\"BadPixelsFFGotthard2\") is not None:\n", " bpix |= const_data[mod][\"BadPixelsFFGotthard2\"]\n", " const_data[mod][\"Mask\"] = bpix\n", "\n", " # Prepare empty arrays for missing constants.\n", " if const_data[mod].get(\"OffsetGotthard2\") is None:\n", " const_data[mod][\"OffsetGotthard2\"] = np.zeros(\n", " (1280, 2, 3), dtype=np.float32)\n", "\n", " if const_data[mod].get(\"RelativeGainGotthard2\") is None:\n", " const_data[mod][\"RelativeGainGotthard2\"] = np.ones(\n", " (1280, 2, 3), dtype=np.float32)\n", " const_data[mod][\"RelativeGainGotthard2\"] = const_data[mod][\"RelativeGainGotthard2\"].astype( # noqa\n", " np.float32, copy=False) # Old gain constants are not float32." ] }, { "cell_type": "code", "execution_count": null, "id": "23fcf7f4-351a-4df7-8829-d8497d94fecc", "metadata": {}, "outputs": [], "source": [ "context = psh.ProcessContext(num_workers=23)" ] }, { "cell_type": "code", "execution_count": null, "id": "daecd662-26d2-4cb8-aa70-383a579cf9f9", "metadata": {}, "outputs": [], "source": [ "def correct_train(wid, index, d):\n", " g = gain[index]\n", " gotthard2algs.convert_to_10bit(d, const_data[mod][\"LUTGotthard2\"], data_corr[index, ...])\n", " gotthard2algs.correct_train(\n", " data_corr[index, ...],\n", " mask[index, ...],\n", " g,\n", " const_data[mod][\"OffsetGotthard2\"],\n", " const_data[mod][\"RelativeGainGotthard2\"], \n", " const_data[mod][\"Mask\"],\n", " apply_offset=offset_correction,\n", " apply_gain=gain_correction,\n", " )" ] }, { "cell_type": "code", "execution_count": null, "id": "f88c1aa6-a735-4b72-adce-b30162f5daea", "metadata": {}, "outputs": [], "source": [ "for mod in karabo_da:\n", " # This is used in case receiver template consists of\n", " # karabo data aggregator index. e.g. detector at DETLAB\n", " instr_mod_src = instrument_src.format(mod[-2:])\n", " data_path = \"INSTRUMENT/\" + instr_mod_src + \"/data\"\n", " for raw_file in seq_files:\n", " step_timer.start()\n", "\n", " dc = H5File(raw_file)\n", " out_file = out_folder / raw_file.name.replace(\"RAW\", \"CORR\")\n", "\n", " # Select module INSTRUMENT source and deselect empty trains.\n", " dc = dc.select(instr_mod_src, require_all=True)\n", " data = dc[instr_mod_src, \"data.adc\"].ndarray()\n", " gain = dc[instr_mod_src, \"data.gain\"].ndarray()\n", " step_timer.done_step(\"preparing raw data\")\n", " dshape = data.shape\n", "\n", " step_timer.start()\n", "\n", " # Allocate shared arrays.\n", " data_corr = context.alloc(shape=dshape, dtype=np.float32)\n", " mask = context.alloc(shape=dshape, dtype=np.uint32)\n", " context.map(correct_train, data)\n", " step_timer.done_step(\"Correcting one sequence file\")\n", "\n", " step_timer.start()\n", "\n", " # Provided PSI gain map has 0 values. Set inf values to nan.\n", " # TODO: This can maybe be removed after creating XFEL gain maps.?\n", " data_corr[np.isinf(data_corr)] = np.nan\n", "\n", " # Create CORR files and add corrected data sections.\n", " image_counts = dc[instrument_src, \"data.adc\"].data_counts(labelled=False)\n", "\n", " with DataFile(out_file, \"w\") as ofile:\n", " # Create INDEX datasets.\n", " ofile.create_index(dc.train_ids, from_file=dc.files[0])\n", " # Create METDATA datasets\n", " ofile.create_metadata(\n", " like=dc,\n", " sequence=dc.run_metadata()[\"sequenceNumber\"],\n", " instrument_channels=(f\"{instrument_src}/data\",)\n", " )\n", "\n", " # Create Instrument section to later add corrected datasets.\n", " outp_source = ofile.create_instrument_source(instrument_src)\n", "\n", " # Create count/first datasets at INDEX source.\n", " outp_source.create_index(data=image_counts)\n", "\n", " # Store uncorrected trainId in the corrected file.\n", " outp_source.create_key(\n", " f\"data.trainId\", data=dc.train_ids,\n", " chunks=min(50, len(dc.train_ids))\n", " )\n", "\n", " # Create datasets with the available corrected data\n", " for field_name, field_data in {\n", " \"adc\": data_corr,\n", " \"gain\": gain,\n", " }.items():\n", " outp_source.create_key(\n", " f\"data.{field_name}\", data=field_data,\n", " chunks=((chunks_data,) + data_corr.shape[1:])\n", " )\n", "\n", " for field in [\"bunchId\", \"memoryCell\", \"frameNumber\", \"timestamp\"]:\n", " outp_source.create_key(\n", " f\"data.{field}\", data=dc[instr_mod_src, f\"data.{field}\"].ndarray(),\n", " chunks=(chunks_data, data_corr.shape[1])\n", " )\n", " outp_source.create_compressed_key(f\"data.mask\", data=mask)\n", "\n", " step_timer.done_step(\"Storing data\")" ] }, { "cell_type": "code", "execution_count": null, "id": "94b8e4d2-9f8c-4c23-a509-39238dd8435c", "metadata": {}, "outputs": [], "source": [ "print(f\"Total processing time {step_timer.timespan():.01f} s\")\n", "step_timer.print_summary()" ] }, { "cell_type": "code", "execution_count": null, "id": "0ccc7f7e-2a3f-4ac0-b854-7d505410d2fd", "metadata": {}, "outputs": [], "source": [ "if skip_plots:\n", " print(\"Skipping plots\")\n", " import sys\n", "\n", " sys.exit(0)" ] }, { "cell_type": "code", "execution_count": null, "id": "ff203f77-3811-46f3-bf7d-226d2dcab13f", "metadata": {}, "outputs": [], "source": [ "mod_dcs = {}\n", "first_seq_raw = seq_files[0]\n", "first_seq_corr = out_folder / first_seq_raw.name.replace(\"RAW\", \"CORR\")\n", "for mod in karabo_da:\n", " mod_dcs[mod] = {}\n", " with H5File(first_seq_corr) as out_dc:\n", " tid, mod_dcs[mod][\"train_corr_data\"] = next(\n", " out_dc[instr_mod_src, \"data.adc\"].trains()\n", " )\n", " with H5File(first_seq_raw) as in_dc:\n", " train_dict = in_dc.train_from_id(tid)[1][instr_mod_src]\n", " mod_dcs[mod][\"train_raw_data\"] = train_dict[\"data.adc\"]\n", " mod_dcs[mod][\"train_raw_gain\"] = train_dict[\"data.gain\"]" ] }, { "cell_type": "code", "execution_count": null, "id": "1b379438-eb1d-42b2-ac83-eb8cf88c46db", "metadata": {}, "outputs": [], "source": [ "display(Markdown(\"### Mean RAW and CORRECTED across pulses for one train:\"))\n", "display(Markdown(f\"Train: {tid}\"))\n", "\n", "step_timer.start()\n", "for mod, pdu in zip(karabo_da, db_modules):\n", "\n", " fig, ax = plt.subplots(figsize=(20, 10))\n", " raw_data = mod_dcs[mod][\"train_raw_data\"]\n", " im = ax.plot(np.mean(raw_data, axis=0))\n", " ax.set_title(f\"RAW module {mod} ({pdu})\")\n", " ax.set_xlabel(\"Strip #\", size=20)\n", " ax.set_ylabel(\"12-bit ADC output\", size=20)\n", " plt.xticks(fontsize=20)\n", " plt.yticks(fontsize=20)\n", " pass\n", "\n", " fig, ax = plt.subplots(figsize=(20, 10))\n", " corr_data = mod_dcs[mod][\"train_corr_data\"]\n", " im = ax.plot(np.mean(corr_data, axis=0))\n", " ax.set_title(f\"CORRECTED module {mod} ({pdu})\")\n", " ax.set_xlabel(\"Strip #\", size=20)\n", " ax.set_ylabel(\"10-bit KeV. output\", size=20)\n", " plt.xticks(fontsize=20)\n", " plt.yticks(fontsize=20)\n", " pass\n", "step_timer.done_step(\"Plotting mean data\")" ] }, { "cell_type": "code", "execution_count": null, "id": "58a6a276", "metadata": {}, "outputs": [], "source": [ "display(Markdown(f\"### RAW and CORRECTED strips across pulses for train {tid}\"))\n", "\n", "step_timer.start()\n", "for mod, pdu in zip(karabo_da, db_modules):\n", " for plt_data, dname in zip(\n", " [\"train_raw_data\", \"train_corr_data\"], [\"RAW\", \"CORRECTED\"]\n", " ):\n", " fig, ax = plt.subplots(figsize=(15, 20))\n", " plt.rcParams.update({\"font.size\": 20})\n", "\n", " heatmapPlot(\n", " mod_dcs[mod][plt_data],\n", " y_label=\"Pulses\",\n", " x_label=\"Strips\",\n", " title=f\"{dname} module {mod} ({pdu})\",\n", " use_axis=ax,\n", " )\n", " pass\n", "step_timer.done_step(\"Plotting RAW and CORRECTED data for one train\")" ] }, { "cell_type": "code", "execution_count": null, "id": "cd8f5e08-fcee-4bff-ba63-6452b3d892a2", "metadata": {}, "outputs": [], "source": [ "# Validate given \"pulse_idx_preview\"\n", "\n", "if pulse_idx_preview + 1 > data.shape[1]:\n", " print(\n", " f\"WARNING: selected pulse_idx_preview {pulse_idx_preview} is not available in data.\"\n", " \" Previewing 1st pulse.\"\n", " )\n", " pulse_idx_preview = 1\n", "\n", "if data.shape[1] == 1:\n", " odd_pulse = 1\n", " even_pulse = None\n", "else:\n", " odd_pulse = pulse_idx_preview if pulse_idx_preview % 2 else pulse_idx_preview + 1\n", " even_pulse = (\n", " pulse_idx_preview if not (pulse_idx_preview % 2) else pulse_idx_preview + 1\n", " )\n", "\n", "if pulse_idx_preview + 1 > data.shape[1]:\n", " pulse_idx_preview = 1\n", " if data.shape[1] > 1:\n", " pulse_idx_preview = 2" ] }, { "cell_type": "code", "execution_count": null, "id": "e5f0d4d8-e32c-4f2c-8469-4ebbfd3f644c", "metadata": {}, "outputs": [], "source": [ "display(Markdown(\"### RAW and CORRECTED even/odd pulses for one train:\"))\n", "display(Markdown(f\"Train: {tid}\"))\n", "for mod, pdu in zip(karabo_da, db_modules):\n", " fig, ax = plt.subplots(figsize=(20, 20))\n", " raw_data = mod_dcs[mod][\"train_raw_data\"]\n", " corr_data = mod_dcs[mod][\"train_corr_data\"]\n", "\n", " ax.plot(raw_data[odd_pulse], label=f\"Odd Pulse {odd_pulse}\")\n", " if even_pulse:\n", " ax.plot(raw_data[even_pulse], label=f\"Even Pulse {even_pulse}\")\n", "\n", " ax.set_title(f\"RAW module {mod} ({pdu})\")\n", " ax.set_xlabel(\"Strip #\", size=20)\n", " ax.set_ylabel(\"12-bit ADC RAW\", size=20)\n", " plt.xticks(fontsize=20)\n", " plt.yticks(fontsize=20)\n", " ax.legend()\n", " pass\n", "\n", " fig, ax = plt.subplots(figsize=(20, 20))\n", " ax.plot(corr_data[odd_pulse], label=f\"Odd Pulse {odd_pulse}\")\n", " if even_pulse:\n", " ax.plot(corr_data[even_pulse], label=f\"Even Pulse {even_pulse}\")\n", " ax.set_title(f\"CORRECTED module {mod} ({pdu})\")\n", " ax.set_xlabel(\"Strip #\", size=20)\n", " ax.set_ylabel(\"10-bit KeV CORRECTED\", size=20)\n", " plt.xticks(fontsize=20)\n", " plt.yticks(fontsize=20)\n", " ax.legend()\n", " pass\n", "step_timer.done_step(\"Plotting RAW and CORRECTED odd/even pulses.\")" ] } ], "metadata": { "kernelspec": { "display_name": "cal4_venv", "language": "python", "name": "cal4_venv" }, "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.11" }, "vscode": { "interpreter": { "hash": "916dbcbb3f70747c44a77c7bcd40155683ae19c65e1c03b4aa3499c5328201f1" } } }, "nbformat": 4, "nbformat_minor": 5 }