{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# QCoDeS config\n", "\n", "The QCoDeS config module uses JSON files to store QCoDeS configuration modeled after the module structure with some high-level switches. \n", "\n", "The config file controls various options to QCoDeS such as the default path and name of the database in which your data is stored and logging level of the debug output. QCoDeS is shipped with a default configuration. As we describe below, you may overwrite these default values to customize QCoDeS for your nanoelectronics research. \n", "\n", "You may want to do something as simple as changing the default path of your database to something complex as including your own configuration variables. In this example notebook, we will explore some of the configuration variables, demonstrate how configuration values may be changed at runtime and saved to files, and explore using custom configuration values to suit your own needs. \n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "QCoDeS loads both the defaults and the active configuration at the module import so that you can directly inspect them" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "import qcodes as qc" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'core': {'default_fmt': 'data/{date}/#{counter}_{name}_{time}',\n", " 'register_magic': False,\n", " 'import_legacy_api': False,\n", " 'db_location': '~/experiments.db',\n", " 'db_debug': False,\n", " 'loglevel': 'WARNING',\n", " 'file_loglevel': 'INFO'},\n", " 'logger': {'start_logging_on_import': 'if_telemetry_set_up',\n", " 'console_level': 'WARNING',\n", " 'file_level': 'INFO',\n", " 'logger_levels': {'pyvisa': 'INFO'}},\n", " 'subscription': {'subscribers': {'QCPlot': {'factory': 'qcplotutils.qcodes_dataset.QCPlotDatasetSubscriber',\n", " 'factory_kwargs': {'log': False},\n", " 'subscription_kwargs': {'min_wait': 0,\n", " 'min_count': 1,\n", " 'callback_kwargs': {}}}},\n", " 'default_subscribers': []},\n", " 'gui': {'notebook': True,\n", " 'plotlib': None,\n", " 'pyqtmaxplots': 100,\n", " 'defaultcolormap': 'hot'},\n", " 'plotting': {'default_color_map': 'viridis',\n", " 'rasterize_threshold': 5000,\n", " 'auto_color_scale': {'enabled': False,\n", " 'cutoff_percentile': [0.5, 0.5],\n", " 'color_over': '#a1c4fc',\n", " 'color_under': '#017000'}},\n", " 'user': {'scriptfolder': '.', 'mainfolder': '.'},\n", " 'station': {'enable_forced_reconnect': False,\n", " 'default_folder': '.',\n", " 'default_file': None,\n", " 'use_monitor': False},\n", " 'GUID_components': {'location': 0, 'work_station': 0, 'sample': 0},\n", " 'dataset': {'write_in_background': False,\n", " 'write_period': 5.0,\n", " 'use_threads': False,\n", " 'dond_plot': False,\n", " 'dond_show_progress': False,\n", " 'export_automatic': False,\n", " 'export_type': None,\n", " 'export_prefix': 'qcodes_',\n", " 'export_path': '~',\n", " 'export_name_elements': ['captured_run_id', 'guid']},\n", " 'telemetry': {'enabled': False,\n", " 'instrumentation_key': '00000000-0000-0000-0000-000000000000'}}" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "qc.config.current_config" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The current config is build from multiple config files.\n", "\n", "The config files are read in the following order:\n", "\n", "* Package defaults\n", "* User's home directory\n", "* QCODES_CONFIG environment variable\n", "* Current working directory\n", "\n", "Meaning that any value set in a config file in the package default is overwritten by the same value in the users home dir and so on. \n", "\n", "Note: A change in the configuration requires reimporting the package, or restarting the notebook kernel." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'core': {'default_fmt': 'data/{date}/#{counter}_{name}_{time}',\n", " 'register_magic': False,\n", " 'import_legacy_api': False,\n", " 'db_location': '~/experiments.db',\n", " 'db_debug': False,\n", " 'loglevel': 'WARNING',\n", " 'file_loglevel': 'INFO'},\n", " 'logger': {'start_logging_on_import': 'if_telemetry_set_up',\n", " 'console_level': 'WARNING',\n", " 'file_level': 'INFO',\n", " 'logger_levels': {'pyvisa': 'INFO'}},\n", " 'subscription': {'subscribers': {'QCPlot': {'factory': 'qcplotutils.qcodes_dataset.QCPlotDatasetSubscriber',\n", " 'factory_kwargs': {'log': False},\n", " 'subscription_kwargs': {'min_wait': 0,\n", " 'min_count': 1,\n", " 'callback_kwargs': {}}}},\n", " 'default_subscribers': []},\n", " 'gui': {'notebook': True,\n", " 'plotlib': None,\n", " 'pyqtmaxplots': 100,\n", " 'defaultcolormap': 'hot'},\n", " 'plotting': {'default_color_map': 'viridis',\n", " 'rasterize_threshold': 5000,\n", " 'auto_color_scale': {'enabled': False,\n", " 'cutoff_percentile': [0.5, 0.5],\n", " 'color_over': '#a1c4fc',\n", " 'color_under': '#017000'}},\n", " 'user': {'scriptfolder': '.', 'mainfolder': '.'},\n", " 'station': {'enable_forced_reconnect': False,\n", " 'default_folder': '.',\n", " 'default_file': None,\n", " 'use_monitor': False},\n", " 'GUID_components': {'location': 0, 'work_station': 0, 'sample': 0},\n", " 'dataset': {'write_in_background': False,\n", " 'write_period': 5.0,\n", " 'use_threads': False,\n", " 'dond_plot': False,\n", " 'dond_show_progress': False,\n", " 'export_automatic': False,\n", " 'export_type': None,\n", " 'export_prefix': 'qcodes_',\n", " 'export_path': '~',\n", " 'export_name_elements': ['captured_run_id', 'guid']},\n", " 'telemetry': {'enabled': False,\n", " 'instrumentation_key': '00000000-0000-0000-0000-000000000000'}}" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "qc.config.defaults" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "One can inspect what the configuration options mean at runtime" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "controls core settings of qcodes.\n", "Current value: {'default_fmt': 'data/{date}/#{counter}_{name}_{time}', 'register_magic': False, 'import_legacy_api': False, 'db_location': '~/experiments.db', 'db_debug': False, 'loglevel': 'WARNING', 'file_loglevel': 'INFO'}. Type: object. Default: Not defined.\n" ] } ], "source": [ "print(qc.config.describe('core'))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The default configuration is explained in more detail in the [api docs](../api/configuration/index.rst#qcodes-default-configuration)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Configuring QCoDeS" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Defaults are the settings that are shipped with the package, which you can overwrite programmatically." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A way to customize QCoDeS is to write your own JSON files, they are expected to be in the directories printed below and documented above.\n", "One will be empty because one needs to define first the environment variable in the OS. \n", "\n", "They are ordered by \"weight\", meaning that the last file always wins if it's overwriting any preconfigured defaults or values in the other files.\n", "\n", "Simply copy the file to the directories and you are good to go." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "C:\\Users\\Jens-work\\qcodesrc.json\n", "\n", "C:\\Users\\Jens-work\\source\\repos\\Qcodes\\docs\\examples\\qcodesrc.json\n" ] } ], "source": [ "print(\"\\n\".join([qc.config.home_file_name, qc.config.env_file_name, qc.config.cwd_file_name]))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The easiest way to add something to the configuration is to use the provided helper:" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "qc.config.add(\"base_location\", \"/dev/random\", value_type=\"string\", description=\"Location of data\", default=\"/dev/random\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This will add a `base_location` with value `/dev/random` to the current configuration, and validate it's value to be of type string, will also set the description and what one would want to have as default.\n", "The new entry is saved in the 'user' part of the configuration." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Location of data.\n", "Current value: /dev/random. Type: string. Default: /dev/random.\n" ] } ], "source": [ "print(qc.config.describe('user.base_location'))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can also manually update the configuration from a specific file by supplying the path of the directory as the argument of `qc.config.update_config` method as follows: " ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'core': {'default_fmt': 'data/{date}/#{counter}_{name}_{time}',\n", " 'register_magic': False,\n", " 'import_legacy_api': False,\n", " 'db_location': '~/experiments.db',\n", " 'db_debug': False,\n", " 'loglevel': 'WARNING',\n", " 'file_loglevel': 'INFO'},\n", " 'logger': {'start_logging_on_import': 'if_telemetry_set_up',\n", " 'console_level': 'WARNING',\n", " 'file_level': 'INFO',\n", " 'logger_levels': {'pyvisa': 'INFO'}},\n", " 'subscription': {'subscribers': {'QCPlot': {'factory': 'qcplotutils.qcodes_dataset.QCPlotDatasetSubscriber',\n", " 'factory_kwargs': {'log': False},\n", " 'subscription_kwargs': {'min_wait': 0,\n", " 'min_count': 1,\n", " 'callback_kwargs': {}}}},\n", " 'default_subscribers': []},\n", " 'gui': {'notebook': True,\n", " 'plotlib': None,\n", " 'pyqtmaxplots': 100,\n", " 'defaultcolormap': 'hot'},\n", " 'plotting': {'default_color_map': 'viridis',\n", " 'rasterize_threshold': 5000,\n", " 'auto_color_scale': {'enabled': False,\n", " 'cutoff_percentile': [0.5, 0.5],\n", " 'color_over': '#a1c4fc',\n", " 'color_under': '#017000'}},\n", " 'user': {'scriptfolder': '.', 'mainfolder': '.'},\n", " 'station': {'enable_forced_reconnect': False,\n", " 'default_folder': '.',\n", " 'default_file': None,\n", " 'use_monitor': False},\n", " 'GUID_components': {'location': 0, 'work_station': 0, 'sample': 0},\n", " 'dataset': {'write_in_background': False,\n", " 'write_period': 5.0,\n", " 'use_threads': False,\n", " 'dond_plot': False,\n", " 'dond_show_progress': False,\n", " 'export_automatic': False,\n", " 'export_type': None,\n", " 'export_prefix': 'qcodes_',\n", " 'export_path': '~',\n", " 'export_name_elements': ['captured_run_id', 'guid']},\n", " 'telemetry': {'enabled': False,\n", " 'instrumentation_key': '00000000-0000-0000-0000-000000000000'}}" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "qc.config.update_config(path=\"C:\\\\Users\\\\jenielse\\\\\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Saving changes" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "All the changes made to the defaults are stored, and one can then decide to save them to the desired location." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Help on method save_to_cwd in module qcodes.configuration.config:\n", "\n", "save_to_cwd() -> None method of qcodes.configuration.config.Config instance\n", " Save config and schema to files in current working dir\n", "\n" ] } ], "source": [ "help(qc.config.save_to_cwd)" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Help on method save_to_env in module qcodes.configuration.config:\n", "\n", "save_to_env() -> None method of qcodes.configuration.config.Config instance\n", " Save config and schema to files in path specified in env variable\n", "\n" ] } ], "source": [ "help(qc.config.save_to_env)" ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Help on method save_to_home in module qcodes.configuration.config:\n", "\n", "save_to_home() -> None method of qcodes.configuration.config.Config instance\n", " Save config and schema to files in home dir\n", "\n" ] } ], "source": [ "help(qc.config.save_to_home)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Using custom variables in your experiment:\n", "\n", "Simply get the value you have set before with dot notation.\n", "For example:" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'core': {'default_fmt': 'data/{date}/#{counter}_{name}_{time}',\n", " 'register_magic': False,\n", " 'import_legacy_api': False,\n", " 'db_location': '~/experiments.db',\n", " 'db_debug': False,\n", " 'loglevel': 'WARNING',\n", " 'file_loglevel': 'INFO'},\n", " 'logger': {'start_logging_on_import': 'if_telemetry_set_up',\n", " 'console_level': 'WARNING',\n", " 'file_level': 'INFO',\n", " 'logger_levels': {'pyvisa': 'INFO'}},\n", " 'subscription': {'subscribers': {'QCPlot': {'factory': 'qcplotutils.qcodes_dataset.QCPlotDatasetSubscriber',\n", " 'factory_kwargs': {'log': False},\n", " 'subscription_kwargs': {'min_wait': 0,\n", " 'min_count': 1,\n", " 'callback_kwargs': {}}}},\n", " 'default_subscribers': []},\n", " 'gui': {'notebook': True,\n", " 'plotlib': None,\n", " 'pyqtmaxplots': 100,\n", " 'defaultcolormap': 'hot'},\n", " 'plotting': {'default_color_map': 'viridis',\n", " 'rasterize_threshold': 5000,\n", " 'auto_color_scale': {'enabled': False,\n", " 'cutoff_percentile': [0.5, 0.5],\n", " 'color_over': '#a1c4fc',\n", " 'color_under': '#017000'}},\n", " 'user': {'scriptfolder': '.',\n", " 'mainfolder': '.',\n", " 'base_location': '/dev/random'},\n", " 'station': {'enable_forced_reconnect': False,\n", " 'default_folder': '.',\n", " 'default_file': None,\n", " 'use_monitor': False},\n", " 'GUID_components': {'location': 0, 'work_station': 0, 'sample': 0},\n", " 'dataset': {'write_in_background': False,\n", " 'write_period': 5.0,\n", " 'use_threads': False,\n", " 'dond_plot': False,\n", " 'dond_show_progress': False,\n", " 'export_automatic': False,\n", " 'export_type': None,\n", " 'export_prefix': 'qcodes_',\n", " 'export_path': '~',\n", " 'export_name_elements': ['captured_run_id', 'guid']},\n", " 'telemetry': {'enabled': False,\n", " 'instrumentation_key': '00000000-0000-0000-0000-000000000000'}}" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "qc.config.add(\"base_location\", \"/dev/random\", value_type=\"string\", description=\"Location of data\", default=\"/dev/random\")\n", "\n", "qc.config.current_config" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Changing core values" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "One can change the core values at runtime, but there is no guarantee that they are going to be valid.\n", "Since user configuration shadows the default one that comes with QCoDeS, apply care when changing the values under `core` section. This section is, primarily, meant for the settings that are determined by QCoDeS core developers. " ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [], "source": [ "qc.config.current_config.core.loglevel = 'INFO'" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "But one can manually validate via " ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [], "source": [ "qc.config.validate()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Which will raise an exception in case of bad inputs" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "ename": "ValidationError", "evalue": "'YOLO' is not one of ['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG']\n\nFailed validating 'enum' in schema['properties']['core']['properties']['loglevel']:\n {'default': 'DEBUG',\n 'description': 'deprecated - use logger.console_level',\n 'enum': ['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG'],\n 'type': 'string'}\n\nOn instance['core']['loglevel']:\n 'YOLO'", "output_type": "error", "traceback": [ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[1;31mValidationError\u001b[0m Traceback (most recent call last)", "\u001b[1;32mC:\\Users\\JENS-W~1\\AppData\\Local\\Temp/ipykernel_25952/2764489439.py\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[0;32m 1\u001b[0m \u001b[0mqc\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mconfig\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mcurrent_config\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mcore\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mloglevel\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;34m'YOLO'\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 2\u001b[1;33m \u001b[0mqc\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mconfig\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mvalidate\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 3\u001b[0m \u001b[1;31m# NOTE that you how have a broken config!\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", "\u001b[1;32m~\\Miniconda3\\envs\\qcodespip\\lib\\site-packages\\qcodes\\configuration\\config.py\u001b[0m in \u001b[0;36mvalidate\u001b[1;34m(self, json_config, schema, extra_schema_path)\u001b[0m\n\u001b[0;32m 210\u001b[0m \u001b[1;32melse\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 211\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mjson_config\u001b[0m \u001b[1;32mis\u001b[0m \u001b[1;32mNone\u001b[0m \u001b[1;32mand\u001b[0m \u001b[0mschema\u001b[0m \u001b[1;32mis\u001b[0m \u001b[1;32mNone\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 212\u001b[1;33m \u001b[0mjsonschema\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mvalidate\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mcurrent_config\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mcurrent_schema\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 213\u001b[0m \u001b[1;32melse\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 214\u001b[0m \u001b[0mjsonschema\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mvalidate\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mjson_config\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mschema\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", "\u001b[1;32m~\\Miniconda3\\envs\\qcodespip\\lib\\site-packages\\jsonschema\\validators.py\u001b[0m in \u001b[0;36mvalidate\u001b[1;34m(instance, schema, cls, *args, **kwargs)\u001b[0m\n\u001b[0;32m 1020\u001b[0m \u001b[0merror\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mexceptions\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mbest_match\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mvalidator\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0miter_errors\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0minstance\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 1021\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0merror\u001b[0m \u001b[1;32mis\u001b[0m \u001b[1;32mnot\u001b[0m \u001b[1;32mNone\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m-> 1022\u001b[1;33m \u001b[1;32mraise\u001b[0m \u001b[0merror\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 1023\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 1024\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n", "\u001b[1;31mValidationError\u001b[0m: 'YOLO' is not one of ['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG']\n\nFailed validating 'enum' in schema['properties']['core']['properties']['loglevel']:\n {'default': 'DEBUG',\n 'description': 'deprecated - use logger.console_level',\n 'enum': ['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG'],\n 'type': 'string'}\n\nOn instance['core']['loglevel']:\n 'YOLO'" ] } ], "source": [ "qc.config.current_config.core.loglevel = 'YOLO'\n", "qc.config.validate()\n", "# NOTE that you how have a broken config! " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "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.8.12" }, "nbsphinx": { "allow_errors": true } }, "nbformat": 4, "nbformat_minor": 4 }