{ "cells": [ { "cell_type": "code", "execution_count": null, "metadata": { "nbsphinx": "hidden", "tags": [] }, "outputs": [], "source": [ "# Workaround for determining the notebook folder within a running notebook\n", "# This cell is not visible when the documentation is built.\n", "\n", "from __future__ import annotations # noqa: F404\n", "\n", "try:\n", " from _finder import _find_current_folder\n", "\n", " notebook_folder = _find_current_folder()\n", "except ImportError:\n", " from pathlib import Path\n", "\n", " notebook_folder = Path().cwd()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Extending xclim\n", "\n", "`xclim` tries to make it easy for users to add their own indices and indicators. The following goes into details on how to create `**Indices**` and document them so that xclim can parse most of the metadata directly. We then explain the multiple ways new `**Indicators**` can be created and, finally, how we can regroup and structure them in virtual submodules.\n", "\n", "Central to `xclim` are the **Indicators**, objects computing indices over climate variables, but `xclim` also provides many other modules:\n", "\n", "![modules](./Modules.svg)\n", "\n", "This introduction will focus on the Indicator/Index part of `xclim` and how one can extend it by implementing new ones.\n", "\n", "## Indices vs Indicators\n", "\n", "Internally and in the documentation, `xclim` makes a distinction between \"indices\" and \"indicators\".\n", "\n", "### index\n", "\n", " * A python function accepting DataArrays and other parameters (usually built-in types)\n", " * Returns one or several DataArrays.\n", " * Handles the units : checks input units and set proper CF-compliant output units. But doesn't usually prescribe specific units, the output will at minimum have the proper dimensionality.\n", " * Performs **no** other checks or set any (non-unit) metadata.\n", " * Accessible through [xclim.indices](../indices.rst).\n", "\n", "### indicator\n", "\n", " * An instance of a subclass of `xclim.core.indicator.Indicator` that wraps around an `index` (stored in its `compute` property).\n", " * Returns one or several DataArrays.\n", " * Handles missing values, performs input data and metadata checks (see [usage](usage.ipynb#indicators)).\n", " * Always outputs data in the same units.\n", " * Adds dynamically generated metadata to the output after computation.\n", " * Accessible through [xclim.indicators](../indicators.rst)\n", "\n", "Most metadata stored in the Indicators is parsed from the underlying index documentation, so defining indices with complete documentation and an appropriate signature helps the process. The two next sections go into details on the definition of both objects.\n", "\n", "#### Call sequence\n", "\n", "The following graph shows the steps done when calling an Indicator. Attributes and methods of the Indicator object relating to those steps are listed on the right side.\n", "\n", "![indicator](Indicator.svg)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Defining new indices\n", "\n", "The annotated example below shows the general template to be followed when defining proper _indices_. In the comments, `Ind` is the indicator instance that would be created from this function.\n", "\n", "
\n", "\n", "Note that it is not _needed_ to follow these standards when writing indices that will be wrapped in indicators. Problems in parsing will not raise errors at runtime, but might raise warnings and will result in Indicators with poorer metadata than expected by most users, especially those that dynamically use indicators in other applications where the code is inaccessible, like web services.\n", "\n", "
\n", "\n", "![index doc](Indice.svg)\n", "\n", "The following code is another example." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "import xarray as xr\n", "from IPython.display import Code, display\n", "\n", "import xclim\n", "from xclim.core.units import convert_units_to, declare_units\n", "from xclim.indices.generic import threshold_count\n", "\n", "\n", "@declare_units(tasmax=\"[temperature]\", thresh=\"[temperature]\")\n", "def tx_days_compare(tasmax: xr.DataArray, thresh: str = \"0 degC\", op: str = \">\", freq: str = \"YS\"):\n", " r\"\"\"\n", " Number of days where maximum daily temperature is above or under a threshold.\n", "\n", " The daily maximum temperature is compared to a threshold using a given operator and the number\n", " of days where the condition is true is returned.\n", "\n", " It assumes a daily input.\n", "\n", " Parameters\n", " ----------\n", " tasmax : xarray.DataArray\n", " Maximum daily temperature.\n", " thresh : str\n", " Threshold temperature to compare to.\n", " op : {'>', '<'}\n", " The operator to use.\n", " # A fixed set of choices can be imposed. Only strings, numbers, booleans or None are accepted.\n", " freq : str\n", " Resampling frequency.\n", "\n", " Returns\n", " -------\n", " xarray.DataArray, [temperature]\n", " Maximum value of daily maximum temperature.\n", "\n", " Notes\n", " -----\n", " Let :math:`TX_{ij}` be the maximum temperature at day :math:`i` of period :math:`j`. Then the maximum\n", " daily maximum temperature for period :math:`j` is:\n", "\n", " .. math::\n", "\n", " TXx_j = max(TX_{ij})\n", "\n", " References\n", " ----------\n", " :cite:cts:`smith_citation_2020`\n", " \"\"\"\n", " thresh = convert_units_to(thresh, tasmax)\n", " out = threshold_count(tasmax, op, thresh, freq)\n", " out.attrs[\"units\"] = \"days\"\n", " return out" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Naming and conventions\n", "\n", "Variable names should correspond to CMIP6 variables, whenever possible. The file `xclim/data/variables.yml` lists all variables that xclim can use when generating indicators from YAML files (see below), and new indices should try to reflect these also.\n", "\n", "### Generic functions for common operations\n", "\n", "The [xclim.indices.generic](../indices.rst#generic-indices-submodule) submodule contains useful functions for common computations (like `threshold_count` or `select_resample_op`) and many basic index functions, as defined by [clix-meta](https://github.com/clix-meta/clix-meta). In order to reduce duplicate code, their use is recommended for xclim's indices. As previously said, the units handling has to be made explicitly when non-trivial, [xclim.core.units](../api.rst#units-handling-submodule) also exposes a few helpers for that (like `convert_units_to`, `to_agg_units` or `rate2amount`).\n", "\n", "### Documentation\n", "\n", "As shown in both example, a certain level of convention is best followed when writing the docstring of the index function. The general structure follows the NumpyDoc conventions, and some fields might be parsed when creating the indicator (see the image above and the section below). If you are contributing to the xclim codebase, when adding a citation to the docstring, this is best done by adding that reference to the ``references.bib`` file and then citing it using its label with the `:cite:cts:` directive (or one of its variant). See the [contributing docs](../contributing.rst#write-Documentation).\n", "\n", "## Defining new indicators\n", "\n", "xclim's Indicators are instances of (subclasses of) `xclim.core.indicator.Indicator`. While they are the central to xclim, their construction can be somewhat tricky as a lot happens backstage. Essentially, they act as self-aware functions, taking a set of input variables (DataArrays) and parameters (usually strings, integers or floats), performing some health checks on them and returning one or multiple DataArrays, with CF-compliant (and potentially translated) metadata attributes, masked according to a given missing value set of rules.\n", "They define the following key attributes:\n", "\n", "* the `identifier`, as string that uniquely identifies the indicator, usually all caps.\n", "* the `realm`, one of \"atmos\", \"land\", \"seaIce\" or \"ocean\", classifying the domain of use of the indicator.\n", "* the `compute` function that returns one or more DataArrays, the \"index\",\n", "* the `cfcheck` and `datacheck` methods that make sure the inputs are appropriate and valid.\n", "* the `missing` function that masks elements based on null values in the input.\n", "* all metadata attributes that will be attributed to the output and that document the indicator:\n", " - Indicator-level attribute are : `title`, `abstract`, `keywords`, `references` and `notes`.\n", " - Output variables attributes (respecting CF conventions) are: `var_name`, `standard_name`, `long_name`, `units`, `cell_methods`, `description` and `comment`.\n", "\n", "Output variables attributes are regrouped in `Indicator.cf_attrs` and input parameters are documented in `Indicator.parameters`.\n", "\n", "A particularity of Indicators is that each instance corresponds to a single class: when creating a new indicator, a new class is automatically created. This is done for easy construction of indicators based on others, like shown further down.\n", "\n", "See the [class documentation](../api.rst#indicator-tools) for more info on the meaning of each attribute. The [indicators](https://github.com/Ouranosinc/xclim/tree/main/xclim/indicators) module contains over 50 examples of indicators to draw inspiration from.\n", "\n", "### Identifier vs python name\n", "\n", "An indicator's identifier is **not** the same as the name it has within the python module. For example, `xclim.atmos.relative_humidity` has `hurs` as its identifier. As explained below, indicator _classes_ can be accessed through `xclim.core.indicator.registry` with their _identifier_.\n", "\n", "### Metadata parsing vs explicit setting\n", "\n", "As explained above, most metadata can be parsed from the index's signature and docstring. Otherwise, it can always be set when creating a new Indicator instance *or* a new subclass. When _creating_ an indicator, output metadata attributes can be given as strings, or list of strings in the case of an indicator returning multiple outputs. However, they are stored in the `cf_attrs` list of dictionaries on the instance.\n", "\n", "### Internationalization of metadata\n", "\n", "xclim offers the possibility to translate the main Indicator metadata field and automatically add the translations to the outputs. The mechanic is explained in the [Internationalization](../internationalization.rst) page.\n", "\n", "### Inputs and checks\n", "\n", "xclim decides which input arguments of the indicator's call function are considered _variables_ and which are _parameters_ using the annotations of the underlying index (the `compute` method). Arguments annotated with the `xarray.DataArray` type are considered _variables_ and can be read from the dataset passed in `ds`.\n", "\n", "### Indicator creation\n", "\n", "There are two ways of creating indicators:\n", "\n", "1) By initializing an existing indicator (sub)class\n", "2) From a dictionary\n", "\n", "The first method is best when defining indicators in scripts or external modules and are explained here. The second is best used when building virtual modules through YAML files, and is explained further down and in the [submodule doc](../api.rst#indicator-tools).\n", "\n", "Creating a new indicator that simply modifies some metadata output of an existing one is a simple call like:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "from xclim.core.indicator import registry\n", "\n", "# An indicator based on tg_mean, but returning Celsius and fixed on annual resampling\n", "tg_mean_c = registry[\"TG_MEAN\"](\n", " identifier=\"tg_mean_c\",\n", " units=\"degC\",\n", " title=\"Mean daily mean temperature but in degC\",\n", " parameters=dict(freq=\"YS\"), # We inject the freq arg.\n", ")" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "display(Code(tg_mean_c.__doc__, language=\"rst\"))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The registry is a dictionary mapping indicator identifiers (in uppercase) to their class. This way, we could subclass `tg_mean` to create our new indicator. `tg_mean_c` is the exact same as `atmos.tg_mean`, but outputs the result in Celsius instead of Kelvins, has a different title and removes control over the `freq` argument, resampling to \"YS\". The `identifier` keyword is here needed in order to differentiate the new indicator from `tg_mean` itself. If it wasn't given, a warning would have been raised and further subclassing of `tg_mean` would have in fact subclassed `tg_mean_c`, which is not wanted!\n", "\n", "By default, indicator classes are registered in `xclim.core.indicator.registry`, using their identifier, which is prepended by the indicator's module **if** that indicator is declared outside xclim. A \"child\" indicator inherits its module from its parent:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "tg_mean_c.__module__ == xclim.atmos.tg_mean.__module__" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To create indicators with a different module, for example, in a goal to differentiate them in the registry, two methods can be used : passing `module` to the constructor, or using conventional class inheritance." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "# Passing module\n", "tg_mean_c2 = registry[\"TG_MEAN_C\"](module=\"test\") # we didn't change the identifier!\n", "print(tg_mean_c2.__module__)\n", "\"test.TG_MEAN_C\" in registry" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "# Conventional class inheritance, uses the current module name\n", "\n", "\n", "class TG_MEAN_C3(registry[\"TG_MEAN_C\"]): # noqa\n", " pass # nothing to change really\n", "\n", "\n", "tg_mean_c3 = TG_MEAN_C3()\n", "\n", "print(tg_mean_c3.__module__)\n", "\"__main__.TG_MEAN_C\" in registry" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "While the former method is shorter, the latter is what xclim uses internally, as it provides some clean code structure. See [the code in the GitHub repo](https://github.com/Ouranosinc/xclim/tree/main/xclim/indicators).\n", "\n", "## Virtual modules\n", "\n", "`xclim` gives users the ability to generate their own modules from existing indices' library. These mappings can help in emulating existing libraries (such as `icclim`), with the added benefit of CF-compliant metadata, multilingual metadata support, and optimized calculations using federated resources (using Dask). This can be used for example to tailor existing indices with predefined thresholds without having to rewrite indices.\n", "\n", "Presently, xclim is capable of approximating the indices developed in [icclim](https://icclim.readthedocs.io/en/stable/explanation/climate_indices.html), [ANUCLIM](https://fennerschool.anu.edu.au/files/anuclim61.pdf) and [clix-meta](https://github.com/clix-meta/clix-meta) and is open to contributions of new indices and library mappings.\n", "\n", "This notebook serves as an example of how one might go about creating their own library of mapped indices. Two ways are possible:\n", "\n", "1. From a YAML file (recommended way)\n", "2. From a mapping (dictionary) of indicators\n", "\n", "### YAML file\n", "\n", "The first method is based on the YAML syntax proposed by `clix-meta`, expanded to xclim's needs. The full documentation on that syntax is [here](../api.rst#indicator-tools). This notebook shows an example of different complexities of indicator creation. It creates a minimal python module defining an index, creates a YAML file with the metadata for several indicators and then parses it into xclim." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "nbsphinx": "hidden", "tags": [] }, "outputs": [], "source": [ "example_dir = notebook_folder / \"example\"\n", "\n", "with open(example_dir / \"example.py\") as f:\n", " pydata = f.read()\n", "\n", "with open(example_dir / \"example.yml\") as f:\n", " ymldata = f.read()\n", "\n", "with open(example_dir / \"example.fr.json\") as f:\n", " jsondata = f.read()" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "# These variables were generated by a hidden cell above that syntax-colored them.\n", "print(\"Content of example.py :\")\n", "display(Code(pydata, language=\"python\"))\n", "print(\"\\n\\nContent of example.yml :\")\n", "display(Code(ymldata, language=\"yaml\"))\n", "print(\"\\n\\nContent of example.fr.json :\")\n", "display(Code(jsondata, language=\"json\"))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`example.yml` created a module of 7 indicators.\n", "\n", "
\n", "\n", "Values of the `base` arguments are the **identifier** of the associated indicators, and those can be different from their name within the Python modules. For example, `xclim.atmos.relative_humidity` has `HURS` as identifier. One can always access `xclim.atmos.relative_humidity.identifier` to get the correct name to use. The `base` argument also accepts generic base classes which are registered in `xc.core.indicator.base_registry`.\n", "\n", "
\n", "\n", "- `RX1day` is as `registry['RX1DAY']`, but with an updated `long_name` and an injected argument : its `indexer` arg is now set to only compute over may to september.\n", "- `RX5day_canopy` is based on `registry['MAX_N_DAY_PRECIPITATION_AMOUNT']`, changed the `long_name` and injects the `window` and `freq` arguments.\n", " * It also requests a different variable than the original indicator : `prveg` instead of `pr`. As xclim doesn't know about `prveg`, a definition is given in the `variables` section.\n", "- `R75pdays` is based on `registry['DAYS_OVER_PRECIP_THRESH']`, injects the `thresh` argument and changes the description of the `per` argument.\n", "- `first_frost_day` is a more complex example. As there were no `base:` entry, the `Daily` class serves as a base by default. This class doesn't do much, so a lot has to be given explicitly:\n", " * A compute function name if given. Here it refers a \"generic\" function (in `xclim.indices.generic`), which means it doesn't provide any pertinent metadata.\n", " * Thus, output metadata fields are given\n", " * Some parameters are injected, the default for `freq` is modified, but left as an argument.\n", " * The input variable `data` is mapped to a known variable. \"Generic\" functions do not handle the units, so we need to tell xclim that the `data` argument is minimum daily temperature. This will activate the proper units check and CF-compliance checks within the indicator class.\n", "- `winter_fd`'s `compute` uses an index function instead of a \"generic\" one . Functions directly in `xclim.indices` have docstrings that the indicator builder can parse to populate the indicator's metadata. They also handle units and expose that information to the indicator class. This example also specifies a base indicator class that supports indexing (which the default `Daily` does not), which allows the injection of an indexer.\n", "- `R95p` is similar to `first_frost_day` but here the `compute` is not defined in `xclim` but rather in `example.py`. Also, the custom function returns two outputs, so the `output` section is a list of mappings rather than a mapping directly.\n", "- `R99p` is the same as `R95p` but changes the injected value. In order to avoid rewriting the output metadata, and allowed periods, we based it on `R95p` : as the latter was defined within the current YAML file, the identifier is prefixed by a dot (.).\n", "\n", "\n", "Additionally, the YAML specified a `realm` and `references` to be used on all indices and provided a submodule docstring.\n", "\n", "Finally, French translations for the main attributes and the new indicators are given in `example.fr.json`. Even though new indicator objects are created for each YAML entry, non-specified translations are taken from the base classes if missing in the JSON file.\n", "\n", "Note that all files are named the same way : `example.`, with the translations having an additional suffix giving the locale name. In the next cell, we build the module by passing only the path without extension. This absence of extension is what tells xclim to try to parse a module (`*.py`) and custom translations (`*..json`). Those two could also be read beforehand and passed through the `indices=` and `translations=` arguments.\n", "\n", "\n", "#### Validation of the YAML file\n", "\n", "Using [yamale](https://github.com/23andMe/Yamale), it is possible to check if the YAML file is valid. `xclim` ships with a schema (in `xclim/data/schema.yml`) file." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The validation can be executed in a python session:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "from importlib.resources import files\n", "\n", "import yamale\n", "\n", "data = files(\"xclim.data\").joinpath(\"schema.yml\")\n", "schema = yamale.make_schema(data)\n", "\n", "example_module = yamale.make_data(example_dir / \"example.yml\") # in the example folder\n", "\n", "yamale.validate(schema, example_module)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Or the validation can alternatively be run from the command line with:\n", "\n", "```bash\n", "yamale -s path/to/schema.yml path/to/module.yml\n", "```\n", "\n", "Note that xclim builds indicators from a yaml file, as shown in the next example, it validates it first.\n", "\n", "#### Loading the module and computing indicators." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "import xclim as xc\n", "\n", "example = xc.core.indicator.build_indicator_module_from_yaml(example_dir / \"example\", mode=\"raise\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "docstring = f\"{example.__doc__}\\n---\\n\\n{xc.indicators.example.R99p.__doc__}\"\n", "display(Code(docstring, language=\"rst\"))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Useful for using this technique in large projects, we can iterate over the indicators like so:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "from xclim.testing import open_dataset\n", "\n", "ds = open_dataset(\"ERA5/daily_surface_cancities_1990-1993.nc\")\n", "with xr.set_options(keep_attrs=True):\n", " ds2 = ds.assign(\n", " pr_per=xc.core.calendar.percentile_doy(ds.pr, window=5, per=75).isel(percentiles=0),\n", " prveg=ds.pr * 1.1, # Very realistic\n", " )\n", " ds2.prveg.attrs[\"standard_name\"] = \"precipitation_flux_onto_canopy\"\n", "\n", "outs = []\n", "with xc.set_options(metadata_locales=\"fr\"):\n", " inds = [\"Indicators:\"]\n", " for name, ind in example.iter_indicators():\n", " inds.append(f\" {name}:\")\n", " inds.append(f\" identifier: {ind.identifier}\")\n", " out = ind(ds=ds2) # Use all default arguments and variables from the dataset\n", " if isinstance(out, tuple):\n", " outs.extend(out)\n", " for i, o in enumerate(out):\n", " inds.append(f\" long_name_{i}: ({o.name}) {o.long_name}\")\n", " else:\n", " outs.append(out)\n", " inds.append(f\" long_name: ({out.name}) {out.long_name}\")\n", "\n", "display(Code(\"\\n\".join(inds), language=\"yaml\"))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`out` contains all the computed indices, with translated metadata.\n", "Note that this merge doesn't make much sense with the current list of indicators since they have different frequencies (`freq`)." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "out = xr.merge(outs)\n", "out.attrs = {\n", " \"title\": \"Indicators computed from the example module.\"\n", "} # Merge puts the attributes of the first variable, we don't want that.\n", "out" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Mapping of indicators\n", "\n", "For more complex mappings, submodules can be constructed from Indicators directly. This is not the recommended way, but can sometimes be a workaround when the YAML version is lacking features." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "from xclim.core.indicator import build_indicator_module, registry\n", "\n", "mapping = dict(\n", " egg_cooking_season=registry[\"MAXIMUM_CONSECUTIVE_WARM_DAYS\"](\n", " module=\"awesome\",\n", " compute=xc.indices.maximum_consecutive_tx_days,\n", " parameters=dict(thresh=\"35 degC\"),\n", " long_name=\"Season for outdoor egg cooking.\",\n", " ),\n", " fish_feeling_days=registry[\"WETDAYS\"](\n", " module=\"awesome\",\n", " compute=xc.indices.wetdays,\n", " parameters=dict(thresh=\"14.0 mm/day\"),\n", " long_name=\"Days where we feel we are fishes\",\n", " ),\n", " sweater_weather=xc.atmos.tg_min.__class__(module=\"awesome\"),\n", ")\n", "\n", "awesome = build_indicator_module(\n", " name=\"awesome\",\n", " objs=mapping,\n", " doc=\"\"\"\n", " =========================\n", " My Awesome Custom indices\n", " =========================\n", " There are only 3 indices that really matter when you come down to brass tacks.\n", " This mapping library exposes them to users who want to perform real deal\n", " climate science.\n", " \"\"\",\n", ")" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "print(xc.indicators.awesome.__doc__)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "nbsphinx": "hidden", "tags": [] }, "outputs": [], "source": [ "# Let's look at our new awesome module\n", "print(awesome.__doc__)\n", "for name, ind in awesome.iter_indicators():\n", " print(f\"{name} : {ind}\")" ] } ], "metadata": { "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.13.3" }, "toc": { "base_numbering": 1, "nav_menu": {}, "number_sections": true, "sideBar": true, "skip_h1_title": false, "title_cell": "Table of Contents", "title_sidebar": "Contents", "toc_cell": false, "toc_position": {}, "toc_section_display": true, "toc_window_display": false } }, "nbformat": 4, "nbformat_minor": 4 }