diff --git a/.gitignore b/.gitignore index 870125286..cd7ca61c4 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,8 @@ pip-log.txt doc/_build/ doc/build/ .jupyterlite.doit.db +/doc/jupyter_execute/ +doc/source/regression/*.ipynb # Editors .idea diff --git a/doc/source/_static/myst-nb.css b/doc/source/_static/myst-nb.css new file mode 100644 index 000000000..bd30038a1 --- /dev/null +++ b/doc/source/_static/myst-nb.css @@ -0,0 +1,82 @@ +/* MyST-NB + +This stylesheet targets elements output by MyST-NB that represent notebook +cells. + +In some cases these rules override MyST-NB. In some cases they override PyData +Sphinx Theme or Sphinx. And in some cases they do not override existing styling +but add new styling. */ + +/* Set up a few variables for this stylesheet */ +.cell, +.pywt-handcoded-cell-output { + --pywt-cell-input-border-left-width: .2rem; + + /* This matches the padding applied to
elements by PyData Sphinx Theme */
+ --pywt-code-block-padding: 1rem;
+
+ /* override mystnb */
+ --mystnb-source-border-radius: .25rem; /* match PyData Sphinx Theme */
+}
+
+.cell .cell_input::before {
+ content: "In";
+ border-bottom: var(--mystnb-source-border-width) solid var(--pst-color-border);
+ font-weight: var(--pst-font-weight-caption);
+
+ /* Left-aligns the text in this box and the one that follows it */
+ padding-left: var(--pywt-code-block-padding);
+}
+
+/* Cannot use `.cell .cell_input` selector because the stylesheet from MyST-NB
+ uses `div.cell div.cell_input` and we want to override those rules */
+div.cell div.cell_input {
+ background-color: inherit;
+ border-color: var(--pst-color-border);
+ border-left-width: var(--pywt-cell-input-border-left-width);
+ background-clip: padding-box;
+ overflow: hidden;
+}
+
+.cell .cell_output,
+.pywt-handcoded-cell-output {
+ border: var(--mystnb-source-border-width) solid var(--pst-color-surface);
+ border-radius: var(--mystnb-source-border-radius);
+ background-clip: padding-box;
+ overflow: hidden;
+}
+
+.cell .cell_output::before,
+.pywt-handcoded-cell-output::before {
+ content: "Out";
+ display: block;
+ font-weight: var(--pst-font-weight-caption);
+
+ /* Left-aligns the text in this box and the one that follows it */
+ padding-left: var(--pywt-code-block-padding);
+}
+
+.cell .cell_output .output {
+ background-color: inherit;
+ border: none;
+ margin-top: 0;
+}
+
+.cell .cell_output,
+/* must prefix the following selector with `div.` to override Sphinx margin rule on div[class*=highlight-] */
+div.pywt-handcoded-cell-output {
+ /* Left-align the text in the output with the text in the input */
+ margin-left: calc(var(--pywt-cell-input-border-left-width) - var(--mystnb-source-border-width));
+}
+
+.cell .cell_output .output,
+.cell .cell_input pre,
+.cell .cell_output pre,
+.pywt-handcoded-cell-output .highlight,
+.pywt-handcoded-cell-output pre {
+ border-radius: 0;
+}
+
+.pywt-handcoded-cell-output pre {
+ border: none; /* MyST-NB sets border to none for tags inside div.cell */
+}
diff --git a/doc/source/_static/pywavelets.css b/doc/source/_static/pywavelets.css
index 56f12e4df..7aa4e8326 100644
--- a/doc/source/_static/pywavelets.css
+++ b/doc/source/_static/pywavelets.css
@@ -1,27 +1,37 @@
/* Custom CSS rules for the interactive documentation button */
.try_examples_button {
- color: white;
- background-color: #0054a6;
- border: none;
- padding: 5px 10px;
- border-radius: 10px;
- margin-bottom: 5px;
- box-shadow: 0 2px 5px rgba(108,108,108,0.2);
- font-weight: bold;
- font-size: small;
+ color: white;
+ background-color: #0054a6;
+ border: none;
+ padding: 5px 10px;
+ border-radius: 10px;
+ margin-bottom: 5px;
+ box-shadow: 0 2px 5px rgba(108,108,108,0.2);
+ font-weight: bold;
+ font-size: small;
}
.try_examples_button:hover {
-background-color: #0066cc;
-transform: scale(1.02);
-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
-cursor: pointer;
+ background-color: #0066cc;
+ transform: scale(1.02);
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
+ cursor: pointer;
}
.try_examples_button_container {
- display: flex;
- justify-content: flex-start;
- gap: 10px;
- margin-bottom: 20px;
+ display: flex;
+ justify-content: flex-start;
+ gap: 10px;
+ margin-bottom: 20px;
+}
+
+/*
+Admonitions on this site are styled with some top margin. This makes sense when
+the admonition appears within the flow of the article. But when it is the very
+first child of an article, its top margin gets added to the article's top
+padding, resulting in too much whitespace.
+*/
+.admonition.pywt-margin-top-0 {
+ margin-top: 0;
}
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 2820d8a08..1472a694e 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -11,9 +11,10 @@
# serve to show the default.
import datetime
-import importlib.metadata
+import os
+import re
+from pathlib import Path
-import jinja2.filters
import numpy as np
import pywt
@@ -25,6 +26,76 @@
except TypeError:
pass
+from sphinx.application import Sphinx
+
+HERE = Path(__file__).parent
+
+
+def preprocess_notebooks(app: Sphinx, *args, **kwargs):
+ """Preprocess Markdown notebooks to convert them to IPyNB format."""
+
+ import jupytext
+ import nbformat
+
+ print("Converting Markdown files to IPyNB...")
+ for path in (HERE / "regression").glob("*.md"):
+ if any(path.match(pattern) for pattern in exclude_patterns):
+ continue
+ nb = jupytext.read(str(path))
+
+ # In .md to .ipynb conversion, do not include any cells that have the
+ # jupyterlite_sphinx_strip tag
+ nb.cells = [
+ cell for cell in nb.cells if "jupyterlite_sphinx_strip" not in cell.metadata.get("tags", [])
+ ]
+
+ ipynb_path = path.with_suffix(".ipynb")
+ with open(ipynb_path, "w") as f:
+ nbformat.write(nb, f)
+ print(f"Converted {path} to {ipynb_path}")
+
+
+# Should match {{ parent_docname }} or {{parent_docname}}
+parent_docname_substitution_re = re.compile(r"{{\s*parent_docname\s*}}")
+
+def sub_parent_docname_in_header(
+ app: Sphinx, relative_path: Path, parent_docname: str, content: list[str]
+):
+ """Fill in the name of the document in the header.
+
+ When regression/header.md is read via the include directive, replace
+ {{ parent_docname }} with the name of the parent document that included
+ header.md.
+
+ Note: parent_docname does not include the file extension.
+
+ Here is a simplified example of how this works.
+
+ Contents of header.md:
+
+ {download}`Download {{ parent_docname }}.md <{{ parent_docname }}.md>`
+
+ Contents of foobar.md:
+
+ ```{include} header.md
+ ```
+
+ After this function and others are run...
+
+ Contents of foobar.md:
+
+ {download}`Download foobar.md `
+ """
+ if not relative_path.match("regression/header.md"):
+ return
+
+ for i, value in enumerate(content):
+ content[i] = re.sub(parent_docname_substitution_re, parent_docname, value)
+
+
+def setup(app):
+ app.connect("config-inited", preprocess_notebooks)
+ app.connect("include-read", sub_parent_docname_in_header)
# 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
@@ -38,6 +109,7 @@
extensions = [
'jupyterlite_sphinx',
'matplotlib.sphinxext.plot_directive',
+ 'myst_nb',
'numpydoc',
'sphinx.ext.autodoc',
'sphinx.ext.autosummary',
@@ -47,15 +119,19 @@
'sphinx.ext.mathjax',
'sphinx.ext.todo',
'sphinx_copybutton',
+ 'sphinx_design',
'sphinx_togglebutton',
-
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
-source_suffix = '.rst'
+source_suffix = {
+ '.rst': 'restructuredtext',
+ '.md': 'myst-nb',
+ 'ipynb': None, # do not parse IPyNB files
+}
# The encoding of source files.
source_encoding = 'utf-8'
@@ -141,8 +217,8 @@
"show_prev_next": True,
"footer_start": ["copyright", "sphinx-version"],
"footer_end": ["theme-version"],
-"pygment_light_style": "a11y-high-contrast-light",
-"pygment_dark_style": "a11y-high-contrast-dark",
+"pygments_light_style": "a11y-high-contrast-light",
+"pygments_dark_style": "a11y-high-contrast-dark",
"icon_links": [
{
"name": "Discussion group on Google Groups",
@@ -200,6 +276,7 @@
# _static directory.
html_css_files = [
"pywavelets.css",
+ "myst-nb.css"
]
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
@@ -278,7 +355,12 @@
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
-exclude_patterns = ['substitutions.rst', ]
+exclude_patterns = [
+ 'substitutions.rst',
+ 'regression/header.md',
+ 'regression/README.md',
+ 'regression/*.ipynb' # exclude IPyNB files from the build
+]
# numpydoc_show_class_members = False
numpydoc_class_members_toctree = False
@@ -310,3 +392,25 @@
Shall you encounter any issues, please feel free to report them on the
[PyWavelets issue tracker](https://github.com/PyWavelets/pywt/issues)."""
)
+
+jupyterlite_silence = True
+strip_tagged_cells = True
+
+# -- Options for MyST-NB and Markdown-based content --------------------------
+
+os.environ["PYDEVD_DISABLE_FILE_VALIDATION"] = "1"
+
+# https://myst-nb.readthedocs.io/en/latest/configuration.html
+nb_execution_mode = 'auto'
+nb_execution_timeout = 60
+nb_execution_allow_errors = False
+nb_execution_raise_on_error = True
+nb_render_markdown_format = "myst"
+nb_remove_code_source = False
+nb_remove_code_outputs = False
+
+myst_enable_extensions = [
+ 'amsmath',
+ 'colon_fence',
+ 'dollarmath',
+]
diff --git a/doc/source/regression/README.md b/doc/source/regression/README.md
new file mode 100644
index 000000000..7b1b58c96
--- /dev/null
+++ b/doc/source/regression/README.md
@@ -0,0 +1,54 @@
+# Regression folder
+
+This folder contains various useful examples illustrating how to use and how not
+to use PyWavelets.
+
+The examples are written in the [MyST markdown notebook
+format](https://myst-nb.readthedocs.io/en/v0.13.2/use/markdown.html). This
+allows each .md file to function simultaneously as documentation that can be fed
+into Sphinx and as a source file that can be converted to the Jupyter notebook
+format (.ipynb), which can then be opened in notebook applications such as
+JupyterLab. For this reason, each example page in this folder includes a header template
+that adds a blurb to the top of each page about how the page can be
+run or downloaded as a Jupyter notebook.
+
+There are a few shortcomings to this approach of generating the code cell outputs in
+the documentation pages at build time rather than hand editing them into the
+document source file. One is that we can no longer compare the generated outputs
+with the expected outputs as we used to do with doctest. Another is that we
+lose some control over how we want the outputs to appear, unless we use a workaround.
+
+Here is the workaround we created. First we tell MyST-NB to remove the generated
+cell output from the documentation page by adding the `remove-output` tag to the
+`code-cell` directive in the markdown file. Then we hand code the output in a
+`code-block` directive, not to be confused with `code-cell`! The `code-cell`
+directive says "I am notebook code cell input, run me!" The `code-block`
+directive says, "I am just a block of code for documentation purposes, don't run
+me!" To the code block, we add the `.pywt-handcoded-cell-output` class so that
+we can style it to look the same as other cell outputs on the same HTML page.
+Finally, we tag the handcoded output with `jupyterlite_sphinx_strip` so that we
+can exclude it when converting from .md to .ipynb. That way only generated
+output appears in the .ipynb notebook.
+
+To recap:
+
+- We use the `remove-output` tag to remove the **generated** code cell output
+ during .md to .html conversion (this conversion is done by MyST-NB).
+- We use the `jupyterlite_sphinx_strip` tag to remove the **handcoded** output
+ during .md to .ipynb conversion (this conversion is done by Jupytext).
+
+Example markdown:
+
+ ```{code-cell}
+ :tags: [raises-exception, remove-output]
+ 1 / 0
+ ```
+
+ +++ {"tags" ["jupyterlite_sphinx_strip"]}
+
+ ```{code-block} python
+ :class: pywt-handcoded-cell-output
+ Traceback (most recent call last):
+ ...
+ ZeroDivisionError: division by zero
+ ```
diff --git a/doc/source/regression/dwt-idwt.md b/doc/source/regression/dwt-idwt.md
new file mode 100644
index 000000000..101295aca
--- /dev/null
+++ b/doc/source/regression/dwt-idwt.md
@@ -0,0 +1,212 @@
+---
+jupytext:
+ text_representation:
+ extension: .md
+ format_name: myst
+ format_version: 1.0.0
+ jupytext_version: 1.16.1
+kernelspec:
+ display_name: Python 3 (ipykernel)
+ language: python
+ name: python3
+---
+
++++ {"tags": ["jupyterlite_sphinx_strip"]}
+
+```{include} header.md
+
+```
+
+# DWT and IDWT
+
+## Discrete Wavelet Transform
+
+Let's do a [Discrete Wavelet Transform](ref-dwt) of some sample data `x`
+using the `db2` wavelet. It's simple:
+
+```{code-cell}
+import pywt
+x = [3, 7, 1, 1, -2, 5, 4, 6]
+cA, cD = pywt.dwt(x, 'db2')
+```
+
+And the approximation and details coefficients are in `cA` and `cD`
+respectively:
+
+```{code-cell}
+cA
+```
+
+```{code-cell}
+cD
+```
+
+## Inverse Discrete Wavelet Transform
+
+Now let's do the opposite operation, an [Inverse Discrete Wavelet Transform](ref-idwt):
+
+```{code-cell}
+pywt.idwt(cA, cD, 'db2')
+```
+
+VoilĂ ! That's it!
+
+## More examples
+
+Now let's experiment with `dwt()` some more. For example, let's pass a
+`Wavelet` object instead of the wavelet name and specify the signal
+extension mode (the default is `Modes.symmetric`) for the border effect
+handling:
+
+```{code-cell}
+w = pywt.Wavelet('sym3')
+cA, cD = pywt.dwt(x, wavelet=w, mode='constant')
+```
+
+```{code-cell}
+print(cA)
+```
+
+```{code-cell}
+print(cD)
+```
+
+Note that the output coefficients arrays' length depends not only on the
+input data length but also on the `Wavelet` type (particularly on its
+filters length `Wavelet.dec_len` that are used in the transformation).
+
+To find out what the size of the output data will be, use the `dwt_coeff_len()`
+function:
+
+```{code-cell}
+pywt.dwt_coeff_len(data_len=len(x), filter_len=w.dec_len, mode='symmetric')
+```
+
+```{code-cell}
+pywt.dwt_coeff_len(len(x), w, 'symmetric')
+```
+
+and the length of the output is:
+
+```{code-cell}
+len(cA)
+```
+
+Looks fine. (And if you expected that the output length would be a half of the
+input data length, well, that's the trade-off that allows for the perfect
+reconstruction...).
+
+The third argument of the `dwt_coeff_len()` function is the already mentioned signal
+extension mode (please refer to the PyWavelets' documentation for the `modes`
+description). Currently, there are six extension modes available under `Modes`:
+
+```{code-cell}
+pywt.Modes.modes
+```
+
+As you see in the above example, the periodization (`Modes.periodization`) mode
+is slightly different from the others. Its aim when doing the `pywt.dwt` transform
+is to output coefficients arrays that are half of the length of the input data.
+
+Knowing that, you should never mix the periodization mode with other modes when
+doing `dwt` and `idwt`. Otherwise, it will produce **invalid results**:
+
+```{code-cell}
+x = [3, 7, 1, 1, -2, 5, 4, 6]
+
+cA, cD = pywt.dwt(x, wavelet=w, mode='periodization')
+print(pywt.idwt(cA, cD, 'sym3', 'symmetric')) # invalid mode
+```
+
+```{code-cell}
+print(pywt.idwt(cA, cD, 'sym3', 'periodization'))
+```
+
+## Tips & tricks
+
+### Passing `None` instead of coefficients data to `pywt.idwt()`
+
+Now, we showcase some tips and tricks. Passing `None` as one of the coefficient
+arrays parameters is similar to passing a _zero-filled_ array. The results are
+simply the same:
+
+```{code-cell}
+print(pywt.idwt([1,2,0,1], None, 'db2', 'symmetric'))
+```
+
+```{code-cell}
+print(pywt.idwt([1, 2, 0, 1], [0, 0, 0, 0], 'db2', 'symmetric'))
+```
+
+```{code-cell}
+print(pywt.idwt(None, [1, 2, 0, 1], 'db2', 'symmetric'))
+```
+
+```{code-cell}
+print(pywt.idwt([0, 0, 0, 0], [1, 2, 0, 1], 'db2', 'symmetric'))
+```
+
+Remember that only one argument at a time can be `None`:
+
+```{code-cell}
+---
+tags: [raises-exception, remove-output]
+---
+print(pywt.idwt(None, None, 'db2', 'symmetric'))
+```
+
++++ {"tags": ["jupyterlite_sphinx_strip"]}
+
+```{code-block} python
+:class: pywt-handcoded-cell-output
+Traceback (most recent call last):
+...
+ValueError: At least one coefficient parameter must be specified.
+```
+
+### Coefficients data size in `pywt.idwt`
+
+When doing the `idwt` transform, usually the coefficient arrays
+must have the same size.
+
+```{code-cell}
+---
+tags: [raises-exception, remove-output]
+---
+print(pywt.idwt([1, 2, 3, 4, 5], [1, 2, 3, 4], 'db2', 'symmetric'))
+```
+
++++ {"tags": ["jupyterlite_sphinx_strip"]}
+
+```{code-block} python
+:class: pywt-handcoded-cell-output
+Traceback (most recent call last):
+...
+ValueError: Coefficients arrays must have the same size.
+```
+
+Not every coefficient array can be used in `idwt`. In the
+following example the `idwt` will fail because the input arrays are
+invalid - they couldn't be created as a result of `dwt`, because
+the minimal output length for dwt using `db4` wavelet and the `Modes.symmetric`
+mode is `4`, not `3`:
+
+```{code-cell}
+---
+tags: [raises-exception, remove-output]
+---
+pywt.idwt([1,2,4], [4,1,3], 'db4', 'symmetric')
+```
+
++++ {"tags": ["jupyterlite_sphinx_strip"]}
+
+```{code-block} python
+:class: pywt-handcoded-cell-output
+Traceback (most recent call last):
+...
+ValueError: Invalid coefficient arrays length for specified wavelet. Wavelet and mode must be the same as used for decomposition.
+```
+
+```{code-cell}
+int(pywt.dwt_coeff_len(1, pywt.Wavelet('db4').dec_len, 'symmetric'))
+```
diff --git a/doc/source/regression/dwt-idwt.rst b/doc/source/regression/dwt-idwt.rst
deleted file mode 100644
index a0552759b..000000000
--- a/doc/source/regression/dwt-idwt.rst
+++ /dev/null
@@ -1,151 +0,0 @@
-.. _reg-dwt-idwt:
-
-.. currentmodule:: pywt
-
-DWT and IDWT
-============
-
-Discrete Wavelet Transform
---------------------------
-
-Let's do a :func:`Discrete Wavelet Transform ` of a sample data ``x``
-using the ``db2`` wavelet. It's simple..
-
- >>> import pywt
- >>> x = [3, 7, 1, 1, -2, 5, 4, 6]
- >>> cA, cD = pywt.dwt(x, 'db2')
-
-And the approximation and details coefficients are in ``cA`` and ``cD``
-respectively:
-
- >>> print(cA)
- [ 5.65685425 7.39923721 0.22414387 3.33677403 7.77817459]
- >>> print(cD)
- [-2.44948974 -1.60368225 -4.44140056 -0.41361256 1.22474487]
-
-Inverse Discrete Wavelet Transform
-----------------------------------
-
-Now let's do an opposite operation
-- :func:`Inverse Discrete Wavelet Transform `:
-
- >>> print(pywt.idwt(cA, cD, 'db2'))
- [ 3. 7. 1. 1. -2. 5. 4. 6.]
-
-VoilĂ ! That's it!
-
-More Examples
--------------
-
-Now let's experiment with the :func:`dwt` some more. For example let's pass a
-:class:`Wavelet` object instead of the wavelet name and specify signal
-extension mode (the default is :ref:`symmetric `) for the
-border effect handling:
-
- >>> w = pywt.Wavelet('sym3')
- >>> cA, cD = pywt.dwt(x, wavelet=w, mode='constant')
- >>> print(cA)
- [ 4.38354585 3.80302657 7.31813271 -0.58565539 4.09727044 7.81994027]
- >>> print(cD)
- [-1.33068221 -2.78795192 -3.16825651 -0.67715519 -0.09722957 -0.07045258]
-
-Note that the output coefficients arrays length depends not only on the input
-data length but also on the :class:Wavelet type (particularly on its
-:attr:`filters length <~Wavelet.dec_len>` that are used in the transformation).
-
-To find out what will be the output data size use the :func:`dwt_coeff_len`
-function:
-
- >>> # int() is for normalizing Python integers and long integers for documentation tests
- >>> int(pywt.dwt_coeff_len(data_len=len(x), filter_len=w.dec_len, mode='symmetric'))
- 6
- >>> int(pywt.dwt_coeff_len(len(x), w, 'symmetric'))
- 6
- >>> len(cA)
- 6
-
-Looks fine. (And if you expected that the output length would be a half of the
-input data length, well, that's the trade-off that allows for the perfect
-reconstruction...).
-
-The third argument of the :func:`dwt_coeff_len` is the already mentioned signal
-extension mode (please refer to the PyWavelets' documentation for the
-:ref:`modes ` description). Currently there are six
-:ref:`extension modes ` available:
-
- >>> pywt.Modes.modes
- ['zero', 'constant', 'symmetric', 'periodic', 'smooth', 'periodization', 'reflect', 'antisymmetric', 'antireflect']
-
-As you see in the above example, the :ref:`periodization `
-(periodization) mode is slightly different from the others. It's aim when
-doing the :func:`DWT ` transform is to output coefficients arrays that
-are half of the length of the input data.
-
-Knowing that, you should never mix the periodization mode with other modes when
-doing :func:`DWT ` and :func:`IDWT `. Otherwise, it will produce
-**invalid results**:
-
- >>> x
- [3, 7, 1, 1, -2, 5, 4, 6]
- >>> cA, cD = pywt.dwt(x, wavelet=w, mode='periodization')
- >>> print(pywt.idwt(cA, cD, 'sym3', 'symmetric')) # invalid mode
- [ 1. 1. -2. 5.]
- >>> print(pywt.idwt(cA, cD, 'sym3', 'periodization'))
- [ 3. 7. 1. 1. -2. 5. 4. 6.]
-
-
-Tips & tricks
--------------
-
-Passing ``None`` instead of coefficients data to :func:`idwt`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Now some tips & tricks. Passing ``None`` as one of the coefficient arrays
-parameters is similar to passing a *zero-filled* array. The results are simply
-the same:
-
- >>> print(pywt.idwt([1,2,0,1], None, 'db2', 'symmetric'))
- [ 1.19006969 1.54362308 0.44828774 -0.25881905 0.48296291 0.8365163 ]
-
- >>> print(pywt.idwt([1, 2, 0, 1], [0, 0, 0, 0], 'db2', 'symmetric'))
- [ 1.19006969 1.54362308 0.44828774 -0.25881905 0.48296291 0.8365163 ]
-
- >>> print(pywt.idwt(None, [1, 2, 0, 1], 'db2', 'symmetric'))
- [ 0.57769726 -0.93125065 1.67303261 -0.96592583 -0.12940952 -0.22414387]
-
- >>> print(pywt.idwt([0, 0, 0, 0], [1, 2, 0, 1], 'db2', 'symmetric'))
- [ 0.57769726 -0.93125065 1.67303261 -0.96592583 -0.12940952 -0.22414387]
-
-Remember that only one argument at a time can be ``None``:
-
- >>> print(pywt.idwt(None, None, 'db2', 'symmetric'))
- Traceback (most recent call last):
- ...
- ValueError: At least one coefficient parameter must be specified.
-
-
-Coefficients data size in :attr:`idwt`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-When doing the :func:`IDWT ` transform, usually the coefficient arrays
-must have the same size.
-
- >>> print(pywt.idwt([1, 2, 3, 4, 5], [1, 2, 3, 4], 'db2', 'symmetric'))
- Traceback (most recent call last):
- ...
- ValueError: Coefficients arrays must have the same size.
-
-
-Not every coefficient array can be used in :func:`IDWT `. In the
-following example the :func:`idwt` will fail because the input arrays are
-invalid - they couldn't be created as a result of :func:`DWT `, because
-the minimal output length for dwt using ``db4`` wavelet and the :ref:`symmetric
-` mode is ``4``, not ``3``:
-
- >>> pywt.idwt([1,2,4], [4,1,3], 'db4', 'symmetric')
- Traceback (most recent call last):
- ...
- ValueError: Invalid coefficient arrays length for specified wavelet. Wavelet and mode must be the same as used for decomposition.
-
- >>> int(pywt.dwt_coeff_len(1, pywt.Wavelet('db4').dec_len, 'symmetric'))
- 4
diff --git a/doc/source/regression/gotchas.md b/doc/source/regression/gotchas.md
new file mode 100644
index 000000000..6cb955730
--- /dev/null
+++ b/doc/source/regression/gotchas.md
@@ -0,0 +1,53 @@
+---
+jupytext:
+ text_representation:
+ extension: .md
+ format_name: myst
+ format_version: 1.0.0
+ jupytext_version: 1.16.1
+kernelspec:
+ display_name: Python 3 (ipykernel)
+ language: python
+ name: python3
+---
+
++++ {"tags": ["jupyterlite_sphinx_strip"]}
+
+```{include} header.md
+
+```
+
+# Gotchas
+
+PyWavelets utilizes `NumPy` under the hood. That's why handling the data
+that contains `None` values can be surprising. `None` values are converted to
+'not a number' (`numpy.nan`) values:
+
+```{code-cell}
+import numpy, pywt
+x = [None, None]
+mode = 'symmetric'
+wavelet = 'db1'
+cA, cD = pywt.dwt(x, wavelet, mode)
+```
+
+The results are:
+
+```{code-cell}
+numpy.all(numpy.isnan(cA))
+```
+
+
+
+```{code-cell}
+numpy.all(numpy.isnan(cD))
+```
+
+
+
+```{code-cell}
+rec = pywt.idwt(cA, cD, wavelet, mode)
+numpy.all(numpy.isnan(rec))
+```
+
+
diff --git a/doc/source/regression/gotchas.rst b/doc/source/regression/gotchas.rst
deleted file mode 100644
index 04e267ff0..000000000
--- a/doc/source/regression/gotchas.rst
+++ /dev/null
@@ -1,25 +0,0 @@
-.. _reg-gotchas:
-
-.. currentmodule:: pywt
-
-
-=======
-Gotchas
-=======
-
-PyWavelets utilizes ``NumPy`` under the hood. That's why handling the data
-containing ``None`` values can be surprising. ``None`` values are converted to
-'not a number' (``numpy.NaN``) values:
-
- >>> import numpy, pywt
- >>> x = [None, None]
- >>> mode = 'symmetric'
- >>> wavelet = 'db1'
- >>> cA, cD = pywt.dwt(x, wavelet, mode)
- >>> numpy.all(numpy.isnan(cA))
- True
- >>> numpy.all(numpy.isnan(cD))
- True
- >>> rec = pywt.idwt(cA, cD, wavelet, mode)
- >>> numpy.all(numpy.isnan(rec))
- True
diff --git a/doc/source/regression/header.md b/doc/source/regression/header.md
new file mode 100644
index 000000000..05b713223
--- /dev/null
+++ b/doc/source/regression/header.md
@@ -0,0 +1,15 @@
+````{tip}
+:class: pywt-margin-top-0
+
+This page can also be run or downloaded as a Jupyter notebook.
+
+- Run in a new tab using JupyterLite[^what-is-lite]:
+ ```{notebooklite} {{ parent_docname }}.ipynb
+ :new_tab: True
+ ```
+- Download:
+ - Jupyter notebook: {download}`{{ parent_docname }}.ipynb <{{ parent_docname }}.ipynb>`
+ - MyST markdown: {download}`{{ parent_docname }}.md <{{ parent_docname }}.md>`
+
+[^what-is-lite]: [JupyterLite](https://jupyterlite.readthedocs.io) is a distribution of JupyterLab that runs entirely within a web browser. All computations run in the browser; none are sent to a server.
+````
diff --git a/doc/source/regression/index.rst b/doc/source/regression/index.rst
index 21065886a..1e5ab54dc 100644
--- a/doc/source/regression/index.rst
+++ b/doc/source/regression/index.rst
@@ -5,11 +5,8 @@
Usage examples
==============
-The following examples are used as doctest regression tests written using reST
-markup. They are included in the documentation since they contain various useful
-examples illustrating how to use and how not to use PyWavelets.
-
-For more usage examples see the `demo`_ directory in the source package.
+The following pages contain various useful examples illustrating how to use and
+how not to use PyWavelets.
.. toctree::
:maxdepth: 1
@@ -22,4 +19,19 @@ For more usage examples see the `demo`_ directory in the source package.
wp2d
gotchas
+The examples in this section are written in the `MyST markdown notebook format`_. This allows
+each .md file to function simultaneously as documentation that can be fed into
+Sphinx and as a source file that can be converted to the Jupyter notebook format
+(.ipynb), which can then be opened in notebook applications such as JupyterLab.
+For this reason, each example page in this folder includes a header template
+that adds a blurb to the top of each page about how the page can be run or
+downloaded as a Jupyter notebook. This is explained in greater detail in the
+`regression folder README`_.
+
+For more usage examples see the `demo`_ directory in the source package.
+
+
.. include:: ../common_refs.rst
+
+.. _MyST markdown notebook format: https://myst-nb.readthedocs.io/en/v0.13.2/use/markdown.html
+.. _regression folder readme: https://github.com/PyWavelets/pywt/blob/main/doc/source/regression/README.md
diff --git a/doc/source/regression/modes.md b/doc/source/regression/modes.md
new file mode 100644
index 000000000..4905593a0
--- /dev/null
+++ b/doc/source/regression/modes.md
@@ -0,0 +1,101 @@
+---
+jupytext:
+ text_representation:
+ extension: .md
+ format_name: myst
+ format_version: 1.0.0
+ jupytext_version: 1.16.1
+kernelspec:
+ display_name: Python 3 (ipykernel)
+ language: python
+ name: python3
+---
+
++++ {"tags": ["jupyterlite_sphinx_strip"]}
+
+```{include} header.md
+
+```
+
+# Signal Extension Modes
+
+Import `pywt` first:
+
+```{code-cell}
+import pywt
+```
+
+and construct a helper function that can format arrays in a consistent manner
+across different systems. Please note that this function is just for the purpose of
+this example and is not part of the PyWavelets library, and it is not necessary or
+required to use it in your own code:
+
+```{code-cell}
+def format_array(a):
+ """Consistent array representation across different systems"""
+ import numpy
+ a = numpy.where(numpy.abs(a) < 1e-5, 0, a)
+ return numpy.array2string(a, precision=5, separator=' ', suppress_small=True)
+```
+
+A list of available signal extension [modes](Modes):
+
+```{code-cell}
+pywt.Modes.modes
+```
+
+An invalid mode name should raise a `ValueError`:
+
+```{code-cell}
+---
+tags: [raises-exception, remove-output]
+---
+pywt.dwt([1,2,3,4], 'db2', 'invalid')
+```
+
+```{code-block} python
+:class: pywt-handcoded-cell-output
+Traceback (most recent call last):
+...
+ValueError: Unknown mode name 'invalid'.
+```
+
+You can also refer to modes via the attributes of the `Modes` class:
+
+```{code-cell}
+x = [1, 2, 1, 5, -1, 8, 4, 6]
+for mode_name in ['zero', 'constant', 'symmetric', 'reflect', 'periodic', 'smooth', 'periodization']:
+ mode = getattr(pywt.Modes, mode_name)
+ cA, cD = pywt.dwt(x, 'db2', mode)
+ print("Mode: %d (%s)" % (mode, mode_name))
+```
+
+The default mode is [symmetric](Modes.symmetric):
+
+```{code-cell}
+cA, cD = pywt.dwt(x, 'db2')
+cA
+```
+
+```{code-cell}
+cD
+```
+
+```{code-cell}
+pywt.idwt(cA, cD, 'db2')
+```
+
+Specify the mode using a keyword argument:
+
+```{code-cell}
+cA, cD = pywt.dwt(x, 'db2', mode='symmetric')
+cA
+```
+
+```{code-cell}
+cD
+```
+
+```{code-cell}
+pywt.idwt(cA, cD, 'db2')
+```
diff --git a/doc/source/regression/modes.rst b/doc/source/regression/modes.rst
deleted file mode 100644
index 9d13ba216..000000000
--- a/doc/source/regression/modes.rst
+++ /dev/null
@@ -1,68 +0,0 @@
-.. _reg-modes:
-
-.. currentmodule:: pywt
-
-
-Signal Extension Modes
-======================
-
-Import :mod:`pywt` first
-
- >>> import pywt
-
- >>> def format_array(a):
- ... """Consistent array representation across different systems"""
- ... import numpy
- ... a = numpy.where(numpy.abs(a) < 1e-5, 0, a)
- ... return numpy.array2string(a, precision=5, separator=' ', suppress_small=True)
-
-List of available signal extension :ref:`modes `:
-
- >>> print(pywt.Modes.modes)
- ['zero', 'constant', 'symmetric', 'periodic', 'smooth', 'periodization', 'reflect', 'antisymmetric', 'antireflect']
-
-
-Invalid mode name should rise a :exc:`ValueError`:
-
- >>> pywt.dwt([1,2,3,4], 'db2', 'invalid')
- Traceback (most recent call last):
- ...
- ValueError: Unknown mode name 'invalid'.
-
-
-You can also refer to modes via :ref:`Modes ` class attributes:
-
- >>> x = [1, 2, 1, 5, -1, 8, 4, 6]
- >>> for mode_name in ['zero', 'constant', 'symmetric', 'reflect', 'periodic', 'smooth', 'periodization']:
- ... mode = getattr(pywt.Modes, mode_name)
- ... cA, cD = pywt.dwt(x, 'db2', mode)
- ... print("Mode: %d (%s)" % (mode, mode_name))
- Mode: 0 (zero)
- Mode: 2 (constant)
- Mode: 1 (symmetric)
- Mode: 6 (reflect)
- Mode: 4 (periodic)
- Mode: 3 (smooth)
- Mode: 5 (periodization)
-
-
-The default mode is :ref:`symmetric `:
-
- >>> cA, cD = pywt.dwt(x, 'db2')
- >>> print(cA)
- [ 1.76776695 1.73309178 3.40612438 6.32928585 7.77817459]
- >>> print(cD)
- [-0.61237244 -2.15599552 -5.95034847 -1.21545369 1.22474487]
- >>> print(pywt.idwt(cA, cD, 'db2'))
- [ 1. 2. 1. 5. -1. 8. 4. 6.]
-
-
-And using a keyword argument:
-
- >>> cA, cD = pywt.dwt(x, 'db2', mode='symmetric')
- >>> print(cA)
- [ 1.76776695 1.73309178 3.40612438 6.32928585 7.77817459]
- >>> print(cD)
- [-0.61237244 -2.15599552 -5.95034847 -1.21545369 1.22474487]
- >>> print(pywt.idwt(cA, cD, 'db2'))
- [ 1. 2. 1. 5. -1. 8. 4. 6.]
diff --git a/doc/source/regression/multilevel.md b/doc/source/regression/multilevel.md
new file mode 100644
index 000000000..ff0a95dd1
--- /dev/null
+++ b/doc/source/regression/multilevel.md
@@ -0,0 +1,111 @@
+---
+jupytext:
+ text_representation:
+ extension: .md
+ format_name: myst
+ format_version: 1.0.0
+ jupytext_version: 1.16.1
+kernelspec:
+ display_name: Python 3 (ipykernel)
+ language: python
+ name: python3
+---
+
++++ {"tags": ["jupyterlite_sphinx_strip"]}
+
+```{include} header.md
+
+```
+
+# Multilevel DWT, IDWT and SWT
+
+## Multilevel DWT decomposition
+
+Here is an example of multilevel DWT decomposition using the `db1` wavelet into
+approximation and detail coefficients:
+
+```{code-cell}
+import pywt
+x = [3, 7, 1, 1, -2, 5, 4, 6]
+db1 = pywt.Wavelet('db1')
+cA3, cD3, cD2, cD1 = pywt.wavedec(x, db1)
+```
+
+```{code-cell}
+cA3
+```
+
+```{code-cell}
+cD3
+```
+
+```{code-cell}
+cD2
+```
+
+```{code-cell}
+cD1
+```
+
+The number of levels in the decomposition can be determined as well:
+
+```{code-cell}
+pywt.dwt_max_level(len(x), db1)
+```
+
+or decompose to a specific level:
+
+```{code-cell}
+cA2, cD2, cD1 = pywt.wavedec(x, db1, mode='constant', level=2)
+```
+
+## Multilevel IDWT reconstruction
+
+```{code-cell}
+coeffs = pywt.wavedec(x, db1)
+pywt.waverec(coeffs, db1)
+```
+
+## Multilevel SWT decomposition
+
+```{code-cell}
+x = [3, 7, 1, 3, -2, 6, 4, 6]
+(cA2, cD2), (cA1, cD1) = pywt.swt(x, db1, level=2)
+```
+
+```{code-cell}
+cA1
+```
+
+```{code-cell}
+cD1
+```
+
+```{code-cell}
+cA2
+```
+
+```{code-cell}
+cD2
+```
+
+```{code-cell}
+[(cA2, cD2)] = pywt.swt(cA1, db1, level=1, start_level=1)
+```
+
+```{code-cell}
+cA2
+```
+
+```{code-cell}
+cD2
+```
+
+```{code-cell}
+coeffs = pywt.swt(x, db1)
+len(coeffs)
+```
+
+```{code-cell}
+pywt.swt_max_level(len(x))
+```
diff --git a/doc/source/regression/multilevel.rst b/doc/source/regression/multilevel.rst
deleted file mode 100644
index c4916834e..000000000
--- a/doc/source/regression/multilevel.rst
+++ /dev/null
@@ -1,64 +0,0 @@
-.. _reg-multilevel:
-
-.. currentmodule:: pywt
-
-Multilevel DWT, IDWT and SWT
-============================
-
-Multilevel DWT decomposition
-----------------------------
-
->>> import pywt
->>> x = [3, 7, 1, 1, -2, 5, 4, 6]
->>> db1 = pywt.Wavelet('db1')
->>> cA3, cD3, cD2, cD1 = pywt.wavedec(x, db1)
->>> print(cA3)
-[ 8.83883476]
->>> print(cD3)
-[-0.35355339]
->>> print(cD2)
-[ 4. -3.5]
->>> print(cD1)
-[-2.82842712 0. -4.94974747 -1.41421356]
-
->>> pywt.dwt_max_level(len(x), db1)
-3
-
->>> cA2, cD2, cD1 = pywt.wavedec(x, db1, mode='constant', level=2)
-
-
-Multilevel IDWT reconstruction
-------------------------------
-
->>> coeffs = pywt.wavedec(x, db1)
->>> print(pywt.waverec(coeffs, db1))
-[ 3. 7. 1. 1. -2. 5. 4. 6.]
-
-
-Multilevel SWT decomposition
-----------------------------
-
->>> x = [3, 7, 1, 3, -2, 6, 4, 6]
->>> (cA2, cD2), (cA1, cD1) = pywt.swt(x, db1, level=2)
->>> print(cA1)
-[ 7.07106781 5.65685425 2.82842712 0.70710678 2.82842712 7.07106781
- 7.07106781 6.36396103]
->>> print(cD1)
-[-2.82842712 4.24264069 -1.41421356 3.53553391 -5.65685425 1.41421356
- -1.41421356 2.12132034]
->>> print(cA2)
-[ 7. 4.5 4. 5.5 7. 9.5 10. 8.5]
->>> print(cD2)
-[ 3. 3.5 0. -4.5 -3. 0.5 0. 0.5]
-
->>> [(cA2, cD2)] = pywt.swt(cA1, db1, level=1, start_level=1)
->>> print(cA2)
-[ 7. 4.5 4. 5.5 7. 9.5 10. 8.5]
->>> print(cD2)
-[ 3. 3.5 0. -4.5 -3. 0.5 0. 0.5]
-
->>> coeffs = pywt.swt(x, db1)
->>> len(coeffs)
-3
->>> pywt.swt_max_level(len(x))
-3
diff --git a/doc/source/regression/wavelet.md b/doc/source/regression/wavelet.md
new file mode 100644
index 000000000..688f907c5
--- /dev/null
+++ b/doc/source/regression/wavelet.md
@@ -0,0 +1,235 @@
+---
+jupytext:
+ text_representation:
+ extension: .md
+ format_name: myst
+ format_version: 1.0.0
+ jupytext_version: 1.16.1
+kernelspec:
+ display_name: Python 3 (ipykernel)
+ language: python
+ name: python3
+---
+
++++ {"tags": ["jupyterlite_sphinx_strip"]}
+
+```{include} header.md
+
+```
+
+# The Wavelet object
+
+## Wavelet families and builtin Wavelets names
+
+`pywt.Wavelet` objects are really handy carriers of a bunch of DWT-specific
+data like _quadrature mirror filters_ and some general properties associated
+with them.
+
+At first let's go through the methods of creating a `Wavelet` object.
+The easiest and the most convenient way is to use built-in named Wavelets.
+
+These wavelets are organized into groups called wavelet families. The most
+commonly used families are:
+
+```{code-cell}
+import pywt
+pywt.families()
+```
+
+The {func}`wavelist` function with family name passed as an argument is used to
+obtain the list of wavelet names in each family.
+
+```{code-cell}
+for family in pywt.families():
+ print("%s family: " % family + ', '.join(pywt.wavelist(family)))
+```
+
+To get the full list of built-in wavelets' names, just use the `pywt.wavelist` function
+without any arguments.
+
+## Creating Wavelet objects
+
+Now that we know all the names, let's finally create a `Wavelet` object:
+
+```{code-cell}
+w = pywt.Wavelet('db3')
+```
+
+and, that's it!
+
+## Wavelet properties
+
+But what can we do with `Wavelet` objects? Well, they carry some
+interesting pieces of information.
+
+First, let's try printing a `Wavelet` object that we used earlier.
+This shows a brief information about its name, its family name and some
+properties like orthogonality and symmetry.
+
+```{code-cell}
+print(w)
+```
+
+But the most important bits of information are the wavelet filters coefficients,
+which are used in Discrete Wavelet Transform. These coefficients
+can be obtained via the `Wavelet.dec_lo`, `dec_hi`, `rec_lo`,
+and the `rec_hi` attributes, which
+correspond to lowpass & highpass decomposition filters, and lowpass &
+highpass reconstruction filters respectively:
+
+```{code-cell}
+def print_array(arr):
+ print("[%s]" % ", ".join(["%.14f" % x for x in arr]))
+```
+
+Another way to get the filters data is to use the `filter_bank`
+attribute, which returns all four filters in a tuple:
+
+```{code-cell}
+w.filter_bank == (w.dec_lo, w.dec_hi, w.rec_lo, w.rec_hi)
+```
+
+Other properties of a `Wavelet` object are:
+
+`name`, `short_family_name`, and `family_name`:
+
+```{code-cell}
+print(w.name)
+print(w.short_family_name)
+print(w.family_name)
+```
+
+Decomposition (`dec_len`) and reconstruction (`rec_len`) filter lengths:
+
+```{code-cell}
+w.dec_len
+```
+
+```{code-cell}
+w.rec_len
+```
+
+Orthogonality (`orthogonal`) and biorthogonality (`biorthogonal`):
+
+```{code-cell}
+w.orthogonal
+```
+
+```{code-cell}
+w.biorthogonal
+```
+
+Symmetry (`symmetry`):
+
+```{code-cell}
+print(w.symmetry)
+```
+
+Number of vanishing moments for the scaling function `phi` (`vanishing_moments_phi`)
+and the wavelet function `psi` (`vanishing_moments_psi`), associated with the filters:
+
+```{code-cell}
+ w.vanishing_moments_phi
+```
+
+```{code-cell}
+w.vanishing_moments_psi
+```
+
+Now when we know a bit about the builtin Wavelets, let's see how to create
+[custom wavelets](custom-wavelets). These can be created in two ways:
+
+1. Passing the filter bank object that implements the `filter_bank` attribute. The
+ attribute must return four filters coefficients.
+
+```{code-cell}
+class MyHaarFilterBank(object):
+ @property
+ def filter_bank(self):
+ from math import sqrt
+ return (
+ [sqrt(2)/2, sqrt(2)/2], [-sqrt(2)/2, sqrt(2)/2],
+ [sqrt(2)/2, sqrt(2)/2], [sqrt(2)/2, -sqrt(2)/2]
+ )
+
+my_wavelet = pywt.Wavelet('My Haar Wavelet', filter_bank=MyHaarFilterBank())
+```
+
+2. Passing the filters coefficients directly as the `filter_bank` parameter.
+
+```{code-cell}
+from math import sqrt
+my_filter_bank = (
+ [sqrt(2)/2, sqrt(2)/2], [-sqrt(2)/2, sqrt(2)/2],
+ [sqrt(2)/2, sqrt(2)/2], [sqrt(2)/2, -sqrt(2)/2]
+)
+my_wavelet = pywt.Wavelet('My Haar Wavelet', filter_bank=my_filter_bank)
+```
+
+Note that such custom wavelets **will not** have all the properties set
+to correct values and some of them could be missing:
+
+```{code-cell}
+print(my_wavelet)
+```
+
+You can, however, set a couple of them on your own:
+
+```{code-cell}
+my_wavelet.orthogonal = True
+my_wavelet.biorthogonal = True
+```
+
+Let's view the values of the custom wavelet properties again:
+
+```{code-cell}
+print(my_wavelet)
+```
+
+## And now... the `wavefun`!
+
+We all know that the fun with wavelets is in wavelet functions.
+Now, what would this package be without a tool to compute wavelet
+and scaling functions approximations?
+
+This is the purpose of the `wavefun()` method, which is used to
+approximate scaling function (`phi`) and wavelet function (`psi`) at the
+given level of refinement, based on the filters coefficients.
+
+The number of returned values varies depending on the wavelet's
+orthogonality property. For orthogonal wavelets, the result is a tuple
+with the scaling function, wavelet function, and the xgrid coordinates.
+
+```{code-cell}
+w = pywt.Wavelet('sym3')
+w.orthogonal
+```
+
+```{code-cell}
+(phi, psi, x) = w.wavefun(level=5)
+```
+
+For biorthogonal (non-orthogonal) wavelets, different scaling and wavelet
+functions are used for decomposition and reconstruction, and thus, five
+elements are returned: decomposition scaling and wavelet functions
+approximations, reconstruction scaling and wavelet functions approximations,
+and the xgrid.
+
+```{code-cell}
+w = pywt.Wavelet('bior1.3')
+w.orthogonal
+```
+
+```{code-cell}
+(phi_d, psi_d, phi_r, psi_r, x) = w.wavefun(level=5)
+```
+
+:::{seealso}
+You can find live examples of the usage of `wavefun()` and
+images of all the built-in wavelets on the
+[Wavelet Properties Browser](http://wavelets.pybytes.com) page.
+
+However, **this website is no longer actively maintained** and does not
+include every wavelet present in PyWavelets. The precision of the wavelet
+coefficients at that site is also lower than those included in PyWavelets.
+:::
diff --git a/doc/source/regression/wavelet.rst b/doc/source/regression/wavelet.rst
deleted file mode 100644
index e46745371..000000000
--- a/doc/source/regression/wavelet.rst
+++ /dev/null
@@ -1,233 +0,0 @@
-.. _reg-wavelet:
-
-.. currentmodule:: pywt
-
-The Wavelet object
-==================
-
-Wavelet families and builtin Wavelets names
--------------------------------------------
-
-:class:`Wavelet` objects are really a handy carriers of a bunch of DWT-specific
-data like *quadrature mirror filters* and some general properties associated
-with them.
-
-At first let's go through the methods of creating a :class:`Wavelet` object.
-The easiest and the most convenient way is to use builtin named Wavelets.
-
-These wavelets are organized into groups called wavelet families. The most
-commonly used families are:
-
- >>> import pywt
- >>> pywt.families()
- ['haar', 'db', 'sym', 'coif', 'bior', 'rbio', 'dmey', 'gaus', 'mexh', 'morl', 'cgau', 'shan', 'fbsp', 'cmor']
-
-The :func:`wavelist` function with family name passed as an argument is used to
-obtain the list of wavelet names in each family.
-
- >>> for family in pywt.families():
- ... print("%s family: " % family + ', '.join(pywt.wavelist(family)))
- haar family: haar
- db family: db1, db2, db3, db4, db5, db6, db7, db8, db9, db10, db11, db12, db13, db14, db15, db16, db17, db18, db19, db20, db21, db22, db23, db24, db25, db26, db27, db28, db29, db30, db31, db32, db33, db34, db35, db36, db37, db38
- sym family: sym2, sym3, sym4, sym5, sym6, sym7, sym8, sym9, sym10, sym11, sym12, sym13, sym14, sym15, sym16, sym17, sym18, sym19, sym20
- coif family: coif1, coif2, coif3, coif4, coif5, coif6, coif7, coif8, coif9, coif10, coif11, coif12, coif13, coif14, coif15, coif16, coif17
- bior family: bior1.1, bior1.3, bior1.5, bior2.2, bior2.4, bior2.6, bior2.8, bior3.1, bior3.3, bior3.5, bior3.7, bior3.9, bior4.4, bior5.5, bior6.8
- rbio family: rbio1.1, rbio1.3, rbio1.5, rbio2.2, rbio2.4, rbio2.6, rbio2.8, rbio3.1, rbio3.3, rbio3.5, rbio3.7, rbio3.9, rbio4.4, rbio5.5, rbio6.8
- dmey family: dmey
- gaus family: gaus1, gaus2, gaus3, gaus4, gaus5, gaus6, gaus7, gaus8
- mexh family: mexh
- morl family: morl
- cgau family: cgau1, cgau2, cgau3, cgau4, cgau5, cgau6, cgau7, cgau8
- shan family: shan
- fbsp family: fbsp
- cmor family: cmor
-
-To get the full list of builtin wavelets' names just use the :func:`wavelist`
-with no argument.
-
-
-Creating Wavelet objects
-------------------------
-
-Now when we know all the names let's finally create a :class:`Wavelet` object:
-
- >>> w = pywt.Wavelet('db3')
-
-So.. that's it.
-
-
-Wavelet properties
-------------------
-
-But what can we do with :class:`Wavelet` objects? Well, they carry some
-interesting information.
-
-First, let's try printing a :class:`Wavelet` object. This shows a brief
-information about its name, its family name and some properties like
-orthogonality and symmetry.
-
- >>> print(w)
- Wavelet db3
- Family name: Daubechies
- Short name: db
- Filters length: 6
- Orthogonal: True
- Biorthogonal: True
- Symmetry: asymmetric
- DWT: True
- CWT: False
-
-But the most important information are the wavelet filters coefficients, which
-are used in :ref:`Discrete Wavelet Transform `. These coefficients can
-be obtained via the :attr:`~Wavelet.dec_lo`, :attr:`Wavelet.dec_hi`,
-:attr:`~Wavelet.rec_lo` and :attr:`~Wavelet.rec_hi` attributes, which
-corresponds to lowpass and highpass decomposition filters and lowpass and
-highpass reconstruction filters respectively:
-
- >>> def print_array(arr):
- ... print("[%s]" % ", ".join(["%.14f" % x for x in arr]))
-
-
-
-Another way to get the filters data is to use the :attr:`~Wavelet.filter_bank`
-attribute, which returns all four filters in a tuple:
-
- >>> w.filter_bank == (w.dec_lo, w.dec_hi, w.rec_lo, w.rec_hi)
- True
-
-
-Other Wavelet's properties are:
-
- Wavelet :attr:`~Wavelet.name`, :attr:`~Wavelet.short_family_name` and :attr:`~Wavelet.family_name`:
-
- >>> print(w.name)
- db3
- >>> print(w.short_family_name)
- db
- >>> print(w.family_name)
- Daubechies
-
- - Decomposition (:attr:`~Wavelet.dec_len`) and reconstruction
- (:attr:`~.Wavelet.rec_len`) filter lengths:
-
- >>> int(w.dec_len) # int() is for normalizing longs and ints for doctest
- 6
- >>> int(w.rec_len)
- 6
-
- - Orthogonality (:attr:`~Wavelet.orthogonal`) and biorthogonality (:attr:`~Wavelet.biorthogonal`):
-
- >>> w.orthogonal
- True
- >>> w.biorthogonal
- True
-
- - Symmetry (:attr:`~Wavelet.symmetry`):
-
- >>> print(w.symmetry)
- asymmetric
-
- - Number of vanishing moments for the scaling function ``phi``
- (:attr:`~Wavelet.vanishing_moments_phi`) and the wavelet function ``psi``
- (:attr:`~Wavelet.vanishing_moments_psi`) associated with the filters:
-
- >>> w.vanishing_moments_phi
- 0
- >>> w.vanishing_moments_psi
- 3
-
-Now when we know a bit about the builtin Wavelets, let's see how to create
-:ref:`custom Wavelets ` objects. These can be done in two ways:
-
- 1) Passing the filter bank object that implements the ``filter_bank``
- attribute. The attribute must return four filters coefficients.
-
- >>> class MyHaarFilterBank(object):
- ... @property
- ... def filter_bank(self):
- ... from math import sqrt
- ... return ([sqrt(2)/2, sqrt(2)/2], [-sqrt(2)/2, sqrt(2)/2],
- ... [sqrt(2)/2, sqrt(2)/2], [sqrt(2)/2, -sqrt(2)/2])
-
-
- >>> my_wavelet = pywt.Wavelet('My Haar Wavelet', filter_bank=MyHaarFilterBank())
-
-
- 2) Passing the filters coefficients directly as the ``filter_bank``
- parameter.
-
- >>> from math import sqrt
- >>> my_filter_bank = ([sqrt(2)/2, sqrt(2)/2], [-sqrt(2)/2, sqrt(2)/2],
- ... [sqrt(2)/2, sqrt(2)/2], [sqrt(2)/2, -sqrt(2)/2])
- >>> my_wavelet = pywt.Wavelet('My Haar Wavelet', filter_bank=my_filter_bank)
-
-
-Note that such custom wavelets **will not** have all the properties set
-to correct values:
-
- >>> print(my_wavelet)
- Wavelet My Haar Wavelet
- Family name:
- Short name:
- Filters length: 2
- Orthogonal: False
- Biorthogonal: False
- Symmetry: unknown
- DWT: True
- CWT: False
-
- You can however set a couple of them on your own:
-
- >>> my_wavelet.orthogonal = True
- >>> my_wavelet.biorthogonal = True
-
- >>> print(my_wavelet)
- Wavelet My Haar Wavelet
- Family name:
- Short name:
- Filters length: 2
- Orthogonal: True
- Biorthogonal: True
- Symmetry: unknown
- DWT: True
- CWT: False
-
-And now... the ``wavefun``!
----------------------------
-
-We all know that the fun with wavelets is in wavelet functions.
-Now what would be this package without a tool to compute wavelet
-and scaling functions approximations?
-
-This is the purpose of the :meth:`~Wavelet.wavefun` method, which is used to
-approximate scaling function (``phi``) and wavelet function (``psi``) at the
-given level of refinement, based on the filters coefficients.
-
-The number of returned values varies depending on the wavelet's
-orthogonality property. For orthogonal wavelets the result is tuple
-with scaling function, wavelet function and xgrid coordinates.
-
- >>> w = pywt.Wavelet('sym3')
- >>> w.orthogonal
- True
- >>> (phi, psi, x) = w.wavefun(level=5)
-
-For biorthogonal (non-orthogonal) wavelets different scaling and wavelet
-functions are used for decomposition and reconstruction, and thus five
-elements are returned: decomposition scaling and wavelet functions
-approximations, reconstruction scaling and wavelet functions approximations,
-and the xgrid.
-
- >>> w = pywt.Wavelet('bior1.3')
- >>> w.orthogonal
- False
- >>> (phi_d, psi_d, phi_r, psi_r, x) = w.wavefun(level=5)
-
-.. seealso::
- You can find live examples of :meth:`~Wavelet.wavefun` usage and
- images of all the built-in wavelets on the
- `Wavelet Properties Browser `_ page.
- However, **this website is no longer actively maintained** and does not
- include every wavelet present in PyWavelets. The precision of the wavelet
- coefficients at that site is also lower than those included in
- PyWavelets.
diff --git a/doc/source/regression/wp.md b/doc/source/regression/wp.md
new file mode 100644
index 000000000..b3200a80a
--- /dev/null
+++ b/doc/source/regression/wp.md
@@ -0,0 +1,382 @@
+---
+jupytext:
+ text_representation:
+ extension: .md
+ format_name: myst
+ format_version: 1.0.0
+ jupytext_version: 1.16.1
+kernelspec:
+ display_name: Python 3 (ipykernel)
+ language: python
+ name: python3
+---
+
++++ {"tags": ["jupyterlite_sphinx_strip"]}
+
+```{include} header.md
+
+```
+
+# Wavelet Packets
+
+## `import pywt` and construct a helper function
+
+```{code-cell}
+import pywt
+```
+
+This helper function can format arrays in a consistent manner across
+different systems. Please note that this function is just for the purpose of
+this example and is not part of the PyWavelets library, and it is not necessary
+or required to use it in your own code:
+
+```{code-cell}
+def format_array(a):
+ """Consistent array representation across different systems"""
+ import numpy
+ a = numpy.where(numpy.abs(a) < 1e-5, 0, a)
+ return numpy.array2string(a, precision=5, separator=' ', suppress_small=True)
+```
+
+## Create Wavelet Packet structure
+
+Okay, let's create a sample instance of the `WaveletPacket` class:
+
+```{code-cell}
+x = [1, 2, 3, 4, 5, 6, 7, 8]
+wp = pywt.WaveletPacket(data=x, wavelet='db1', mode='symmetric')
+```
+
+The input data and decomposition coefficients are stored in the
+`WaveletPacket.data` attribute:
+
+```{code-cell}
+print(wp.data)
+```
+
+Nodes are identified by their paths, i.e., `Node.path`. For the root
+node, the path is `''` and the decomposition level is `0`.
+
+```{code-cell}
+print(repr(wp.path))
+```
+
+```{code-cell}
+print(wp.level)
+```
+
+The `maxlevel` attribute, if not given as param in the constructor, is
+automatically computed:
+
+```{code-cell}
+print(wp['ad'].maxlevel)
+```
+
+## Traversing the WP tree
+
+### Accessing subnodes
+
+```{code-cell}
+x = [1, 2, 3, 4, 5, 6, 7, 8]
+wp = pywt.WaveletPacket(data=x, wavelet='db1', mode='symmetric')
+```
+
+First check what is the maximum level of decomposition:
+
+```{code-cell}
+print(wp.maxlevel)
+```
+
+and try accessing subnodes of the WP tree.
+
+1st level:
+
+```{code-cell}
+print(wp['a'].data)
+```
+
+```{code-cell}
+print(wp['a'].path)
+```
+
+2nd level:
+
+```{code-cell}
+print(wp['aa'].data)
+```
+
+```{code-cell}
+print(wp['aa'].path)
+```
+
+3rd level:
+
+```{code-cell}
+print(wp['aaa'].data)
+```
+
+```{code-cell}
+print(wp['aaa'].path)
+```
+
+Oops, we have reached the maximum level of decomposition and received an `IndexError`:
+
+```{code-cell}
+---
+tags: [raises-exception, remove-output]
+---
+print(wp['aaaa'].data)
+```
+
++++ {"tags": ["jupyterlite_sphinx_strip"]}
+
+```{code-block} python
+:class: pywt-handcoded-cell-output
+Traceback (most recent call last):
+...
+IndexError: Path length is out of range.
+```
+
+Now, try an invalid path:
+
+```{code-cell}
+---
+tags: [raises-exception, remove-output]
+---
+print(wp['ac'])
+```
+
++++ {"tags": ["jupyterlite_sphinx_strip"]}
+
+```{code-block} python
+:class: pywt-handcoded-cell-output
+Traceback (most recent call last):
+...
+ValueError: Subnode name must be in ['a', 'd'], not 'c'.
+```
+
+which just yielded a `ValueError`.
+
+### Accessing `Node`'s attributes
+
+The `WaveletPacket` object is a tree data structure, which evaluates to a set
+of `Node` objects. `WaveletPacket` is just a special subclass
+of the `Node` class (which in turn inherits from the `BaseNode` class).
+
+Tree nodes can be accessed using the `obj[x]` (`Node.__getitem__`)
+operator.
+
+Each tree node has a set of attributes: `data`, `path`,
+`node_name`, `parent`, `level`,
+`maxlevel` and `mode`.
+
+```{code-cell}
+x = [1, 2, 3, 4, 5, 6, 7, 8]
+wp = pywt.WaveletPacket(data=x, wavelet='db1', mode='symmetric')
+```
+
+```{code-cell}
+print(wp['ad'].data)
+```
+
+```{code-cell}
+print(wp['ad'].path)
+```
+
+```{code-cell}
+print(wp['ad'].node_name)
+```
+
+```{code-cell}
+print(wp['ad'].parent.path)
+```
+
+```{code-cell}
+print(wp['ad'].level)
+```
+
+```{code-cell}
+print(wp['ad'].maxlevel)
+```
+
+```{code-cell}
+print(wp['ad'].mode)
+```
+
+### Collecting nodes
+
+```{code-cell}
+x = [1, 2, 3, 4, 5, 6, 7, 8]
+wp = pywt.WaveletPacket(data=x, wavelet='db1', mode='symmetric')
+```
+
+We can get all nodes on the particular level either in `natural` order:
+
+```{code-cell}
+print([node.path for node in wp.get_level(3, 'natural')])
+```
+
+or sorted based on the band frequency (`freq`):
+
+```{code-cell}
+print([node.path for node in wp.get_level(3, 'freq')])
+```
+
+Note that `WaveletPacket.get_level()` also performs automatic decomposition
+until it reaches the specified `level`.
+
+## Reconstructing data from Wavelet Packets
+
+```{code-cell}
+x = [1, 2, 3, 4, 5, 6, 7, 8]
+wp = pywt.WaveletPacket(data=x, wavelet='db1', mode='symmetric')
+```
+
+Now create a new instance of the `WaveletPacket` class and set its nodes
+with some data.
+
+```{code-cell}
+new_wp = pywt.WaveletPacket(data=None, wavelet='db1', mode='symmetric')
+```
+
+```{code-cell}
+new_wp['aa'] = wp['aa'].data
+new_wp['ad'] = [-2., -2.]
+```
+
+For convenience, `Node.data` gets automatically extracted from the `Node` object:
+
+```{code-cell}
+new_wp['d'] = wp['d']
+```
+
+And reconstruct the data from the `aa`, `ad` and `d` packets.
+
+```{code-cell}
+print(new_wp.reconstruct(update=False))
+```
+
+If the `update` param in the reconstruct method is set to `False`, the
+node's `Node.data` will not be updated.
+
+```{code-cell}
+print(new_wp.data)
+```
+
+Otherwise, the `Node.data` attribute will be set to the reconstructed
+value.
+
+```{code-cell}
+print(new_wp.reconstruct(update=True))
+```
+
+```{code-cell}
+print(new_wp.data)
+```
+
+```{code-cell}
+print([n.path for n in new_wp.get_leaf_nodes(False)])
+```
+
+```{code-cell}
+print([n.path for n in new_wp.get_leaf_nodes(True)])
+```
+
+## Removing nodes from Wavelet Packet tree
+
+Let's create some sample data:
+
+```{code-cell}
+x = [1, 2, 3, 4, 5, 6, 7, 8]
+wp = pywt.WaveletPacket(data=x, wavelet='db1', mode='symmetric')
+```
+
+First, let's start with a tree decomposition at level 2. The leaf nodes in the tree are:
+
+```{code-cell}
+dummy = wp.get_level(2)
+for n in wp.get_leaf_nodes(False):
+ print(n.path, format_array(n.data))
+```
+
+```{code-cell}
+node = wp['ad']
+print(node)
+```
+
+To remove a node from the WP tree, use Python's `del obj[x]` (`Node.__delitem__`):
+
+```{code-cell}
+del wp['ad']
+```
+
+The leaf nodes that left in the tree are:
+
+```{code-cell}
+for n in wp.get_leaf_nodes():
+ print(n.path, format_array(n.data))
+```
+
+And the reconstruction is:
+
+```{code-cell}
+print(wp.reconstruct())
+```
+
+Now, restore the deleted node value.
+
+```{code-cell}
+wp['ad'].data = node.data
+```
+
+Printing leaf nodes and tree reconstruction confirms the original state of the
+tree:
+
+```{code-cell}
+for n in wp.get_leaf_nodes(False):
+ print(n.path, format_array(n.data))
+```
+
+```{code-cell}
+print(wp.reconstruct())
+```
+
+## Lazy evaluation
+
+:::{note}
+This section is for demonstration of PyWavelets' internal purposes
+only. Do not rely on the attribute access to nodes as presented in
+this example.
+:::
+
+```{code-cell}
+x = [1, 2, 3, 4, 5, 6, 7, 8]
+wp = pywt.WaveletPacket(data=x, wavelet='db1', mode='symmetric')
+```
+
+At first the wp's attribute `a` is `None`:
+
+```{code-cell}
+print(wp.a)
+```
+
+**Remember that you should not rely on the attribute access.**
+
+At the first attempt to access the node, it is computed via the decomposition
+of its parent node (which is the `wp` object itself).
+
+```{code-cell}
+print(wp['a'])
+```
+
+Now `wp.a` is set to the newly created node:
+
+```{code-cell}
+print(wp.a)
+```
+
+And so is `wp.d`:
+
+```{code-cell}
+print(wp.d)
+```
diff --git a/doc/source/regression/wp.rst b/doc/source/regression/wp.rst
deleted file mode 100644
index 8afc12ec4..000000000
--- a/doc/source/regression/wp.rst
+++ /dev/null
@@ -1,303 +0,0 @@
-.. _reg-wp:
-
-.. currentmodule:: pywt
-
->>> from __future__ import print_function
-
-Wavelet Packets
-===============
-
-Import pywt
------------
-
- >>> import pywt
-
- >>> def format_array(a):
- ... """Consistent array representation across different systems"""
- ... import numpy
- ... a = numpy.where(numpy.abs(a) < 1e-5, 0, a)
- ... return numpy.array2string(a, precision=5, separator=' ', suppress_small=True)
-
-
-Create Wavelet Packet structure
--------------------------------
-
-Ok, let's create a sample :class:`WaveletPacket`:
-
- >>> x = [1, 2, 3, 4, 5, 6, 7, 8]
- >>> wp = pywt.WaveletPacket(data=x, wavelet='db1', mode='symmetric')
-
-The input ``data`` and decomposition coefficients are stored in the
-:attr:`WaveletPacket.data` attribute:
-
- >>> print(wp.data)
- [1, 2, 3, 4, 5, 6, 7, 8]
-
-:class:`Nodes ` are identified by :attr:`paths <~Node.path>`. For the root
-node the path is ``''`` and the decomposition level is ``0``.
-
- >>> print(repr(wp.path))
- ''
- >>> print(wp.level)
- 0
-
-The ``maxlevel``, if not given as param in the constructor, is automatically
-computed:
-
- >>> print(wp['ad'].maxlevel)
- 3
-
-
-Traversing WP tree:
--------------------
-
-Accessing subnodes:
-~~~~~~~~~~~~~~~~~~~
-
->>> x = [1, 2, 3, 4, 5, 6, 7, 8]
->>> wp = pywt.WaveletPacket(data=x, wavelet='db1', mode='symmetric')
-
-First check what is the maximum level of decomposition:
-
- >>> print(wp.maxlevel)
- 3
-
-and try accessing subnodes of the WP tree:
-
- * 1st level:
-
- >>> print(wp['a'].data)
- [ 2.12132034 4.94974747 7.77817459 10.60660172]
- >>> print(wp['a'].path)
- a
-
- * 2nd level:
-
- >>> print(wp['aa'].data)
- [ 5. 13.]
- >>> print(wp['aa'].path)
- aa
-
-
- * 3rd level:
-
- >>> print(wp['aaa'].data)
- [ 12.72792206]
- >>> print(wp['aaa'].path)
- aaa
-
-
- Ups, we have reached the maximum level of decomposition and got an
- :exc:`IndexError`:
-
- >>> print(wp['aaaa'].data)
- Traceback (most recent call last):
- ...
- IndexError: Path length is out of range.
-
-Now try some invalid path:
-
- >>> print(wp['ac'])
- Traceback (most recent call last):
- ...
- ValueError: Subnode name must be in ['a', 'd'], not 'c'.
-
-which just yielded a :exc:`ValueError`.
-
-
-Accessing Node's attributes:
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-:class:`WaveletPacket` object is a tree data structure, which evaluates to a set
-of :class:`Node` objects. :class:`WaveletPacket` is just a special subclass
-of the :class:`Node` class (which in turn inherits from the :class:`BaseNode`).
-
-Tree nodes can be accessed using the ``obj[x]`` (:meth:`Node.__getitem__`)
-operator.
-Each tree node has a set of attributes: :attr:`~Node.data`, :attr:`~Node.path`,
-:attr:`~Node.node_name`, :attr:`~Node.parent`, :attr:`~Node.level`,
-:attr:`~Node.maxlevel` and :attr:`~Node.mode`.
-
->>> x = [1, 2, 3, 4, 5, 6, 7, 8]
->>> wp = pywt.WaveletPacket(data=x, wavelet='db1', mode='symmetric')
-
->>> print(wp['ad'].data)
-[-2. -2.]
-
->>> print(wp['ad'].path)
-ad
-
->>> print(wp['ad'].node_name)
-d
-
->>> print(wp['ad'].parent.path)
-a
-
->>> print(wp['ad'].level)
-2
-
->>> print(wp['ad'].maxlevel)
-3
-
->>> print(wp['ad'].mode)
-symmetric
-
-
-Collecting nodes
-~~~~~~~~~~~~~~~~
-
->>> x = [1, 2, 3, 4, 5, 6, 7, 8]
->>> wp = pywt.WaveletPacket(data=x, wavelet='db1', mode='symmetric')
-
-
-We can get all nodes on the particular level either in ``natural`` order:
-
- >>> print([node.path for node in wp.get_level(3, 'natural')])
- ['aaa', 'aad', 'ada', 'add', 'daa', 'dad', 'dda', 'ddd']
-
-or sorted based on the band frequency (``freq``):
-
- >>> print([node.path for node in wp.get_level(3, 'freq')])
- ['aaa', 'aad', 'add', 'ada', 'dda', 'ddd', 'dad', 'daa']
-
-Note that :meth:`WaveletPacket.get_level` also performs automatic decomposition
-until it reaches the specified ``level``.
-
-
-Reconstructing data from Wavelet Packets:
------------------------------------------
-
->>> x = [1, 2, 3, 4, 5, 6, 7, 8]
->>> wp = pywt.WaveletPacket(data=x, wavelet='db1', mode='symmetric')
-
-
-Now create a new :class:`Wavelet Packet ` and set its nodes with
-some data.
-
- >>> new_wp = pywt.WaveletPacket(data=None, wavelet='db1', mode='symmetric')
-
- >>> new_wp['aa'] = wp['aa'].data
- >>> new_wp['ad'] = [-2., -2.]
-
-For convenience, :attr:`Node.data` gets automatically extracted from the
-:class:`Node` object:
-
- >>> new_wp['d'] = wp['d']
-
-And reconstruct the data from the ``aa``, ``ad`` and ``d`` packets.
-
- >>> print(new_wp.reconstruct(update=False))
- [ 1. 2. 3. 4. 5. 6. 7. 8.]
-
-If the ``update`` param in the reconstruct method is set to ``False``, the
-node's :attr:`~Node.data` will not be updated.
-
- >>> print(new_wp.data)
- None
-
-Otherwise, the :attr:`~Node.data` attribute will be set to the reconstructed
-value.
-
- >>> print(new_wp.reconstruct(update=True))
- [ 1. 2. 3. 4. 5. 6. 7. 8.]
- >>> print(new_wp.data)
- [ 1. 2. 3. 4. 5. 6. 7. 8.]
-
-
->>> print([n.path for n in new_wp.get_leaf_nodes(False)])
-['aa', 'ad', 'd']
-
->>> print([n.path for n in new_wp.get_leaf_nodes(True)])
-['aaa', 'aad', 'ada', 'add', 'daa', 'dad', 'dda', 'ddd']
-
-
-Removing nodes from Wavelet Packet tree:
-----------------------------------------
-
-Let's create a sample data:
-
- >>> x = [1, 2, 3, 4, 5, 6, 7, 8]
- >>> wp = pywt.WaveletPacket(data=x, wavelet='db1', mode='symmetric')
-
-First, start with a tree decomposition at level 2. Leaf nodes in the tree are:
-
- >>> dummy = wp.get_level(2)
- >>> for n in wp.get_leaf_nodes(False):
- ... print(n.path, format_array(n.data))
- aa [ 5. 13.]
- ad [-2. -2.]
- da [-1. -1.]
- dd [ 0. 0.]
-
- >>> node = wp['ad']
- >>> print(node)
- ad: [-2. -2.]
-
-To remove a node from the WP tree, use Python's ``del obj[x]``
-(:class:`Node.__delitem__`):
-
- >>> del wp['ad']
-
-The leaf nodes that left in the tree are:
-
- >>> for n in wp.get_leaf_nodes():
- ... print(n.path, format_array(n.data))
- aa [ 5. 13.]
- da [-1. -1.]
- dd [ 0. 0.]
-
-And the reconstruction is:
-
- >>> print(wp.reconstruct())
- [ 2. 3. 2. 3. 6. 7. 6. 7.]
-
-Now restore the deleted node value.
-
- >>> wp['ad'].data = node.data
-
-Printing leaf nodes and tree reconstruction confirms the original state of the
-tree:
-
- >>> for n in wp.get_leaf_nodes(False):
- ... print(n.path, format_array(n.data))
- aa [ 5. 13.]
- ad [-2. -2.]
- da [-1. -1.]
- dd [ 0. 0.]
-
- >>> print(wp.reconstruct())
- [ 1. 2. 3. 4. 5. 6. 7. 8.]
-
-
-Lazy evaluation:
-----------------
-
-.. note:: This section is for demonstration of pywt internals purposes
- only. Do not rely on the attribute access to nodes as presented in
- this example.
-
->>> x = [1, 2, 3, 4, 5, 6, 7, 8]
->>> wp = pywt.WaveletPacket(data=x, wavelet='db1', mode='symmetric')
-
-1) At first the wp's attribute ``a`` is None
-
- >>> print(wp.a)
- None
-
- **Remember that you should not rely on the attribute access.**
-
-2) At first attempt to access the node it is computed via decomposition
- of its parent node (the wp object itself).
-
- >>> print(wp['a'])
- a: [ 2.12132034 4.94974747 7.77817459 10.60660172]
-
-3) Now the ``wp.a`` is set to the newly created node:
-
- >>> print(wp.a)
- a: [ 2.12132034 4.94974747 7.77817459 10.60660172]
-
- And so is ``wp.d``:
-
- >>> print(wp.d)
- d: [-0.70710678 -0.70710678 -0.70710678 -0.70710678]
diff --git a/doc/source/regression/wp2d.md b/doc/source/regression/wp2d.md
new file mode 100644
index 000000000..178d1e689
--- /dev/null
+++ b/doc/source/regression/wp2d.md
@@ -0,0 +1,430 @@
+---
+jupytext:
+ text_representation:
+ extension: .md
+ format_name: myst
+ format_version: 1.0.0
+ jupytext_version: 1.16.1
+kernelspec:
+ display_name: Python 3 (ipykernel)
+ language: python
+ name: python3
+---
+
++++ {"tags": ["jupyterlite_sphinx_strip"]}
+
+```{include} header.md
+
+```
+
+# 2D Wavelet Packets
+
+## Import pywt
+
+```{code-cell}
+import pywt
+import numpy
+```
+
+## Create 2D Wavelet Packet structure
+
+Start with preparing test data:
+
+```{code-cell}
+x = numpy.array([[1, 2, 3, 4, 5, 6, 7, 8]] * 8, 'd')
+print(x)
+```
+
+Now create a 2D [Wavelet Packet](ref-wp) object:
+
+```{code-cell}
+wp = pywt.WaveletPacket2D(data=x, wavelet='db1', mode='symmetric')
+```
+
+The input `data` and decomposition coefficients are stored in the
+`WaveletPacket2D.data` attribute:
+
+```{code-cell}
+print(wp.data)
+```
+
+Nodes (the `Node2D` class) are identified by paths. For the root node, the path is
+`''` and the decomposition level is `0`.
+
+```{code-cell}
+print(repr(wp.path))
+```
+
+```{code-cell}
+print(wp.level)
+```
+
+`WaveletPacket2D.maxlevel`, if not given in the constructor, is
+automatically computed based on the data size:
+
+```{code-cell}
+print(wp.maxlevel)
+```
+
+## Traversing WP tree
+
+Wavelet Packet nodes (`Node2D`) are arranged in a tree. Each node in a WP
+tree is uniquely identified and addressed by a `Node2D.path` string.
+
+In the 1D `WaveletPacket` case nodes were accessed using `'a'`
+(approximation) and `'d'` (details) path names (each node has two 1D
+children).
+
+Because now we deal with a bit more complex structure (each node has four
+children), we have four basic path names based on the dwt 2D output convention
+to address the WP2D structure:
+
+- `a` - LL, low-low coefficients
+- `h` - LH, low-high coefficients
+- `v` - HL, high-low coefficients
+- `d` - HH, high-high coefficients
+
+In other words, subnode naming corresponds to the `dwt2` function output
+naming convention (as wavelet packet transform is based on the dwt2 transform):
+
+```
+ -------------------
+ | | |
+ | cA(LL) | cH(LH) |
+ | | |
+(cA, (cH, cV, cD)) <---> -------------------
+ | | |
+ | cV(HL) | cD(HH) |
+ | | |
+ -------------------
+
+ (fig.1: DWT 2D output and interpretation)
+```
+
+Knowing what the nodes names are, we can now access them using the indexing
+operator `obj[x]` (`WaveletPacket2D.__getitem__`):
+
+```{code-cell}
+print(wp['a'].data)
+```
+
+```{code-cell}
+print(wp['h'].data)
+```
+
+```{code-cell}
+print(wp['v'].data)
+```
+
+```{code-cell}
+print(wp['d'].data)
+```
+
+Similarly, a subnode of a subnode can be accessed by:
+
+```{code-cell}
+print(wp['aa'].data)
+```
+
+Indexing base 2D (`WaveletPacket2D`) (as well as 1D `WaveletPacket`)
+using compound paths is just the same as indexing the WP subnode:
+
+```{code-cell}
+node = wp['a']
+print(node['a'].data)
+```
+
+```{code-cell}
+print(wp['a']['a'].data is wp['aa'].data)
+```
+
+Following down the decomposition path:
+
+```{code-cell}
+print(wp['aaa'].data)
+```
+
+```{code-cell}
+---
+tags: [raises-exception, remove-output]
+---
+print(wp['aaaa'].data)
+```
+
++++ {"tags": ["jupyterlite_sphinx_strip"]}
+
+```{code-block} python
+:class: pywt-handcoded-cell-output
+Traceback (most recent call last):
+...
+IndexError: Path length is out of range.
+```
+
+Oops, we have reached the maximum level of decomposition for the `'aaaa'` path,
+which, by the way, was:
+
+```{code-cell}
+print(wp.maxlevel)
+```
+
+Now, try an invalid path:
+
+```{code-cell}
+---
+tags: [raises-exception, remove-output]
+---
+print(wp['f'])
+```
+
++++ {"tags": ["jupyterlite_sphinx_strip"]}
+
+```{code-block} python
+:class: pywt-handcoded-cell-output
+Traceback (most recent call last):
+...
+ValueError: Subnode name must be in ['a', 'h', 'v', 'd'], not 'f'.
+```
+
+### Accessing Node2D's attributes
+
+`WaveletPacket2D` is a tree data structure, which evaluates to a set
+of `Node2D` objects. `WaveletPacket2D` is just a special the `Node2D` class (which in turn inherits from a `BaseNode` class
+just like with `Node` and `WaveletPacket` for the 1D case).
+
+```{code-cell}
+print(wp['av'].data)
+```
+
+```{code-cell}
+print(wp['av'].path)
+```
+
+```{code-cell}
+print(wp['av'].node_name)
+```
+
+```{code-cell}
+print(wp['av'].parent.path)
+```
+
+```{code-cell}
+print(wp['av'].parent.data)
+```
+
+```{code-cell}
+print(wp['av'].level)
+```
+
+```{code-cell}
+print(wp['av'].maxlevel)
+```
+
+```{code-cell}
+print(wp['av'].mode)
+```
+
+### Collecting nodes
+
+We can get all nodes on the particular level using the
+`WaveletPacket2D.get_level` method:
+
+0 level - the root `wp` node:
+
+```{code-cell}
+len(wp.get_level(0))
+```
+
+```{code-cell}
+print([node.path for node in wp.get_level(0)])
+```
+
+- 1st level of decomposition:
+
+```{code-cell}
+len(wp.get_level(1))
+```
+
+```{code-cell}
+print([node.path for node in wp.get_level(1)])
+```
+
+2nd level of decomposition:
+
+```{code-cell}
+len(wp.get_level(2))
+```
+
+```{code-cell}
+paths = [node.path for node in wp.get_level(2)]
+for i, path in enumerate(paths):
+ if (i+1) % 4 == 0:
+ print(path)
+ else:
+ print(path, end=' ')
+```
+
+3rd level of decomposition:
+
+```{code-cell}
+print(len(wp.get_level(3)))
+```
+
+```{code-cell}
+paths = [node.path for node in wp.get_level(3)]
+for i, path in enumerate(paths):
+ if (i+1) % 8 == 0:
+ print(path)
+ else:
+ print(path, end=' ')
+```
+
+Note that `WaveletPacket2D.get_level` performs automatic decomposition
+until it reaches the given level.
+
+## Reconstructing data from Wavelet Packets
+
+Let's create a new empty 2D Wavelet Packet structure and set its nodes
+values with known data from the previous examples:
+
+```{code-cell}
+new_wp = pywt.WaveletPacket2D(data=None, wavelet='db1', mode='symmetric')
+```
+
+```{code-cell}
+new_wp['vh'] = wp['vh'].data # [[0.0, 0.0], [0.0, 0.0]]
+new_wp['vv'] = wp['vh'].data # [[0.0, 0.0], [0.0, 0.0]]
+new_wp['vd'] = [[0.0, 0.0], [0.0, 0.0]]
+```
+
+```{code-cell}
+new_wp['a'] = [[3.0, 7.0, 11.0, 15.0], [3.0, 7.0, 11.0, 15.0],
+ [3.0, 7.0, 11.0, 15.0], [3.0, 7.0, 11.0, 15.0]]
+new_wp['d'] = [[0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0]]
+```
+
+For convenience, `Node2D.data` gets automatically extracted from the
+base `Node2D` object:
+
+```{code-cell}
+new_wp['h'] = wp['h'] # all zeros
+```
+
+Note: just remember to not assign to the `node.data parameter directly (TODO).
+
+And reconstruct the data from the `a`, `d`, `vh`, `vv`, `vd` and `h`
+packets (Note that `va` node was not set and the WP tree is "not complete"
+\- the `va` branch will be treated as _zero-array_):
+
+```{code-cell}
+print(new_wp.reconstruct(update=False))
+```
+
+Now set the `va` node with the known values and do the reconstruction again:
+
+```{code-cell}
+new_wp['va'] = wp['va'].data # [[-2.0, -2.0], [-2.0, -2.0]]
+print(new_wp.reconstruct(update=False))
+```
+
+which is just the same as the base sample data `x`.
+
+Of course we can go the other way and remove nodes from the tree. If we delete
+the `va` node, again, we get the "not complete" tree from one of the previous
+examples:
+
+```{code-cell}
+del new_wp['va']
+print(new_wp.reconstruct(update=False))
+```
+
+Just restore the node before the next examples:
+
+```{code-cell}
+new_wp['va'] = wp['va'].data
+```
+
+If the `update` param in the `WaveletPacket2D.reconstruct` method is set
+to `False`, the node's `Node2D.data` attribute will not be updated.
+
+```{code-cell}
+print(new_wp.data)
+```
+
+Otherwise, the `WaveletPacket2D.data` attribute will be set to the
+reconstructed value.
+
+```{code-cell}
+print(new_wp.reconstruct(update=True))
+```
+
+```{code-cell}
+print(new_wp.data)
+```
+
+Since we have an interesting WP structure built, it is a good occasion to
+present the `WaveletPacket2D.get_leaf_nodes()` method, which collects
+non-zero leaf nodes from the WP tree:
+
+```{code-cell}
+print([n.path for n in new_wp.get_leaf_nodes()])
+```
+
+Passing the `decompose = True` parameter to the method will force the WP
+object to do a full decomposition up to the _maximum level_ of decomposition:
+
+```{code-cell}
+paths = [n.path for n in new_wp.get_leaf_nodes(decompose=True)]
+len(paths)
+```
+
+```{code-cell}
+for i, path in enumerate(paths):
+ if (i+1) % 8 == 0:
+ print(path)
+ else:
+ try:
+ print(path, end=' ')
+ except:
+ print(path, end=' ')
+```
+
+## Lazy evaluation
+
+:::{note}
+This section is for the demonstration of PyWavelets' internals' purposes
+only. Do not rely on the attribute access to nodes as presented in
+this example.
+:::
+
+```{code-cell}
+x = numpy.array([[1, 2, 3, 4, 5, 6, 7, 8]] * 8)
+wp = pywt.WaveletPacket2D(data=x, wavelet='db1', mode='symmetric')
+```
+
+At first, the `wp`'s attribute `a` is `None`
+
+```{code-cell}
+print(wp.a)
+```
+
+**Remember that you should not rely on the attribute access.**
+
+During the first attempt to access the node it is computed
+via decomposition of its parent node (the wp object itself).
+
+```{code-cell}
+print(wp['a'])
+```
+
+Now the `a` is set to the newly created node:
+
+```{code-cell}
+print(wp.a)
+```
+
+And so is `wp.d`:
+
+```{code-cell}
+print(wp.d)
+```
diff --git a/doc/source/regression/wp2d.rst b/doc/source/regression/wp2d.rst
deleted file mode 100644
index 8d70d9185..000000000
--- a/doc/source/regression/wp2d.rst
+++ /dev/null
@@ -1,429 +0,0 @@
-.. _reg-wp2d:
-
-.. currentmodule:: pywt
-
-
-2D Wavelet Packets
-==================
-
-Import pywt
------------
-
->>> from __future__ import print_function
->>> import pywt
->>> import numpy
-
-
-Create 2D Wavelet Packet structure
-----------------------------------
-
-Start with preparing test data:
-
- >>> x = numpy.array([[1, 2, 3, 4, 5, 6, 7, 8]] * 8, 'd')
- >>> print(x)
- [[ 1. 2. 3. 4. 5. 6. 7. 8.]
- [ 1. 2. 3. 4. 5. 6. 7. 8.]
- [ 1. 2. 3. 4. 5. 6. 7. 8.]
- [ 1. 2. 3. 4. 5. 6. 7. 8.]
- [ 1. 2. 3. 4. 5. 6. 7. 8.]
- [ 1. 2. 3. 4. 5. 6. 7. 8.]
- [ 1. 2. 3. 4. 5. 6. 7. 8.]
- [ 1. 2. 3. 4. 5. 6. 7. 8.]]
-
-Now create a :class:`2D Wavelet Packet ` object:
-
- >>> wp = pywt.WaveletPacket2D(data=x, wavelet='db1', mode='symmetric')
-
-The input ``data`` and decomposition coefficients are stored in the
-:attr:`WaveletPacket2D.data` attribute:
-
- >>> print(wp.data)
- [[ 1. 2. 3. 4. 5. 6. 7. 8.]
- [ 1. 2. 3. 4. 5. 6. 7. 8.]
- [ 1. 2. 3. 4. 5. 6. 7. 8.]
- [ 1. 2. 3. 4. 5. 6. 7. 8.]
- [ 1. 2. 3. 4. 5. 6. 7. 8.]
- [ 1. 2. 3. 4. 5. 6. 7. 8.]
- [ 1. 2. 3. 4. 5. 6. 7. 8.]
- [ 1. 2. 3. 4. 5. 6. 7. 8.]]
-
-:class:`Nodes ` are identified by paths. For the root node the path is
-``''`` and the decomposition level is ``0``.
-
- >>> print(repr(wp.path))
- ''
- >>> print(wp.level)
- 0
-
-The :attr:`WaveletPacket2D.maxlevel`, if not given in the constructor, is
-automatically computed based on the data size:
-
- >>> print(wp.maxlevel)
- 3
-
-
-Traversing WP tree:
--------------------
-
-Wavelet Packet :class:`nodes ` are arranged in a tree. Each node in a WP
-tree is uniquely identified and addressed by a :attr:`~Node2D.path` string.
-
-In the 1D :class:`WaveletPacket` case nodes were accessed using ``'a'``
-(approximation) and ``'d'`` (details) path names (each node has two 1D
-children).
-
-Because now we deal with a bit more complex structure (each node has four
-children), we have four basic path names based on the dwt 2D output convention
-to address the WP2D structure:
-
- * ``a`` - LL, low-low coefficients
- * ``h`` - LH, low-high coefficients
- * ``v`` - HL, high-low coefficients
- * ``d`` - HH, high-high coefficients
-
-In other words, subnode naming corresponds to the :func:`dwt2` function output
-naming convention (as wavelet packet transform is based on the dwt2 transform)::
-
- -------------------
- | | |
- | cA(LL) | cH(LH) |
- | | |
- (cA, (cH, cV, cD)) <---> -------------------
- | | |
- | cV(HL) | cD(HH) |
- | | |
- -------------------
-
- (fig.1: DWT 2D output and interpretation)
-
-
-Knowing what the nodes names are, we can now access them using the indexing
-operator ``obj[x]`` (:meth:`WaveletPacket2D.__getitem__`):
-
- >>> print(wp['a'].data)
- [[ 3. 7. 11. 15.]
- [ 3. 7. 11. 15.]
- [ 3. 7. 11. 15.]
- [ 3. 7. 11. 15.]]
- >>> print(wp['h'].data)
- [[ 0. 0. 0. 0.]
- [ 0. 0. 0. 0.]
- [ 0. 0. 0. 0.]
- [ 0. 0. 0. 0.]]
- >>> print(wp['v'].data)
- [[-1. -1. -1. -1.]
- [-1. -1. -1. -1.]
- [-1. -1. -1. -1.]
- [-1. -1. -1. -1.]]
- >>> print(wp['d'].data)
- [[ 0. 0. 0. 0.]
- [ 0. 0. 0. 0.]
- [ 0. 0. 0. 0.]
- [ 0. 0. 0. 0.]]
-
-Similarly, a subnode of a subnode can be accessed by:
-
- >>> print(wp['aa'].data)
- [[ 10. 26.]
- [ 10. 26.]]
-
-Indexing base :class:`WaveletPacket2D` (as well as 1D :class:`WaveletPacket`)
-using compound path is just the same as indexing WP subnode:
-
- >>> node = wp['a']
- >>> print(node['a'].data)
- [[ 10. 26.]
- [ 10. 26.]]
- >>> print(wp['a']['a'].data is wp['aa'].data)
- True
-
-Following down the decomposition path:
-
- >>> print(wp['aaa'].data)
- [[ 36.]]
- >>> print(wp['aaaa'].data)
- Traceback (most recent call last):
- ...
- IndexError: Path length is out of range.
-
-Ups, we have reached the maximum level of decomposition for the ``'aaaa'`` path,
-which btw. was:
-
- >>> print(wp.maxlevel)
- 3
-
-
-Now try some invalid path:
-
- >>> print(wp['f'])
- Traceback (most recent call last):
- ...
- ValueError: Subnode name must be in ['a', 'h', 'v', 'd'], not 'f'.
-
-
-Accessing Node2D's attributes:
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-:class:`WaveletPacket2D` is a tree data structure, which evaluates to a set
-of :class:`Node2D` objects. :class:`WaveletPacket2D` is just a special subclass
-of the :class:`Node2D` class (which in turn inherits from a :class:`BaseNode`,
-just like with :class:`Node` and :class:`WaveletPacket` for the 1D case.).
-
- >>> print(wp['av'].data)
- [[-4. -4.]
- [-4. -4.]]
-
- >>> print(wp['av'].path)
- av
-
- >>> print(wp['av'].node_name)
- v
-
- >>> print(wp['av'].parent.path)
- a
-
- >>> print(wp['av'].parent.data)
- [[ 3. 7. 11. 15.]
- [ 3. 7. 11. 15.]
- [ 3. 7. 11. 15.]
- [ 3. 7. 11. 15.]]
-
- >>> print(wp['av'].level)
- 2
-
- >>> print(wp['av'].maxlevel)
- 3
-
- >>> print(wp['av'].mode)
- symmetric
-
-
-Collecting nodes
-~~~~~~~~~~~~~~~~
-
-We can get all nodes on the particular level using the
-:meth:`WaveletPacket2D.get_level` method:
-
- * 0 level - the root `wp` node:
-
- >>> len(wp.get_level(0))
- 1
- >>> print([node.path for node in wp.get_level(0)])
- ['']
-
- * 1st level of decomposition:
-
- >>> len(wp.get_level(1))
- 4
- >>> print([node.path for node in wp.get_level(1)])
- ['a', 'h', 'v', 'd']
-
- * 2nd level of decomposition:
-
- >>> len(wp.get_level(2))
- 16
- >>> paths = [node.path for node in wp.get_level(2)]
- >>> for i, path in enumerate(paths):
- ... if (i+1) % 4 == 0:
- ... print(path)
- ... else:
- ... print(path, end=' ')
- aa ah av ad
- ha hh hv hd
- va vh vv vd
- da dh dv dd
-
- * 3rd level of decomposition:
-
- >>> print(len(wp.get_level(3)))
- 64
- >>> paths = [node.path for node in wp.get_level(3)]
- >>> for i, path in enumerate(paths):
- ... if (i+1) % 8 == 0:
- ... print(path)
- ... else:
- ... print(path, end=' ')
- aaa aah aav aad aha ahh ahv ahd
- ava avh avv avd ada adh adv add
- haa hah hav had hha hhh hhv hhd
- hva hvh hvv hvd hda hdh hdv hdd
- vaa vah vav vad vha vhh vhv vhd
- vva vvh vvv vvd vda vdh vdv vdd
- daa dah dav dad dha dhh dhv dhd
- dva dvh dvv dvd dda ddh ddv ddd
-
-Note that :meth:`WaveletPacket2D.get_level` performs automatic decomposition
-until it reaches the given level.
-
-
-Reconstructing data from Wavelet Packets:
------------------------------------------
-
-Let's create a new empty 2D Wavelet Packet structure and set its nodes
-values with known data from the previous examples:
-
- >>> new_wp = pywt.WaveletPacket2D(data=None, wavelet='db1', mode='symmetric')
-
- >>> new_wp['vh'] = wp['vh'].data # [[0.0, 0.0], [0.0, 0.0]]
- >>> new_wp['vv'] = wp['vh'].data # [[0.0, 0.0], [0.0, 0.0]]
- >>> new_wp['vd'] = [[0.0, 0.0], [0.0, 0.0]]
-
- >>> new_wp['a'] = [[3.0, 7.0, 11.0, 15.0], [3.0, 7.0, 11.0, 15.0],
- ... [3.0, 7.0, 11.0, 15.0], [3.0, 7.0, 11.0, 15.0]]
- >>> new_wp['d'] = [[0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0],
- ... [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0]]
-
- For convenience, :attr:`Node2D.data` gets automatically extracted from the
- base :class:`Node2D` object:
-
- >>> new_wp['h'] = wp['h'] # all zeros
-
- Note: just remember to not assign to the node.data parameter directly (todo).
-
-And reconstruct the data from the ``a``, ``d``, ``vh``, ``vv``, ``vd`` and ``h``
-packets (Note that ``va`` node was not set and the WP tree is "not complete"
-- the ``va`` branch will be treated as *zero-array*):
-
- >>> print(new_wp.reconstruct(update=False))
- [[ 1.5 1.5 3.5 3.5 5.5 5.5 7.5 7.5]
- [ 1.5 1.5 3.5 3.5 5.5 5.5 7.5 7.5]
- [ 1.5 1.5 3.5 3.5 5.5 5.5 7.5 7.5]
- [ 1.5 1.5 3.5 3.5 5.5 5.5 7.5 7.5]
- [ 1.5 1.5 3.5 3.5 5.5 5.5 7.5 7.5]
- [ 1.5 1.5 3.5 3.5 5.5 5.5 7.5 7.5]
- [ 1.5 1.5 3.5 3.5 5.5 5.5 7.5 7.5]
- [ 1.5 1.5 3.5 3.5 5.5 5.5 7.5 7.5]]
-
-Now set the ``va`` node with the known values and do the reconstruction again:
-
- >>> new_wp['va'] = wp['va'].data # [[-2.0, -2.0], [-2.0, -2.0]]
- >>> print(new_wp.reconstruct(update=False))
- [[ 1. 2. 3. 4. 5. 6. 7. 8.]
- [ 1. 2. 3. 4. 5. 6. 7. 8.]
- [ 1. 2. 3. 4. 5. 6. 7. 8.]
- [ 1. 2. 3. 4. 5. 6. 7. 8.]
- [ 1. 2. 3. 4. 5. 6. 7. 8.]
- [ 1. 2. 3. 4. 5. 6. 7. 8.]
- [ 1. 2. 3. 4. 5. 6. 7. 8.]
- [ 1. 2. 3. 4. 5. 6. 7. 8.]]
-
-which is just the same as the base sample data ``x``.
-
-Of course we can go the other way and remove nodes from the tree. If we delete
-the ``va`` node, again, we get the "not complete" tree from one of the previous
-examples:
-
- >>> del new_wp['va']
- >>> print(new_wp.reconstruct(update=False))
- [[ 1.5 1.5 3.5 3.5 5.5 5.5 7.5 7.5]
- [ 1.5 1.5 3.5 3.5 5.5 5.5 7.5 7.5]
- [ 1.5 1.5 3.5 3.5 5.5 5.5 7.5 7.5]
- [ 1.5 1.5 3.5 3.5 5.5 5.5 7.5 7.5]
- [ 1.5 1.5 3.5 3.5 5.5 5.5 7.5 7.5]
- [ 1.5 1.5 3.5 3.5 5.5 5.5 7.5 7.5]
- [ 1.5 1.5 3.5 3.5 5.5 5.5 7.5 7.5]
- [ 1.5 1.5 3.5 3.5 5.5 5.5 7.5 7.5]]
-
-Just restore the node before next examples.
-
- >>> new_wp['va'] = wp['va'].data
-
-If the ``update`` param in the :meth:`WaveletPacket2D.reconstruct` method is set
-to ``False``, the node's :attr:`Node2D.data` attribute will not be updated.
-
- >>> print(new_wp.data)
- None
-
-Otherwise, the :attr:`WaveletPacket2D.data` attribute will be set to the
-reconstructed value.
-
- >>> print(new_wp.reconstruct(update=True))
- [[ 1. 2. 3. 4. 5. 6. 7. 8.]
- [ 1. 2. 3. 4. 5. 6. 7. 8.]
- [ 1. 2. 3. 4. 5. 6. 7. 8.]
- [ 1. 2. 3. 4. 5. 6. 7. 8.]
- [ 1. 2. 3. 4. 5. 6. 7. 8.]
- [ 1. 2. 3. 4. 5. 6. 7. 8.]
- [ 1. 2. 3. 4. 5. 6. 7. 8.]
- [ 1. 2. 3. 4. 5. 6. 7. 8.]]
- >>> print(new_wp.data)
- [[ 1. 2. 3. 4. 5. 6. 7. 8.]
- [ 1. 2. 3. 4. 5. 6. 7. 8.]
- [ 1. 2. 3. 4. 5. 6. 7. 8.]
- [ 1. 2. 3. 4. 5. 6. 7. 8.]
- [ 1. 2. 3. 4. 5. 6. 7. 8.]
- [ 1. 2. 3. 4. 5. 6. 7. 8.]
- [ 1. 2. 3. 4. 5. 6. 7. 8.]
- [ 1. 2. 3. 4. 5. 6. 7. 8.]]
-
-Since we have an interesting WP structure built, it is a good occasion to
-present the :meth:`WaveletPacket2D.get_leaf_nodes` method, which collects
-non-zero leaf nodes from the WP tree:
-
- >>> print([n.path for n in new_wp.get_leaf_nodes()])
- ['a', 'h', 'va', 'vh', 'vv', 'vd', 'd']
-
-Passing the ``decompose = True`` parameter to the method will force the WP
-object to do a full decomposition up to the *maximum level* of decomposition:
-
- >>> paths = [n.path for n in new_wp.get_leaf_nodes(decompose=True)]
- >>> len(paths)
- 64
- >>> for i, path in enumerate(paths):
- ... if (i+1) % 8 == 0:
- ... print(path)
- ... else:
- ... try:
- ... print(path, end=' ')
- ... except:
- ... print(path, end=' ')
- aaa aah aav aad aha ahh ahv ahd
- ava avh avv avd ada adh adv add
- haa hah hav had hha hhh hhv hhd
- hva hvh hvv hvd hda hdh hdv hdd
- vaa vah vav vad vha vhh vhv vhd
- vva vvh vvv vvd vda vdh vdv vdd
- daa dah dav dad dha dhh dhv dhd
- dva dvh dvv dvd dda ddh ddv ddd
-
-Lazy evaluation:
-----------------
-
-.. note:: This section is for demonstration of pywt internals purposes
- only. Do not rely on the attribute access to nodes as presented in
- this example.
-
->>> x = numpy.array([[1, 2, 3, 4, 5, 6, 7, 8]] * 8)
->>> wp = pywt.WaveletPacket2D(data=x, wavelet='db1', mode='symmetric')
-
-1) At first the wp's attribute ``a`` is ``None``
-
- >>> print(wp.a)
- None
-
- **Remember that you should not rely on the attribute access.**
-
-2) During the first attempt to access the node it is computed
- via decomposition of its parent node (the wp object itself).
-
- >>> print(wp['a'])
- a: [[ 3. 7. 11. 15.]
- [ 3. 7. 11. 15.]
- [ 3. 7. 11. 15.]
- [ 3. 7. 11. 15.]]
-
-3) Now the ``a`` is set to the newly created node:
-
- >>> print(wp.a)
- a: [[ 3. 7. 11. 15.]
- [ 3. 7. 11. 15.]
- [ 3. 7. 11. 15.]
- [ 3. 7. 11. 15.]]
-
- And so is `wp.d`:
-
- >>> print(wp.d)
- d: [[ 0. 0. 0. 0.]
- [ 0. 0. 0. 0.]
- [ 0. 0. 0. 0.]
- [ 0. 0. 0. 0.]]
diff --git a/pyproject.toml b/pyproject.toml
index 92967a8a8..dedcbead1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -61,7 +61,7 @@ documentation = "https://pywavelets.readthedocs.io/"
[tool.ruff]
line-length = 88
target-version = 'py310'
-select = [
+lint.select = [
"I",
"UP",
"C4",
@@ -76,7 +76,7 @@ select = [
"SIM118",
"SIM2",
]
-ignore = [
+lint.ignore = [
"UP038", # https://github.com/astral-sh/ruff/issues/7871
]
diff --git a/pywt/__init__.py b/pywt/__init__.py
index 3720f5504..2916bb0d1 100644
--- a/pywt/__init__.py
+++ b/pywt/__init__.py
@@ -10,8 +10,6 @@
wavelet packets signal decomposition and reconstruction module.
"""
-from __future__ import division, print_function, absolute_import
-
from ._extensions._pywt import *
from ._functions import *
from ._multilevel import *
diff --git a/pywt/_extensions/_pywt.pyx b/pywt/_extensions/_pywt.pyx
index 3ffe41359..ddef57500 100644
--- a/pywt/_extensions/_pywt.pyx
+++ b/pywt/_extensions/_pywt.pyx
@@ -4,7 +4,7 @@
# See COPYING for license details.
__doc__ = """Cython wrapper for low-level C wavelet transform implementation."""
-__all__ = ['MODES', 'Modes', 'DiscreteContinuousWavelet', 'Wavelet',
+__all__ = ['Modes', 'DiscreteContinuousWavelet', 'Wavelet',
'ContinuousWavelet', 'wavelist', 'families']
diff --git a/pywt/tests/test_deprecations.py b/pywt/tests/test_deprecations.py
index 2b7d57bfb..aedaaa867 100644
--- a/pywt/tests/test_deprecations.py
+++ b/pywt/tests/test_deprecations.py
@@ -53,27 +53,6 @@ def get_mode(Modes, name):
assert_warns(DeprecationWarning, get_mode, pywt.Modes, mode)
-def test_MODES_deprecation_new():
- def use_MODES_new():
- return pywt.MODES.symmetric
-
- assert_warns(DeprecationWarning, use_MODES_new)
-
-
-def test_MODES_deprecation_old():
- def use_MODES_old():
- return pywt.MODES.sym
-
- assert_warns(DeprecationWarning, use_MODES_old)
-
-
-def test_MODES_deprecation_getattr():
- def use_MODES_new():
- return getattr(pywt.MODES, 'symmetric')
-
- assert_warns(DeprecationWarning, use_MODES_new)
-
-
def test_mode_equivalence():
old_new = [('zpd', 'zero'),
('cpd', 'constant'),
diff --git a/util/readthedocs/requirements.txt b/util/readthedocs/requirements.txt
index f21009667..e812cd611 100644
--- a/util/readthedocs/requirements.txt
+++ b/util/readthedocs/requirements.txt
@@ -1,13 +1,21 @@
cython
docutils
-jupyterlite-sphinx>=0.14.0
-jupyterlite-pyodide-kernel
+# Pyodide kernel 0.5.1 comes with Pyodide 0.27.1 and PyWavelets 1.7.0
+# see https://pyodide.org/en/stable/usage/packages-in-pyodide.html
+# and https://jupyterlite-pyodide-kernel.readthedocs.io/en/stable/#with-pyodide
+# for information on updating
+jupyterlite-pyodide-kernel==0.5.1
+jupyterlite-sphinx>=0.18.0
+jupytext
pydata-sphinx-theme
pytest
matplotlib
meson-python
+myst-nb
+nbformat
numpy
numpydoc
sphinx>=7
-sphinx-togglebutton
sphinx-copybutton
+sphinx-design
+sphinx-togglebutton