diff --git a/.flake8 b/.flake8 index 6245062..84ed8a4 100644 --- a/.flake8 +++ b/.flake8 @@ -12,4 +12,4 @@ exclude= ./myokit/formats/python/template, ./build, ./venv, - ./venv2, \ No newline at end of file + ./venv2, diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..7d2cd05 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,33 @@ +name: PyBaMM-TEA tests + +on: + workflow_dispatch: + pull_request: + +jobs: + build: + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11"] + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install coverage flake8 + pip install . + - name: Lint with flake8 + run: | + flake8 . --count --exit-zero --show-source --statistics + - name: Test with unittest + run: | + coverage run -m unittest + - name: Upload Coverage to Codecov + if: matrix.os == 'ubuntu-latest' && matrix.python-version == 3.9 + uses: codecov/codecov-action@v2 diff --git a/README.md b/README.md index ef8ecb1..b018de7 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,10 @@ -# -![logo](https://raw.githubusercontent.com/pybamm-team/pybamm-tea/main/docs/pybamm_tea_logo.png) +![PyBaMM-TEA-logo](https://raw.githubusercontent.com/pybamm-team/pybamm-tea/main/docs/pybamm_tea_logo.png) # PyBaMM-TEA -This repository contains the work of the "Google Summer of Code" project on a techno-economic analysis library for battery cells, which can be combined with PyBaMM's functionality. -So far, there is a method to visualize mass- and volume loadings of an electrode stack and to estimate energy densities without simulation. The project further aims to estimate cell metrics with simulations (e.g. a Ragone plot) and manufacturing metrics with a Process-based Cost Model. - +This repository contains the work of the "Google Summer of Code" project on a techno-economic analysis library for battery cells, which can be combined with PyBaMM's functionality. So far, there is a method to visualize mass- and volume- loadings of an electrode stack and to estimate energy densities without simulation. The project further aims to estimate cell metrics with simulations (e.g. a Ragone plot) and manufacturing metrics with a Process-based Cost Model. ## Installation We recommend installing within a [virtual environment](https://docs.python.org/3/tutorial/venv.html) in order to not alter any python distribution files on your machine. @@ -95,3 +92,13 @@ tea_nco.plot_stack_breakdown() # electrode stack print(tea_nco.stack_breakdown_dataframe) ``` + +## Documentation +API documentation for the `pybamm_tea` package can be built locally using [Sphinx](https://www.sphinx-doc.org/en/master/). To build the documentation first [clone the repository](https://github.com/git-guides/git-clone), then run the following command: +```bash +sphinx-build docs docs/_build/html +``` +This will generate a number of html files in the `docs/_build/html` directory. To view the documentation, open the file `docs/_build/html/index.html` in a web browser, e.g. by running +```bash +open docs/_build/html/index.html +``` diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..46d9ea6 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,19 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = -j auto +SPHINXBUILD = sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_static/main.css b/docs/_static/main.css new file mode 100644 index 0000000..9705ee5 --- /dev/null +++ b/docs/_static/main.css @@ -0,0 +1,144 @@ +@import url("https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400;0,700;0,900;1,400;1,700;1,900&family=Open+Sans:ital,wght@0,400;0,600;1,400;1,600&display=swap"); + +.navbar-brand img { + height: 50px; +} + +.navbar-brand { + height: 50px; +} + +body { + font-family: "Open Sans", sans-serif; +} + +pre, +code { + font-size: 100%; + line-height: 155%; +} + +h1 { + font-family: "Lato", sans-serif; + color: #013243; + /* warm black */ +} + +h2 { + color: #4d77cf; + /* han blue */ + letter-spacing: -0.03em; +} + +h3 { + color: #013243; + /* warm black */ + letter-spacing: -0.03em; +} + +/* Style the active version button. + +- latest: orange +- stable: green +- old, PR: red + +Colors from: + +Wong, B. Points of view: Color blindness. +Nat Methods 8, 441 (2011). https://doi.org/10.1038/nmeth.1618 +*/ + +/* If the active version has the name "latest", style it orange */ +.version-switcher__button[data-active-version-name*="latest"] { + background-color: #e69f00; + border-color: #e69f00; + color: #000000; +} + +/* green for `stable` */ +.version-switcher__button[data-active-version-name*="stable"] { + background-color: #009e73; + border-color: #009e73; +} + +/* red for `old` */ +.version-switcher__button:not([data-active-version-name*="latest"], + [data-active-version-name*="stable"]) { + background-color: #980f0f; + border-color: #980f0f; +} + +/* Main page overview cards */ + +.sd-card { + background: #fff; + border-radius: 0; + padding: 30px 10px 20px 10px; + margin: 10px 0px; +} + +.sd-card .sd-card-header { + text-align: center; +} + +.sd-card .sd-card-header .sd-card-text { + margin: 0px; +} + +.sd-card .sd-card-img-top { + height: 52px; + width: 52px; + margin-left: auto; + margin-right: auto; +} + +.sd-card .sd-card-header { + border: none; + background-color: white; + color: #150458 !important; + font-size: var(--pst-font-size-h5); + font-weight: bold; + padding: 2.5rem 0rem 0.5rem 0rem; +} + +.sd-card .sd-card-footer { + border: none; + background-color: white; +} + +.sd-card .sd-card-footer .sd-card-text { + max-width: 220px; + margin-left: auto; + margin-right: auto; +} + +/* Dark theme tweaking */ +html[data-theme="dark"] .sd-card img[src*=".svg"] { + filter: invert(0.82) brightness(0.8) contrast(1.2); +} + +/* Main index page overview cards */ +html[data-theme="dark"] .sd-card { + background-color: var(--pst-color-background); +} + +html[data-theme="dark"] .sd-shadow-sm { + box-shadow: 0 0.1rem 1rem rgba(250, 250, 250, 0.6) !important; +} + +html[data-theme="dark"] .sd-card .sd-card-header { + background-color: var(--pst-color-background); + color: #150458 !important; +} + +html[data-theme="dark"] .sd-card .sd-card-footer { + background-color: var(--pst-color-background); +} + +html[data-theme="dark"] h1 { + color: var(--pst-color-primary); +} + +html[data-theme="dark"] h3 { + color: #0a6774; +} diff --git a/docs/pybamm_tea_logo.PNG b/docs/_static/pybamm_tea_logo.PNG similarity index 100% rename from docs/pybamm_tea_logo.PNG rename to docs/_static/pybamm_tea_logo.PNG diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..2047b60 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,244 @@ +# -*- coding: utf-8 -*- +# +# Configuration file for the Sphinx documentation builder. +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys + +# Path for repository root +sys.path.insert(0, os.path.abspath("../")) + +# Path for local Sphinx extensions +sys.path.append(os.path.abspath("./sphinxext/")) + + +# -- Project information ----------------------------------------------------- + +project = "PyBaMM TEA" +copyright = "2023, The PyBaMM Team" +author = "Julian Evers" + +# The short X.Y version +version = "0.0.0" +# The full version, including alpha/beta/rc tags +release = version + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.doctest", + "sphinx.ext.intersphinx", + "sphinx.ext.mathjax", + "sphinx.ext.viewcode", + "sphinx.ext.napoleon", + "sphinx_design", + "sphinx_copybutton", + "myst_parser", + "sphinx_inline_tabs", +] + + +napoleon_use_rtype = True +napoleon_google_docstring = False + +doctest_global_setup = """ +from docs import * +""" + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = ".rst" + +# The master toctree document. +master_doc = "index" + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = "en" + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", ".ipynb_checkpoints"] + +# Suppress warnings generated by Sphinx and/or by Sphinx extensions +suppress_warnings = ["git.too_shallow"] + +# -- Options for HTML output ------------------------------------------------- + +html_theme = "pydata_sphinx_theme" + +html_static_path = ["_static"] + +# Theme + +# pydata theme options (see +# https://pydata-sphinx-theme.readthedocs.io/en/latest/index.html# for more information) +# mostly copied from numpy, scipy, pandas +# html_logo = "_static/logo.png" +# html_favicon = "_static/favicon/favicon.png" + +html_theme_options = { + "icon_links": [ + { + "name": "GitHub", + "url": "https://github.com/pybamm-team/pybamm-tea", + "icon": "fa-brands fa-square-github", + }, + ], + "collapse_navigation": True, + "footer_start": [ + "copyright", + "sphinx-version", + ], + "footer_end": [ + "theme-version", + "last-updated", + ], +} + +html_title = "%s v%s Manual" % (project, version) +html_last_updated_fmt = "%Y-%m-%d" +html_css_files = ["main.css"] +html_context = {"default_mode": "light"} +html_use_modindex = True +html_copy_source = False +html_domain_indices = False +html_file_suffix = ".html" + +htmlhelp_basename = "pybamm-tea" + +html_sidebars = {"**": ["sidebar-nav-bs.html"]} + +# For edit button +html_context = { + "github_user": "pybamm-team", + "github_repo": "pybamm-tea", + "github_version": "main", + "doc_path": "docs/", +} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = "PyBaMMTEAdoc" + + +# -- Options for LaTeX output ------------------------------------------------ + +# Note: we exclude the examples directory from the LaTeX build because it has +# problems with the creation of PDFs on Read the Docs +# https://github.com/readthedocs/readthedocs.org/issues/2045 + +# Detect if we are building LaTeX output through the invocation of the build commands +if any("latex" in arg for arg in sys.argv) or any("latexmk" in arg for arg in sys.argv): + exclude_patterns.append("source/examples/*") + print("Skipping compilation of .ipynb files for LaTeX build.") + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, "PyBAMM-TEA.tex", "PyBaMM-TEA Documentation", author, "manual") +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [(master_doc, "PyBaMM-TEA", "PyBaMM-TEA Documentation", [author], 1)] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ( + master_doc, + "PyBaMM-TEA", + "PyBaMM-TEA Documentation", + author, + "PyBaMM-TEA", + "One line description of project.", + "Miscellaneous", + ) +] + + +# -- Options for Epub output ------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# +# epub_identifier = '' + +# A unique identification for the text. +# +# epub_uid = '' + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ["search.html"] + + +# -- Extension configuration ------------------------------------------------- + +# -- Options for intersphinx extension --------------------------------------- + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = { + "python": ("https://docs.python.org/3/", None), + "sphinx": ("https://www.sphinx-doc.org/en/master/", None), + "numpy": ("https://numpy.org/doc/stable", None), + "scipy": ("https://docs.scipy.org/doc/scipy", None), + "matplotlib": ("https://matplotlib.org/stable/", None), +} diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..a4461d5 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,21 @@ +.. Root of all bpx docs + +######################## +PyBaMM-TEA documentation +######################## + +.. This TOC defines what goes in the top navbar +.. toctree:: + :maxdepth: 1 + :hidden: + + source/api/index + +**Version**: |version| + +**Useful links**: +`PyBaMM Home Page `_ | +`Source Repository `_ | +`Issue Tracker `_ | + +PyBaMM-TEA is the work of the "Google Summer of Code" project on a techno-economic analysis library for battery cells, which can be combined with PyBaMM's functionality. So far, there is a method to visualize mass- and volume- loadings of an electrode stack and to estimate energy densities without simulation. The project further aims to estimate cell metrics with simulations (e.g. a Ragone plot) and manufacturing metrics with a Process-based Cost Model. diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..27f573b --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst new file mode 100644 index 0000000..6013dfd --- /dev/null +++ b/docs/source/api/index.rst @@ -0,0 +1,18 @@ +.. module:: pybamm_tea + +.. _api_docs: + +################# +API documentation +################# + +:Release: |version| +:Date: |today| + +This reference manual details functions, modules, and objects +included in PyBAMM-TEA, describing what they are and what they do. + +.. toctree:: + :maxdepth: 2 + + tea diff --git a/docs/source/api/tea.rst b/docs/source/api/tea.rst new file mode 100644 index 0000000..5050252 --- /dev/null +++ b/docs/source/api/tea.rst @@ -0,0 +1,5 @@ +Techno-Economic Analysis +======================== + +.. autoclass:: pybamm_tea.TEA + :members: diff --git a/pybamm_tea/tea.py b/pybamm_tea/tea.py index b2435f0..9099102 100644 --- a/pybamm_tea/tea.py +++ b/pybamm_tea/tea.py @@ -11,14 +11,16 @@ class TEA: """ - A Techno-Economic Analysis class for estimation of cell metrics: + A Techno-Economic Analysis class for estimation of cell metrics. Parameters ---------- parameter_values : dict A dictionary of parameters and their corresponding numerical values. Default is NoneParameters - inputs : dict + inputs : dict, optional + A dictionary of inputs and their corresponding numerical values. + Default is None. """ def __init__(self, parameter_values=None, inputs=None): @@ -56,7 +58,10 @@ def stack_breakdown(self): @property def stack_breakdown_dataframe(self): - """A dataframe with components, volume-, mass loadings and densities on stack level.""" + """ + A dataframe with components, volume-, mass loadings and densities on stack + level. + """ if self._stack_breakdown_dataframe is not None: return self._stack_breakdown_dataframe else: @@ -74,7 +79,10 @@ def stack_energy_densities(self): @property def stack_energy_densities_dataframe(self): - """A dataframe with energy densities and summary variables for their calculation.""" + """ + A dataframe with energy densities and summary variables for their + calculation. + """ if self._stack_energy_densities_dataframe is not None: return self._stack_energy_densities_dataframe else: @@ -93,7 +101,10 @@ def capacities_and_potentials_dataframe(self): return self._capacities_and_potentials_dataframe def initialize(self): - """Initialize class by calculating/updating densities, mass and volume fractions.""" + """ + Initialize class by calculating/updating densities, mass and volume + fractions. + """ pava = self.parameter_values if pava.get("Separator dry density [kg.m-3]") is not None: pava["Separator density [kg.m-3]"] = pava.get( @@ -257,7 +268,8 @@ def initialize(self): ) ) warnings.warn( - "Warning: 'Negative electrode thickness [m]' has been calculated from 'Theoretical n/p ratio' and 'Positive electrode thickness [m]'" + "Warning: 'Negative electrode thickness [m]' has been calculated from " + "'Theoretical n/p ratio' and 'Positive electrode thickness [m]'" ) if ( pava.get("Theoretical n/p ratio") is not None @@ -274,7 +286,8 @@ def initialize(self): * pava.get("Maximum concentration in positive electrode [mol.m-3]") ) warnings.warn( - "Warning: 'Negative electrode thickness [m]' has been calculated from 'Theoretical n/p ratio' and 'Positive electrode thickness [m]'" + "Warning: 'Negative electrode thickness [m]' has been calculated from " + "'Theoretical n/p ratio' and 'Positive electrode thickness [m]'" ) if ( pava.get("Negative electrode thickness [m]") is not None @@ -293,10 +306,14 @@ def initialize(self): self.parameter_values = {**self.parameter_values, **pava} def print_stack_breakdown(self): - """A dataframe with components, volume-, mass loadings and densities on stack level.""" + """ + A dataframe with components, volume-, mass loadings and densities on stack + level. + """ stack_bd = self.stack_breakdown - # Create a dataframe with columns for components, volume- and mass loadings and densities + # Create a dataframe with columns for components, volume- and mass- loadings + # and densities components = pd.DataFrame( [ c.replace(" volume loading [uL.cm-2]", "") @@ -331,7 +348,10 @@ def print_stack_breakdown(self): return df def print_stack_energy_densities(self): - """A dataframe with capacities, energy densities, stoichiometry- and potential windows, n/p ratios, (single-)stack thickness and stack density.""" + """ + A dataframe with capacities, energy densities, stoichiometry- and potential- + windows, n/p ratios, (single-)stack thickness and stack density. + """ stack_ed = self.stack_energy_densities data = [ { @@ -370,7 +390,10 @@ def print_stack_energy_densities(self): return df def print_capacities_and_potentials(self): - """A dataframe with capacities, energy densities, stoichiometry- and potential windows, n/p ratios, (single-)stack thickness and stack density.""" + """ + A dataframe with capacities, energy densities, stoichiometry- and potential + windows, n/p ratios, (single-)stack thickness and stack density. + """ stack_ed = self.stack_energy_densities data = [ # stack potentials @@ -533,12 +556,15 @@ def print_capacities_and_potentials(self): return df def calculate_stack_energy_densities(self): - """Calculate ideal volumetric and gravimetric energy densities on stack level.""" + """ + Calculate ideal volumetric and gravimetric energy densities on stack level. + """ stack_ed = {} # stack energy densities dict pava = None pava = self.parameter_values # parameter values - # stoichiometries - calculation based on input stoichiometries or cell potential limits + # stoichiometries - calculation based on input stoichiometries or cell + # potential limits if ( pava.get("Negative electrode stoichiometry at 0%") is not None and pava.get("Negative electrode stoichiometry at 100%") is not None @@ -715,7 +741,8 @@ def calculate_stack_energy_densities(self): f"{compartment} thickness [m]" ) - # volumetric stack capacity in [Ah.L-1] and volumetric stack energy density in [Wh.L-1] + # volumetric stack capacity in [Ah.L-1] and volumetric stack energy density in + # [Wh.L-1] stack_ed["Volumetric stack capacity [Ah.L-1]"] = ( stack_ed.get("Capacity [mA.h.cm-2]") / stack_ed.get("Stack thickness [m]") @@ -749,7 +776,8 @@ def calculate_stack_energy_densities(self): "Stack density [kg.m-3]" ) / stack_ed.get("Stack thickness [m]") - # gravimetric stack capacity in [Ah.L-1] and gravimetric stack energy density in [Wh.L-1] + # gravimetric stack capacity in [Ah.L-1] and gravimetric stack energy density + # in [Wh.L-1] stack_ed["Gravimetric stack capacity [Ah.kg-1]"] = ( stack_ed.get("Volumetric stack capacity [Ah.L-1]") / stack_ed.get("Stack density [kg.m-3]") @@ -862,7 +890,8 @@ def calculate_stack_breakdown(self): f"{electrode} dry density [mg.uL-1]" ] warnings.warn( - f"Warning: {electrode} inactive material volume fraction is 0, {electrode} inactive material density is set to 0" + f"Warning: {electrode} inactive material volume fraction is 0, " + f"{electrode} inactive material density is set to 0" ) else: stack_bd[f"{electrode} inactive material density [mg.uL-1]"] = ( @@ -888,7 +917,8 @@ def calculate_stack_breakdown(self): if pava.get("Separator porosity") == 1: stack_bd["Separator material density [mg.uL-1]"] = 0 warnings.warn( - "Warning: Separator porosity is 1, separator material density is set to 0" + "Warning: Separator porosity is 1, separator material density is " + "set to 0" ) else: stack_bd["Separator material density [mg.uL-1]"] = ( diff --git a/pyproject.toml b/pyproject.toml index 38b16b6..e69d604 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,4 +17,12 @@ dependencies = [ dev = [ 'coverage', # Coverage checking 'flake8>=3', # Style checking + "sphinx>=6", + "sphinx_rtd_theme>=0.5", + "pydata-sphinx-theme", + "sphinx_design", + "sphinx-copybutton", + "myst-parser", + "sphinx-inline-tabs", ] +