Repository: computational-psychology/stimupy (language: python)

Issue:
Topic Guide: step-by-step tutorial for create and submitting stimulus for a paper
The main purpose of the package is to make it easier to deposit a stimulus, so that it can be easily replicated by others. Therefore, ot would be very useful to have a step-by-step guide on how to create a corresponding file for your own work. The stimulus itself could be very simple, it is more about the minimal file structure, necessary functions, documentation, etc. 

In addition, this guide should contain information on how best to distribute it. Is there a way to include my own stimulus into the package apart from a pull request? Is it even an intention of the authors to turn the package into the stimulus repository (in this case CRAN-like submission and website with a gallery would be very useful)? Should one use the package but keep the stimulus as part of separate open methods files? Guidelines on this would be very important to build stimuli community.


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index b5df8126..d6dc8d3e 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -11,15 +11,17 @@ jobs:
     steps:
       - name: Fetch wheel(s) from release
         uses: dsaltares/fetch-gh-release-asset@1.1.0
-        regex: true
-        file: '*.whl'
-        target: 'dist/'
+        with:
+          regex: true
+          file: 'stimupy-.*\\.whl'
+          target: 'dist/'
 
       - name: Fetch sdist(s) from release
         uses: dsaltares/fetch-gh-release-asset@1.1.0
-        regex: true
-        file: '*.tar.gz'
-        target: 'dist/'
+        with:
+          regex: true
+          file: 'stimupy-.*\\.tar.gz'
+          target: 'dist/'
 
       - name: Publish to TestPyPI
         uses: pypa/gh-action-pypi-publish@release/v1
diff --git a/README.md b/README.md
index cf369b26..5d156c2d 100644
--- a/README.md
+++ b/README.md
@@ -3,12 +3,19 @@
 <p align=center>
 A pure-Python package
 for creating new and existing visual stimuli
-commonly used in the sudy of contrast, brightness/lightness,
+commonly used in the study of contrast, brightness, lightness,
 and other aspects of visual perception.
 </p>
 
+<p align=center>
+
 [![Tests](https://github.com/computational-psychology/stimupy/actions/workflows/test.yml/badge.svg)](https://github.com/computational-psychology/stimupy/actions/workflows/test.yml) [![](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
 [![Documentation Status](https://readthedocs.org/projects/stimupy/badge/?version=latest)](https://stimupy.readthedocs.io/en/latest/?badge=latest)
+</p>
+
+<p align=center>
+<img src=docs/logo.png width=400>
+</p>
 
 ---
 - documentation: https://stimupy.readthedocs.io/en/latest/
@@ -24,8 +31,8 @@ and other aspects of visual perception.
   exactly as described in the original manuscripts (including experimental data)
 - *build* new stimulus sets or benchmarks (e.g. for testing computational models),
   and easily add them to `stimupy`
-- *support* vision science by providing a large,openly-available and flexible battery of relevant stimulus functions
-- *unify and automate* stimulus creation
+- *support* vision science by providing a large, openly-available and flexible battery of relevant stimulus functions
+- *unify* and *automate* stimulus creation
 - be [**FAIR**](https://doi.org/10.1038/s41597-022-01710-x):
   **F**indable, **A**ccessible, **I**nteroperable, and **R**eusable
 
@@ -93,7 +100,7 @@ any stimulus or stimulus set not currently available, can be easily added.
 Open an [issue](https://github.com/computational-psychology/stimupy/issues/new)
 and let us know what you'd like to see added.
 
-If you want to contribute yourself, see [contributing](https://stimupy.readthedocs.io/en/latest/contributing/contributing.html)
+If you want to contribute yourself, see [contributing](https://stimupy.readthedocs.io/en/latest/contributing/contribute.html)
 
 
 ---
diff --git a/docs/_config.yml b/docs/_config.yml
index 39e8f145..a0f618f8 100644
--- a/docs/_config.yml
+++ b/docs/_config.yml
@@ -90,4 +90,5 @@ sphinx:
         "mod": "tooltip",
         "class": "tooltip",
         "func": "tooltip",
+        "obj": "tooltip"
     }
\ No newline at end of file
diff --git a/docs/_toc.yml b/docs/_toc.yml
index 355df225..18191f42 100644
--- a/docs/_toc.yml
+++ b/docs/_toc.yml
@@ -8,14 +8,13 @@ parts:
     chapters:
     - file: getting_started/installation
     - file: getting_started/getting_started
+    - file: howtos/howtos
   - caption: Topic guides
     chapters:
-    - file: topic_guides/topic_guides
     - file: topic_guides/organization
-#    - file: topic_guides/sets_papers
-#    - file: topic_guides/resolution
-#    - file: topic_guides/dimensions
-#    - file: topic_guides/gratings
+    - file: topic_guides/resolution
+    - file: topic_guides/dimensions
+    - file: topic_guides/waves
 #    - file: topic_guides/visual_noise
   - caption: Reference
     chapters:
diff --git a/docs/contributing/contribute.md b/docs/contributing/contribute.md
index 6545a2c1..7cee4f0e 100644
--- a/docs/contributing/contribute.md
+++ b/docs/contributing/contribute.md
@@ -1,83 +1,7 @@
 # Contribute to `stimupy`
 
-## Setting up a development environment
-1. **Fork** the [GitHub repository](https://github.com/computational-psychology/stimupy/)
-2. **Clone** the fork repository to your local machine
-3. **Install**`stimupy` with the development requirements `pip install -e ".[dev]"`
-    - Recommended to create an *editable* installation using 
-    - `".[dev,docs]"` to also edit and build the documentation
-
-The `stimupy` project uses a couple of development tools
-to work towards more consisten code quality:
-
-- `pytest` for unit and integration tests
-- `pyupgrade` for possible syntax improvements using newer language features
-- `black` for consistent code formatting
-- `flake8` for all kinds of linting
-
-These tools get installed as part of the `[dev]` extra dependencies.
-Additionally, to run these more consistently, one can install:
-
-- `nox` for automatically running tests across different Python versions
-- `precommit` for automatically running formatters and linters
-
-## Edit documentation
-`stimupy`'s documentation is build using [Jupyter Book](https://jupyterbook.org/en/stable/intro.html)
-which is installed as part of the `[docs]` extra dependencies.
-
-### Building the documentation
-To compile the documentation locally (from the toplevel directory):
-```
-jupyter-book build --all docs/
+```{toctree}
+dev_environ
+docs
+sets
 ```
-
-### How the documentation is organized
-All documentation lives in the `docs` subdirectory,
-and consists of a collection of Markdown (`.md`) files,
-specifically in the [MyST Markdown](https://jupyterbook.org/en/stable/content/myst.html) flavor
-(as well as some ReStructured Text).
-
-The documentation is generally organized along [The Documentation System](https://documentation.divio.com/),
-consisting of the following categories:
-- learning-oriented tutorials, under <project:getting_started/getting_started>
-- understanding-oriented <project:topic_guides/topic_guides>
-- information-oriented <project:reference/reference>
-
-### Executable content
-Some pages are pure (MyST) Markdown files;
-others (e.g. the tutorials) are executable notebooks, in the same MyST Markdown format.
-These MyST Notebooks can contain both Markdown syntax,
-as well as `code-cell` blocks which are executed during the build
-and their output is "woven" into the resulting page.
-These MyST Notebooks have a YAML frontmatter
-directing [JupyText](https://jupytext.readthedocs.io/en/latest/)
-how to convert them.
-
-
-
-
-## Add a stimulus set
-
-- Create a new file in `stimupy/papers`
-- Add only stimuli that use `stimupy` functionality
-- Each stimulus should be its own function
-- Each function should only take a `ppd` argument;
-  it should replicate the exact size and geometry of the stimulus,
-  allowing only the resolution (i.e., pixels per degree) to be changed.
-- Also create a corresponding tests file
-
-
-## Edit or add a stimulus function
-- To contribute a stimulus function, add it to the corresponding module
-
-
-## Contributing back to `stimupy`
-
-0. **Edit** code/documentation
-1. **Commit & Push** changes to your fork
-2. **Pull request** from your fork to our repository
-    - GitHub Actions will automatically run tests and linters
-    - If linters fail, run `black`, `pyupgrade` and `flake8` --
-      either separately or all together through `pre-commit`:
-      `pre-commit run --all-files`
-3. Changes will be reviewed by one of the maintainers
diff --git a/docs/contributing/dev_environ.md b/docs/contributing/dev_environ.md
new file mode 100644
index 00000000..0f69586c
--- /dev/null
+++ b/docs/contributing/dev_environ.md
@@ -0,0 +1,21 @@
+# Setting up a development environment
+
+1. **Fork** the [GitHub repository](https://github.com/computational-psychology/stimupy/)
+2. **Clone** the fork repository to your local machine
+3. **Install**`stimupy` with the development requirements `pip install -e ".[dev]"`
+    - Recommended to create an *editable* installation using 
+    - `".[dev,docs]"` to also edit and build the documentation
+
+The `stimupy` project uses a couple of development tools
+to work towards more consisten code quality:
+
+- `pytest` for unit and integration tests
+- `pyupgrade` for possible syntax improvements using newer language features
+- `black` for consistent code formatting
+- `flake8` for all kinds of linting
+
+These tools get installed as part of the `[dev]` extra dependencies.
+Additionally, to run these more consistently, one can install:
+
+- `nox` for automatically running tests across different Python versions
+- `precommit` for automatically running formatters and linters
diff --git a/docs/contributing/docs.md b/docs/contributing/docs.md
new file mode 100644
index 00000000..b1ca8787
--- /dev/null
+++ b/docs/contributing/docs.md
@@ -0,0 +1,46 @@
+# Contribute documentation
+
+## How the documentation is organized
+All documentation lives in the `docs` subdirectory,
+and consists of a collection of Markdown (`.md`) files,
+specifically in the [MyST Markdown](https://jupyterbook.org/en/stable/content/myst.html) flavor
+(as well as some ReStructured Text).
+
+The documentation is generally organized along [The Documentation System](https://documentation.divio.com/),
+consisting of the following categories:
+- learning-oriented tutorials, under <project:getting_started/getting_started>
+- understanding-oriented <project:topic_guides/topic_guides>
+- information-oriented <project:reference/reference>
+
+
+## Building the documentation
+`stimupy`'s documentation is build using [Jupyter Book](https://jupyterbook.org/en/stable/intro.html)
+which is installed as part of the `[docs]` extra dependencies.
+To compile the documentation locally (from the toplevel directory):
+```
+jupyter-book build --all docs/
+```
+which will then provide an output message on how to view the locally-built documentation.
+
+## Executable content
+Some pages are pure (MyST) Markdown files;
+others (e.g. the tutorials) are executable notebooks, in the same MyST Markdown format.
+These MyST Notebooks can contain both Markdown syntax,
+as well as `code-cell` blocks which are executed during the build
+and their output is "woven" into the resulting page.
+These MyST Notebooks have a YAML frontmatter
+directing [JupyText](https://jupytext.readthedocs.io/en/latest/)
+how to convert them.
+
+## Contributing back to `stimupy`
+
+0. **Edit** documentation
+1. **Commit & Push** changes to your fork
+    - We use [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) messages;
+    for documentation please start your commit message(s) with `docs: ...`
+2. **Pull request** from your fork to our repository
+    - GitHub Actions will automatically run tests and linters
+    - If linters fail, run `black`, `pyupgrade` and `flake8` --
+      either separately or all together through `pre-commit`:
+      `pre-commit run --all-files`
+3. Changes will be reviewed by one of the maintainers
\ No newline at end of file
diff --git a/docs/contributing/sets.md b/docs/contributing/sets.md
new file mode 100644
index 00000000..fd69a9a5
--- /dev/null
+++ b/docs/contributing/sets.md
@@ -0,0 +1,149 @@
+# Contribrute a stimulus (set)
+`stimupy` contains both a wide range of generalized [stimulus-functions](stimupy.stimuli)
+that can generate many parameterizated stimulus images,
+and several complete sets of stimuli
+that all come from the same {py:mod}`source <stimupy.papers>`.
+Development, organization and maintanance of these sets can be arduous,
+and we are committed to providing a high-quality and well-tested suite of functions.
+Thus, `stimupy` is a curated and maintained _library_ of stimuli
+(stimulus-functions, to be precise),
+not a _repository_ or platform for uploading stimuli.
+We welcome contributions to this library of stimuli,
+and stimulus sets, from the literature.
+
+## Add a stimulus set
+If you wish to contribute a complete set of stimuli from a single paper,
+please follow these instructions for organizing and formatting the code.
+
+A {py:mod}`paper <stimupy.papers>` should have the following features:
+
+- Each paper is identified using a `paper_key`: a cite-key like indicator,
+  usually some combination of author names/initials and publication date.
+  - For {py:mod}`modelfest <stimupy.papers.modelfest>` we made an exception.
+- All stimuli belonging a `paper` should be in a single python _module_, i.e., `.py` code-file;
+  this file is found under `stimupy/papers/<paper_key>.py`
+- Only stimuli that `stimupy` functionality should be included in the `<paper_key>.py`
+  - We have made a select few exceptions to this rule
+- Each stimulus should be its own function
+- Each function should only take a `ppd` argument;
+  it should replicate the exact size and geometry of the stimulus,
+  allowing only the resolution (i.e., pixels per degree) to be changed.
+  This argument should have an actual default value,
+  which corresponds to the `ppd` used in the original project.
+- Thus, different parameterizations of the "same" stimulus
+  (e.g. different geometries, polarities, etc.) should be separate functions.
+- As a result, each function should run without any arguments, e.g.
+  ```{code-block} python
+  stim = stimupy.papers.RHS2007.WE_thick()
+  ```
+- The output stim-`dict`s must contain at least the `img`,
+  but should also contain relevant stimulus parameters.
+- The output stim-`dict`s may contain additional information (keys),
+  such as experimental results,
+  if they do not take up too much space.
+- Each function should have a [NumPy-style docstring](https://numpydoc.readthedocs.io/en/latest/format.html#docstring-standard),
+  which at least documents the `ppd` argument and the returned dict as follows:
+  ```{code-block} python
+    """ <short summary of stimulus function>
+
+    Parameters
+    ----------
+    ppd : int
+        Resolution of stimulus in pixels per degree. (default: 32)
+
+    Returns
+    -------
+    dict of str
+        dict with the stimulus (key: "img")
+        and additional keys containing stimulus parameters
+    """
+  ```
+  Ideally it also provides detailed information of which exact stimulus it is,
+  e.g., through reference to a specific figure in the original paper.
+- The paper-module should also contain an `__all__` attribute,
+  which contains a list of all function names of the stimuli in the paper.
+  Anything in here is considered a stimulus, anything not in here will not be available
+  outside the paper module itself.
+- The paper-module should have a NumPy style docstring as well,
+  documenting at least the full bibliographical reference for the source of the stimuli.
+
+### An example
+```{tip}
+This file is included as `stimupy/papers/example.py`
+and can be `import`ed as `stimupy.papers.example`
+```
+
+```{literalinclude} ../../stimupy/papers/example.py
+```
+
+### Testing
+For each paper, there is a corresponding `tests/papers/<paper_key>.py` file
+containing _regression tests_ code.
+
+These tests generate each stimulus from the paper-module,
+and compares the produce `img` (and `masks`s) to a _hash_ of the `img` (and `masks`).
+The hashed `img`s for all stimuli is stored in a corresponding
+`tests/papers/<paper_key>.json` JSON file.
+The JSON files can be generated by running
+the `tests/papers.gen_ground_truth.py` script.
+See `stimupy/tests/papers/paper_tests_template.py` for a template;
+to create this regression test suite for your own paper,
+replace `paper_key` with the <paper_key>
+
+### Additional features
+The paper-modules already included in `stimupy` have a couple quality-of-life features
+which can be copied into a new module.
+
+A function to generate all stimuli in the set:
+```{code-block} python
+def gen_all(ppd=PPD, skip=False):
+    stims = {}  # save the stimulus-dicts in a larger dict, with name as key
+    for stim_name in __all__:
+        print(f"Generating RHS2007.{stim_name}")
+
+        # Get a reference to the actual function
+        func = globals()[stim_name]
+        try:
+            stim = func(ppd=ppd, pad=pad)
+
+            # Accumulate
+            stims[stim_name] = stim
+        except NotImplementedError as e:
+            if not skip:
+                raise e
+            # Skip stimuli that aren't implemented
+            print("-- not implemented")
+            pass
+
+    return stims
+```
+
+A code block that gets executed when the `.py` file is run as a script
+(rather than imported), and shows an overview of all stimuli in this set:
+```{code-block} python
+if __name__ == "__main__":
+    from stimupy.utils import plot_stimuli
+
+    stims = gen_all(pad=True, skip=True)
+    plot_stimuli(stims, mask=True)
+
+```
+
+
+## Edit or add a stimulus function
+- To contribute a stimulus function, add it to the corresponding module
+
+
+## Contributing back to `stimupy`
+
+0. **Edit** code/documentation
+1. **Commit & Push** changes to your fork
+    - We use [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) messages.
+    For bugfixes, please start your commit message(s) with `fix: ...`.
+    For add features (e.g., stimuli, set), please start your commit message(s) with `feat: ...`.
+2. **Pull request** from your fork to our repository
+    - GitHub Actions will automatically run tests and linters
+    - If linters fail, run `black`, `pyupgrade` and `flake8` --
+      either separately or all together through `pre-commit`:
+      `pre-commit run --all-files`
+3. Changes will be reviewed by one of the maintainers
\ No newline at end of file
diff --git a/docs/getting_started/composition.md b/docs/getting_started/composition.md
index 7af52ce6..296a1d16 100644
--- a/docs/getting_started/composition.md
+++ b/docs/getting_started/composition.md
@@ -11,12 +11,11 @@ kernelspec:
 ---
 
 # Composing stimuli, composed stimuli
-
 Most stimuli consist not just of one shape or element,
-but as a composition of multiple components.
+but consist of a composition of multiple components.
 The geometric {py:mod}`components <stimupy.components>`
 form the basic building blocks for all stimuli implemented in `stimupy`.
-In this tutorial, we will explore how stimulus consisting of multiple geometric elements,
+In this tutorial, we will explore how a simple example stimulus consisting of multiple geometric elements,
 can be composed using the functions that generate components.
 
 ```{code-cell}
@@ -27,7 +26,7 @@ from stimupy.utils import plot_stim, plot_stimuli
 from stimupy.components import shapes
 ```
 
-First we will create our two components that we want to combine in some way;
+First, we will create the two components that we want to combine in our example;
 here we will use the {py:func}`rectangle <stimupy.components.shapes.rectangle>`
 and {py:func}`disc <stimupy.components.shapes.disc>`
 (from our [previous tutorial](first_stim))
@@ -46,16 +45,16 @@ plt.show()
 ```
 
 Since the `"img"`s of the two stimuli are {py:class}`numpy.ndarray`s,
-we can simple manipulate these to combine our stimuli:
+the simplest manipulation that we can perform to combine our components is to add or subtract them:
 ```{code-cell}
 new_img = disc["img"] - rectangle["img"]
 
 plt.imshow(new_img, cmap="gray")
 ```
 
-We can even turn this into a something resembling more of a `stimupy` stimulus
--- and use the `stimupy` tooling
-like {py:func}`plot_stimuli <stimupy.utils.plot_stimuli>` on it --
+We can even turn this new stimulus-array into something that resembles a `stimupy` stimulus more
+-- in order to use `stimupy` tooling
+like {py:func}`plot_stimuli <stimupy.utils.plotting.plot_stimuli>` on it --
 simply by wrapping it in a {py:class}`dict`,
 and optionally adding some additional metadata:
 ```{code-cell}
@@ -70,19 +69,19 @@ plot_stimuli({"rect": rectangle, "disc": disc, "composed": new_stim})
 ## Masked regions
 
 The downside of this, is that such an operation (`-`) on the `"img"`s
-uses the *whole* `imgs`s, including the background.
+uses the *whole* `"img"`s, including the background.
 In other words, these operations aren't *content aware*;
 they treat all pixels the same, rather than restricting operations
 to the regions that we care about (e.g., the geometric shapes).
-We'd like to be to have a bit more control,
-for instance subtracting just a where the shapes overlap.
+In many cases, however, we might want to have more control,
+for instance only subtracting stimulus regions in which the two shapes overlap.
 
-One key in the stimulus-{py:class}`dict`s not addressed yet, is the `mask`.
-Just like `"img"`, this too is a {py:class}`numpy.ndarray`
-where each entry corresponds to a pixel in `"img"`
+For this, we would like to introduce one key feature of `stimupy`: stimulus `"mask"`s.
+Just like the `"img"`, the stimulus `"mask"` is a {py:class}`numpy.ndarray` which can be found in the stimulus-{py:class}`dict`.
+Each entry of the stimulus `"mask"` corresponds to a pixel in `"img"`
 (i.e., it has the same shape as `"img"`).
 
-However, rather the `"mask"` contains only integer-values
+Importantly, the `"mask"` contains only integer-values
 (compared to the floating point pixel-intensities in `"img"`).
 Each integer-value in the mask,
 corresponds to a geometric region of interest,
@@ -251,9 +250,9 @@ plt.show()
 
 ## Using masks to alter the simulus after creation
 
-An advantage of having these kinds of `mask`s that index regions
+One advantage of having these kinds of `"mask"`s that index regions
 (rather than just binary masks)
-is that we can the `mask` to selectively alter one region in an existing stimulus
+is that we can use the `"mask"` to selectively alter one region in an existing stimulus
 without having to recreate the whole image:
 
 ```{code-cell}
diff --git a/docs/getting_started/first_stim.md b/docs/getting_started/first_stim.md
index d40d409d..9627cf2c 100644
--- a/docs/getting_started/first_stim.md
+++ b/docs/getting_started/first_stim.md
@@ -24,7 +24,7 @@ This page can also be launched as an interactive Jupyter Notebook on Binder -- s
 ## Drawing a basic shape
 
 The most basic stimuli that `stimupy` provides, are basic geometric shapes.
-These functions be found in the {py:mod}`stimupy.components.shapes` *module*.
+These functions can be found in the {py:mod}`stimupy.components.shapes` *module*.
 To be able to access these,
 we first have to *import* this module
 into our current Python session:
@@ -50,14 +50,14 @@ This module contains the following functions:
 print(f"Available basic shapes: {shapes.__all__}")
 ```
 
-Each of these is a separate function.
-Let's take a look at one of these:
+Each of these `str`ings represents a separate function.
+Let's take a look at one of them:
 ```{code-cell}
 stim = shapes.rectangle(visual_size=(6,8), ppd=10, rectangle_size=(4,4))
 ```
 
 This function creates an image, and draws a rectangle inside it.
-Everywhere the rectangle isn't, is considered the "background".
+Every pixel which is not part of the rectangle, is considered the "background".
 It returns a {py:class}`dict`ionary,
 mapping `str`ings as *keys*, to all kinds of *values*.
 
@@ -90,7 +90,7 @@ The values of the entries in the `"img"` {py:class}`numpy.ndarray` represent the
 by default in range $[0,1]$.
 
 ## Stimulus parameters
-All `stimupy` stimulus-functions require and take a bunch of arguments.
+All `stimupy` stimulus-functions require and take multiple arguments.
 To see what arguments a given function takes,
 and what each of these arguments controls,
 you can look at the (online) [function reference](../reference/api.md)
@@ -198,11 +198,11 @@ The advantages of this are:
    % TODO: import in PsychoPy (?)
 2. anything that is compliant with this basic structure
    can use (some of) `stimupy` tooling,
-   e.g., {py:func}`plot_stim <stimupy.utils.plotting.plot_stim>`
+   e.g., {py:func}`plot_stim <stimupy.utils.plotting.plot_stim>`, {py:mod}`exports <stimupy.utils.export>`, or  {py:mod}`contrast manipulations <stimupy.utils.contrast_conversions>` to name a few
 3. since Python {py:class}`dict`s are *mutable*, you as user can add, create, remove, rename
    any of the keys and values.
 
-The main disadvantages, is that there are no controls or guarantees
-*after* a stimulus is created,
+The main disadvantage is that there are no controls or guarantees
+*after* a stimulus is created
 for the accuracy of any of its fields,
-since the user can change values.
\ No newline at end of file
+since the user can manually change values at any point in time.
\ No newline at end of file
diff --git a/docs/getting_started/replicate.md b/docs/getting_started/replicate.md
index e497fb54..7002fb11 100644
--- a/docs/getting_started/replicate.md
+++ b/docs/getting_started/replicate.md
@@ -20,7 +20,8 @@ and thus specific existing parameterizations can be recreated.
 
 For a large selection of the generic {py:mod}`stimupy.stimuli`,
 there are specific parameterizations in the published literature.
-Some of these are implement in stimupy as well,
+In order to make it easier for the community to access and work with these stimuli,
+we have re-implemented some existing stimulus sets in stimupy,
 under the corresponding {py:mod}`stimupy.papers`.
 
 ```{code-cell}
diff --git a/docs/getting_started/stimulus.md b/docs/getting_started/stimulus.md
index 1d68cde7..b9b1380d 100644
--- a/docs/getting_started/stimulus.md
+++ b/docs/getting_started/stimulus.md
@@ -66,23 +66,21 @@ also have 3 overall categories of parameters:
 
 However, some of the stimulus parameters
 have different names in {py:mod}`stimuli <stimupy.stimuli>`.
-In particular, **all** {py:mod}`stimuli <stimupy.stimuli>`
+In particular, many {py:mod}`stimuli <stimupy.stimuli>`
 have the concept of a `target` region(s):
 image regions that are of some particular scientific interest in this stimulus.
-For an SBC stimulus, this would be the rectangular path.
+For an SBC stimulus, this would be the rectangle.
 Thus, the {py:func}`sbcs.basic <stimupy.stimuli.sbcs.basic>` function takes a
 - `target_size`, compared to `rectangle_size`
 - `intensity_target`, rather than an `intensity_rectangle`
   (with a default intermediate value of `0.5`)
-In addition, the output stimulus contains a `target_mask`,
-which masks the pixels of the target region.
+- In addition, the output stimulus contains a `target_mask`, which masks the pixels of the target region.
 
 For this specific stimulus,
 the {py:func}`sbcs.basic <stimupy.stimuli.sbcs.basic>` provides little use over
 the base component.
-But, realistically, we would show not just this one part of an SBC display.
-Instead, this would likely be only half
-of a {py:func}`two-sided <stimupy.stimuli.sbsc.two_sized>` display:
+But, realistically, we would not show just this one part of a SBC display, but rather
+show both sides of the SBC displays as implemented in {py:func}`two_sided <stimupy.stimuli.sbcs.two_sided>`:
 ```{code-cell}
 two_sided_stim = sbcs.two_sided(visual_size=(6,8), ppd=10,
                                 target_size=(2,2))
@@ -179,7 +177,7 @@ OR as having a [cross-shaped target](todorovics.cross) with adjoining squares.
 For a single stimulus parameterization,
 these two conceptions may produce perfectly identical images ([see fig, top](fig_todorovics)).
 However, when changing parameters,
-you would expect different *behavior* from the stimulus fuction
+you would expect different *behavior* from the stimulus function
 dependent on your conception/interpretation of the stimulus ([see fig, bottom](fig_todorovics)).
 
 ```{code-cell}
diff --git a/docs/howtos/display.md b/docs/howtos/display.md
new file mode 100644
index 00000000..261045b2
--- /dev/null
+++ b/docs/howtos/display.md
@@ -0,0 +1,86 @@
+# Display stimuli / integrate in experiments
+`stimupy` is a package which is focused on generating stimuli,
+separate from stimulus display, use, experimentation, etc.
+Hence, `stimupy` is designed agnostic to how stimuli are used after stimulus generation,
+which allows for a broad range of uses.
+Principally, the stimulus image is stored
+as a matrix of intensity values, in a bare {py:obj}`numpy.ndarray`.
+Thus, using a `stimupy` stimulus in an application
+just requires the possibility to input a `numpy.ndarray`.
+
+
+## PsychoPy-based display
+[PsychoPy](https://psychopy.org/) is a leading stimulus display
+and experiment development package for the Python ecosystem.
+It provides lots of functionality for running experiments, interfacing with hardware
+and some stimulus-drawing capabilities.
+Most of the stimulus-drawing is done using an OpenGL backend
+directly in the screen buffer.
+While this approach is very fast and accurate,
+its difficult to create complex stimulus images, such as the ones implemented in `stimupy`.
+For these kinds of stimuli,
+PsychoPy can display predefined digital images as a static texture
+using the [ImageStim](https://psychopy.org/api/visual/imagestim.html#psychopy.visual.ImageStim) function:
+```{code-block} python
+stim = psychopy.visual.ImageStim(my_window, image=my_numpy_array)
+```
+
+To use `stimupy` stimuli in PyschoPy experiments, then, is quite straightforward
+and requires no e.g. additional exporting of the stimulus:
+
+1. Set up experiment using `PsychoPy`
+   1. Set up the screen/window information
+2. Generate the `stim` using `stimupy` code
+3. Create the PsychoPy `ImageStim` from the `stim["img"]`-array
+4. Display the `ImageStim` in the PsychoPy experiment.
+
+```{warning}
+`stimupy` is designed for flexibility, robustness, and ease of use -- not for speed.
+Some of the quality-of-life features have some processing overhead,
+e.g., automatic resolution resolving.
+As a result, `stimupy` stimulus functions may be slower than generating
+equivalent stimuli using OpenGL based rendering.
+If speed is of the essence, you may want to pregenerate all your `stimupy` stimuli
+before the experiment-loop,
+such that displaying a stimulus is just a single lookup.
+```
+
+## PsychToolBox-based (MATLAB) display
+[PsychToolBox (PTB)](http://psychtoolbox.org/) is the MATLAB progenitor of the PsychoPy.
+It provides lots of functionality for running experiments, interfacing with hardware
+and some stimulus-drawing capabilities.
+Most of the stimulus-drawing is done using an OpenGL backend
+directly in the screen buffer.
+While this approach is very fast and accurate,
+its difficult to create complex stimulus images, such as the ones implemented in `stimupy`.
+For these kinds of stimuli,
+PTB can convert a (predefined) digital image from an array to a static texture
+and then display it,
+using the [Screen('MakeTexture')](http://psychtoolbox.org/docs/Screen) function:
+```{code-block} matlab
+% Convert my_stim_array to a PTB texture
+stim_texture = Screen('MakeTexture', my_window, my_stim_array);
+
+% Put texture on screen
+Screen('DrawTexture', my_window, stim_texture);
+```
+
+To use `stimupy` stimuli in PyschoPy experiments then
+requires creating the stimuli in, and exporting them from Python,
+and importing them into MATLAB for display:
+
+1. Generate the `stim` using `stimupy` code (Python)
+2. [Export the `stim`, e.g. to `.mat` file](./export.md) (Python)
+3. Set up experiment using `PTB` (MATLAB)
+   1. Set up the screen/window information
+4. Import the `stim` in MATLAB, e.g.:
+    ```{code-block} matlab
+    % Load from .mat file into a struct
+    my_stim = load('path_to_my_stim_file.mat');
+    ```
+5. Create the PTB texture from the `img`-array:
+    ```{code-block} matlab
+    % Convert my_stim_array to a PTB texture
+    stim_texture = Screen('MakeTexture', my_window, my_stim.img);
+    ```
+6. Display the texture in the PTB experiment
\ No newline at end of file
diff --git a/docs/howtos/export.md b/docs/howtos/export.md
new file mode 100644
index 00000000..03269aff
--- /dev/null
+++ b/docs/howtos/export.md
@@ -0,0 +1,87 @@
+# Save & export stimuli
+
+Stimuli created with `stimupy` should not be locked in
+to any specific programming environment. 
+As such, you can save and export stimuli to various formats.
+Some formats support only saving the image-array
+without any metadata --
+others support saving the whole stimulus-`dict`.
+
+```{contents}
+```
+## Fileformats
+
+### Python `.pickle`
+The Python standard library provides functionality for
+saving Python objects to a binary file format called {py:mod}`pickle`.
+This format is useful for quick, short-term storage of data,
+although [there are some concerns](https://nedbatchelder.com/blog/202006/pickles_nine_flaws.html)
+about using it for long-term storage and sharing.
+`stimupy` stimuli can be saved to this format,
+either the whole stimulus-dict ({py:func}`stimupy.utils.export.to_pickle`)
+or just the bare image-array ({py:func}`stimupy.utils.export.array_to_pickle`).
+
+### Numpy `.npy`
+The stimulus image-information of `stimupy` stimuli
+are stored in {py:obj}`numpy.ndarray`s.
+NumPy provides an efficient fileformat for saving these arrays:
+[`.npy`](https://numpy.org/doc/stable/user/absolute_beginners.html#how-to-save-and-load-numpy-objects).
+`stimupy` stimuli can be saved to this format,
+but only the bare image-array ({py:func}`stimupy.utils.export.array_to_npy`).
+
+### JSON `.json`
+[JavaScript Object Notation (JSON) files](https://www.wikiwand.com/en/JSON)
+are a human- and machine-readable plain-text structured fileformat.
+It is particularly well-suited for storing Python data structures
+since JSON syntax is very similar to Python syntax for lists and dicts.
+`stimupy` stimuli can be saved to this format,
+as the whole stimulus-dict ({py:func}`stimupy.utils.export.to_json`)
+
+### MATLAB `.mat`
+To use stimuli in a MATLAB environment,
+e.g., in a PsychToolBox-based experiment,
+or a computational model that is (only) implemented in MATLAB,
+the stimuli have to be exported to a format that MATLAB can interpret.
+MATLAB does provide (some) support for JSON files
+but `stimupy` can also export directly to MATLAB's native `.mat` fileformat.
+`stimupy` stimuli can be saved to this format,
+either the whole stimulus-dict ({py:func}`stimupy.utils.export.to_mat`)
+or just the bare image-array ({py:func}`stimupy.utils.export.array_to_mat`).
+
+### Image formats
+For many uses, it can be convenient to just save the stimulus-array (or the mask) as an image.
+Hence, `stimupy` provides a function to save/export bare image-arrays to common image formats (`.png`, `.jpeg`, etc).
+Alternatively, there exist multiple image-processing libraries for Python which allow for further image processing and exporting,
+e.g., [Pillow/PIL](https://pillow.readthedocs.io/en/stable/)
+or [OpenCV](https://github.com/opencv/opencv-python)
+
+
+## Exporting only parameters
+In some cases, you may wish to save/export only the stimulus parameters
+-- for a given version of `stimupy`,
+these parameters should reproduce the exact same stimulus everytime.
+For stimuli created using just `stimupy` functions,
+the output stimulus-`dict` contains not just the `img`-array
+but also all parameters for creating that stimulus.
+Thus, taking the `img` (and any `mask`s) out of this dict
+creates a `dict` with just the parameters.
+Since `dict`s can be _expanded_ in function calls,
+this parameter-`dict` can then be used to recreate the stimulus:
+```{code-block}
+# Create a stimulus-dict
+stim = stimuli.some_stim_func(...)
+
+# 'strip' the dict of any key-value pairs that are not parameters to the given function
+stim_params = utils.strip_dict(stim, stimuli.some_stim_func)
+
+# reproduce the stimulus-dict by expanding stim_params
+# reproduced_stim should be identical to stim
+reproduced_stim = stimuli.some_stim_func(**stim_params)
+```
+
+To share only parameter values of an existing `stimupy` function
+we recommend this approach of `strip`ping the `dict`
+and then exporting this `strip`ed `dict` of params
+to a `.json` file.
+
+%% SOMETHING ABOUT HASHING IMGS
\ No newline at end of file
diff --git a/docs/howtos/howtos.md b/docs/howtos/howtos.md
new file mode 100644
index 00000000..e5af0c52
--- /dev/null
+++ b/docs/howtos/howtos.md
@@ -0,0 +1,9 @@
+# How-to... (recipes)
+
+```{toctree}
+:titlesonly:
+
+display
+export
+share
+```
\ No newline at end of file
diff --git a/docs/howtos/share.md b/docs/howtos/share.md
new file mode 100644
index 00000000..fb797e10
--- /dev/null
+++ b/docs/howtos/share.md
@@ -0,0 +1,143 @@
+# Share & distribute stimuli
+A core principle of `stimupy` is sharing and reusing stimuli.
+One part of this endeavor is the large number of stimuli that are already implemented,
+such that you can generate various parameterizations of these
+without having to write any custom code.
+Besides this, we are hoping that people who use `stimupy` for their stimulus generation
+want to share their stimulus implementations with other `stimupy`-users.
+
+## What to share?
+To accurately reproduce a stimulus,
+a complete description of the parameters (size, geometry, radiometry)
+should be enough.
+However, in developing `stimupy` it has been our experience that
+such descriptions are often incomplete and parameters can be easily overlooked.
+
+Another option is to share the exact digital image (i.e., the matrix of pixel values)
+of the stimulus.
+While this allows that exact digital image to be reused,
+any modifications may be hard to implement --
+especially changing the [resolution](../topic_guides/resolution.md).
+Moreover, sets of digital images can take up quite some storage space,
+and are not ideal for easy and quick transfer.
+
+Instead, we believe that the _code_ underlying stimulus creation should be shared,
+alongside the parameters used to call that code.
+The combination of the same exact code plus the same parameters should *always*
+produce the exact same digital image.
+Thus, you can share the code and parameters,
+and a recipient can reproduce your exact stimulus.
+
+This is why we distribute `stimupy`:
+it contains lots of (generalized) code to generate lots of (parameterizable) stimuli.
+When you use this to generate specific stimuli in your own work
+there are several ways to share the require code and parameters,
+depending on whether you "just" used specific parameterization of a function in `stimupy`,
+or added some custom code (e.g., composition).
+
+````{important}
+In general, the following should be communicated / shared:
+
+0. Platform and environment information
+  - Operating System (and version)
+  - Python version
+  - versions of all other requisite packages:
+    A full list of installed Python packages and their version numbers can be generated
+    using (from outside Python):
+    ```{code-block} bash
+    pip freeze
+    ```
+
+1. exact `stimupy` version
+   This version can be found using (from within Python):
+   ```{code-block} python
+   >>> import stimupy
+   >>> stimupy.__version__
+   '1.0.0'
+   >>>
+   ```
+2. parameters for stimupy-stimuli
+3. any custom code, e.g. for composition
+````
+
+## Existing stimulus, specific parameterization
+
+If your work uses a stimulus as it is implemented in `stimupy`,
+e.g., some {py:func}`gabor <stimupy.stimuli.gabors.gabor>`
+or some parameterization of the {py:func}`Todorovic illusion <stimupy.stimuli.todorovics.cross>`,
+without any additional customization / custom code (e.g., composition),
+you only need to share the parameter-values for this stimulus.
+All the code necessary to reproduce the stimulus from those values
+is included in `stimupy`.
+Thus, you'll need to:
+
+- indicate which exact version of `stimupy` you used.
+  A recipient can then install this version of `stimupy`
+  and reproduce the exact stimulus from the provide parameter values.
+
+- provide all parameter-values for this stimulus, ideally by [exporting them](export.md).
+  For just sharing parameter-values, we recommend exporting the stimulus-`dict`
+  without the `img` and `masks` to a [JSON file](./export.md).
+  The reason for this is that JSON files are both human-readable --
+  so that a non-stimupy / non-Python recipient can still evaluate the parameter values --
+  and safely machine-readable -- so that a Python / stimupy user can easily load and recreate the stimulus.
+
+%% SOMETHING ABOUT HASHING
+
+## Custom stimulus
+If your stimulus is not just a parameterization of an existing `stimupy` function
+and contains some custom code (e.g., composition),
+you will also need to distribute your custom code.
+This can be done in several ways.
+
+If you are already sharing all the code
+underlying the rest of your experiment or project (whether in PsychoPy or otherwise),
+you can simply include your custom stimulus code in this as well.
+You should also indicate which exact version of `stimupy` was used,
+which is then an exactly versioned dependency of your code.
+
+In this case, it is probably clearest to _modularize_
+your (custom) stimulus-generating code by putting it in a separate code file,
+e.g. `my_stimulus.py` (or `my_stim.m` for MATLAB)
+-- which then gets used in the main experiment / project code.
+This modularization of the stimulus makes it so that changes to the stimulus
+can be isolated from changes to the rest of the project.
+Additionally, if you or someone else wants to use the same stimulus
+in a different experiment/project,
+only that file needs to be shared (together with versioning and parameterization info).
+
+If you are _not_ already sharing all the code
+underlying the rest of your experiment or project,
+firstly we highly encourage you to do so, in the spirit of the Open and FAIR science
+that `stimupy` tries to contribute to.
+However, if reasons preclude sharing the whole code
+but you still wish to share & distribute your stimuli,
+we also recommend modularizing your stimulus-generating code into a separate file.
+This can then be shared, together with versioning and parameterization info.
+
+```{important}
+`stimupy` is focused on stimulus generation, not on code or data distribution.
+If you wish to distribute your parameter-values and/or custom stimulus code,
+we recommend code- and data-sharing platforms
+such as [GitHub](https://www.github.com), [FigShare](https://www.figshare.com), and the [Open Science Framework](https://www.OSF.io)
+```
+
+
+## Stimulus-sets
+`stimupy` contains both a wide range of generalized [stimulus-functions](stimupy.stimuli)
+that can generate many parameterizated stimulus images.
+Also included are several complete sets of stimuli
+that all come from the same {py:mod}`paper <stimupy.papers>` (/ source).
+The ones currently included span wide range of the stimuli implemented in `stimupy`
+and have formed both the inspiration and a testing benchmark for development.
+
+Development, organization and maintanance of these sets can be arduous,
+and we are committed to providing a high-quality and well-tested suite of functions.
+Thus, `stimupy` is a curated and maintained _library_ of stimuli
+(stimulus-functions, to be precise),
+not a _repository_ or platform for uploading stimuli.
+We welcome contributions to this library of stimuli,
+and stimulus sets, from the literature.
+If you wish to contribute such a complete set of stimuli from a single paper,
+please see our [contributing guide](../contributing/contribute.md).
+
diff --git a/docs/reference/demos/components/angulars.md b/docs/reference/demos/components/angulars.md
index e016684e..097e6b8a 100644
--- a/docs/reference/demos/components/angulars.md
+++ b/docs/reference/demos/components/angulars.md
@@ -12,26 +12,27 @@ kernelspec:
   name: python3
 ---
 
-```{important}
+```{tip}
 [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/components/angulars.md)
  to get interactivity
 ```
+```{attention}
+To run locally, the code for these interactive demos requires
+a [Jupyter Notebook](https://jupyter.org/) environment,
+and the [Jupyter Widgets extension (`ipywidgets`)](https://ipywidgets.readthedocs.io/en/latest/index.html).
+```
 
 # Components - Angulars
 {py:mod}`stimupy.components.angulars`
 
-```{code-cell} ipython3
-:tags: [remove-cell]
 
-import IPython
-import ipywidgets as iw
-from stimupy.utils import plot_stim
-```
 
 ## Wedge
 {py:func}`stimupy.components.angulars.wedge`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.components.angulars import wedge
 
 # Define widgets
@@ -73,18 +74,21 @@ def show_wedge(
     origin=None,
     add_mask=False,
 ):
-    stim = wedge(
-        visual_size=(height, width),
-        ppd=ppd,
-        angle=wwidth,
-        radius=radius,
-        rotation=rotation,
-        inner_radius=inner_radius,
-        intensity_wedge=intensity_wedge,
-        intensity_background=intensity_background,
-        origin=origin,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = wedge(
+            visual_size=(height, width),
+            ppd=ppd,
+            angle=wwidth,
+            radius=radius,
+            rotation=rotation,
+            inner_radius=inner_radius,
+            intensity_wedge=intensity_wedge,
+            intensity_background=intensity_background,
+            origin=origin,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -112,6 +116,8 @@ display(ui, out)
 {py:func}`stimupy.components.angulars.segments`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.components.angulars import segments
 
 # Define widgets
@@ -158,16 +164,19 @@ def show_segments(
     origin=None,
     add_mask=False,
 ):
-    stim = segments(
-        visual_size=(height, width),
-        ppd=ppd,
-        angles=(wwidth1, wwidth2, wwidth3),
-        rotation=rotation,
-        intensity_segments=(int1, int2, int3),
-        intensity_background=intensity_background,
-        origin=origin,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = segments(
+            visual_size=(height, width),
+            ppd=ppd,
+            angles=(wwidth1, wwidth2, wwidth3),
+            rotation=rotation,
+            intensity_segments=(int1, int2, int3),
+            intensity_background=intensity_background,
+            origin=origin,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
diff --git a/docs/reference/demos/components/edges.md b/docs/reference/demos/components/edges.md
index 4d5dcd35..6d530b31 100644
--- a/docs/reference/demos/components/edges.md
+++ b/docs/reference/demos/components/edges.md
@@ -12,26 +12,27 @@ kernelspec:
   name: python3
 ---
 
-```{important}
+```{tip}
 [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/components/edges.md)
  to get interactivity
 ```
+```{attention}
+To run locally, the code for these interactive demos requires
+a [Jupyter Notebook](https://jupyter.org/) environment,
+and the [Jupyter Widgets extension (`ipywidgets`)](https://ipywidgets.readthedocs.io/en/latest/index.html).
+```
 
 # Components - Edges
 {py:mod}`stimupy.components.edges`
 
-```{code-cell} ipython3
-:tags: [remove-cell]
 
-import IPython
-import ipywidgets as iw
-from stimupy.utils import plot_stim
-```
 
 ## Step edge
 {py:func}`stimupy.components.edges.step`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.components.edges import step
 
 # Define widgets
@@ -61,14 +62,16 @@ def show_step_edge(
     intensity2=None,
     add_mask=False,
 ):
-
-    stim = step(
-        visual_size=(height, width),
-        ppd=ppd,
-        rotation=rotation,
-        intensity_edges=(intensity1, intensity2),
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = step(
+            visual_size=(height, width),
+            ppd=ppd,
+            rotation=rotation,
+            intensity_edges=(intensity1, intensity2),
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -92,6 +95,8 @@ display(ui, out)
 {py:func}`stimupy.components.edges.gaussian`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.components.edges import gaussian
 
 # Define widgets
@@ -126,16 +131,18 @@ def show_gaussian_edge(
     intensity_background=None,
     add_mask=False,
 ):
-
-    stim = gaussian(
-        visual_size=(height, width),
-        ppd=ppd,
-        sigma=sigma,
-        rotation=rotation,
-        intensity_edges=(intensity1, intensity2),
-        intensity_background=intensity_background,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = gaussian(
+            visual_size=(height, width),
+            ppd=ppd,
+            sigma=sigma,
+            rotation=rotation,
+            intensity_edges=(intensity1, intensity2),
+            intensity_background=intensity_background,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -161,6 +168,8 @@ display(ui, out)
 {py:func}`stimupy.components.edges.cornsweet`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.components.edges import cornsweet
 
 # Define widgets
@@ -197,17 +206,19 @@ def show_cornsweet_edge(
     intensity_plateau=None,
     add_mask=False,
 ):
-
-    stim = cornsweet(
-        visual_size=(height, width),
-        ppd=ppd,
-        ramp_width=ramp_width,
-        exponent=exponent,
-        rotation=rotation,
-        intensity_edges=(intensity1, intensity2),
-        intensity_plateau=intensity_plateau,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = cornsweet(
+            visual_size=(height, width),
+            ppd=ppd,
+            ramp_width=ramp_width,
+            exponent=exponent,
+            rotation=rotation,
+            intensity_edges=(intensity1, intensity2),
+            intensity_plateau=intensity_plateau,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
diff --git a/docs/reference/demos/components/frames.md b/docs/reference/demos/components/frames.md
index 99abc0a9..2c12290a 100644
--- a/docs/reference/demos/components/frames.md
+++ b/docs/reference/demos/components/frames.md
@@ -12,26 +12,27 @@ kernelspec:
   name: python3
 ---
 
-```{important}
+```{tip}
 [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/components/frames.md)
  to get interactivity
 ```
+```{attention}
+To run locally, the code for these interactive demos requires
+a [Jupyter Notebook](https://jupyter.org/) environment,
+and the [Jupyter Widgets extension (`ipywidgets`)](https://ipywidgets.readthedocs.io/en/latest/index.html).
+```
 
 # Components - Frames
 {py:mod}`stimupy.components.frames`
 
-```{code-cell} ipython3
-:tags: [remove-cell]
 
-import IPython
-import ipywidgets as iw
-from stimupy.utils import plot_stim
-```
 
 ## Frames
 {py:func}`stimupy.components.frames.frames`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.components.frames import frames
 
 # Define widgets
@@ -73,15 +74,18 @@ def show_frames(
     origin=None,
     add_mask=False,
 ):
-    stim = frames(
-        visual_size=(height, width),
-        ppd=ppd,
-        radii=(radius1, radius2, radius3),
-        intensity_frames=(int1, int2, int3),
-        intensity_background=intensity_background,
-        origin=origin,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = frames(
+            visual_size=(height, width),
+            ppd=ppd,
+            radii=(radius1, radius2, radius3),
+            intensity_frames=(int1, int2, int3),
+            intensity_background=intensity_background,
+            origin=origin,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
diff --git a/docs/reference/demos/components/gaussians.md b/docs/reference/demos/components/gaussians.md
index 602830c7..1b139ea1 100644
--- a/docs/reference/demos/components/gaussians.md
+++ b/docs/reference/demos/components/gaussians.md
@@ -12,26 +12,27 @@ kernelspec:
   name: python3
 ---
 
-```{important}
+```{tip}
 [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/components/gaussians.md)
  to get interactivity
 ```
+```{attention}
+To run locally, the code for these interactive demos requires
+a [Jupyter Notebook](https://jupyter.org/) environment,
+and the [Jupyter Widgets extension (`ipywidgets`)](https://ipywidgets.readthedocs.io/en/latest/index.html).
+```
 
 # Components - Gaussians
 {py:mod}`stimupy.components.gaussians`
 
-```{code-cell} ipython3
-:tags: [remove-cell]
 
-import IPython
-import ipywidgets as iw
-from stimupy.utils import plot_stim
-```
 
 ## Gaussian
 {py:func}`stimupy.components.gaussians.gaussian`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.components.gaussians import gaussian
 
 # Define widgets
@@ -66,16 +67,18 @@ def show_gaussian(
     intensity_max=None,
     add_mask=False,
 ):
-
-    stim = gaussian(
-        visual_size=(height, width),
-        ppd=ppd,
-        sigma=(sigma1, sigma2),
-        origin=origin,
-        rotation=rotation,
-        intensity_max=intensity_max,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = gaussian(
+            visual_size=(height, width),
+            ppd=ppd,
+            sigma=(sigma1, sigma2),
+            origin=origin,
+            rotation=rotation,
+            intensity_max=intensity_max,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
diff --git a/docs/reference/demos/components/lines.md b/docs/reference/demos/components/lines.md
index 6d5d9aed..3d32435c 100644
--- a/docs/reference/demos/components/lines.md
+++ b/docs/reference/demos/components/lines.md
@@ -12,26 +12,27 @@ kernelspec:
   name: python3
 ---
 
-```{important}
+```{tip}
 [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/components/lines.md)
  to get interactivity
 ```
+```{attention}
+To run locally, the code for these interactive demos requires
+a [Jupyter Notebook](https://jupyter.org/) environment,
+and the [Jupyter Widgets extension (`ipywidgets`)](https://ipywidgets.readthedocs.io/en/latest/index.html).
+```
 
 # Components - Lines
 {py:mod}`stimupy.components.lines`
 
-```{code-cell} ipython3
-:tags: [remove-cell]
 
-import IPython
-import ipywidgets as iw
-from stimupy.utils import plot_stim
-```
 
 ## Line
 {py:func}`stimupy.components.lines.line`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.components.lines import line
 
 # Define widgets
@@ -75,19 +76,21 @@ def show_line(
     add_mask=False,
     origin=None,
 ):
-
-    stim = line(
-        visual_size=(height, width),
-        ppd=ppd,
-        line_position=(ypos, xpos),
-        line_length=line_length,
-        line_width=line_width,
-        rotation=rotation,
-        intensity_line=intensity_line,
-        intensity_background=intensity_background,
-        origin=origin,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = line(
+            visual_size=(height, width),
+            ppd=ppd,
+            line_position=(ypos, xpos),
+            line_length=line_length,
+            line_width=line_width,
+            rotation=rotation,
+            intensity_line=intensity_line,
+            intensity_background=intensity_background,
+            origin=origin,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None    
 
 # Set interactivity
 out = iw.interactive_output(
@@ -116,6 +119,8 @@ display(ui, out)
 {py:func}`stimupy.components.lines.dipole`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.components.lines import dipole
 
 # Define widgets
@@ -152,17 +157,19 @@ def show_dipole(
     int2=None,
     add_mask=False,
 ):
-
-    stim = dipole(
-        visual_size=(height, width),
-        ppd=ppd,
-        line_length=line_length,
-        line_width=line_width,
-        line_gap=line_gap,
-        rotation=rotation,
-        intensity_lines=(int1, int2),
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = dipole(
+            visual_size=(height, width),
+            ppd=ppd,
+            line_length=line_length,
+            line_width=line_width,
+            line_gap=line_gap,
+            rotation=rotation,
+            intensity_lines=(int1, int2),
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -189,6 +196,8 @@ display(ui, out)
 {py:func}`stimupy.components.lines.ellipse`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.components.lines import ellipse
 
 # Define widgets
@@ -223,16 +232,18 @@ def show_ellipse(
     intensity_background=None,
     add_mask=False,
 ):
-
-    stim = ellipse(
-        visual_size=(height, width),
-        ppd=ppd,
-        radius=(rad1, rad2),
-        line_width=line_width,
-        intensity_line=intensity_line,
-        intensity_background=intensity_background,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = ellipse(
+            visual_size=(height, width),
+            ppd=ppd,
+            radius=(rad1, rad2),
+            line_width=line_width,
+            intensity_line=intensity_line,
+            intensity_background=intensity_background,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -258,6 +269,8 @@ display(ui, out)
 {py:func}`stimupy.components.lines.circle`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.components.lines import circle
 
 # Define widgets
@@ -290,16 +303,18 @@ def show_circle(
     intensity_background=None,
     add_mask=False,
 ):
-
-    stim = circle(
-        visual_size=(height, width),
-        ppd=ppd,
-        radius=radius,
-        line_width=line_width,
-        intensity_line=intensity_line,
-        intensity_background=intensity_background,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = circle(
+            visual_size=(height, width),
+            ppd=ppd,
+            radius=radius,
+            line_width=line_width,
+            intensity_line=intensity_line,
+            intensity_background=intensity_background,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
diff --git a/docs/reference/demos/components/radials.md b/docs/reference/demos/components/radials.md
index 0a1d6b2c..f4def908 100644
--- a/docs/reference/demos/components/radials.md
+++ b/docs/reference/demos/components/radials.md
@@ -12,26 +12,27 @@ kernelspec:
   name: python3
 ---
 
-```{important}
+```{tip}
 [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/components/radials.md)
  to get interactivity
 ```
+```{attention}
+To run locally, the code for these interactive demos requires
+a [Jupyter Notebook](https://jupyter.org/) environment,
+and the [Jupyter Widgets extension (`ipywidgets`)](https://ipywidgets.readthedocs.io/en/latest/index.html).
+```
 
 # Components - Radials
 {py:mod}`stimupy.components.radials`
 
-```{code-cell} ipython3
-:tags: [remove-cell]
 
-import IPython
-import ipywidgets as iw
-from stimupy.utils import plot_stim
-```
 
 ## Disc
 {py:func}`stimupy.components.radials.disc`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.components.radials import disc
 
 # Define widgets
@@ -63,15 +64,18 @@ def show_disc(
     origin=None,
     add_mask=False,
 ):
-    stim = disc(
-        visual_size=(height, width),
-        ppd=ppd,
-        radius=radius,
-        intensity_disc=intensity_disc,
-        intensity_background=intensity_background,
-        origin=origin,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = disc(
+            visual_size=(height, width),
+            ppd=ppd,
+            radius=radius,
+            intensity_disc=intensity_disc,
+            intensity_background=intensity_background,
+            origin=origin,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -96,6 +100,8 @@ display(ui, out)
 {py:func}`stimupy.components.radials.annulus`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.components.radials import annulus
 
 # Define widgets
@@ -131,15 +137,18 @@ def show_annulus(
     origin=None,
     add_mask=False,
 ):
-    stim = annulus(
-        visual_size=(height, width),
-        ppd=ppd,
-        radii=(radius1, radius2),
-        intensity_ring=intensity_ring,
-        intensity_background=intensity_background,
-        origin=origin,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = annulus(
+            visual_size=(height, width),
+            ppd=ppd,
+            radii=(radius1, radius2),
+            intensity_ring=intensity_ring,
+            intensity_background=intensity_background,
+            origin=origin,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -166,6 +175,8 @@ display(ui, out)
 {py:func}`stimupy.components.radials.rings`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.components.radials import rings
 
 # Define widgets
@@ -207,15 +218,18 @@ def show_rings(
     origin=None,
     add_mask=False,
 ):
-    stim = rings(
-        visual_size=(height, width),
-        ppd=ppd,
-        radii=(radius1, radius2, radius3),
-        intensity_rings=(int1, int2, int3),
-        intensity_background=intensity_background,
-        origin=origin,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = rings(
+            visual_size=(height, width),
+            ppd=ppd,
+            radii=(radius1, radius2, radius3),
+            intensity_rings=(int1, int2, int3),
+            intensity_background=intensity_background,
+            origin=origin,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
diff --git a/docs/reference/demos/components/shapes.md b/docs/reference/demos/components/shapes.md
index 0517b469..ecaa646d 100644
--- a/docs/reference/demos/components/shapes.md
+++ b/docs/reference/demos/components/shapes.md
@@ -12,27 +12,28 @@ kernelspec:
   name: python3
 ---
 
-```{important}
+```{tip}
 [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/components/shapes.md)
  to get interactivity
 ```
+```{attention}
+To run locally, the code for these interactive demos requires
+a [Jupyter Notebook](https://jupyter.org/) environment,
+and the [Jupyter Widgets extension (`ipywidgets`)](https://ipywidgets.readthedocs.io/en/latest/index.html).
+```
 
 # Components - Shapes
 {py:mod}`stimupy.components.shapes`
 
-```{code-cell} ipython3
-:tags: [remove-cell]
 
-import IPython
-import ipywidgets as iw
-from stimupy.utils import plot_stim
-```
 
 ## Rectangle
 {py:func}`stimupy.components.shapes.rectangle`
 
 ```{code-cell} ipython3
-:tags: [hide-input]
+import ipywidgets as iw
+from stimupy.utils import plot_stim
+
 
 from stimupy.components.shapes import rectangle
 
@@ -74,16 +75,19 @@ def show_rect(
     intensity_background=None,
     add_mask=False,
 ):
-    stim = rectangle(
-        visual_size=(height, width),
-        ppd=ppd,
-        rectangle_size=(rect_height, rect_width),
-        rectangle_position=(pos_y, pos_x),
-        rotation=rotation,
-        intensity_rectangle=intensity_rectangle,
-        intensity_background=intensity_background,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = rectangle(
+            visual_size=(height, width),
+            ppd=ppd,
+            rectangle_size=(rect_height, rect_width),
+            rectangle_position=(pos_y, pos_x),
+            rotation=rotation,
+            intensity_rectangle=intensity_rectangle,
+            intensity_background=intensity_background,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -111,7 +115,9 @@ display(ui, out)
 {py:func}`stimupy.components.shapes.triangle`
 
 ```{code-cell} ipython3
-:tags: [hide-input]
+import ipywidgets as iw
+from stimupy.utils import plot_stim
+
 
 from stimupy.components.shapes import triangle
 
@@ -150,16 +156,19 @@ def show_triangle(
     include_corners=None,
     add_mask=False,
 ):
-    stim = triangle(
-        visual_size=(height, width),
-        ppd=ppd,
-        rotation=rotation,
-        triangle_size=(triangle_height, triangle_width),
-        intensity_triangle=intensity_triangle,
-        intensity_background=intensity_background,
-        include_corners=include_corners,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = triangle(
+            visual_size=(height, width),
+            ppd=ppd,
+            rotation=rotation,
+            triangle_size=(triangle_height, triangle_width),
+            intensity_triangle=intensity_triangle,
+            intensity_background=intensity_background,
+            include_corners=include_corners,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -186,7 +195,9 @@ display(ui, out)
 {py:func}`stimupy.components.shapes.cross`
 
 ```{code-cell} ipython3
-:tags: [hide-input]
+import ipywidgets as iw
+from stimupy.utils import plot_stim
+
 
 from stimupy.components.shapes import cross
 
@@ -230,17 +241,20 @@ def show_cross(
     intensity_background=None,
     add_mask=False,
 ):
-    stim = cross(
-        visual_size=(height, width),
-        ppd=ppd,
-        rotation=rotation,
-        cross_size=(cross_height, cross_width),
-        cross_thickness=thickness,
-        cross_arm_ratios=(ratio1, ratio2),
-        intensity_cross=intensity_cross,
-        intensity_background=intensity_background,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = cross(
+            visual_size=(height, width),
+            ppd=ppd,
+            rotation=rotation,
+            cross_size=(cross_height, cross_width),
+            cross_thickness=thickness,
+            cross_arm_ratios=(ratio1, ratio2),
+            intensity_cross=intensity_cross,
+            intensity_background=intensity_background,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -269,7 +283,9 @@ display(ui, out)
 {py:func}`stimupy.components.shapes.parallelogram`
 
 ```{code-cell} ipython3
-:tags: [hide-input]
+import ipywidgets as iw
+from stimupy.utils import plot_stim
+
 
 from stimupy.components.shapes import parallelogram
 
@@ -307,15 +323,18 @@ def show_parallelogram(
     intensity_background=None,
     add_mask=False,
 ):
-    stim = parallelogram(
-        visual_size=(height, width),
-        ppd=ppd,
-        rotation=rotation,
-        parallelogram_size=(p_height, p_width, p_depth),
-        intensity_parallelogram=intensity_parallelogram,
-        intensity_background=intensity_background,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = parallelogram(
+            visual_size=(height, width),
+            ppd=ppd,
+            rotation=rotation,
+            parallelogram_size=(p_height, p_width, p_depth),
+            intensity_parallelogram=intensity_parallelogram,
+            intensity_background=intensity_background,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -342,6 +361,8 @@ display(ui, out)
 {py:func}`stimupy.components.shapes.ellipse`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.components.shapes import ellipse
 
 # Define widgets
@@ -379,16 +400,19 @@ def show_ellipse(
     origin=None,
     add_mask=False,
 ):
-    stim = ellipse(
-        visual_size=(height, width),
-        ppd=ppd,
-        rotation=rotation,
-        radius=(radius1, radius2),
-        intensity_ellipse=intensity_ellipse,
-        intensity_background=intensity_background,
-        origin=origin,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = ellipse(
+            visual_size=(height, width),
+            ppd=ppd,
+            rotation=rotation,
+            radius=(radius1, radius2),
+            intensity_ellipse=intensity_ellipse,
+            intensity_background=intensity_background,
+            origin=origin,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -415,6 +439,8 @@ display(ui, out)
 {py:func}`stimupy.components.shapes.circle`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.components.shapes import circle
 
 # Define widgets
@@ -447,15 +473,18 @@ def show_circle(
     origin=None,
     add_mask=False,
 ):
-    stim = circle(
-        visual_size=(height, width),
-        ppd=ppd,
-        radius=radius,
-        intensity_circle=intensity_circle,
-        intensity_background=intensity_background,
-        origin=origin,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = circle(
+            visual_size=(height, width),
+            ppd=ppd,
+            radius=radius,
+            intensity_circle=intensity_circle,
+            intensity_background=intensity_background,
+            origin=origin,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -480,6 +509,8 @@ display(ui, out)
 {py:func}`stimupy.components.shapes.wedge`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.components.shapes import wedge
 
 # Define widgets
@@ -519,18 +550,21 @@ def show_wedge(
     origin=None,
     add_mask=False,
 ):
-    stim = wedge(
-        visual_size=(height, width),
-        ppd=ppd,
-        angle=wwidth,
-        radius=radius,
-        rotation=rotation,
-        inner_radius=inner_radius,
-        intensity_wedge=intensity_wedge,
-        intensity_background=intensity_background,
-        origin=origin,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = wedge(
+            visual_size=(height, width),
+            ppd=ppd,
+            angle=wwidth,
+            radius=radius,
+            rotation=rotation,
+            inner_radius=inner_radius,
+            intensity_wedge=intensity_wedge,
+            intensity_background=intensity_background,
+            origin=origin,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -558,6 +592,8 @@ display(ui, out)
 {py:func}`stimupy.components.shapes.disc`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.components.shapes import disc
 
 # Define widgets
@@ -589,15 +625,18 @@ def show_disc(
     origin=None,
     add_mask=False,
 ):
-    stim = disc(
-        visual_size=(height, width),
-        ppd=ppd,
-        radius=radius,
-        intensity_disc=intensity_disc,
-        intensity_background=intensity_background,
-        origin=origin,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = disc(
+            visual_size=(height, width),
+            ppd=ppd,
+            radius=radius,
+            intensity_disc=intensity_disc,
+            intensity_background=intensity_background,
+            origin=origin,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -622,6 +661,8 @@ display(ui, out)
 {py:func}`stimupy.components.shapes.annulus`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.components.shapes import annulus
 
 # Define widgets
@@ -657,15 +698,18 @@ def show_annulus(
     origin=None,
     add_mask=False,
 ):
-    stim = annulus(
-        visual_size=(height, width),
-        ppd=ppd,
-        radii=(radius1, radius2),
-        intensity_ring=intensity_ring,
-        intensity_background=intensity_background,
-        origin=origin,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = annulus(
+            visual_size=(height, width),
+            ppd=ppd,
+            radii=(radius1, radius2),
+            intensity_ring=intensity_ring,
+            intensity_background=intensity_background,
+            origin=origin,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -691,6 +735,8 @@ display(ui, out)
 {py:func}`stimupy.components.shapes.ring`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.components.shapes import ring
 
 # Define widgets
@@ -726,15 +772,18 @@ def show_ring(
     origin=None,
     add_mask=False,
 ):
-    stim = annulus(
-        visual_size=(height, width),
-        ppd=ppd,
-        radii=(radius1, radius2),
-        intensity_ring=intensity_ring,
-        intensity_background=intensity_background,
-        origin=origin,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = annulus(
+            visual_size=(height, width),
+            ppd=ppd,
+            radii=(radius1, radius2),
+            intensity_ring=intensity_ring,
+            intensity_background=intensity_background,
+            origin=origin,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
diff --git a/docs/reference/demos/components/waves.md b/docs/reference/demos/components/waves.md
index 1c5c4b02..9f089e5d 100644
--- a/docs/reference/demos/components/waves.md
+++ b/docs/reference/demos/components/waves.md
@@ -12,26 +12,27 @@ kernelspec:
   name: python3
 ---
 
-```{important}
+```{tip}
 [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/components/waves.md)
  to get interactivity
 ```
+```{attention}
+To run locally, the code for these interactive demos requires
+a [Jupyter Notebook](https://jupyter.org/) environment,
+and the [Jupyter Widgets extension (`ipywidgets`)](https://ipywidgets.readthedocs.io/en/latest/index.html).
+```
 
 # Components - Waves
 {py:mod}`stimupy.components.waves`
 
-```{code-cell} ipython3
-:tags: [remove-cell]
 
-import IPython
-import ipywidgets as iw
-from stimupy.utils import plot_stim
-```
 
 ## Sinewave
 {py:func}`stimupy.components.waves.sine`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.components.waves import sine
 
 # Define widgets
@@ -75,19 +76,22 @@ def show_sine(
     period=None,
     add_mask=False,
 ):
-    stim = sine(
-        visual_size=(height, width),
-        ppd=ppd,
-        distance_metric=distance_metric,
-        rotation=rotation,
-        frequency=frequency,
-        phase_shift=phase_shift,
-        intensities=(int1, int2),
-        origin=origin,
-        round_phase_width=round_phase_width,
-        period=period,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = sine(
+            visual_size=(height, width),
+            ppd=ppd,
+            distance_metric=distance_metric,
+            rotation=rotation,
+            frequency=frequency,
+            phase_shift=phase_shift,
+            intensities=(int1, int2),
+            origin=origin,
+            round_phase_width=round_phase_width,
+            period=period,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -117,6 +121,8 @@ display(ui, out)
 {py:func}`stimupy.components.waves.square`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.components.waves import square
 
 # Define widgets
@@ -160,19 +166,22 @@ def show_square(
     period=None,
     add_mask=False,
 ):
-    stim = square(
-        visual_size=(height, width),
-        ppd=ppd,
-        distance_metric=distance_metric,
-        rotation=rotation,
-        frequency=frequency,
-        phase_shift=phase_shift,
-        intensities=(int1, int2),
-        origin=origin,
-        round_phase_width=round_phase_width,
-        period=period,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = square(
+            visual_size=(height, width),
+            ppd=ppd,
+            distance_metric=distance_metric,
+            rotation=rotation,
+            frequency=frequency,
+            phase_shift=phase_shift,
+            intensities=(int1, int2),
+            origin=origin,
+            round_phase_width=round_phase_width,
+            period=period,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -202,6 +211,8 @@ display(ui, out)
 {py:func}`stimupy.components.waves.staircase`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.components.waves import staircase
 
 # Define widgets
@@ -245,19 +256,22 @@ def show_staircase(
     add_mask=False,
     origin=None,
 ):
-    stim = staircase(
-        visual_size=(height, width),
-        ppd=ppd,
-        distance_metric=distance_metric,
-        rotation=rotation,
-        frequency=frequency,
-        phase_shift=phase_shift,
-        intensities=(int1, int2),
-        round_phase_width=round_phase_width,
-        period=period,
-        origin=origin,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = staircase(
+            visual_size=(height, width),
+            ppd=ppd,
+            distance_metric=distance_metric,
+            rotation=rotation,
+            frequency=frequency,
+            phase_shift=phase_shift,
+            intensities=(int1, int2),
+            round_phase_width=round_phase_width,
+            period=period,
+            origin=origin,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -287,6 +301,8 @@ display(ui, out)
 {py:func}`stimupy.components.waves.bessel`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.components.waves import bessel
 
 # Define widgets
@@ -322,15 +338,18 @@ def show_bessel(
     origin=None,
     add_mask=False,
 ):
-    stim = bessel(
-        visual_size=(height, width),
-        ppd=ppd,
-        frequency=frequency,
-        order=order,
-        intensities=(int1, int2),
-        origin=origin,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = bessel(
+            visual_size=(height, width),
+            ppd=ppd,
+            frequency=frequency,
+            order=order,
+            intensities=(int1, int2),
+            origin=origin,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
diff --git a/docs/reference/demos/noises/binaries.md b/docs/reference/demos/noises/binaries.md
index 5576b8f3..f246d7a4 100644
--- a/docs/reference/demos/noises/binaries.md
+++ b/docs/reference/demos/noises/binaries.md
@@ -12,26 +12,27 @@ kernelspec:
   name: python3
 ---
 
-```{important}
+```{tip}
 [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/noises/binaries.md)
  to get interactivity
 ```
+```{attention}
+To run locally, the code for these interactive demos requires
+a [Jupyter Notebook](https://jupyter.org/) environment,
+and the [Jupyter Widgets extension (`ipywidgets`)](https://ipywidgets.readthedocs.io/en/latest/index.html).
+```
 
 # Noises - Binaries
 {py:mod}`stimupy.noises.binaries`
 
-```{code-cell} ipython3
-:tags: [remove-cell]
 
-import IPython
-import ipywidgets as iw
-from stimupy.utils import plot_stim
-```
 
 ## Binary
 {py:func}`stimupy.noises.binaries.binary`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.noises.binaries import binary
 
 # Define widgets
@@ -55,13 +56,15 @@ def show_binary(
     intensity1=None,
     intensity2=None,
 ):
-
-    stim = binary(
-        visual_size=(height, width),
-        ppd=ppd,
-        intensity_range=(intensity1, intensity2),
-    )
-    plot_stim(stim, mask=False)
+    try:
+        stim = binary(
+            visual_size=(height, width),
+            ppd=ppd,
+            intensity_range=(intensity1, intensity2),
+        )
+        plot_stim(stim, mask=False)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
diff --git a/docs/reference/demos/noises/narrowbands.md b/docs/reference/demos/noises/narrowbands.md
index 971af221..9ec917e4 100644
--- a/docs/reference/demos/noises/narrowbands.md
+++ b/docs/reference/demos/noises/narrowbands.md
@@ -12,26 +12,27 @@ kernelspec:
   name: python3
 ---
 
-```{important}
+```{tip}
 [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/noises/narrowbands.md)
  to get interactivity
 ```
+```{attention}
+To run locally, the code for these interactive demos requires
+a [Jupyter Notebook](https://jupyter.org/) environment,
+and the [Jupyter Widgets extension (`ipywidgets`)](https://ipywidgets.readthedocs.io/en/latest/index.html).
+```
 
 # Noises - Narrowbands
 {py:mod}`stimupy.noises.narrowbands`
 
-```{code-cell} ipython3
-:tags: [remove-cell]
 
-import IPython
-import ipywidgets as iw
-from stimupy.utils import plot_stim
-```
 
 ## Narrowband
 {py:func}`stimupy.noises.narrowbands.narrowband`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.noises.narrowbands import narrowband
 
 # Define widgets
@@ -64,16 +65,18 @@ def show_narrowband(
     intensity2=None,
     pseudo_noise=False,
 ):
-
-    stim = narrowband(
-        visual_size=(height, width),
-        ppd=ppd,
-        intensity_range=(intensity1, intensity2),
-        center_frequency=frequency,
-        bandwidth=bandwidth,
-        pseudo_noise=pseudo_noise,
-    )
-    plot_stim(stim, mask=False)
+    try:
+        stim = narrowband(
+            visual_size=(height, width),
+            ppd=ppd,
+            intensity_range=(intensity1, intensity2),
+            center_frequency=frequency,
+            bandwidth=bandwidth,
+            pseudo_noise=pseudo_noise,
+        )
+        plot_stim(stim, mask=False)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
diff --git a/docs/reference/demos/noises/naturals.md b/docs/reference/demos/noises/naturals.md
index a9b9a676..6816940f 100644
--- a/docs/reference/demos/noises/naturals.md
+++ b/docs/reference/demos/noises/naturals.md
@@ -12,26 +12,27 @@ kernelspec:
   name: python3
 ---
 
-```{important}
+```{tip}
 [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/noises/naturals.md)
  to get interactivity
 ```
+```{attention}
+To run locally, the code for these interactive demos requires
+a [Jupyter Notebook](https://jupyter.org/) environment,
+and the [Jupyter Widgets extension (`ipywidgets`)](https://ipywidgets.readthedocs.io/en/latest/index.html).
+```
 
 # Noises - Naturals
 {py:mod}`stimupy.noises.naturals`
 
-```{code-cell} ipython3
-:tags: [remove-cell]
 
-import IPython
-import ipywidgets as iw
-from stimupy.utils import plot_stim
-```
 
 ## One over f
 {py:func}`stimupy.noises.naturals.one_over_f`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.noises.naturals import one_over_f
 
 # Define widgets
@@ -61,15 +62,17 @@ def show_one_over_f(
     intensity2=None,
     pseudo_noise=False,
 ):
-
-    stim = one_over_f(
-        visual_size=(height, width),
-        ppd=ppd,
-        intensity_range=(intensity1, intensity2),
-        exponent=exponent,
-        pseudo_noise=pseudo_noise,
-    )
-    plot_stim(stim, mask=False)
+    try:
+        stim = one_over_f(
+            visual_size=(height, width),
+            ppd=ppd,
+            intensity_range=(intensity1, intensity2),
+            exponent=exponent,
+            pseudo_noise=pseudo_noise,
+        )
+        plot_stim(stim, mask=False)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -93,6 +96,8 @@ display(ui, out)
 {py:func}`stimupy.noises.naturals.pink`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.noises.naturals import pink
 
 # Define widgets
@@ -119,14 +124,16 @@ def show_pink(
     intensity2=None,
     pseudo_noise=False,
 ):
-
-    stim = pink(
-        visual_size=(height, width),
-        ppd=ppd,
-        intensity_range=(intensity1, intensity2),
-        pseudo_noise=pseudo_noise,
-    )
-    plot_stim(stim, mask=False)
+    try:
+        stim = pink(
+            visual_size=(height, width),
+            ppd=ppd,
+            intensity_range=(intensity1, intensity2),
+            pseudo_noise=pseudo_noise,
+        )
+        plot_stim(stim, mask=False)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -149,6 +156,8 @@ display(ui, out)
 {py:func}`stimupy.noises.naturals.brown`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.noises.naturals import brown
 
 # Define widgets
@@ -175,14 +184,16 @@ def show_brown(
     intensity2=None,
     pseudo_noise=False,
 ):
-
-    stim = brown(
-        visual_size=(height, width),
-        ppd=ppd,
-        intensity_range=(intensity1, intensity2),
-        pseudo_noise=pseudo_noise,
-    )
-    plot_stim(stim, mask=False)
+    try:
+        stim = brown(
+            visual_size=(height, width),
+            ppd=ppd,
+            intensity_range=(intensity1, intensity2),
+            pseudo_noise=pseudo_noise,
+        )
+        plot_stim(stim, mask=False)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
diff --git a/docs/reference/demos/noises/whites.md b/docs/reference/demos/noises/whites.md
index 33ca216f..58dc1034 100644
--- a/docs/reference/demos/noises/whites.md
+++ b/docs/reference/demos/noises/whites.md
@@ -12,26 +12,27 @@ kernelspec:
   name: python3
 ---
 
-```{important}
+```{tip}
 [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/noises/whites.md)
  to get interactivity
 ```
+```{attention}
+To run locally, the code for these interactive demos requires
+a [Jupyter Notebook](https://jupyter.org/) environment,
+and the [Jupyter Widgets extension (`ipywidgets`)](https://ipywidgets.readthedocs.io/en/latest/index.html).
+```
 
 # Noises - Whites
 {py:mod}`stimupy.noises.whites`
 
-```{code-cell} ipython3
-:tags: [remove-cell]
 
-import IPython
-import ipywidgets as iw
-from stimupy.utils import plot_stim
-```
 
 ## White
 {py:func}`stimupy.noises.whites.white`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.noises.whites import white
 
 # Define widgets
@@ -58,14 +59,16 @@ def show_white(
     intensity2=None,
     pseudo_noise=False,
 ):
-
-    stim = white(
-        visual_size=(height, width),
-        ppd=ppd,
-        intensity_range=(intensity1, intensity2),
-        pseudo_noise=pseudo_noise,
-    )
-    plot_stim(stim, mask=False)
+    try:
+        stim = white(
+            visual_size=(height, width),
+            ppd=ppd,
+            intensity_range=(intensity1, intensity2),
+            pseudo_noise=pseudo_noise,
+        )
+        plot_stim(stim, mask=False)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
diff --git a/docs/reference/demos/stimuli/benarys.md b/docs/reference/demos/stimuli/benarys.md
index 8e8a360a..f8fa79d7 100644
--- a/docs/reference/demos/stimuli/benarys.md
+++ b/docs/reference/demos/stimuli/benarys.md
@@ -12,26 +12,27 @@ kernelspec:
   name: python3
 ---
 
-```{important}
+```{tip}
 [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/benarys.md)
  to get interactivity
 ```
+```{attention}
+To run locally, the code for these interactive demos requires
+a [Jupyter Notebook](https://jupyter.org/) environment,
+and the [Jupyter Widgets extension (`ipywidgets`)](https://ipywidgets.readthedocs.io/en/latest/index.html).
+```
 
 # Stimuli - Benary's cross
 {py:mod}`stimupy.stimuli.benarys`
 
-```{code-cell} ipython3
-:tags: [remove-cell]
 
-import IPython
-import ipywidgets as iw
-from stimupy.utils import plot_stim
-```
 
 ## Cross, generalized
 {py:func}`stimupy.stimuli.benarys.cross_generalized`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.benarys import cross_generalized
 
 # Define widgets
@@ -81,20 +82,23 @@ def show_cross_generalized(
     target_rotation=None,
     add_mask=False,
 ):
-    stim = cross_generalized(
-        visual_size=(height, width),
-        ppd=ppd,
-        cross_thickness=cross_thickness,
-        intensity_cross=intensity1,
-        intensity_background=intensity_background,
-        target_size=(target_height, target_width),
-        target_type=target_type,
-        intensity_target=intensity_target,
-        target_x=target_x,
-        target_y=target_y,
-        target_rotation=target_rotation,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = cross_generalized(
+            visual_size=(height, width),
+            ppd=ppd,
+            cross_thickness=cross_thickness,
+            intensity_cross=intensity1,
+            intensity_background=intensity_background,
+            target_size=(target_height, target_width),
+            target_type=target_type,
+            intensity_target=intensity_target,
+            target_x=target_x,
+            target_y=target_y,
+            target_rotation=target_rotation,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -125,6 +129,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.benarys.cross_rectangles`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.benarys import cross_rectangles
 
 # Define widgets
@@ -164,16 +170,19 @@ def show_cross_rectangles(
     intensity_target=None,
     add_mask=False,
 ):
-    stim = cross_rectangles(
-        visual_size=(height, width),
-        ppd=ppd,
-        cross_thickness=cross_thickness,
-        intensity_cross=intensity1,
-        intensity_background=intensity_background,
-        target_size=(target_height, target_width),
-        intensity_target=intensity_target,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = cross_rectangles(
+            visual_size=(height, width),
+            ppd=ppd,
+            cross_thickness=cross_thickness,
+            intensity_cross=intensity1,
+            intensity_background=intensity_background,
+            target_size=(target_height, target_width),
+            intensity_target=intensity_target,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -200,6 +209,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.benarys.cross_triangles`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.benarys import cross_triangles
 
 # Define widgets
@@ -237,16 +248,19 @@ def show_cross_triangles(
     intensity_target=None,
     add_mask=False,
 ):
-    stim = cross_triangles(
-        visual_size=(height, width),
-        ppd=ppd,
-        cross_thickness=cross_thickness,
-        intensity_cross=intensity1,
-        intensity_background=intensity_background,
-        target_size=target_size,
-        intensity_target=intensity_target,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = cross_triangles(
+            visual_size=(height, width),
+            ppd=ppd,
+            cross_thickness=cross_thickness,
+            intensity_cross=intensity1,
+            intensity_background=intensity_background,
+            target_size=target_size,
+            intensity_target=intensity_target,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -272,6 +286,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.benarys.todorovic_generalized`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.benarys import todorovic_generalized
 
 # Define widgets
@@ -321,20 +337,23 @@ def show_todorovic_generalized(
     target_rotation=None,
     add_mask=False,
 ):
-    stim = todorovic_generalized(
-        visual_size=(height, width),
-        ppd=ppd,
-        L_width=cross_thickness,
-        intensity_cross=intensity1,
-        intensity_background=intensity_background,
-        target_size=(target_height, target_width),
-        target_type=target_type,
-        intensity_target=intensity_target,
-        target_x=target_x,
-        target_y=target_y,
-        target_rotation=target_rotation,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = todorovic_generalized(
+            visual_size=(height, width),
+            ppd=ppd,
+            L_width=cross_thickness,
+            intensity_cross=intensity1,
+            intensity_background=intensity_background,
+            target_size=(target_height, target_width),
+            target_type=target_type,
+            intensity_target=intensity_target,
+            target_x=target_x,
+            target_y=target_y,
+            target_rotation=target_rotation,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -365,6 +384,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.benarys.todorovic_rectangles`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.benarys import todorovic_rectangles
 
 # Define widgets
@@ -404,16 +425,19 @@ def show_todorovic_rectangles(
     intensity_target=None,
     add_mask=False,
 ):
-    stim = todorovic_rectangles(
-        visual_size=(height, width),
-        ppd=ppd,
-        L_width=cross_thickness,
-        intensity_cross=intensity1,
-        intensity_background=intensity_background,
-        target_size=(target_height, target_width),
-        intensity_target=intensity_target,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = todorovic_rectangles(
+            visual_size=(height, width),
+            ppd=ppd,
+            L_width=cross_thickness,
+            intensity_cross=intensity1,
+            intensity_background=intensity_background,
+            target_size=(target_height, target_width),
+            intensity_target=intensity_target,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -440,6 +464,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.benarys.todorovic_triangles`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.benarys import todorovic_triangles
 
 # Define widgets
@@ -477,16 +503,19 @@ def show_todorovic_triangles(
     intensity_target=None,
     add_mask=False,
 ):
-    stim = todorovic_triangles(
-        visual_size=(height, width),
-        ppd=ppd,
-        L_width=cross_thickness,
-        intensity_cross=intensity1,
-        intensity_background=intensity_background,
-        target_size=target_size,
-        intensity_target=intensity_target,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = todorovic_triangles(
+            visual_size=(height, width),
+            ppd=ppd,
+            L_width=cross_thickness,
+            intensity_cross=intensity1,
+            intensity_background=intensity_background,
+            target_size=target_size,
+            intensity_target=intensity_target,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
diff --git a/docs/reference/demos/stimuli/bullseyes.md b/docs/reference/demos/stimuli/bullseyes.md
index 2f6b1c2e..58dfa2a2 100644
--- a/docs/reference/demos/stimuli/bullseyes.md
+++ b/docs/reference/demos/stimuli/bullseyes.md
@@ -12,26 +12,27 @@ kernelspec:
   name: python3
 ---
 
-```{important}
+```{tip}
 [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/bullseyes.md)
  to get interactivity
 ```
+```{attention}
+To run locally, the code for these interactive demos requires
+a [Jupyter Notebook](https://jupyter.org/) environment,
+and the [Jupyter Widgets extension (`ipywidgets`)](https://ipywidgets.readthedocs.io/en/latest/index.html).
+```
 
 # Stimuli - Bullseyes
 {py:mod}`stimupy.stimuli.bullseyes`
 
-```{code-cell} ipython3
-:tags: [remove-cell]
 
-import IPython
-import ipywidgets as iw
-from stimupy.utils import plot_stim
-```
 
 ## Circular
 {py:func}`stimupy.stimuli.bullseyes.circular`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.bullseyes import circular
 
 # Define widgets
@@ -75,18 +76,21 @@ def show_bullseye(
     add_mask=False,
     intensity_target=None,
 ):
-    stim = circular(
-        visual_size=(height, width),
-        ppd=ppd,
-        frequency=frequency,
-        phase_shift=phase_shift,
-        intensity_rings=(int1, int2),
-        intensity_background=intensity_background,
-        origin=origin,
-        clip=clip,
-        intensity_target=intensity_target,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = circular(
+            visual_size=(height, width),
+            ppd=ppd,
+            frequency=frequency,
+            phase_shift=phase_shift,
+            intensity_rings=(int1, int2),
+            intensity_background=intensity_background,
+            origin=origin,
+            clip=clip,
+            intensity_target=intensity_target,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -116,6 +120,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.bullseyes.circular_two_sided`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.bullseyes import circular_two_sided
 
 # Define widgets
@@ -157,17 +163,20 @@ def show_two_sided_bullseye(
     add_mask=False,
     intensity_target=None,
 ):
-    stim = circular_two_sided(
-        visual_size=(height, width),
-        ppd=ppd,
-        frequency=frequency,
-        phase_shift=phase_shift,
-        intensity_rings=(int1, int2),
-        intensity_background=intensity_background,
-        origin=origin,
-        intensity_target=intensity_target,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = circular_two_sided(
+            visual_size=(height, width),
+            ppd=ppd,
+            frequency=frequency,
+            phase_shift=phase_shift,
+            intensity_rings=(int1, int2),
+            intensity_background=intensity_background,
+            origin=origin,
+            intensity_target=intensity_target,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -195,6 +204,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.bullseyes.rectangular`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.bullseyes import rectangular
 
 # Define widgets
@@ -240,19 +251,22 @@ def show_rectangular(
     intensity_target=None,
     rotation=0.0,
 ):
-    stim = rectangular(
-        visual_size=(height, width),
-        ppd=ppd,
-        frequency=frequency,
-        phase_shift=phase_shift,
-        intensity_frames=(int1, int2),
-        intensity_background=intensity_background,
-        origin=origin,
-        clip=clip,
-        intensity_target=intensity_target,
-        rotation=rotation,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = rectangular(
+            visual_size=(height, width),
+            ppd=ppd,
+            frequency=frequency,
+            phase_shift=phase_shift,
+            intensity_frames=(int1, int2),
+            intensity_background=intensity_background,
+            origin=origin,
+            clip=clip,
+            intensity_target=intensity_target,
+            rotation=rotation,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -282,6 +296,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.bullseyes.rectangular_generalized`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.bullseyes import rectangular_generalized
 
 # Define widgets
@@ -329,17 +345,20 @@ def show_rectangular_generalized(
     intensity_target=None,
     rotation=0.0,
 ):
-    stim = rectangular_generalized(
-        visual_size=(height, width),
-        ppd=ppd,
-        radii=(radius1, radius2, radius3),
-        intensity_frames=(int1, int2, int3),
-        intensity_background=intensity_background,
-        origin=origin,
-        intensity_target=intensity_target,
-        rotation=rotation,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = rectangular_generalized(
+            visual_size=(height, width),
+            ppd=ppd,
+            radii=(radius1, radius2, radius3),
+            intensity_frames=(int1, int2, int3),
+            intensity_background=intensity_background,
+            origin=origin,
+            intensity_target=intensity_target,
+            rotation=rotation,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -370,6 +389,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.bullseyes.rectangular_two_sided`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.bullseyes import rectangular_two_sided
 
 # Define widgets
@@ -412,17 +433,20 @@ def show_rectangular_two_sided(
     intensity_target=None,
     rotation=0.0,
 ):
-    stim = rectangular_two_sided(
-        visual_size=(height, width),
-        ppd=ppd,
-        frequency=frequency,
-        phase_shift=phase_shift,
-        intensity_frames=(int1, int2),
-        intensity_background=intensity_background,
-        intensity_target=intensity_target,
-        rotation=rotation,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = rectangular_two_sided(
+            visual_size=(height, width),
+            ppd=ppd,
+            frequency=frequency,
+            phase_shift=phase_shift,
+            intensity_frames=(int1, int2),
+            intensity_background=intensity_background,
+            intensity_target=intensity_target,
+            rotation=rotation,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
diff --git a/docs/reference/demos/stimuli/checkerboards.md b/docs/reference/demos/stimuli/checkerboards.md
index 1f28a818..722fbc13 100644
--- a/docs/reference/demos/stimuli/checkerboards.md
+++ b/docs/reference/demos/stimuli/checkerboards.md
@@ -12,26 +12,27 @@ kernelspec:
   name: python3
 ---
 
-```{important}
+```{tip}
 [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/checkerboards.md)
  to get interactivity
 ```
+```{attention}
+To run locally, the code for these interactive demos requires
+a [Jupyter Notebook](https://jupyter.org/) environment,
+and the [Jupyter Widgets extension (`ipywidgets`)](https://ipywidgets.readthedocs.io/en/latest/index.html).
+```
 
 # Stimuli - Checkerboards
 {py:mod}`stimupy.stimuli.checkerboards`
 
-```{code-cell} ipython3
-:tags: [remove-cell]
 
-import IPython
-import ipywidgets as iw
-from stimupy.utils import plot_stim
-```
 
 ## Checkerboard
 {py:func}`stimupy.stimuli.checkerboards.checkerboard`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.checkerboards import checkerboard
 
 # Define widgets
@@ -81,20 +82,22 @@ def show_checkerboard(
     intensity_target=None,
     extend_targets=False,
 ):
-
-    stim = checkerboard(
-        visual_size=(height, width),
-        ppd=ppd,
-        frequency=(frequency1, frequency2),
-        period=period,
-        rotation=rotation,
-        intensity_checks=(intensity1, intensity2),
-        round_phase_width=round_phase_width,
-        target_indices=((target_y, target_x),),
-        intensity_target=intensity_target,
-        extend_targets=extend_targets,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = checkerboard(
+            visual_size=(height, width),
+            ppd=ppd,
+            frequency=(frequency1, frequency2),
+            period=period,
+            rotation=rotation,
+            intensity_checks=(intensity1, intensity2),
+            round_phase_width=round_phase_width,
+            target_indices=((target_y, target_x),),
+            intensity_target=intensity_target,
+            extend_targets=extend_targets,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -126,6 +129,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.checkerboards.contrast_contrast`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.checkerboards import contrast_contrast
 
 # Define widgets
@@ -175,20 +180,22 @@ def show_contrast_contrast(
     alpha=None,
     tau=False,
 ):
-
-    stim = contrast_contrast(
-        visual_size=(height, width),
-        ppd=ppd,
-        frequency=(frequency1, frequency2),
-        period=period,
-        rotation=rotation,
-        intensity_checks=(intensity1, intensity2),
-        round_phase_width=round_phase_width,
-        target_shape=(target_y, target_x),
-        alpha=alpha,
-        tau=tau,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = contrast_contrast(
+            visual_size=(height, width),
+            ppd=ppd,
+            frequency=(frequency1, frequency2),
+            period=period,
+            rotation=rotation,
+            intensity_checks=(intensity1, intensity2),
+            round_phase_width=round_phase_width,
+            target_shape=(target_y, target_x),
+            alpha=alpha,
+            tau=tau,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
diff --git a/docs/reference/demos/stimuli/cornsweets.md b/docs/reference/demos/stimuli/cornsweets.md
index 7da850ce..2f72c06f 100644
--- a/docs/reference/demos/stimuli/cornsweets.md
+++ b/docs/reference/demos/stimuli/cornsweets.md
@@ -12,26 +12,27 @@ kernelspec:
   name: python3
 ---
 
-```{important}
+```{tip}
 [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/cornsweets.md)
  to get interactivity
 ```
+```{attention}
+To run locally, the code for these interactive demos requires
+a [Jupyter Notebook](https://jupyter.org/) environment,
+and the [Jupyter Widgets extension (`ipywidgets`)](https://ipywidgets.readthedocs.io/en/latest/index.html).
+```
 
 # Stimuli - Cornsweets
 {py:mod}`stimupy.stimuli.cornsweets`
 
-```{code-cell} ipython3
-:tags: [remove-cell]
 
-import IPython
-import ipywidgets as iw
-from stimupy.utils import plot_stim
-```
 
 ## Cornsweet
 {py:func}`stimupy.stimuli.cornsweets.cornsweet`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.cornsweets import cornsweet
 
 # Define widgets
@@ -69,16 +70,19 @@ def show_cornsweet(
     exponent=None,
     add_mask=False,
 ):
-    stim = cornsweet(
-        visual_size=(height, width),
-        ppd=ppd,
-        rotation=rotation,
-        intensity_edges=(intensity1, intensity2),
-        intensity_plateau=intensity_plateau,
-        ramp_width=ramp_width,
-        exponent=exponent,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = cornsweet(
+            visual_size=(height, width),
+            ppd=ppd,
+            rotation=rotation,
+            intensity_edges=(intensity1, intensity2),
+            intensity_plateau=intensity_plateau,
+            ramp_width=ramp_width,
+            exponent=exponent,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
diff --git a/docs/reference/demos/stimuli/cubes.md b/docs/reference/demos/stimuli/cubes.md
index c0530573..a591bb25 100644
--- a/docs/reference/demos/stimuli/cubes.md
+++ b/docs/reference/demos/stimuli/cubes.md
@@ -12,26 +12,27 @@ kernelspec:
   name: python3
 ---
 
-```{important}
+```{tip}
 [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/cubes.md)
  to get interactivity
 ```
+```{attention}
+To run locally, the code for these interactive demos requires
+a [Jupyter Notebook](https://jupyter.org/) environment,
+and the [Jupyter Widgets extension (`ipywidgets`)](https://ipywidgets.readthedocs.io/en/latest/index.html).
+```
 
 # Stimuli - Cubes
 {py:mod}`stimupy.stimuli.cubes`
 
-```{code-cell} ipython3
-:tags: [remove-cell]
 
-import IPython
-import ipywidgets as iw
-from stimupy.utils import plot_stim
-```
 
 ## Varying cells
 {py:func}`stimupy.stimuli.cubes.varying_cells`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.cubes import varying_cells
 
 # Define widgets
@@ -77,17 +78,20 @@ def show_varying_cells(
     cell_t=None,
     cell_s=None,
 ):
-    stim = varying_cells(
-        ppd=ppd,
-        cell_lengths=(cell_l1, cell_l2, cell_l3, cell_l4),
-        cell_thickness=cell_t,
-        cell_spacing=cell_s,
-        target_indices=target_indices,
-        intensity_background=intensity_background,
-        intensity_cells=intensity1,
-        intensity_target=intensity_target,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = varying_cells(
+            ppd=ppd,
+            cell_lengths=(cell_l1, cell_l2, cell_l3, cell_l4),
+            cell_thickness=cell_t,
+            cell_spacing=cell_s,
+            target_indices=target_indices,
+            intensity_background=intensity_background,
+            intensity_cells=intensity1,
+            intensity_target=intensity_target,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -116,6 +120,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.cubes.cube`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.cubes import cube
 
 # Define widgets
@@ -157,18 +163,21 @@ def show_cube(
     cell_t=None,
     cell_s=None,
 ):
-    stim = cube(
-        visual_size=(height, width),
-        ppd=ppd,
-        n_cells=n_cells,
-        cell_thickness=cell_t,
-        cell_spacing=cell_s,
-        target_indices=target_indices,
-        intensity_background=intensity_background,
-        intensity_cells=intensity1,
-        intensity_target=intensity_target,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = cube(
+            visual_size=(height, width),
+            ppd=ppd,
+            n_cells=n_cells,
+            cell_thickness=cell_t,
+            cell_spacing=cell_s,
+            target_indices=target_indices,
+            intensity_background=intensity_background,
+            intensity_cells=intensity1,
+            intensity_target=intensity_target,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
diff --git a/docs/reference/demos/stimuli/delboeufs.md b/docs/reference/demos/stimuli/delboeufs.md
index 2f0f10aa..528e2760 100644
--- a/docs/reference/demos/stimuli/delboeufs.md
+++ b/docs/reference/demos/stimuli/delboeufs.md
@@ -12,26 +12,27 @@ kernelspec:
   name: python3
 ---
 
-```{important}
+```{tip}
 [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/delboeufs.md)
  to get interactivity
 ```
+```{attention}
+To run locally, the code for these interactive demos requires
+a [Jupyter Notebook](https://jupyter.org/) environment,
+and the [Jupyter Widgets extension (`ipywidgets`)](https://ipywidgets.readthedocs.io/en/latest/index.html).
+```
 
 # Stimuli - Delboeufs
 {py:mod}`stimupy.stimuli.delboeufs`
 
-```{code-cell} ipython3
-:tags: [remove-cell]
 
-import IPython
-import ipywidgets as iw
-from stimupy.utils import plot_stim
-```
 
 ## Delboeuf
 {py:func}`stimupy.stimuli.delboeufs.delboeuf`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.delboeufs import delboeuf
 
 # Define widgets
@@ -71,17 +72,20 @@ def show_delboeuf(
     intensity_target=None,
     add_mask=False,
 ):
-    stim = delboeuf(
-        visual_size=(height, width),
-        ppd=ppd,
-        outer_radius=outer_radius,
-        outer_line_width=outer_line_width,
-        target_radius=target_radius,
-        intensity_outer_line=intensity_outer_line,
-        intensity_background=intensity_background,
-        intensity_target=intensity_target,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = delboeuf(
+            visual_size=(height, width),
+            ppd=ppd,
+            outer_radius=outer_radius,
+            outer_line_width=outer_line_width,
+            target_radius=target_radius,
+            intensity_outer_line=intensity_outer_line,
+            intensity_background=intensity_background,
+            intensity_target=intensity_target,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -108,6 +112,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.delboeufs.two_sided`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.delboeufs import two_sided
 
 # Define widgets
@@ -149,17 +155,20 @@ def show_two_sided(
     intensity_target=None,
     add_mask=False,
 ):
-    stim = two_sided(
-        visual_size=(height, width),
-        ppd=ppd,
-        outer_radii=(outer_radius1, outer_radius2),
-        outer_line_width=outer_line_width,
-        target_radius=target_radius,
-        intensity_outer_line=intensity_outer_line,
-        intensity_background=intensity_background,
-        intensity_target=intensity_target,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = two_sided(
+            visual_size=(height, width),
+            ppd=ppd,
+            outer_radii=(outer_radius1, outer_radius2),
+            outer_line_width=outer_line_width,
+            target_radius=target_radius,
+            intensity_outer_line=intensity_outer_line,
+            intensity_background=intensity_background,
+            intensity_target=intensity_target,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
diff --git a/docs/reference/demos/stimuli/dungeons.md b/docs/reference/demos/stimuli/dungeons.md
index facf3914..37ee2075 100644
--- a/docs/reference/demos/stimuli/dungeons.md
+++ b/docs/reference/demos/stimuli/dungeons.md
@@ -12,26 +12,27 @@ kernelspec:
   name: python3
 ---
 
-```{important}
+```{tip}
 [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/dungeons.md)
  to get interactivity
 ```
+```{attention}
+To run locally, the code for these interactive demos requires
+a [Jupyter Notebook](https://jupyter.org/) environment,
+and the [Jupyter Widgets extension (`ipywidgets`)](https://ipywidgets.readthedocs.io/en/latest/index.html).
+```
 
 # Stimuli - Dungeons
 {py:mod}`stimupy.stimuli.dungeons`
 
-```{code-cell} ipython3
-:tags: [remove-cell]
 
-import IPython
-import ipywidgets as iw
-from stimupy.utils import plot_stim
-```
 
 ## Dungeon
 {py:func}`stimupy.stimuli.dungeons.dungeon`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.dungeons import dungeon
 
 # Define widgets
@@ -71,16 +72,19 @@ def show_dungeon(
     intensity_target=None,
     add_mask=False,
 ):
-    stim = dungeon(
-        visual_size=(height, width),
-        ppd=ppd,
-        n_cells=(n_cells1, n_cells2),
-        intensity_grid=intensity1,
-        intensity_background=intensity_background,
-        target_radius=target_radius,
-        intensity_target=intensity_target,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = dungeon(
+            visual_size=(height, width),
+            ppd=ppd,
+            n_cells=(n_cells1, n_cells2),
+            intensity_grid=intensity1,
+            intensity_background=intensity_background,
+            target_radius=target_radius,
+            intensity_target=intensity_target,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
diff --git a/docs/reference/demos/stimuli/edges.md b/docs/reference/demos/stimuli/edges.md
new file mode 100644
index 00000000..a6f908dd
--- /dev/null
+++ b/docs/reference/demos/stimuli/edges.md
@@ -0,0 +1,242 @@
+---
+jupytext:
+  formats: md:myst
+  text_representation:
+    extension: .md
+    format_name: myst
+    format_version: 0.13
+    jupytext_version: 1.14.5
+kernelspec:
+  display_name: Python 3 (ipykernel)
+  language: python
+  name: python3
+---
+
+```{tip}
+[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/edges.md)
+ to get interactivity
+```
+```{attention}
+To run locally, the code for these interactive demos requires
+a [Jupyter Notebook](https://jupyter.org/) environment,
+and the [Jupyter Widgets extension (`ipywidgets`)](https://ipywidgets.readthedocs.io/en/latest/index.html).
+```
+
+# Stimuli - Edges
+{py:mod}`stimupy.stimuli.edges`
+
+
+
+## Step edge
+{py:func}`stimupy.stimuli.edges.step`
+
+```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
+from stimupy.stimuli.edges import step
+
+# Define widgets
+w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
+w_width = iw.IntSlider(value=10, min=1, max=20, description="width [deg]")
+w_ppd = iw.IntSlider(value=20, min=1, max=40, description="ppd")
+
+w_rot = iw.IntSlider(value=0, min=0, max=360, description="rotation [deg]")
+
+w_int1 = iw.FloatSlider(value=1., min=0, max=1, description="intensity1")
+w_int2 = iw.FloatSlider(value=0., min=0, max=1, description="intensity2")
+
+w_mask = iw.ToggleButton(value=False, disabled=False, description="add mask")
+
+# Layout
+b_im_size = iw.HBox([w_height, w_width, w_ppd])
+b_intensities = iw.HBox([w_int1, w_int2])
+ui = iw.VBox([b_im_size, w_rot, b_intensities, w_mask])
+
+# Function for showing stim
+def show_step_edge(
+    height=None,
+    width=None,
+    ppd=None,
+    rotation=0,
+    intensity1=None,
+    intensity2=None,
+    add_mask=False,
+):
+    try:
+        stim = step(
+            visual_size=(height, width),
+            ppd=ppd,
+            rotation=rotation,
+            intensity_edges=(intensity1, intensity2),
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
+
+# Set interactivity
+out = iw.interactive_output(
+    show_step_edge,
+    {
+        "height": w_height,
+        "width": w_width,
+        "ppd": w_ppd,
+        "rotation": w_rot,
+        "intensity1": w_int1,
+        "intensity2": w_int2,
+        "add_mask": w_mask,
+    },
+)
+
+# Show
+display(ui, out)
+```
+
+## Gaussian edge
+{py:func}`stimupy.stimuli.edges.gaussian`
+
+```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
+from stimupy.stimuli.edges import gaussian
+
+# Define widgets
+w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
+w_width = iw.IntSlider(value=10, min=1, max=20, description="width [deg]")
+w_ppd = iw.IntSlider(value=20, min=1, max=40, description="ppd")
+
+w_sigma = iw.FloatSlider(value=2., min=0, max=4, description="sigma")
+w_rot = iw.IntSlider(value=0, min=0, max=360, description="rotation [deg]")
+
+w_int1 = iw.FloatSlider(value=1., min=0, max=1, description="intensity1")
+w_int2 = iw.FloatSlider(value=0., min=0, max=1, description="intensity2")
+w_int_back = iw.FloatSlider(value=0.5, min=0, max=1, description="intensity background")
+
+w_mask = iw.Dropdown(value=None, options=[None, 'gaussian_mask', 'edge_mask'], description="add mask")
+
+# Layout
+b_im_size = iw.HBox([w_height, w_width, w_ppd])
+b_geometry = iw.HBox([w_sigma, w_rot])
+b_intensities = iw.HBox([w_int1, w_int2, w_int_back])
+ui = iw.VBox([b_im_size, b_geometry, b_intensities, w_mask])
+
+# Function for showing stim
+def show_gaussian_edge(
+    height=None,
+    width=None,
+    ppd=None,
+    rotation=0,
+    sigma=None,
+    intensity1=None,
+    intensity2=None,
+    intensity_background=None,
+    add_mask=False,
+):
+    try:
+        stim = gaussian(
+            visual_size=(height, width),
+            ppd=ppd,
+            sigma=sigma,
+            rotation=rotation,
+            intensity_edges=(intensity1, intensity2),
+            intensity_background=intensity_background,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
+
+# Set interactivity
+out = iw.interactive_output(
+    show_gaussian_edge,
+    {
+        "height": w_height,
+        "width": w_width,
+        "ppd": w_ppd,
+        "sigma": w_sigma,
+        "rotation": w_rot,
+        "intensity1": w_int1,
+        "intensity2": w_int2,
+        "intensity_background": w_int_back,
+        "add_mask": w_mask,
+    },
+)
+
+# Show
+display(ui, out)
+```
+
+## Cornsweet edge
+{py:func}`stimupy.stimuli.edges.cornsweet`
+
+```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
+from stimupy.stimuli.edges import cornsweet
+
+# Define widgets
+w_height = iw.IntSlider(value=10, min=1, max=20, description="height [deg]")
+w_width = iw.IntSlider(value=10, min=1, max=20, description="width [deg]")
+w_ppd = iw.IntSlider(value=20, min=1, max=40, description="ppd")
+
+w_ramp = iw.FloatSlider(value=2., min=0, max=4, description="ramp width [deg]")
+w_exp = iw.FloatSlider(value=1., min=0, max=3, description="exponent")
+w_rot = iw.IntSlider(value=0, min=0, max=360, description="rotation [deg]")
+
+w_int1 = iw.FloatSlider(value=1., min=0, max=1, description="intensity1")
+w_int2 = iw.FloatSlider(value=0., min=0, max=1, description="intensity2")
+w_int_back = iw.FloatSlider(value=0.5, min=0, max=1, description="intensity background")
+
+w_mask = iw.ToggleButton(value=False, disabled=False, description="add mask")
+
+# Layout
+b_im_size = iw.HBox([w_height, w_width, w_ppd])
+b_geometry = iw.HBox([w_ramp, w_exp, w_rot])
+b_intensities = iw.HBox([w_int1, w_int2, w_int_back])
+ui = iw.VBox([b_im_size, b_geometry, b_intensities, w_mask])
+
+# Function for showing stim
+def show_cornsweet_edge(
+    height=None,
+    width=None,
+    ppd=None,
+    rotation=0,
+    ramp_width=None,
+    exponent=None,
+    intensity1=None,
+    intensity2=None,
+    intensity_plateau=None,
+    add_mask=False,
+):
+    try:
+        stim = cornsweet(
+            visual_size=(height, width),
+            ppd=ppd,
+            ramp_width=ramp_width,
+            exponent=exponent,
+            rotation=rotation,
+            intensity_edges=(intensity1, intensity2),
+            intensity_plateau=intensity_plateau,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
+
+# Set interactivity
+out = iw.interactive_output(
+    show_cornsweet_edge,
+    {
+        "height": w_height,
+        "width": w_width,
+        "ppd": w_ppd,
+        "ramp_width": w_ramp,
+        "exponent": w_exp,
+        "rotation": w_rot,
+        "intensity1": w_int1,
+        "intensity2": w_int2,
+        "intensity_plateau": w_int_back,
+        "add_mask": w_mask,
+    },
+)
+
+# Show
+display(ui, out)
+```
diff --git a/docs/reference/demos/stimuli/gabors.md b/docs/reference/demos/stimuli/gabors.md
index 207c64be..343f5dfa 100644
--- a/docs/reference/demos/stimuli/gabors.md
+++ b/docs/reference/demos/stimuli/gabors.md
@@ -12,26 +12,27 @@ kernelspec:
   name: python3
 ---
 
-```{important}
+```{tip}
 [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/gabors.md)
  to get interactivity
 ```
+```{attention}
+To run locally, the code for these interactive demos requires
+a [Jupyter Notebook](https://jupyter.org/) environment,
+and the [Jupyter Widgets extension (`ipywidgets`)](https://ipywidgets.readthedocs.io/en/latest/index.html).
+```
 
 # Stimuli - Gabors
 {py:mod}`stimupy.stimuli.gabors`
 
-```{code-cell} ipython3
-:tags: [remove-cell]
 
-import IPython
-import ipywidgets as iw
-from stimupy.utils import plot_stim
-```
 
 ## Gabor
 {py:func}`stimupy.stimuli.gabors.gabor`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.gabors import gabor
 
 # Define widgets
@@ -73,18 +74,21 @@ def show_gabor(
     period=None,
     add_mask=False,
 ):
-    stim = gabor(
-        visual_size=(height, width),
-        ppd=ppd,
-        rotation=rotation,
-        frequency=frequency,
-        phase_shift=phase_shift,
-        sigma=sigma,
-        intensity_bars=(int1, int2),
-        origin=origin,
-        period=period,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = gabor(
+            visual_size=(height, width),
+            ppd=ppd,
+            rotation=rotation,
+            frequency=frequency,
+            phase_shift=phase_shift,
+            sigma=sigma,
+            intensity_bars=(int1, int2),
+            origin=origin,
+            period=period,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
diff --git a/docs/reference/demos/stimuli/gratings.md b/docs/reference/demos/stimuli/gratings.md
index 49635d74..6c8d5d33 100644
--- a/docs/reference/demos/stimuli/gratings.md
+++ b/docs/reference/demos/stimuli/gratings.md
@@ -12,27 +12,28 @@ kernelspec:
   name: python3
 ---
 
-```{important}
+```{tip}
 [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/gratings.md)
  to get interactivity
 ```
+```{attention}
+To run locally, the code for these interactive demos requires
+a [Jupyter Notebook](https://jupyter.org/) environment,
+and the [Jupyter Widgets extension (`ipywidgets`)](https://ipywidgets.readthedocs.io/en/latest/index.html).
+```
 
 # Stimuli - Gratings
 {py:mod}`stimupy.stimuli.gratings`
 
-```{code-cell} ipython3
-:tags: [remove-cell]
 
-import IPython
-import ipywidgets as iw
-from stimupy.utils import plot_stim
-```
 
 
 ## On uniform background
 {py:func}`stimupy.stimuli.gratings.on_uniform`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.gratings import on_uniform
 
 # Define widgets
@@ -88,22 +89,25 @@ def show_on_uniform(
     grating_height=None,
     grating_width=None,
 ):
-    stim = on_uniform(
-        visual_size=(height, width),
-        ppd=ppd,
-        rotation=rotation,
-        frequency=frequency,
-        phase_shift=phase_shift,
-        intensity_bars=(int1, int2),
-        origin=origin,
-        round_phase_width=round_phase_width,
-        period=period,
-        target_indices=target_indices,
-        intensity_target=intensity_target,
-        intensity_background=intensity_background,
-        grating_size=(grating_height, grating_width),
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = on_uniform(
+            visual_size=(height, width),
+            ppd=ppd,
+            rotation=rotation,
+            frequency=frequency,
+            phase_shift=phase_shift,
+            intensity_bars=(int1, int2),
+            origin=origin,
+            round_phase_width=round_phase_width,
+            period=period,
+            target_indices=target_indices,
+            intensity_target=intensity_target,
+            intensity_background=intensity_background,
+            grating_size=(grating_height, grating_width),
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
diff --git a/docs/reference/demos/stimuli/hermanns.md b/docs/reference/demos/stimuli/hermanns.md
index 5c6a877d..e6302c6c 100644
--- a/docs/reference/demos/stimuli/hermanns.md
+++ b/docs/reference/demos/stimuli/hermanns.md
@@ -12,26 +12,27 @@ kernelspec:
   name: python3
 ---
 
-```{important}
+```{tip}
 [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/hermanns.md)
  to get interactivity
 ```
+```{attention}
+To run locally, the code for these interactive demos requires
+a [Jupyter Notebook](https://jupyter.org/) environment,
+and the [Jupyter Widgets extension (`ipywidgets`)](https://ipywidgets.readthedocs.io/en/latest/index.html).
+```
 
 # Stimuli - Hermanns
 {py:mod}`stimupy.stimuli.hermanns`
 
-```{code-cell} ipython3
-:tags: [remove-cell]
 
-import IPython
-import ipywidgets as iw
-from stimupy.utils import plot_stim
-```
 
 ## Grid
 {py:func}`stimupy.stimuli.hermanns.grid`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.hermanns import grid
 
 # Define widgets
@@ -63,14 +64,17 @@ def show_grid(
     int_grid=None,
     int_back=None,
 ):
-    stim = grid(
-        visual_size=(height, width),
-        ppd=ppd,
-        element_size=(eheight, ewidth, ethick),
-        intensity_background=int_back,
-        intensity_grid=int_grid,
-    )
-    plot_stim(stim, mask=False)
+    try:
+        stim = grid(
+            visual_size=(height, width),
+            ppd=ppd,
+            element_size=(eheight, ewidth, ethick),
+            intensity_background=int_back,
+            intensity_grid=int_grid,
+        )
+        plot_stim(stim, mask=False)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
diff --git a/docs/reference/demos/stimuli/mondrians.md b/docs/reference/demos/stimuli/mondrians.md
index 45be86d1..ebb0ff9e 100644
--- a/docs/reference/demos/stimuli/mondrians.md
+++ b/docs/reference/demos/stimuli/mondrians.md
@@ -12,16 +12,23 @@ kernelspec:
   name: python3
 ---
 
-```{important}
+```{tip}
 [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/mondrians.md)
  to get interactivity
 ```
+```{attention}
+To run locally, the code for these interactive demos requires
+a [Jupyter Notebook](https://jupyter.org/) environment,
+and the [Jupyter Widgets extension (`ipywidgets`)](https://ipywidgets.readthedocs.io/en/latest/index.html).
+```
 
 # Stimuli - Mondrians
 {py:mod}`stimupy.stimuli.mondrians`
 
 ```{code-cell} ipython3
-:tags: [remove-cell]
+import ipywidgets as iw
+from stimupy.utils import plot_stim
+
 
 import numpy as np
 import IPython
@@ -33,6 +40,8 @@ from stimupy.utils import plot_stim
 {py:func}`stimupy.stimuli.mondrians.mondrian`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.mondrians import mondrian
 
 # Define widgets
@@ -97,16 +106,18 @@ def show_mondrian(
     int3=None,
     add_mask=False,
 ):
-
-    stim = mondrian(
-        visual_size=(height, width),
-        ppd=ppd,
-        positions=((y1, x1), (y2, x2), (y3, x3)),
-        sizes=((h1, w1), (h2, w2), (h3, w3)),
-        intensities=(int1, int2, int3),
-        intensity_background=intensity_background,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = mondrian(
+            visual_size=(height, width),
+            ppd=ppd,
+            positions=((y1, x1), (y2, x2), (y3, x3)),
+            sizes=((h1, w1), (h2, w2), (h3, w3)),
+            intensities=(int1, int2, int3),
+            intensity_background=intensity_background,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -143,6 +154,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.mondrians.corrugated`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.mondrians import corrugated_mondrian
 
 # Define widgets
@@ -182,18 +195,20 @@ def show_corrugated_mondrian(
     target_idx2=None,
     intensity_target=None,
 ):
-    intensities = np.random.rand(3, 4)
-
-    stim = corrugated_mondrian(
-        visual_size=(height, width),
-        ppd=ppd,
-        depths=(depth1, depth2, depth3),
-        intensities=intensities,
-        intensity_background=intensity_background,
-        intensity_target=intensity_target,
-        target_indices=((target_idx1, target_idx2),),
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        intensities = np.random.rand(3, 4)
+        stim = corrugated_mondrian(
+            visual_size=(height, width),
+            ppd=ppd,
+            depths=(depth1, depth2, depth3),
+            intensities=intensities,
+            intensity_background=intensity_background,
+            intensity_target=intensity_target,
+            target_indices=((target_idx1, target_idx2),),
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
diff --git a/docs/reference/demos/stimuli/mueller_lyers.md b/docs/reference/demos/stimuli/mueller_lyers.md
index 4146c370..260c2cb3 100644
--- a/docs/reference/demos/stimuli/mueller_lyers.md
+++ b/docs/reference/demos/stimuli/mueller_lyers.md
@@ -12,26 +12,27 @@ kernelspec:
   name: python3
 ---
 
-```{important}
+```{tip}
 [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/mueller_lyers.md)
  to get interactivity
 ```
+```{attention}
+To run locally, the code for these interactive demos requires
+a [Jupyter Notebook](https://jupyter.org/) environment,
+and the [Jupyter Widgets extension (`ipywidgets`)](https://ipywidgets.readthedocs.io/en/latest/index.html).
+```
 
 # Stimuli - Mueller-Lyers
 {py:mod}`stimupy.stimuli.mueller_lyers`
 
-```{code-cell} ipython3
-:tags: [remove-cell]
 
-import IPython
-import ipywidgets as iw
-from stimupy.utils import plot_stim
-```
 
 ## Mueller-Lyer
 {py:func}`stimupy.stimuli.mueller_lyers.mueller_lyer`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.mueller_lyers import mueller_lyer
 
 # Define widgets
@@ -73,18 +74,21 @@ def show_mueller_lyer(
     intensity_target=None,
     add_mask=False,
 ):
-    stim = mueller_lyer(
-        visual_size=(height, width),
-        ppd=ppd,
-        outer_lines_length=outer_lines_length,
-        outer_lines_angle=outer_lines_angle,
-        line_width=line_width,
-        target_length=target_length,
-        intensity_outer_lines=intensity_outer_line,
-        intensity_background=intensity_background,
-        intensity_target=intensity_target,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = mueller_lyer(
+            visual_size=(height, width),
+            ppd=ppd,
+            outer_lines_length=outer_lines_length,
+            outer_lines_angle=outer_lines_angle,
+            line_width=line_width,
+            target_length=target_length,
+            intensity_outer_lines=intensity_outer_line,
+            intensity_background=intensity_background,
+            intensity_target=intensity_target,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -112,6 +116,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.delboeufs.two_sided`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.mueller_lyers import two_sided
 
 # Define widgets
@@ -153,18 +159,21 @@ def show_two_sided(
     intensity_target=None,
     add_mask=False,
 ):
-    stim = two_sided(
-        visual_size=(height, width),
-        ppd=ppd,
-        outer_lines_length=outer_lines_length,
-        outer_lines_angle=outer_lines_angle,
-        line_width=line_width,
-        target_length=target_length,
-        intensity_outer_lines=intensity_outer_line,
-        intensity_background=intensity_background,
-        intensity_target=intensity_target,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = two_sided(
+            visual_size=(height, width),
+            ppd=ppd,
+            outer_lines_length=outer_lines_length,
+            outer_lines_angle=outer_lines_angle,
+            line_width=line_width,
+            target_length=target_length,
+            intensity_outer_lines=intensity_outer_line,
+            intensity_background=intensity_background,
+            intensity_target=intensity_target,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
diff --git a/docs/reference/demos/stimuli/pinwheels.md b/docs/reference/demos/stimuli/pinwheels.md
index 3fa56e2c..b1fc4a75 100644
--- a/docs/reference/demos/stimuli/pinwheels.md
+++ b/docs/reference/demos/stimuli/pinwheels.md
@@ -12,26 +12,27 @@ kernelspec:
   name: python3
 ---
 
-```{important}
+```{tip}
 [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/angulars.md)
  to get interactivity
 ```
+```{attention}
+To run locally, the code for these interactive demos requires
+a [Jupyter Notebook](https://jupyter.org/) environment,
+and the [Jupyter Widgets extension (`ipywidgets`)](https://ipywidgets.readthedocs.io/en/latest/index.html).
+```
 
 # Stimuli - Pinwheels
 {py:mod}`stimupy.stimuli.pinwheels`
 
-```{code-cell} ipython3
-:tags: [remove-cell]
 
-import IPython
-import ipywidgets as iw
-from stimupy.utils import plot_stim
-```
 
 ## Pinwheel
 {py:func}`stimupy.stimuli.pinwheels.pinwheel`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.pinwheels import pinwheel
 
 # Define widgets
@@ -79,20 +80,23 @@ def show_pinwheel(
     origin=None,
     add_mask=False,
 ):
-    stim = pinwheel(
-        visual_size=(height, width),
-        ppd=ppd,
-        rotation=rotation,
-        n_segments=n_segments,
-        intensity_segments=(intensity1, intensity2),
-        intensity_background=intensity_background,
-        origin=origin,
-        target_indices=target_indices,
-        target_width=target_width,
-        target_center=target_center,
-        intensity_target=intensity_target,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = pinwheel(
+            visual_size=(height, width),
+            ppd=ppd,
+            rotation=rotation,
+            n_segments=n_segments,
+            intensity_segments=(intensity1, intensity2),
+            intensity_background=intensity_background,
+            origin=origin,
+            target_indices=target_indices,
+            target_width=target_width,
+            target_center=target_center,
+            intensity_target=intensity_target,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
diff --git a/docs/reference/demos/stimuli/plaids.md b/docs/reference/demos/stimuli/plaids.md
index 80c30b61..f332dc87 100644
--- a/docs/reference/demos/stimuli/plaids.md
+++ b/docs/reference/demos/stimuli/plaids.md
@@ -12,26 +12,27 @@ kernelspec:
   name: python3
 ---
 
-```{important}
+```{tip}
 [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/plaids.md)
  to get interactivity
 ```
+```{attention}
+To run locally, the code for these interactive demos requires
+a [Jupyter Notebook](https://jupyter.org/) environment,
+and the [Jupyter Widgets extension (`ipywidgets`)](https://ipywidgets.readthedocs.io/en/latest/index.html).
+```
 
 # Stimuli - Plaids
 {py:mod}`stimupy.stimuli.plaids`
 
-```{code-cell} ipython3
-:tags: [remove-cell]
 
-import IPython
-import ipywidgets as iw
-from stimupy.utils import plot_stim
-```
 
 ## Gabors
 {py:func}`stimupy.stimuli.plaids.gabors`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.plaids import gabors
 
 # Define widgets
@@ -101,36 +102,39 @@ def show_gabors(
     period=None,
     add_mask=False,
 ):
-    p_common = {
-        "visual_size": (height, width),
-        "ppd": ppd,
-        "origin": origin,
-        "round_phase_width": round_phase_width,
-        "period": period,
-        "sigma": sigma,
-    }
-    
-    p_grating1 = {
-        "frequency": frequency1,
-        "rotation": rotation1,
-        "phase_shift": phase_shift1,
-        "intensity_bars": (int11, int12),
-    }
-    
-    p_grating2 = {
-        "frequency": frequency2,
-        "rotation": rotation2,
-        "phase_shift": phase_shift2,
-        "intensity_bars": (int21, int22),
-    }
-
-    stim = gabors(
-        gabor_parameters1={**p_common, **p_grating1},
-        gabor_parameters2={**p_common, **p_grating2},
-        weight1=weight1,
-        weight2=weight2,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        p_common = {
+            "visual_size": (height, width),
+            "ppd": ppd,
+            "origin": origin,
+            "round_phase_width": round_phase_width,
+            "period": period,
+            "sigma": sigma,
+        }
+        
+        p_grating1 = {
+            "frequency": frequency1,
+            "rotation": rotation1,
+            "phase_shift": phase_shift1,
+            "intensity_bars": (int11, int12),
+        }
+        
+        p_grating2 = {
+            "frequency": frequency2,
+            "rotation": rotation2,
+            "phase_shift": phase_shift2,
+            "intensity_bars": (int21, int22),
+        }
+
+        stim = gabors(
+            gabor_parameters1={**p_common, **p_grating1},
+            gabor_parameters2={**p_common, **p_grating2},
+            weight1=weight1,
+            weight2=weight2,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -167,6 +171,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.plaids.sine_waves`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.plaids import sine_waves
 
 # Define widgets
@@ -235,35 +241,38 @@ def show_sine_waves(
     period=None,
     add_mask=False,
 ):
-    p_common = {
-        "visual_size": (height, width),
-        "ppd": ppd,
-        "origin": origin,
-        "round_phase_width": round_phase_width,
-        "period": period,
-    }
-    
-    p_grating1 = {
-        "frequency": frequency1,
-        "rotation": rotation1,
-        "phase_shift": phase_shift1,
-        "intensities": (int11, int12),
-    }
-    
-    p_grating2 = {
-        "frequency": frequency2,
-        "rotation": rotation2,
-        "phase_shift": phase_shift2,
-        "intensities": (int21, int22),
-    }
-
-    stim = sine_waves(
-        grating_parameters1={**p_common, **p_grating1},
-        grating_parameters2={**p_common, **p_grating2},
-        weight1=weight1,
-        weight2=weight2,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        p_common = {
+            "visual_size": (height, width),
+            "ppd": ppd,
+            "origin": origin,
+            "round_phase_width": round_phase_width,
+            "period": period,
+        }
+        
+        p_grating1 = {
+            "frequency": frequency1,
+            "rotation": rotation1,
+            "phase_shift": phase_shift1,
+            "intensities": (int11, int12),
+        }
+        
+        p_grating2 = {
+            "frequency": frequency2,
+            "rotation": rotation2,
+            "phase_shift": phase_shift2,
+            "intensities": (int21, int22),
+        }
+
+        stim = sine_waves(
+            grating_parameters1={**p_common, **p_grating1},
+            grating_parameters2={**p_common, **p_grating2},
+            weight1=weight1,
+            weight2=weight2,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -300,6 +309,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.plaids.square_waves`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.plaids import square_waves
 
 # Define widgets
@@ -368,35 +379,38 @@ def show_square_waves(
     period=None,
     add_mask=False,
 ):
-    p_common = {
-        "visual_size": (height, width),
-        "ppd": ppd,
-        "origin": origin,
-        "round_phase_width": round_phase_width,
-        "period": period,
-    }
-    
-    p_grating1 = {
-        "frequency": frequency1,
-        "rotation": rotation1,
-        "phase_shift": phase_shift1,
-        "intensity_bars": (int11, int12),
-    }
-    
-    p_grating2 = {
-        "frequency": frequency2,
-        "rotation": rotation2,
-        "phase_shift": phase_shift2,
-        "intensity_bars": (int21, int22),
-    }
-
-    stim = square_waves(
-        grating_parameters1={**p_common, **p_grating1},
-        grating_parameters2={**p_common, **p_grating2},
-        weight1=weight1,
-        weight2=weight2,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        p_common = {
+            "visual_size": (height, width),
+            "ppd": ppd,
+            "origin": origin,
+            "round_phase_width": round_phase_width,
+            "period": period,
+        }
+        
+        p_grating1 = {
+            "frequency": frequency1,
+            "rotation": rotation1,
+            "phase_shift": phase_shift1,
+            "intensity_bars": (int11, int12),
+        }
+        
+        p_grating2 = {
+            "frequency": frequency2,
+            "rotation": rotation2,
+            "phase_shift": phase_shift2,
+            "intensity_bars": (int21, int22),
+        }
+
+        stim = square_waves(
+            grating_parameters1={**p_common, **p_grating1},
+            grating_parameters2={**p_common, **p_grating2},
+            weight1=weight1,
+            weight2=weight2,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
diff --git a/docs/reference/demos/stimuli/ponzos.md b/docs/reference/demos/stimuli/ponzos.md
index 2362df36..e9ec8f03 100644
--- a/docs/reference/demos/stimuli/ponzos.md
+++ b/docs/reference/demos/stimuli/ponzos.md
@@ -12,26 +12,27 @@ kernelspec:
   name: python3
 ---
 
-```{important}
+```{tip}
 [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/ponzos.md)
  to get interactivity
 ```
+```{attention}
+To run locally, the code for these interactive demos requires
+a [Jupyter Notebook](https://jupyter.org/) environment,
+and the [Jupyter Widgets extension (`ipywidgets`)](https://ipywidgets.readthedocs.io/en/latest/index.html).
+```
 
 # Stimuli - Ponzos
 {py:mod}`stimupy.stimuli.ponzos`
 
-```{code-cell} ipython3
-:tags: [remove-cell]
 
-import IPython
-import ipywidgets as iw
-from stimupy.utils import plot_stim
-```
 
 ## Ponzo
 {py:func}`stimupy.stimuli.ponzos.ponzo`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.ponzos import ponzo
 
 # Define widgets
@@ -77,20 +78,23 @@ def show_ponzo(
     intensity_target_lines=None,
     add_mask=False,
 ):
-    stim = ponzo(
-        visual_size=(height, width),
-        ppd=ppd,
-        outer_lines_length=outer_lines_length,
-        outer_lines_width=outer_lines_width,
-        outer_lines_angle=outer_lines_angle,
-        target_lines_length=target_lines_length,
-        target_lines_width=target_lines_width,
-        target_distance=target_distance,
-        intensity_outer_lines=intensity_outer_lines,
-        intensity_background=intensity_background,
-        intensity_target_lines=intensity_target_lines,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = ponzo(
+            visual_size=(height, width),
+            ppd=ppd,
+            outer_lines_length=outer_lines_length,
+            outer_lines_width=outer_lines_width,
+            outer_lines_angle=outer_lines_angle,
+            target_lines_length=target_lines_length,
+            target_lines_width=target_lines_width,
+            target_distance=target_distance,
+            intensity_outer_lines=intensity_outer_lines,
+            intensity_background=intensity_background,
+            intensity_target_lines=intensity_target_lines,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
diff --git a/docs/reference/demos/stimuli/rings.md b/docs/reference/demos/stimuli/rings.md
index df335cdb..7defd9d6 100644
--- a/docs/reference/demos/stimuli/rings.md
+++ b/docs/reference/demos/stimuli/rings.md
@@ -12,26 +12,27 @@ kernelspec:
   name: python3
 ---
 
-```{important}
+```{tip}
 [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/rings.md)
  to get interactivity
 ```
+```{attention}
+To run locally, the code for these interactive demos requires
+a [Jupyter Notebook](https://jupyter.org/) environment,
+and the [Jupyter Widgets extension (`ipywidgets`)](https://ipywidgets.readthedocs.io/en/latest/index.html).
+```
 
 # Stimuli - Rings
 {py:mod}`stimupy.stimuli.rings`
 
-```{code-cell} ipython3
-:tags: [remove-cell]
 
-import IPython
-import ipywidgets as iw
-from stimupy.utils import plot_stim
-```
 
 ## Circular
 {py:func}`stimupy.stimuli.rings.circular`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.rings import circular
 
 # Define widgets
@@ -77,19 +78,22 @@ def show_circular(
     target_indices=None,
     intensity_target=None,
 ):
-    stim = circular(
-        visual_size=(height, width),
-        ppd=ppd,
-        frequency=frequency,
-        phase_shift=phase_shift,
-        intensity_rings=(int1, int2),
-        intensity_background=intensity_background,
-        origin=origin,
-        clip=clip,
-        target_indices=target_indices,
-        intensity_target=intensity_target,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = circular(
+            visual_size=(height, width),
+            ppd=ppd,
+            frequency=frequency,
+            phase_shift=phase_shift,
+            intensity_rings=(int1, int2),
+            intensity_background=intensity_background,
+            origin=origin,
+            clip=clip,
+            target_indices=target_indices,
+            intensity_target=intensity_target,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -119,6 +123,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.rings.circular_two_sided`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.rings import circular_two_sided
 
 # Define widgets
@@ -162,18 +168,21 @@ def show_circular_two_sided(
     target_indices=None,
     intensity_target=None,
 ):
-    stim = circular_two_sided(
-        visual_size=(height, width),
-        ppd=ppd,
-        frequency=frequency,
-        phase_shift=phase_shift,
-        intensity_rings=(int1, int2),
-        intensity_background=intensity_background,
-        origin=origin,
-        target_indices=target_indices,
-        intensity_target=intensity_target,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = circular_two_sided(
+            visual_size=(height, width),
+            ppd=ppd,
+            frequency=frequency,
+            phase_shift=phase_shift,
+            intensity_rings=(int1, int2),
+            intensity_background=intensity_background,
+            origin=origin,
+            target_indices=target_indices,
+            intensity_target=intensity_target,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -202,6 +211,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.rings.rectangular`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.rings import rectangular
 
 # Define widgets
@@ -247,19 +258,22 @@ def show_rectangular(
     target_indices=None,
     intensity_target=None,
 ):
-    stim = rectangular(
-        visual_size=(height, width),
-        ppd=ppd,
-        frequency=frequency,
-        phase_shift=phase_shift,
-        intensity_frames=(int1, int2),
-        intensity_background=intensity_background,
-        origin=origin,
-        clip=clip,
-        target_indices=target_indices,
-        intensity_target=intensity_target,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = rectangular(
+            visual_size=(height, width),
+            ppd=ppd,
+            frequency=frequency,
+            phase_shift=phase_shift,
+            intensity_frames=(int1, int2),
+            intensity_background=intensity_background,
+            origin=origin,
+            clip=clip,
+            target_indices=target_indices,
+            intensity_target=intensity_target,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -289,6 +303,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.rings.rectangular_generalized`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.rings import rectangular_generalized
 
 # Define widgets
@@ -336,17 +352,20 @@ def show_rectangular_generalized(
     intensity_target=None,
     target_indices=None,
 ):
-    stim = rectangular_generalized(
-        visual_size=(height, width),
-        ppd=ppd,
-        radii=(radius1, radius2, radius3),
-        intensity_frames=(int1, int2, int3),
-        intensity_background=intensity_background,
-        origin=origin,
-        intensity_target=intensity_target,
-        target_indices=target_indices,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = rectangular_generalized(
+            visual_size=(height, width),
+            ppd=ppd,
+            radii=(radius1, radius2, radius3),
+            intensity_frames=(int1, int2, int3),
+            intensity_background=intensity_background,
+            origin=origin,
+            intensity_target=intensity_target,
+            target_indices=target_indices,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -377,6 +396,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.rings.rectangular_two_sided`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.rings import rectangular_two_sided
 
 # Define widgets
@@ -418,17 +439,20 @@ def show_rectangular_two_sided(
     target_indices=None,
     intensity_target=None,
 ):
-    stim = rectangular_two_sided(
-        visual_size=(height, width),
-        ppd=ppd,
-        frequency=frequency,
-        phase_shift=phase_shift,
-        intensity_frames=(int1, int2),
-        intensity_background=intensity_background,
-        target_indices=target_indices,
-        intensity_target=intensity_target,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = rectangular_two_sided(
+            visual_size=(height, width),
+            ppd=ppd,
+            frequency=frequency,
+            phase_shift=phase_shift,
+            intensity_frames=(int1, int2),
+            intensity_background=intensity_background,
+            target_indices=target_indices,
+            intensity_target=intensity_target,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
diff --git a/docs/reference/demos/stimuli/sbcs.md b/docs/reference/demos/stimuli/sbcs.md
index 33968dc9..434380b0 100644
--- a/docs/reference/demos/stimuli/sbcs.md
+++ b/docs/reference/demos/stimuli/sbcs.md
@@ -12,26 +12,27 @@ kernelspec:
   name: python3
 ---
 
-```{important}
+```{tip}
 [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/sbcs.md)
  to get interactivity
 ```
+```{attention}
+To run locally, the code for these interactive demos requires
+a [Jupyter Notebook](https://jupyter.org/) environment,
+and the [Jupyter Widgets extension (`ipywidgets`)](https://ipywidgets.readthedocs.io/en/latest/index.html).
+```
 
 # Stimuli - SBCs (Simultaneous Brightness Contrast)
 {py:mod}`stimupy.stimuli.sbcs`
 
-```{code-cell} ipython3
-:tags: [remove-cell]
 
-import IPython
-import ipywidgets as iw
-from stimupy.utils import plot_stim
-```
 
 ## Generalized
 {py:func}`stimupy.stimuli.sbcs.generalized`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.sbcs import generalized
 
 # Define widgets
@@ -73,15 +74,18 @@ def show_generalized(
     intensity_target=None,
     add_mask=False,
 ):
-    stim = generalized(
-        visual_size=(height, width),
-        ppd=ppd,
-        target_size=(target_height,target_width),
-        target_position=(target_y,target_x),
-        intensity_background=intensity_background,
-        intensity_target=intensity_target,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = generalized(
+            visual_size=(height, width),
+            ppd=ppd,
+            target_size=(target_height,target_width),
+            target_position=(target_y,target_x),
+            intensity_background=intensity_background,
+            intensity_target=intensity_target,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -108,6 +112,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.sbcs.basic`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.sbcs import basic
 
 # Define widgets
@@ -142,14 +148,17 @@ def show_basic(
     intensity_target=None,
     add_mask=False,
 ):
-    stim = basic(
-        visual_size=(height, width),
-        ppd=ppd,
-        target_size=(target_height,target_width),
-        intensity_background=intensity_background,
-        intensity_target=intensity_target,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = basic(
+            visual_size=(height, width),
+            ppd=ppd,
+            target_size=(target_height,target_width),
+            intensity_background=intensity_background,
+            intensity_target=intensity_target,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -174,6 +183,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.sbcs.two_sided`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.sbcs import two_sided
 
 # Define widgets
@@ -211,14 +222,17 @@ def show_two_sided(
     intensity_target=None,
     add_mask=False,
 ):
-    stim = two_sided(
-        visual_size=(height, width),
-        ppd=ppd,
-        target_size=(target_height,target_width),
-        intensity_backgrounds=(intensity_background_l, intensity_background_r),
-        intensity_target=intensity_target,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = two_sided(
+            visual_size=(height, width),
+            ppd=ppd,
+            target_size=(target_height,target_width),
+            intensity_backgrounds=(intensity_background_l, intensity_background_r),
+            intensity_target=intensity_target,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -244,6 +258,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.sbcs.with_dots`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.sbcs import with_dots
 
 # Define widgets
@@ -290,18 +306,21 @@ def show_with_dots(
     intensity_dots=None,
     add_mask=False,
 ):
-    stim = with_dots(
-        visual_size=(height, width),
-        ppd=ppd,
-        target_shape=(target_height, target_width),
-        n_dots=(ndotsy, ndotsx),
-        dot_radius=dot_radius,
-        distance=dot_distance,
-        intensity_background=intensity_background,
-        intensity_target=intensity_target,
-        intensity_dots=intensity_dots,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = with_dots(
+            visual_size=(height, width),
+            ppd=ppd,
+            target_shape=(target_height, target_width),
+            n_dots=(ndotsy, ndotsx),
+            dot_radius=dot_radius,
+            distance=dot_distance,
+            intensity_background=intensity_background,
+            intensity_target=intensity_target,
+            intensity_dots=intensity_dots,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -331,6 +350,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.sbcs.with_dots_two_sided`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.sbcs import with_dots_two_sided
 
 # Define widgets
@@ -381,18 +402,21 @@ def show_two_sided_with_dots(
     intensity_dots_r=None,
     add_mask=False,
 ):
-    stim = with_dots_two_sided(
-        visual_size=(height, width),
-        ppd=ppd,
-        target_shape=(target_height, target_width),
-        n_dots=(ndotsy, ndotsx),
-        dot_radius=dot_radius,
-        distance=dot_distance,
-        intensity_backgrounds=(intensity_background_l, intensity_background_r),
-        intensity_target=intensity_target,
-        intensity_dots=(intensity_dots_l, intensity_dots_r),
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = with_dots_two_sided(
+            visual_size=(height, width),
+            ppd=ppd,
+            target_shape=(target_height, target_width),
+            n_dots=(ndotsy, ndotsx),
+            dot_radius=dot_radius,
+            distance=dot_distance,
+            intensity_backgrounds=(intensity_background_l, intensity_background_r),
+            intensity_target=intensity_target,
+            intensity_dots=(intensity_dots_l, intensity_dots_r),
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -424,6 +448,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.sbcs.dotted`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.sbcs import dotted
 
 # Define widgets
@@ -470,18 +496,21 @@ def show_dotted(
     intensity_dots=None,
     add_mask=False,
 ):
-    stim = dotted(
-        visual_size=(height, width),
-        ppd=ppd,
-        target_shape=(target_height, target_width),
-        n_dots=(ndotsy, ndotsx),
-        dot_radius=dot_radius,
-        distance=dot_distance,
-        intensity_background=intensity_background,
-        intensity_target=intensity_target,
-        intensity_dots=intensity_dots,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = dotted(
+            visual_size=(height, width),
+            ppd=ppd,
+            target_shape=(target_height, target_width),
+            n_dots=(ndotsy, ndotsx),
+            dot_radius=dot_radius,
+            distance=dot_distance,
+            intensity_background=intensity_background,
+            intensity_target=intensity_target,
+            intensity_dots=intensity_dots,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -511,6 +540,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.sbcs.dotted_two_sided`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.sbcs import dotted_two_sided
 
 # Define widgets
@@ -561,18 +592,21 @@ def show_two_sided_dotted(
     intensity_dots_r=None,
     add_mask=False,
 ):
-    stim = dotted_two_sided(
-        visual_size=(height, width),
-        ppd=ppd,
-        target_shape=(target_height, target_width),
-        n_dots=(ndotsy, ndotsx),
-        dot_radius=dot_radius,
-        distance=dot_distance,
-        intensity_backgrounds=(intensity_background_l, intensity_background_r),
-        intensity_target=intensity_target,
-        intensity_dots=(intensity_dots_l, intensity_dots_r),
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = dotted_two_sided(
+            visual_size=(height, width),
+            ppd=ppd,
+            target_shape=(target_height, target_width),
+            n_dots=(ndotsy, ndotsx),
+            dot_radius=dot_radius,
+            distance=dot_distance,
+            intensity_backgrounds=(intensity_background_l, intensity_background_r),
+            intensity_target=intensity_target,
+            intensity_dots=(intensity_dots_l, intensity_dots_r),
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
diff --git a/docs/reference/demos/stimuli/todorovics.md b/docs/reference/demos/stimuli/todorovics.md
index 4e32b0e0..def7415f 100644
--- a/docs/reference/demos/stimuli/todorovics.md
+++ b/docs/reference/demos/stimuli/todorovics.md
@@ -12,26 +12,27 @@ kernelspec:
   name: python3
 ---
 
-```{important}
+```{tip}
 [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/todorovics.md)
  to get interactivity
 ```
+```{attention}
+To run locally, the code for these interactive demos requires
+a [Jupyter Notebook](https://jupyter.org/) environment,
+and the [Jupyter Widgets extension (`ipywidgets`)](https://ipywidgets.readthedocs.io/en/latest/index.html).
+```
 
 # Stimuli - Todorovics
 {py:mod}`stimupy.stimuli.todorovics`
 
-```{code-cell} ipython3
-:tags: [remove-cell]
 
-import IPython
-import ipywidgets as iw
-from stimupy.utils import plot_stim
-```
 
 ## Rectangle generalized
 {py:func}`stimupy.stimuli.todorovics.rectangle_generalized`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.todorovics import rectangle_generalized
 
 # Define widgets
@@ -90,19 +91,22 @@ def show_rectangle_generalized(
     intback=None,
     add_mask=False,
 ):
-    stim = rectangle_generalized(
-        visual_size=(height, width),
-        ppd=ppd,
-        target_size=(theight, twidth),
-        target_position=(ty, tx),
-        covers_size=(cheight, cwidth),
-        covers_x=(c1x, c2x),
-        covers_y=(c1y, c2y),
-        intensity_background=intback,
-        intensity_target=tint,
-        intensity_covers=cint,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = rectangle_generalized(
+            visual_size=(height, width),
+            ppd=ppd,
+            target_size=(theight, twidth),
+            target_position=(ty, tx),
+            covers_size=(cheight, cwidth),
+            covers_x=(c1x, c2x),
+            covers_y=(c1y, c2y),
+            intensity_background=intback,
+            intensity_target=tint,
+            intensity_covers=cint,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -136,6 +140,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.todorovics.rectangle`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.todorovics import rectangle
 
 # Define widgets
@@ -188,18 +194,21 @@ def show_rectangle(
     intback=None,
     add_mask=False,
 ):
-    stim = rectangle(
-        visual_size=(height, width),
-        ppd=ppd,
-        target_size=(theight, twidth),
-        target_position=(ty, tx),
-        covers_size=(cheight, cwidth),
-        covers_offset=(coffy, coffx),
-        intensity_background=intback,
-        intensity_target=tint,
-        intensity_covers=cint,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = rectangle(
+            visual_size=(height, width),
+            ppd=ppd,
+            target_size=(theight, twidth),
+            target_position=(ty, tx),
+            covers_size=(cheight, cwidth),
+            covers_offset=(coffy, coffx),
+            intensity_background=intback,
+            intensity_target=tint,
+            intensity_covers=cint,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -231,6 +240,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.todorovics.rectangle_two_sided`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.todorovics import rectangle_two_sided
 
 # Define widgets
@@ -281,17 +292,20 @@ def show_two_sided_rectangle(
     intback2=None,
     add_mask=False,
 ):
-    stim = rectangle_two_sided(
-        visual_size=(height, width),
-        ppd=ppd,
-        target_size=(theight, twidth),
-        covers_size=(cheight, cwidth),
-        covers_offset=(coffy, coffx),
-        intensity_backgrounds=(intback1, intback2),
-        intensity_target=tint,
-        intensity_covers=(cint1, cint2),
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = rectangle_two_sided(
+            visual_size=(height, width),
+            ppd=ppd,
+            target_size=(theight, twidth),
+            covers_size=(cheight, cwidth),
+            covers_offset=(coffy, coffx),
+            intensity_backgrounds=(intback1, intback2),
+            intensity_target=tint,
+            intensity_covers=(cint1, cint2),
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -323,6 +337,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.todorovics.cross_generalized`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.todorovics import cross_generalized
 
 # Define widgets
@@ -377,19 +393,22 @@ def show_cross_generalized(
     intback=None,
     add_mask=False,
 ):
-    stim = cross_generalized(
-        visual_size=(height, width),
-        ppd=ppd,
-        cross_size=(theight, twidth),
-        cross_thickness=tthick,
-        covers_size=(cheight, cwidth),
-        covers_x=(c1x, c2x),
-        covers_y=(c1y, c2y),
-        intensity_background=intback,
-        intensity_target=tint,
-        intensity_covers=cint,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = cross_generalized(
+            visual_size=(height, width),
+            ppd=ppd,
+            cross_size=(theight, twidth),
+            cross_thickness=tthick,
+            covers_size=(cheight, cwidth),
+            covers_x=(c1x, c2x),
+            covers_y=(c1y, c2y),
+            intensity_background=intback,
+            intensity_target=tint,
+            intensity_covers=cint,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -422,6 +441,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.todorovics.cross`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.todorovics import cross
 
 # Define widgets
@@ -464,17 +485,20 @@ def show_cross(
     intback=None,
     add_mask=False,
 ):
-    stim = cross(
-        visual_size=(height, width),
-        ppd=ppd,
-        cross_size=(theight, twidth),
-        cross_thickness=tthick,
-        covers_size=(cheight, cwidth),
-        intensity_background=intback,
-        intensity_target=tint,
-        intensity_covers=cint,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = cross(
+            visual_size=(height, width),
+            ppd=ppd,
+            cross_size=(theight, twidth),
+            cross_thickness=tthick,
+            covers_size=(cheight, cwidth),
+            intensity_background=intback,
+            intensity_target=tint,
+            intensity_covers=cint,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -503,6 +527,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.todorovics.cross_two_sided`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.todorovics import cross_two_sided
 
 # Define widgets
@@ -549,17 +575,20 @@ def show_two_sided_cross(
     intback2=None,
     add_mask=False,
 ):
-    stim = cross_two_sided(
-        visual_size=(height, width),
-        ppd=ppd,
-        cross_size=(theight, twidth),
-        cross_thickness=tthick,
-        covers_size=(cheight, cwidth),
-        intensity_backgrounds=(intback1, intback2),
-        intensity_target=tint,
-        intensity_covers=(cint1, cint2),
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = cross_two_sided(
+            visual_size=(height, width),
+            ppd=ppd,
+            cross_size=(theight, twidth),
+            cross_thickness=tthick,
+            covers_size=(cheight, cwidth),
+            intensity_backgrounds=(intback1, intback2),
+            intensity_target=tint,
+            intensity_covers=(cint1, cint2),
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -590,6 +619,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.todorovics.equal`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.todorovics import equal
 
 # Define widgets
@@ -628,16 +659,19 @@ def show_equal(
     intback=None,
     add_mask=False,
 ):
-    stim = equal(
-        visual_size=(height, width),
-        ppd=ppd,
-        cross_size=(theight, twidth),
-        cross_thickness=tthick,
-        intensity_background=intback,
-        intensity_target=tint,
-        intensity_covers=cint,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = equal(
+            visual_size=(height, width),
+            ppd=ppd,
+            cross_size=(theight, twidth),
+            cross_thickness=tthick,
+            intensity_background=intback,
+            intensity_target=tint,
+            intensity_covers=cint,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -664,6 +698,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.todorovics.equal_two_sided`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.todorovics import equal_two_sided
 
 # Define widgets
@@ -706,16 +742,19 @@ def show_two_sided_equal(
     intback2=None,
     add_mask=False,
 ):
-    stim = equal_two_sided(
-        visual_size=(height, width),
-        ppd=ppd,
-        cross_size=(theight, twidth),
-        cross_thickness=tthick,
-        intensity_backgrounds=(intback1, intback2),
-        intensity_target=tint,
-        intensity_covers=(cint1, cint2),
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = equal_two_sided(
+            visual_size=(height, width),
+            ppd=ppd,
+            cross_size=(theight, twidth),
+            cross_thickness=tthick,
+            intensity_backgrounds=(intback1, intback2),
+            intensity_target=tint,
+            intensity_covers=(cint1, cint2),
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
diff --git a/docs/reference/demos/stimuli/waves.md b/docs/reference/demos/stimuli/waves.md
index 8d1ccd2d..2fe954db 100644
--- a/docs/reference/demos/stimuli/waves.md
+++ b/docs/reference/demos/stimuli/waves.md
@@ -12,27 +12,26 @@ kernelspec:
   name: python3
 ---
 
-```{important}
+```{tip}
 [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/waves.md)
  to get interactivity
 ```
+```{attention}
+To run locally, the code for these interactive demos requires
+a [Jupyter Notebook](https://jupyter.org/) environment,
+and the [Jupyter Widgets extension (`ipywidgets`)](https://ipywidgets.readthedocs.io/en/latest/index.html).
+```
 
 # Stimuli - Waves
 {py:mod}`stimupy.stimuli.waves`
 
-```{code-cell} ipython3
-:tags: [remove-cell]
-
-import numpy as np
-import IPython
-import ipywidgets as iw
-from stimupy.utils import plot_stim
-```
 
 ## Sine, linear
 {py:func}`stimupy.stimuli.waves.sine_linear`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.waves import sine_linear
 
 # Define widgets
@@ -80,20 +79,23 @@ def show_sine_linear(
     target_indices=None,
     intensity_target=None,
 ):
-    stim = sine_linear(
-        visual_size=(height, width),
-        ppd=ppd,
-        rotation=rotation,
-        frequency=frequency,
-        phase_shift=phase_shift,
-        intensities=(int1, int2),
-        origin=origin,
-        round_phase_width=round_phase_width,
-        period=period,
-        target_indices=target_indices,
-        intensity_target=intensity_target,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = sine_linear(
+            visual_size=(height, width),
+            ppd=ppd,
+            rotation=rotation,
+            frequency=frequency,
+            phase_shift=phase_shift,
+            intensities=(int1, int2),
+            origin=origin,
+            round_phase_width=round_phase_width,
+            period=period,
+            target_indices=target_indices,
+            intensity_target=intensity_target,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -124,6 +126,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.waves.square_linear`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.waves import square_linear
 
 # Define widgets
@@ -171,20 +175,23 @@ def show_square_linear(
     target_indices=None,
     intensity_target=None,
 ):
-    stim = square_linear(
-        visual_size=(height, width),
-        ppd=ppd,
-        rotation=rotation,
-        frequency=frequency,
-        phase_shift=phase_shift,
-        intensity_bars=(int1, int2),
-        origin=origin,
-        round_phase_width=round_phase_width,
-        period=period,
-        target_indices=target_indices,
-        intensity_target=intensity_target,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = square_linear(
+            visual_size=(height, width),
+            ppd=ppd,
+            rotation=rotation,
+            frequency=frequency,
+            phase_shift=phase_shift,
+            intensity_bars=(int1, int2),
+            origin=origin,
+            round_phase_width=round_phase_width,
+            period=period,
+            target_indices=target_indices,
+            intensity_target=intensity_target,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -215,6 +222,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.waves.staircase_linear`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.waves import staircase_linear
 
 # Define widgets
@@ -262,20 +271,23 @@ def show_staircase_linear(
     target_indices=None,
     intensity_target=None,
 ):
-    stim = staircase_linear(
-        visual_size=(height, width),
-        ppd=ppd,
-        rotation=rotation,
-        frequency=frequency,
-        phase_shift=phase_shift,
-        intensity_bars=(int1, int2),
-        origin=origin,
-        round_phase_width=round_phase_width,
-        period=period,
-        target_indices=target_indices,
-        intensity_target=intensity_target,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = staircase_linear(
+            visual_size=(height, width),
+            ppd=ppd,
+            rotation=rotation,
+            frequency=frequency,
+            phase_shift=phase_shift,
+            intensity_bars=(int1, int2),
+            origin=origin,
+            round_phase_width=round_phase_width,
+            period=period,
+            target_indices=target_indices,
+            intensity_target=intensity_target,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -306,6 +318,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.waves.sine_radial`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.waves import sine_radial
 
 # Define widgets
@@ -355,21 +369,24 @@ def show_sine_radial(
     intensity_target=None,
     clip=True,
 ):
-    stim = sine_radial(
-        visual_size=(height, width),
-        ppd=ppd,
-        rotation=rotation,
-        frequency=frequency,
-        phase_shift=phase_shift,
-        intensities=(int1, int2),
-        origin=origin,
-        round_phase_width=round_phase_width,
-        period=period,
-        target_indices=target_indices,
-        intensity_target=intensity_target,
-        clip=clip,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = sine_radial(
+            visual_size=(height, width),
+            ppd=ppd,
+            rotation=rotation,
+            frequency=frequency,
+            phase_shift=phase_shift,
+            intensities=(int1, int2),
+            origin=origin,
+            round_phase_width=round_phase_width,
+            period=period,
+            target_indices=target_indices,
+            intensity_target=intensity_target,
+            clip=clip,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -401,6 +418,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.waves.square_radial`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.waves import square_radial
 
 # Define widgets
@@ -449,22 +468,25 @@ def show_square_radial(
     target_indices=None,
     intensity_target=None,
     clip=True,
-):
-    stim = square_radial(
-        visual_size=(height, width),
-        ppd=ppd,
-        rotation=rotation,
-        frequency=frequency,
-        phase_shift=phase_shift,
-        intensity_rings=(int1, int2),
-        origin=origin,
-        round_phase_width=round_phase_width,
-        period=period,
-        target_indices=target_indices,
-        intensity_target=intensity_target,
-        clip=clip,
-    )
-    plot_stim(stim, mask=add_mask)
+): 
+    try:
+        stim = square_radial(
+            visual_size=(height, width),
+            ppd=ppd,
+            rotation=rotation,
+            frequency=frequency,
+            phase_shift=phase_shift,
+            intensity_rings=(int1, int2),
+            origin=origin,
+            round_phase_width=round_phase_width,
+            period=period,
+            target_indices=target_indices,
+            intensity_target=intensity_target,
+            clip=clip,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -496,6 +518,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.waves.staircase_radial`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.waves import staircase_radial
 
 # Define widgets
@@ -545,21 +569,24 @@ def show_staircase_radial(
     intensity_target=None,
     clip=True,
 ):
-    stim = staircase_radial(
-        visual_size=(height, width),
-        ppd=ppd,
-        rotation=rotation,
-        frequency=frequency,
-        phase_shift=phase_shift,
-        intensity_rings=(int1, int2),
-        origin=origin,
-        round_phase_width=round_phase_width,
-        period=period,
-        target_indices=target_indices,
-        intensity_target=intensity_target,
-        clip=clip,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = staircase_radial(
+            visual_size=(height, width),
+            ppd=ppd,
+            rotation=rotation,
+            frequency=frequency,
+            phase_shift=phase_shift,
+            intensity_rings=(int1, int2),
+            origin=origin,
+            round_phase_width=round_phase_width,
+            period=period,
+            target_indices=target_indices,
+            intensity_target=intensity_target,
+            clip=clip,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -591,6 +618,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.waves.sine_rectilinear`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.waves import sine_rectilinear
 
 # Define widgets
@@ -640,21 +669,24 @@ def show_sine_rectilinear(
     intensity_target=None,
     clip=True,
 ):
-    stim = sine_rectilinear(
-        visual_size=(height, width),
-        ppd=ppd,
-        rotation=rotation,
-        frequency=frequency,
-        phase_shift=phase_shift,
-        intensities=(int1, int2),
-        origin=origin,
-        round_phase_width=round_phase_width,
-        period=period,
-        target_indices=target_indices,
-        intensity_target=intensity_target,
-        clip=clip,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = sine_rectilinear(
+            visual_size=(height, width),
+            ppd=ppd,
+            rotation=rotation,
+            frequency=frequency,
+            phase_shift=phase_shift,
+            intensities=(int1, int2),
+            origin=origin,
+            round_phase_width=round_phase_width,
+            period=period,
+            target_indices=target_indices,
+            intensity_target=intensity_target,
+            clip=clip,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -686,6 +718,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.waves.square_rectilinear`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.waves import square_rectilinear
 
 # Define widgets
@@ -735,21 +769,24 @@ def show_square_rectilinear(
     intensity_target=None,
     clip=True,
 ):
-    stim = square_rectilinear(
-        visual_size=(height, width),
-        ppd=ppd,
-        rotation=rotation,
-        frequency=frequency,
-        phase_shift=phase_shift,
-        intensity_frames=(int1, int2),
-        origin=origin,
-        round_phase_width=round_phase_width,
-        period=period,
-        target_indices=target_indices,
-        intensity_target=intensity_target,
-        clip=clip,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = square_rectilinear(
+            visual_size=(height, width),
+            ppd=ppd,
+            rotation=rotation,
+            frequency=frequency,
+            phase_shift=phase_shift,
+            intensity_frames=(int1, int2),
+            origin=origin,
+            round_phase_width=round_phase_width,
+            period=period,
+            target_indices=target_indices,
+            intensity_target=intensity_target,
+            clip=clip,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -781,6 +818,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.waves.staircase_rectilinear`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.waves import staircase_rectilinear
 
 # Define widgets
@@ -830,21 +869,24 @@ def show_staircase_rectilinear(
     intensity_target=None,
     clip=True,
 ):
-    stim = staircase_rectilinear(
-        visual_size=(height, width),
-        ppd=ppd,
-        rotation=rotation,
-        frequency=frequency,
-        phase_shift=phase_shift,
-        intensity_frames=(int1, int2),
-        origin=origin,
-        round_phase_width=round_phase_width,
-        period=period,
-        target_indices=target_indices,
-        intensity_target=intensity_target,
-        clip=clip,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = staircase_rectilinear(
+            visual_size=(height, width),
+            ppd=ppd,
+            rotation=rotation,
+            frequency=frequency,
+            phase_shift=phase_shift,
+            intensity_frames=(int1, int2),
+            origin=origin,
+            round_phase_width=round_phase_width,
+            period=period,
+            target_indices=target_indices,
+            intensity_target=intensity_target,
+            clip=clip,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -876,6 +918,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.waves.sine_angular`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.waves import sine_angular
 
 # Define widgets
@@ -923,20 +967,23 @@ def show_sine_angular(
     target_indices=None,
     intensity_target=None,
 ):
-    stim = sine_angular(
-        visual_size=(height, width),
-        ppd=ppd,
-        rotation=rotation,
-        frequency=frequency,
-        phase_shift=phase_shift,
-        intensities=(int1, int2),
-        origin=origin,
-        round_phase_width=round_phase_width,
-        period=period,
-        target_indices=target_indices,
-        intensity_target=intensity_target,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = sine_angular(
+            visual_size=(height, width),
+            ppd=ppd,
+            rotation=rotation,
+            frequency=frequency,
+            phase_shift=phase_shift,
+            intensities=(int1, int2),
+            origin=origin,
+            round_phase_width=round_phase_width,
+            period=period,
+            target_indices=target_indices,
+            intensity_target=intensity_target,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -967,6 +1014,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.waves.square_angular`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.waves import square_angular
 
 # Define widgets
@@ -1014,20 +1063,23 @@ def show_square_angular(
     target_indices=None,
     intensity_target=None,
 ):
-    stim = square_angular(
-        visual_size=(height, width),
-        ppd=ppd,
-        rotation=rotation,
-        frequency=frequency,
-        phase_shift=phase_shift,
-        intensity_segments=(int1, int2),
-        origin=origin,
-        round_phase_width=round_phase_width,
-        period=period,
-        target_indices=target_indices,
-        intensity_target=intensity_target,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = square_angular(
+            visual_size=(height, width),
+            ppd=ppd,
+            rotation=rotation,
+            frequency=frequency,
+            phase_shift=phase_shift,
+            intensity_segments=(int1, int2),
+            origin=origin,
+            round_phase_width=round_phase_width,
+            period=period,
+            target_indices=target_indices,
+            intensity_target=intensity_target,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -1058,6 +1110,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.waves.staircase_angular`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.waves import staircase_angular
 
 # Define widgets
@@ -1105,20 +1159,23 @@ def show_staircase_angular(
     target_indices=None,
     intensity_target=None,
 ):
-    stim = staircase_angular(
-        visual_size=(height, width),
-        ppd=ppd,
-        rotation=rotation,
-        frequency=frequency,
-        phase_shift=phase_shift,
-        intensity_segments=(int1, int2),
-        origin=origin,
-        round_phase_width=round_phase_width,
-        period=period,
-        target_indices=target_indices,
-        intensity_target=intensity_target,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = staircase_angular(
+            visual_size=(height, width),
+            ppd=ppd,
+            rotation=rotation,
+            frequency=frequency,
+            phase_shift=phase_shift,
+            intensity_segments=(int1, int2),
+            origin=origin,
+            round_phase_width=round_phase_width,
+            period=period,
+            target_indices=target_indices,
+            intensity_target=intensity_target,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
diff --git a/docs/reference/demos/stimuli/wedding_cakes.md b/docs/reference/demos/stimuli/wedding_cakes.md
index 359057f8..65322d22 100644
--- a/docs/reference/demos/stimuli/wedding_cakes.md
+++ b/docs/reference/demos/stimuli/wedding_cakes.md
@@ -12,26 +12,27 @@ kernelspec:
   name: python3
 ---
 
-```{important}
+```{tip}
 [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/wedding_cakes.md)
  to get interactivity
 ```
+```{attention}
+To run locally, the code for these interactive demos requires
+a [Jupyter Notebook](https://jupyter.org/) environment,
+and the [Jupyter Widgets extension (`ipywidgets`)](https://ipywidgets.readthedocs.io/en/latest/index.html).
+```
 
 # Stimuli - Wedding cakes
 {py:mod}`stimupy.stimuli.wedding_cakes`
 
-```{code-cell} ipython3
-:tags: [remove-cell]
 
-import IPython
-import ipywidgets as iw
-from stimupy.utils import plot_stim
-```
 
 ## Wedding cake
 {py:func}`stimupy.stimuli.wedding_cakes.wedding_cake`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.wedding_cakes import wedding_cake
 
 # Define widgets
@@ -83,17 +84,20 @@ def show_wedding_cake(
     theight=None,
     int_target=None,
 ):
-    stim = wedding_cake(
-        visual_size=(height, width),
-        ppd=ppd,
-        L_size=(L_height, L_width, L_thick),
-        target_height=theight,
-        intensity_target=int_target,
-        target_indices1=((tidx11, tidx12),),
-        target_indices2=((tidx21, tidx22),),
-        intensity_bars=(int1, int2),
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = wedding_cake(
+            visual_size=(height, width),
+            ppd=ppd,
+            L_size=(L_height, L_width, L_thick),
+            target_height=theight,
+            intensity_target=int_target,
+            target_indices1=((tidx11, tidx12),),
+            target_indices2=((tidx21, tidx22),),
+            intensity_bars=(int1, int2),
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
diff --git a/docs/reference/demos/stimuli/whites.md b/docs/reference/demos/stimuli/whites.md
index d3b587a3..5574b101 100644
--- a/docs/reference/demos/stimuli/whites.md
+++ b/docs/reference/demos/stimuli/whites.md
@@ -12,26 +12,27 @@ kernelspec:
   name: python3
 ---
 
-```{important}
+```{tip}
 [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/computational-psychology/stimupy/HEAD?urlpath=lab/tree/docs/reference/demos/stimuli/whites.md)
  to get interactivity
 ```
+```{attention}
+To run locally, the code for these interactive demos requires
+a [Jupyter Notebook](https://jupyter.org/) environment,
+and the [Jupyter Widgets extension (`ipywidgets`)](https://ipywidgets.readthedocs.io/en/latest/index.html).
+```
 
 # Stimuli - Whites
 {py:mod}`stimupy.stimuli.whites`
 
-```{code-cell} ipython3
-:tags: [remove-cell]
 
-import IPython
-import ipywidgets as iw
-from stimupy.utils import plot_stim
-```
 
 ## Generalized
 {py:func}`stimupy.stimuli.whites.generalized`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.whites import generalized
 
 # Define widgets
@@ -90,20 +91,23 @@ def show_generalized(
     target_center_offsets2=None,
     target_heights2=None,
 ):
-    stim = generalized(
-        visual_size=(height, width),
-        ppd=ppd,
-        rotation=rotation,
-        frequency=frequency,
-        intensity_bars=(int1, int2),
-        origin=origin,
-        period=period,
-        target_indices=(target_idx, target_idx2),
-        intensity_target=(intensity_target, intensity_target2),
-        target_center_offsets=(target_center_offsets, target_center_offsets2),
-        target_heights=(target_heights, target_heights2),
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = generalized(
+            visual_size=(height, width),
+            ppd=ppd,
+            rotation=rotation,
+            frequency=frequency,
+            intensity_bars=(int1, int2),
+            origin=origin,
+            period=period,
+            target_indices=(target_idx, target_idx2),
+            intensity_target=(intensity_target, intensity_target2),
+            target_center_offsets=(target_center_offsets, target_center_offsets2),
+            target_heights=(target_heights, target_heights2),
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -138,6 +142,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.whites.white`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.whites import white
 
 # Define widgets
@@ -192,19 +198,22 @@ def show_white(
     target_heights=None,
     target_heights2=None,
 ):
-    stim = white(
-        visual_size=(height, width),
-        ppd=ppd,
-        rotation=rotation,
-        frequency=frequency,
-        intensity_bars=(int1, int2),
-        origin=origin,
-        period=period,
-        target_indices=(target_idx, target_idx2),
-        intensity_target=(intensity_target, intensity_target2),
-        target_heights=(target_heights, target_heights2),
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = white(
+            visual_size=(height, width),
+            ppd=ppd,
+            rotation=rotation,
+            frequency=frequency,
+            intensity_bars=(int1, int2),
+            origin=origin,
+            period=period,
+            target_indices=(target_idx, target_idx2),
+            intensity_target=(intensity_target, intensity_target2),
+            target_heights=(target_heights, target_heights2),
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -237,6 +246,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.whites.white_two_rows`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.whites import white_two_rows
 
 # Define widgets
@@ -295,21 +306,24 @@ def show_white_two_rows(
     intensity_target2=None,
     target_heights2=None,
 ):
-    stim = white_two_rows(
-        visual_size=(height, width),
-        ppd=ppd,
-        rotation=rotation,
-        frequency=frequency,
-        intensity_bars=(int1, int2),
-        origin=origin,
-        period=period,
-        target_indices_top=target_idx,
-        target_indices_bottom=target_idx2,
-        intensity_target=(intensity_target, intensity_target2),
-        target_center_offset=target_center_offsets,
-        target_heights=(target_heights, target_heights2),
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = white_two_rows(
+            visual_size=(height, width),
+            ppd=ppd,
+            rotation=rotation,
+            frequency=frequency,
+            intensity_bars=(int1, int2),
+            origin=origin,
+            period=period,
+            target_indices_top=target_idx,
+            target_indices_bottom=target_idx2,
+            intensity_target=(intensity_target, intensity_target2),
+            target_center_offset=target_center_offsets,
+            target_heights=(target_heights, target_heights2),
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -343,6 +357,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.whites.anderson`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.whites import anderson
 
 # Define widgets
@@ -406,22 +422,25 @@ def show_anderson(
     stripe_int1=None,
     stripe_int2=None,
 ):
-    stim = anderson(
-        visual_size=(height, width),
-        ppd=ppd,
-        frequency=frequency,
-        intensity_bars=(int1, int2),
-        period=period,
-        target_indices_top=target_idx,
-        target_indices_bottom=target_idx2,
-        intensity_target=(intensity_target, intensity_target2),
-        target_center_offset=target_center_offsets,
-        target_height=(target_heights, target_heights2),
-        stripe_height=stripe_height,
-        stripe_center_offset=stripe_center_offset,
-        intensity_stripes=(stripe_int1, stripe_int2),
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = anderson(
+            visual_size=(height, width),
+            ppd=ppd,
+            frequency=frequency,
+            intensity_bars=(int1, int2),
+            period=period,
+            target_indices_top=target_idx,
+            target_indices_bottom=target_idx2,
+            intensity_target=(intensity_target, intensity_target2),
+            target_center_offset=target_center_offsets,
+            target_height=(target_heights, target_heights2),
+            stripe_height=stripe_height,
+            stripe_center_offset=stripe_center_offset,
+            intensity_stripes=(stripe_int1, stripe_int2),
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -457,6 +476,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.whites.howe`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.whites import howe
 
 # Define widgets
@@ -516,20 +537,23 @@ def show_howe(
     stripe_int1=None,
     stripe_int2=None,
 ):
-    stim = howe(
-        visual_size=(height, width),
-        ppd=ppd,
-        frequency=frequency,
-        intensity_bars=(int1, int2),
-        period=period,
-        target_indices_top=target_idx,
-        target_indices_bottom=target_idx2,
-        intensity_target=(intensity_target, intensity_target2),
-        target_center_offset=target_center_offsets,
-        target_height=(target_heights, target_heights2),
-        intensity_stripes=(stripe_int1, stripe_int2),
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = howe(
+            visual_size=(height, width),
+            ppd=ppd,
+            frequency=frequency,
+            intensity_bars=(int1, int2),
+            period=period,
+            target_indices_top=target_idx,
+            target_indices_bottom=target_idx2,
+            intensity_target=(intensity_target, intensity_target2),
+            target_center_offset=target_center_offsets,
+            target_height=(target_heights, target_heights2),
+            intensity_stripes=(stripe_int1, stripe_int2),
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -563,6 +587,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.whites.yazdanbakhsh`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.whites import yazdanbakhsh
 
 # Define widgets
@@ -618,20 +644,23 @@ def show_yazdanbakhsh(
     target_heights2=None,
     gap_size=None,
 ):
-    stim = yazdanbakhsh(
-        visual_size=(height, width),
-        ppd=ppd,
-        frequency=frequency,
-        intensity_bars=(int1, int2),
-        period=period,
-        target_indices_top=target_idx,
-        target_indices_bottom=target_idx2,
-        intensity_target=(intensity_target, intensity_target2),
-        target_center_offset=target_center_offsets,
-        target_heights=(target_heights, target_heights2),
-        gap_size=gap_size,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = yazdanbakhsh(
+            visual_size=(height, width),
+            ppd=ppd,
+            frequency=frequency,
+            intensity_bars=(int1, int2),
+            period=period,
+            target_indices_top=target_idx,
+            target_indices_bottom=target_idx2,
+            intensity_target=(intensity_target, intensity_target2),
+            target_center_offset=target_center_offsets,
+            target_heights=(target_heights, target_heights2),
+            gap_size=gap_size,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -664,6 +693,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.whites.radial`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.whites import radial
 
 # Define widgets
@@ -713,21 +744,24 @@ def show_radial(
     intensity_target=None,
     clip=True,
 ):
-    stim = radial(
-        visual_size=(height, width),
-        ppd=ppd,
-        rotation=rotation,
-        frequency=frequency,
-        phase_shift=phase_shift,
-        intensity_rings=(int1, int2),
-        origin=origin,
-        round_phase_width=round_phase_width,
-        period=period,
-        target_indices=target_indices,
-        intensity_target=intensity_target,
-        clip=clip,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = radial(
+            visual_size=(height, width),
+            ppd=ppd,
+            rotation=rotation,
+            frequency=frequency,
+            phase_shift=phase_shift,
+            intensity_rings=(int1, int2),
+            origin=origin,
+            round_phase_width=round_phase_width,
+            period=period,
+            target_indices=target_indices,
+            intensity_target=intensity_target,
+            clip=clip,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -759,6 +793,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.whites.angular`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.whites import angular
 
 # Define widgets
@@ -806,20 +842,23 @@ def show_angular(
     origin=None,
     add_mask=False,
 ):
-    stim = angular(
-        visual_size=(height, width),
-        ppd=ppd,
-        rotation=rotation,
-        n_segments=n_segments,
-        intensity_segments=(intensity1, intensity2),
-        intensity_background=intensity_background,
-        origin=origin,
-        target_indices=target_indices,
-        target_width=target_width,
-        target_center=target_center,
-        intensity_target=intensity_target,
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = angular(
+            visual_size=(height, width),
+            ppd=ppd,
+            rotation=rotation,
+            n_segments=n_segments,
+            intensity_segments=(intensity1, intensity2),
+            intensity_background=intensity_background,
+            origin=origin,
+            target_indices=target_indices,
+            target_width=target_width,
+            target_center=target_center,
+            intensity_target=intensity_target,
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
@@ -850,6 +889,8 @@ display(ui, out)
 {py:func}`stimupy.stimuli.whites.wedding_cake`
 
 ```{code-cell} ipython3
+import ipywidgets as iw
+from stimupy.utils import plot_stim
 from stimupy.stimuli.whites import wedding_cake
 
 # Define widgets
@@ -901,17 +942,20 @@ def show_wedding_cake(
     theight=None,
     int_target=None,
 ):
-    stim = wedding_cake(
-        visual_size=(height, width),
-        ppd=ppd,
-        L_size=(L_height, L_width, L_thick),
-        target_height=theight,
-        intensity_target=int_target,
-        target_indices1=((tidx11, tidx12),),
-        target_indices2=((tidx21, tidx22),),
-        intensity_bars=(int1, int2),
-    )
-    plot_stim(stim, mask=add_mask)
+    try:
+        stim = wedding_cake(
+            visual_size=(height, width),
+            ppd=ppd,
+            L_size=(L_height, L_width, L_thick),
+            target_height=theight,
+            intensity_target=int_target,
+            target_indices1=((tidx11, tidx12),),
+            target_indices2=((tidx21, tidx22),),
+            intensity_bars=(int1, int2),
+        )
+        plot_stim(stim, mask=add_mask)
+    except Exception as e:
+        raise ValueError(f"Invalid parameter combination: {e}") from None
 
 # Set interactivity
 out = iw.interactive_output(
diff --git a/docs/topic_guides/dimensions.md b/docs/topic_guides/dimensions.md
index 65db3ce6..84ff9056 100644
--- a/docs/topic_guides/dimensions.md
+++ b/docs/topic_guides/dimensions.md
@@ -1,1 +1,184 @@
-# Dimension in image
+---
+jupytext:
+  formats: md:myst
+  text_representation:
+    extension: .md
+    format_name: myst
+kernelspec:
+  display_name: Python 3
+  language: python
+  name: python3
+---
+# Axes, orientations, and distance metrics
+Digital images have a natural coordinate scheme with two axes:
+a vertical $y$-axis, and horizontal $x$-axis.
+````{margin}
+```{note}
+Note that array-_indexing_, and in general pixel indexing
+is vertically flipped from e.g. graphing coordinates:
+row $0$ is the _top_ row of pixels, and row indices increase going down.
+```
+````
+This is how pixels are organized in a screen,
+and this is how the pixel-intensities are encoded as image matrices (e.g., `numpy.ndarray`).
+But not all stimuli are most sensible _defined_ in these coordinates,
+and `stimupy` has several more axes and distances it can use.
+
+## Horizontal, vertical, oblique
+As mentioned, all images have a natural vertical and horizontal axes.
+Where used, `stimupy` expects them in this order: `(y, x)` or `(height, width)`
+-- in line with computer science and specifically NumPy conventions.
+Thus `visual_size=(12, 16)` indicates and image
+that is $12$ degrees high, and $16$ degrees wide.
+Positions, too, are generally specified in these coordinates:
+`rectangle_position=(2, 1)` places the rectangle
+$3$ degrees from the top and $5$ degree to the right:
+```{code-cell}
+from stimupy.components import shapes
+from stimupy.utils import plot_stim
+
+stim = shapes.rectangle(visual_size=(12, 16), ppd=10,
+                        rectangle_size=(4, 4),
+                        rectangle_position=(3, 5))
+
+plot_stim(stim)
+```
+
+While the image-axes are vertical & horizontal,
+`stimupy` allows you to easily draw oblique or rotated stimuli.
+Almost all stimulus-functions have a `rotation` argument,
+which expresses the rotation in degrees, from counterclockwise from the 3 o'clock position
+(in line with angles around a unit circle).
+Note that the _stimulus_ `size` is determined along the _rotated_ $y, x$ axes,
+but the _image_ size and stimulus `position` are in the original frame of reference.
+```{code-cell}
+stim = shapes.rectangle(visual_size=(12, 16), ppd=10,
+                        rectangle_size=(4, 4),
+                        rectangle_position=(3, 5),
+                        rotation=30)
+
+plot_stim(stim)
+```
+
+<!-- ## Origin
+```{code-cell}
+stim = shapes.rectangle(visual_size=(12, 16), ppd=10,
+                        rectangle_size=(4, 4),
+                        rectangle_position=(3, 5),
+                        rotation=30,
+                        origin="mean")
+
+plot_stim(stim)
+```
+-->
+
+## Radial
+Some stimuli are more naturally defined not along a linear axis like the horizontal,
+vertical, and rotated/oblique axes.
+One example of this is a circle,
+which is more naturally defined using a given _radius_,
+radiating outwards from a point of origin.
+```{code-cell}
+stim = shapes.circle(visual_size=(12, 16), ppd=10,
+                     radius=2)
+
+plot_stim(stim)
+```
+Here the point of origin is by default the center of the image,
+but this can be changed
+The `origin` parameter can be an actual `center` pixel,
+the hypothetical image `mean` (if there is no single pixel at this exact location),
+or the topleft `corner` of the image.
+```{code-cell}
+stim = shapes.circle(visual_size=(12, 16), ppd=10,
+                     radius=2,
+                     origin='corner')
+
+plot_stim(stim)
+```
+
+## Distance metric
+We term these aforementioned axes a _`distance_metric`_,
+as they measure distance from some point of origin.
+This way of thinking is especially important
+for wave specifications in {py:mod}`stimupy.components.waves`
+and their use in wave-like stimuli in {py:mod}`stimupy.stimuli.waves`.
+For the linear metrics (`horizontal`, `vertical`, `oblique`),
+those waves measure distance along that axis (from the `center` by default):
+```{code-cell}
+from stimupy.components import waves
+
+stim = waves.sine(visual_size=(12, 16), ppd=10,
+                  distance_metric="horizontal",
+                  frequency=.2)
+
+plot_stim(stim)
+```
+But this can be changed to another `origin` as well:
+```{code-cell}
+stim = waves.sine(visual_size=(12, 16), ppd=10,
+                  distance_metric="horizontal",
+                  frequency=.2,
+                  origin='corner')
+
+plot_stim(stim)
+```
+
+The `radial` stimuli are a good way of understanding the `distance_metric`:
+each pixel is some distance away from the origin (`center` by default),
+which is the relevant metric for determining whether that pixel
+is part of the targeted region:
+```{code-cell}
+stim = waves.sine(visual_size=(12, 16), ppd=10,
+                  distance_metric="radial",
+                  frequency=.2)
+
+plot_stim(stim)
+```
+```{note}
+The `radial` distance is calculated as $r = \sqrt{x^2 + y^2}$.
+```
+## Rectilinear
+A more intriguing `distance_metric` is the `rectilinear` metric,
+which does not correspond to some singular linear axis in the image.
+```{note}
+The `rectilinear` distance is calculated as $d = \max{x^2, y^2}$
+```
+This creates a frame-like, or "squared-off" radial, distance:
+the `rectilinear` distance of a pixel to the origin
+is determined by the largest of the two linear distances.
+```{code-cell}
+stim = waves.sine(visual_size=(12, 16), ppd=10,
+                  distance_metric="rectilinear",
+                  frequency=.2)
+
+plot_stim(stim)
+```
+It is related to metrics that are often called "Manhattan", "taxicab", "cityblock".
+
+## Angular
+Another interesting `distance_metric` is the `angular` distance,
+which uniquely has a fixed range of $[0, 360]$.
+```{note}
+The `angular` distance is calculated as the angle from 3 o'clock position (in degrees).
+```
+
+```{code-cell}
+stim = waves.sine(visual_size=(12, 16), ppd=10,
+                  distance_metric="angular",
+                  frequency=10)
+
+plot_stim(stim)
+```
+
+<!-- ## Overview
+An overview of all `distance_metrics`, with pixel intensities ranging from $[0, 1]$
+along each metric, looks like this:
+```{code-cell}
+from stimupy.components import image_base
+
+
+
+plot_stim(stim)
+```
+ -->
diff --git a/docs/topic_guides/gratings.md b/docs/topic_guides/gratings.md
deleted file mode 100644
index 3d30b733..00000000
--- a/docs/topic_guides/gratings.md
+++ /dev/null
@@ -1,1 +0,0 @@
-# Specifying gratings
diff --git a/docs/topic_guides/organization.md b/docs/topic_guides/organization.md
index 5c26d1c6..d4b6b04d 100644
--- a/docs/topic_guides/organization.md
+++ b/docs/topic_guides/organization.md
@@ -12,11 +12,11 @@ kernelspec:
 # How stimupy is organized
 
 Broadly, `stimupy` functions currently fall into the following categories, which are also the toplevel submodules of `stimupy`:
-- basic visual stimulus [components](../reference/_api/stimupy.components),
-  such as basic shapes, wave gratings, Gaussians
+- visual stimulus [components](../reference/_api/stimupy.components),
+  such as shapes, wave / gratings, Gaussians, lines
 - visual [noise](../reference/_api/stimupy.noises) textures, of different kinds,
 - parameterized visual [stimuli](../reference/_api/stimupy.stimuli)
-  - Gabors, plaids, edges,
+  - Gabors, plaids, edges, Mondrians,
   - a variety of so-called illusions 
    (e.g. Simultaneous Brightness Contrast, White's illusion, Hermann grid, Ponzo illusion), and many more
 - exact replications of stimuli previously published (e.g. ModelFest)
@@ -35,23 +35,21 @@ from stimupy.stimuli import ponzos, shapes
 will work easily.
 :::
 ::::
-In principle, every component can also be considered a stimulus:
-it is some visual feature that can be drawn as an image.
+There is no objective way to differentiate components from stimuli.
+In principle, every component could be considered a stimulus.
 The distinctions we make in `stimupy` are:
-firstly, that the `components` underly (many) different stimuli,
-and are "atomic" in a sense;
-secondly, most/all `stimuli` have some concept of one or more *target*(s)
--- a region of special scientific interest.
-Thus, most/all `stimuli` come with a `target_mask`
-that indicate these targets.
+firstly, that the `components` are "atomic" in a sense and hence underlie multiple different stimuli;
+secondly, most `stimuli` contain *target*(s)
+-- a region of special scientific interest --, and come with a `target_mask`
+that indicates these targets.
 
 ## Submodules
-Further subdividing the overall structure are lots of submodules.
+The overall structure of `stimupy` is further subdivided into many submodules.
 These submodules are organized along scientific interest,
 history, convention, etc., rather than engineering.
-Thus, a you should find a stimulus in a submodule
-with those stimuli *that is looks like* or *is related to*,
-rather than with those stimuli that it shares components or code with
+Thus, you will find a stimulus in a submodule
+with stimuli that either *look similar* or that are *related to each other*,
+rather than with stimuli that it shares components or code with
 (although these two criteria can overlap, of course).
 
 Moreover, the submodules all have pluralized names,
@@ -67,11 +65,10 @@ there may be multiple functions, see next section.
 
 All roads lead to Rome,
 and many ways lead to the same stimulus.
-For lots of the stimuli provided by `stimupy`,
-there are numerous different ways one can draw it.
-Often, these different ways of drawing
-are informed by the visual and geometric interpretation
-of who is doing the drawing.
+For some stimuli provided by `stimupy`,
+there are multiple ways to generate a specific stimulus.
+Typically, these different ways are
+informed by visual and geometric interpretations.
 
 A good example of this is the Todorovic Illusion, which one can interpret as
 having a [rectangular target](todorovics.rectangle) that is partially occluded by some "covers"
diff --git a/docs/topic_guides/resolution.md b/docs/topic_guides/resolution.md
index 7199a3f0..7179ceaf 100644
--- a/docs/topic_guides/resolution.md
+++ b/docs/topic_guides/resolution.md
@@ -1,1 +1,77 @@
-# Image resolution
+# Image size and resolution
+
+## Shape in pixels
+Digital images, such as stimuli, on a computer screen are drawn
+with a certain `shape` in _pixels_.
+The screen itself has a certain _resolution_,
+e.g. $1024 \times 768$ pixels (width $\times$ height),
+and a stimulus-images will take up some portion of that.
+`stimupy` stimuli are produced and stored as 2-dimensional `numpy.ndarray`s
+where each array-entry corresponds to the intensity of a single pixel.
+In line with `numpy` convention,
+`stimupy` refers to the `shape` of the stimulus in pixels,
+which is the corresponding `.shape`-attribute of the `numpy.ndarray`.
+
+## Visual size, in degrees visual angle
+The proximal visual size of a stimulus as it reaches an observer
+is not just based on the physical size (in cm, in, or pix) of the stimulus,
+but also on the distance between the distal stimulus and the observer.
+A physically large distal stimulus, will still appear small to an observer,
+if it is far away -- and vice versa.
+To take this into account, when specifying stimuli
+we use the `visual_size`, expressed in _degrees of visual angle_.
+This is an unambiguous specification of the size of the proximal stimulus
+(as it arrives at the observer's eye),
+regardless of the physical size of the (distal) image and its distance.
+
+All size-parameters in `stimupy` are expressed in degrees visual angle.
+
+## Resolution, in pixels per degree visual angle
+To draw a stimulus of a given `visual_size`
+as an image with a `shape` composed of discrete pixels,
+a _resolution_ that maps between the two has to be provided as well.
+This is measured in _pixels per degree_  -- `ppd` for short --
+the amount of pixels that comprise a single degree of visual angle.
+
+For stimuli presented on a screen to an observer,
+the `ppd` can be calculated from
+the resolution of the screen (in pixels),
+the physical size of the screen (in cm, in),
+and the distance between observer and screen.
+In other words, the `ppd` depends on the display setup
+and is not a property of the stimulus.
+However, the `ppd` is _necessary_ to take
+a specification of a stimulus defined in "natural" units of `visual_size`
+an implement it as a digital image in pixels.
+
+
+## Specifying size & resolution
+`stimupy` stimulus-functions take these three size & resolution parameters:
+`shape` in pixels, `visual_size` in degrees of visual angle, and `ppd`.
+However, these three parameters are inter-dependent:
+the `shape` can be calculated from `visual_size` and `ppd`;
+a specification with given `visual_size` and `shape` requires a specific `ppd`.
+
+Thus, not all combinations are valid resolutions,
+e.g., if the `visual_size` $\times$ `ppd` does not match the given `shape`.
+`stimupy` functions will raise a ResolutionException in these cases.
+The {py:func}`stimupy.utils.valid_resolution <stimupy.utils.resolution.valid_resolution>`
+function performs this assertion, and can be used to check that a resolution is valid.
+
+This interdependence also means that
+specifying all three parameters is not strictly necessary; 
+if two are specified, the third can always be calculated.
+In `stimupy`, functions that receive only two out of three resolution parameters,
+or where one is explicitly set to `None`, will automatically resolve the resolution:
+```{code-block} python
+rectangle(visual_size=10, ppd=10, shape=None)
+# or implicit None:
+rectangle(visual_size=10, ppd=10)
+```
+This resolving is done using {py:func}`stimupy.utils.resolution.resolve`.
+% ## Additional aspects
+
+% ### Plot units
+% ### Origin
+
+% ### 1D vs. 2D
\ No newline at end of file
diff --git a/docs/topic_guides/sets_papers.md b/docs/topic_guides/sets_papers.md
deleted file mode 100644
index 26e66be2..00000000
--- a/docs/topic_guides/sets_papers.md
+++ /dev/null
@@ -1,1 +0,0 @@
-# Sets of stimuli, papers
diff --git a/docs/topic_guides/waves.md b/docs/topic_guides/waves.md
new file mode 100644
index 00000000..d7f21559
--- /dev/null
+++ b/docs/topic_guides/waves.md
@@ -0,0 +1,297 @@
+---
+jupytext:
+  formats: md:myst
+  text_representation:
+    extension: .md
+    format_name: myst
+kernelspec:
+  display_name: Python 3
+  language: python
+  name: python3
+---
+
+# Periodic stimuli (waves, gratings, grids,...)
+```{tip}
+This guide explains the principles behind, behavior of, and relations between
+the parameters of periodic stimuli in `stimupy`.
+For a more hands-on demonstration of how those paramaters affect a specific stimulus,
+see, e.g., [](../reference/demos/stimuli/waves.md)
+```
+
+Many commonly used visual stimuli are some sort of periodic stimulus:
+sinewaves gratings are the most obvious example,
+but also checkerboards, grids, sets of rings, and pinwheels can be considered periodic.
+`stimupy` includes many of these as stimuli and constructs them as periodic,
+which has some particular advantages.
+The simplest variants can be found in {py:mod}`stimupy.stimuli.waves`
+
+```{code-cell}
+from stimupy.stimuli import waves
+
+from stimupy.utils import plot_stimuli
+```
+
+## Interdependence of parameters
+Periodic stimuli (e.g., `waves`)
+consist of repetitions of some _elements_ or _phases_.
+In a sinewave grating, these are the light and dark phases.
+in a squarewave grating, those same phases visually appears as white and black `bars`:
+```{code-cell}
+sinewave = waves.sine_linear(visual_size=(10, 10), ppd=10, n_bars=5)
+squarewave = waves.square_linear(visual_size=(10, 10), ppd=10, n_bars=5)
+
+plot_stimuli({'Sine': sinewave, 'Square': squarewave})
+```
+
+There is regularity to periodic stimuli (what makes the periodic),
+namely that there are repeated _cycles_ (here of 2 phases/elements per cycle),
+where each cycle is identical to each other cycle.
+This regularity can be expressed in several different ways,
+with several different parameters 
+-- and these parameters have some interdependence on eachother.
+
+```{tip}
+In the examples in this section,
+the code for the `sinewave` will remain the same across examples,
+the code for the `squarewave` will demonstrate the different parameterization
+(keeping the output images the same)
+```
+
+### Frequency $\sim$ number of elements
+The number of elements can also be expressed a _spatial frequency_:
+here we have $5$ bars, which corresponds to $5/2 = 2.5$ _cycles_ of light-dark.
+$2.5$ cycles, in $10$ degrees of visual angle,
+is $2.5/10 = .25$ cycles per degree  of visual angle (_cpd_).
+
+In some cases, you may want to conceptualize your stimulus in terms of this `frequency`,
+in other cases you may want to conceptualize your stimulus in terms of the `n_bars`.
+`stimupy` allows you to do either:
+```{code-cell}
+sinewave = waves.sine_linear(visual_size=(10, 10), ppd=10, n_bars=5)
+squarewave = waves.square_linear(visual_size=(10, 10), ppd=10, frequency=.25)
+
+plot_stimuli({'Sine': sinewave, 'Square': squarewave})
+```
+
+### Phase width vs. visual size
+To specify a periodic stimulus like these waves, a third parameter at play is
+the `phase_width`, i.e., the size of each element (in this case: `bar_width`).
+So far, we have specified the overall `visual_size` of the stimulus (e.g., `(10, 10)`).
+To draw `n_bars=5` ($=$ `frequency=.25`) in $10$ degrees,
+each bar (/ phase) has `bar_width` of $2$ degrees of visual of angle.
+
+Sometimes, you may care more about specifying this size of each element,
+rather than the total visual size of the stimulus (which is then resolved automatically)
+`stimupy` allows you to do either:
+```{code-cell}
+sinewave = waves.sine_linear(visual_size=(10, 10), ppd=10, n_bars=5)
+squarewave = waves.square_linear(visual_size=None, ppd=10, n_bars=5, bar_width=2)
+
+plot_stimuli({'Sine': sinewave, 'Square': squarewave})
+```
+
+### Phase width vs. number of elements / frequency
+Since only a certain number of elements with given width can be drawn
+in a given total size,
+this means that if we specify both `visual_size` and `bar_width`,
+the number of bars is also determined:
+```{code-cell}
+sinewave = waves.sine_linear(visual_size=(10, 10), ppd=10, n_bars=5)
+squarewave = waves.square_linear(visual_size=(10, 10), ppd=10, bar_width=2)
+
+plot_stimuli({'Sine': sinewave, 'Square': squarewave})
+```
+
+### Periodicity & resolution
+In general, to specify these periodic signals in a way that can be drawn,
+there are $7$ relevant/possible parameters:
+- resolution parameters `visual_size`, `ppd`, `shape`
+- periodicity parameters `visual_size`, `frequency`, `bar_width`, `n_bars`
+
+The relations are such that:
+- $2$ out of $3$ resolution parameters are required to resolve
+- `frequency` (in cpd) and `bar_width` (in degrees per phase) are perfectly related:
+  `frequency = 1/(bar_width*2)`
+- $2$ out of $4$ periodicity parameters are required to resolve
+
+Since `visual_size` is _both_ a resolution and a periodicity parameter,
+it can be resolved from one set and used for the other.
+Thus, a specification of, e.g. `ppd`, `n_bars` and `bar_width` is enough:
+```{code-cell}
+sinewave = waves.sine_linear(visual_size=(10, 10), ppd=10, n_bars=5)
+squarewave = waves.square_linear(ppd=10, n_bars=5, bar_width=2)
+
+plot_stimuli({'Sine': sinewave, 'Square': squarewave})
+```
+
+## Even, odd or integer number of elements
+Particularly for gratings, the requirements on the number of elements can be very different depending on the use-case.
+In some cases, you may want to ensure that your grating has an integer number of phases,
+e.g., if you want to avoid partial bars in a squarewave grating.
+For other stimuli, you may be fine with an "incomplete" phase at the end, e.g. if your sinewave grating consists of 7.1 cycles.
+In yet other situations, you may want to be even more specific and may want to make sure that you have an _even_ number of phases in your grating,
+(i.e. a full number of _periods_, as many bright as dark bars),
+or an _odd_ number of phases.
+The `period` argument controls this behavior
+when `stimupy` resolves the periodicity for you.
+it can be `"even"`, `"odd"`, `"either"` (ensures only complete phases), or `"ignore"`:
+```{code-cell}
+sinewave = waves.sine_linear(visual_size=(10, 10), ppd=10, n_bars=5)
+
+# Resolve to "odd" number of phases
+squarewave = waves.square_linear(visual_size=(10, 10), ppd=10, bar_width=2.5, period="odd")
+
+plot_stimuli({'Sine': sinewave, 'Square': squarewave})
+```
+
+## Poor resolution, bad sampling
+Not all `n_phases`/`frequency`s are (equally) possible in a given image `shape`.
+Specifically not all _`phase_widths`_ are possible for a given `ppd`.
+if there $10$ pixels per degree of visual angle,
+that each pixel increase means an increase of $1/10$ degrees of visual angle.
+Thus, at $ppd=5$, the width of a, e.g., a bar can only change in $1/10$ degree intervals
+-- meaning the `phase_width` can be $2, 2.1, 2.2$ degrees, but not $2.12$ degrees.
+
+````{margin}
+```{note}
+This is the same problem as _sampling_ in digital signal processing,
+where a given frequency cannot be displayed/analyzed properly 
+for a given sampling rate ($\sim$ ppd)
+```
+````
+
+If desired (i.e. if _`round_phase_width=True`_), `stimupy` supports the user in cases where the given parameter values
+result in a poor periodicity $\times$ resolution combination.
+Specifically, it will adjust/round the _periodicity_ parameters to try and accomplish
+the closest possible version of the stimulus, and warn you about this change.
+Which way it rounds to, can be determined by the `period` argument:
+```{code-cell}
+sinewave = waves.sine_linear(visual_size=(10, 10), ppd=10, n_bars=5)
+
+# Poor resolution:
+squarewave = waves.square_linear(visual_size=(10, 10), ppd=10, bar_width=2.12, period="either")
+
+plot_stimuli({'Sine': sinewave, 'Square': squarewave})
+```
+
+In fact, it will adjust whichever parameter was/were passed in:
+```{code-cell}
+sinewave = waves.sine_linear(visual_size=(10, 10), ppd=10, n_bars=5)
+
+# Poor frequency:
+squarewave = waves.square_linear(visual_size=(10, 10), ppd=10, frequency=.26, period="either")
+
+plot_stimuli({'Sine': sinewave, 'Square': squarewave})
+```
+
+## Periodic in other orientations, distance metrics
+So far, you have seen _linear_ periodic stimuli, specifically horizontal gratings.
+We can rotate these, using the `rotation` parameter:
+```{code-cell}
+horizontal = waves.square_linear(visual_size=(10, 10), ppd=20, n_bars=10)
+vertical = waves.square_linear(visual_size=(10, 10), ppd=20, n_bars=10, rotation=90)
+oblique1 = waves.square_linear(visual_size=(10, 10), ppd=20, n_bars=10, rotation=45)
+oblique2 = waves.square_linear(visual_size=(10, 10), ppd=20, n_bars=10, rotation=60)
+
+
+plot_stimuli({'Horizontal': horizontal,
+              'Vertical': vertical,
+              '45 deg': oblique1,
+              '60 deg': oblique2,
+              })
+```
+As `stimupy` warns: the rounding of `phase_width` to fit nicely in `ppd`
+is not possible for oblique gratings.
+This has to do with the `ppd` being defined horizontal/vertical,
+while the grating is defined obliquely,
+and thus cannot fall neatly in pixels anyway.
+
+Other periodic stimuli ({py:mod}`stimupy.stimuli.waves`)
+are analogous in the parameterization,
+but are evaluated over different [_`distance_metrics`_](./dimensions.md).
+Additionally, the naming schema of the arguments is adjusted
+to better fit the geometry of the distance metric.
+Particularly the name for each `phase` is different:
+
+- for `linear` waves: `bar`
+- for `radial` waves: `ring`
+- for `rectilinear` waves: `frame`
+- for `angular` waves: `segment`
+
+```{code-cell}
+horizontal = waves.square_linear(visual_size=(10, 10), ppd=20, n_bars=10)
+vertical = waves.square_linear(visual_size=(10, 10), ppd=20, n_bars=10, rotation=90)
+oblique = waves.square_linear(visual_size=(10, 10), ppd=20, n_bars=10, rotation=45)
+radial = waves.square_radial(visual_size=(10, 10), ppd=20, n_rings=10)
+rectilinear = waves.square_rectilinear(visual_size=(10, 10), ppd=20, n_frames=10)
+angular = waves.square_angular(visual_size=(10, 10), ppd=20, n_segments=10)
+
+plot_stimuli({'Horizontal': horizontal,
+              'Vertical': vertical,
+              'Oblique': oblique,
+              'Radial': radial,
+              'Rectilinear': rectilinear,
+              'Angular': angular})
+```
+
+```{warning}
+The `angular` stimuli have a unique property
+that the distance over which periodicity generated,
+is fixed at $[0, 360]$ angular degrees.
+As a result, the `frequency` argument for these stimuli
+is interpreted as _cycles per image_ (rather than _cpd_).
+Thus, it is exactly `n_segments/2`, always.
+```
+
+## Derived stimuli
+These parameters for periodic stimuli are also
+part of the specification of stimuli that use or are derived from these.
+{py:mod}`Gabors <stimupy.stimuli.gabors>` are a straightforward example:
+it uses all the same {py:func}`waves.sine <stimupy.stimuli.waves.sine_linear>` parameters:
+```{code-cell}
+from stimupy.stimuli import gabors
+
+sinewave = waves.sine_linear(visual_size=(10, 10), ppd=10,
+                             n_bars=5,
+                             intensities=(0.0, 1.0),
+                             origin='center')
+
+gabor = gabors.gabor(visual_size=(10, 10), ppd=10,
+                     n_bars=5,
+                     intensity_bars=(0.0, 1.0),
+                     origin='center',
+
+                     sigma=1)
+
+plot_stimuli({'Sine': sinewave, 'Gabor': gabor})
+```
+
+{py:mod}`White's illusions <stimupy.stimuli.whites>` are another example:
+a squarewave grating, with several target-regions embedded in them.
+There are lots of variations on this stimulus,
+-- some of which are implemented in `stimupy` --
+but fundamentally all build on some squarewave grating
+which takes the usual {py:mod}`waves <stimupy.stimuli.waves>`-params.
+```{code-cell}
+from stimupy.stimuli import whites
+
+sinewave = waves.square_linear(visual_size=(10, 10), ppd=10,
+                             n_bars=10,
+                             origin="corner",
+                             intensity_bars=(0.0, 1.0))
+
+whites = whites.yazdanbakhsh(visual_size=(10, 10), ppd=10,
+                             n_bars=10,
+                             intensity_bars=(0.0, 1.0),
+
+                             target_indices_top=2,
+                             target_indices_bottom=-3,
+                             target_heights=2,
+                             target_center_offset=2,
+                             gap_size=.5)
+
+plot_stimuli({'Sine': sinewave, "White's illusion": whites})
+```
+
+
+% ## CHECKERBOARDS, GRIDS
\ No newline at end of file
diff --git a/stimupy/__init__.py b/stimupy/__init__.py
index a6757e53..378f0ca6 100644
--- a/stimupy/__init__.py
+++ b/stimupy/__init__.py
@@ -2,5 +2,6 @@
 
 from stimupy import components, noises, stimuli, utils
 from stimupy.stimuli import *
+from stimupy.logos import *
 
 __all__ = ["components", "noises", "utils", *stimuli.__all__]
diff --git a/stimupy/components/angulars.py b/stimupy/components/angulars.py
index a068c75b..2684fb55 100644
--- a/stimupy/components/angulars.py
+++ b/stimupy/components/angulars.py
@@ -131,10 +131,15 @@ def wedge(
         origin=origin,
     )
 
-    # Remove everything but wedge
+    # Remove everything but wedge, and add additional args
     stim["img"] = np.where(mask["wedge_mask"], stim["img"], intensity_background)
     stim["wedge_mask"] = stim["ring_mask"] * mask["wedge_mask"]
     stim["wedge_mask"] = np.where(stim["wedge_mask"] != 0, 1, 0).astype(int)
+    stim["angle"] = angle
+    stim["radius"] = radius
+    stim["inner_radius"] = inner_radius
+    stim["intensity_wedge"] = intensity_wedge
+    stim["intensity_background"] = intensity_background
     del stim["ring_mask"]
     return stim
 
@@ -250,6 +255,12 @@ def segments(
         intensities=intensity_segments,
         intensity_background=intensity_background,
     )
+
+    # Update args
+    stim["angles"] = angles
+    stim["intensity_background"] = intensity_background
+    stim["intensity_segments"] = intensity_segments
+
     return stim
 
 
diff --git a/stimupy/components/frames.py b/stimupy/components/frames.py
index a8d126ac..6bd84c5e 100644
--- a/stimupy/components/frames.py
+++ b/stimupy/components/frames.py
@@ -113,10 +113,13 @@ def frames(
         rotation=rotation,
     )
 
-    # Draw image
+    # Draw image and update args
     stim["img"] = draw_regions(
         stim["frame_mask"], intensities=intensity_frames, intensity_background=intensity_background
     )
+    stim["radii"] = radii
+    stim["intensity_frames"] = intensity_frames
+    stim["intensity_background"] = intensity_background
     return stim
 
 
diff --git a/stimupy/components/gaussians.py b/stimupy/components/gaussians.py
index 8c10240a..95af1729 100644
--- a/stimupy/components/gaussians.py
+++ b/stimupy/components/gaussians.py
@@ -92,6 +92,8 @@ def gaussian(
         "visual_size": base["visual_size"],
         "shape": base["shape"],
         "ppd": base["ppd"],
+        "intensity_max": intensity_max,
+        "origin": origin,
     }
     return stim
 
diff --git a/stimupy/components/lines.py b/stimupy/components/lines.py
index 062ee9c8..529a1cc4 100644
--- a/stimupy/components/lines.py
+++ b/stimupy/components/lines.py
@@ -219,6 +219,9 @@ def dipole(
 
     stim1["img"] = stim1["img"] + stim2["img"]
     stim1["line_mask"] = (stim1["line_mask"] + stim2["line_mask"] * 2).astype(int)
+    stim1["line_gap"] = line_gap
+    stim1["intensity_lines"] = intensity_lines
+    del stim1["intensity_line"]
 
     if line_width == 0:
         line_width = 1 / np.unique(stim1["ppd"])
@@ -270,7 +273,9 @@ def ellipse(
     # Resolve resolution
     shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd)
     if line_width * ppd[0] == 0:
-        line_width = 1 / ppd[0]
+        line_width_ = 1 / ppd[0]
+    else:
+        line_width_ = line_width
 
     stim = ellipse_shape(
         radius=np.array(radius),
@@ -283,7 +288,7 @@ def ellipse(
     )
 
     stim2 = ellipse_shape(
-        radius=np.array(radius) - line_width,
+        radius=np.array(radius) - line_width_,
         visual_size=visual_size,
         ppd=ppd,
         shape=shape,
@@ -292,7 +297,9 @@ def ellipse(
 
     stim["img"] = np.where(stim2["ellipse_mask"] == 1, intensity_background, stim["img"])
     stim["line_mask"] = np.where(stim2["ellipse_mask"] == 1, 0, stim["ellipse_mask"])
-    del stim["ellipse_mask"]
+    stim["intensity_line"] = intensity_line
+    stim["line_width"] = line_width
+    del stim["ellipse_mask"], stim["intensity_ellipse"]
     return stim
 
 
diff --git a/stimupy/components/radials.py b/stimupy/components/radials.py
index c70fe9e7..c2202454 100644
--- a/stimupy/components/radials.py
+++ b/stimupy/components/radials.py
@@ -152,6 +152,8 @@ def rings(
 
     # Assemble output
     params["intensity_rings"] = intensity_rings
+    params["radii"] = radii
+    params["intensity_background"] = intensity_background
     return {"img": img, **params}
 
 
@@ -214,6 +216,9 @@ def disc(
         shape=shape,
         origin=origin,
     )
+    stim["radius"] = radius
+    stim["intensity_disc"] = intensity_disc
+    del stim["radii"], stim["intensity_rings"]
     return stim
 
 
@@ -285,6 +290,7 @@ def ring(
         origin=origin,
     )
     stim["ring_mask"] = np.where(stim["ring_mask"] == 2, 1, 0)
+    stim["intensity_ring"] = intensity_ring
     return stim
 
 
diff --git a/stimupy/components/shapes.py b/stimupy/components/shapes.py
index 9c1aff57..4d96503f 100644
--- a/stimupy/components/shapes.py
+++ b/stimupy/components/shapes.py
@@ -217,6 +217,7 @@ def triangle(
         "visual_size": base["visual_size"],
         "ppd": base["ppd"],
         "shape": base["shape"],
+        "triangle_size": triangle_size,
         "intensity_background": intensity_background,
         "intensity_triangle": intensity_triangle,
         "rotation": rotation,
@@ -593,7 +594,8 @@ def circle(
         restrict_size=restrict_size,
     )
     stim["circle_mask"] = stim["ellipse_mask"]
-    stim.pop("ellipse_mask")
+    stim["intensity_circle"] = intensity_circle
+    stim.pop("ellipse_mask", "intensity_ellipse")
     return stim
 
 
diff --git a/stimupy/components/waves.py b/stimupy/components/waves.py
index c72c668b..96d90301 100644
--- a/stimupy/components/waves.py
+++ b/stimupy/components/waves.py
@@ -352,6 +352,10 @@ def sine(
         warnings.warn("Period ignored for oblique gratings")
 
     # Resolve params
+    if distance_metric == "radial" or distance_metric == "rectilinear":
+        if n_phases is not None:
+            n_phases = n_phases * 2
+
     if distance_metric == "angular":
         params = resolve_grating_params(
             visual_angle=360,
@@ -373,6 +377,7 @@ def sine(
             period=period,
             round_phase_width=round_phase_width,
         )
+
     length = params["length"]
     ppd_1D = params["ppd"]
     visual_angle = params["visual_angle"]
@@ -380,6 +385,9 @@ def sine(
     phase_width = params["phase_width"]
     n_phases = params["n_phases"]
 
+    if distance_metric == "radial" or distance_metric == "rectilinear":
+        n_phases = n_phases / 2
+
     # Determine size/shape of whole image
     if None in shape:
         shape = [length * alpha[1], length * alpha[0]]
@@ -704,7 +712,8 @@ def bessel(
         "shape": base["shape"],
         "order": order,
         "frequency": frequency,
-        "intensity_rings": intensities,
+        "intensities": intensities,
+        "origin": origin,
     }
     return stim
 
diff --git a/stimupy/logos.py b/stimupy/logos.py
new file mode 100644
index 00000000..1258ef98
--- /dev/null
+++ b/stimupy/logos.py
@@ -0,0 +1,178 @@
+import copy
+
+import numpy as np
+
+import stimupy
+
+np.random.seed(1)
+
+__all__ = ["logo"]
+
+
+def logo(
+    ppd=128,
+    intensity_background=0.5,
+):
+    """Generate stimupy logo
+
+    Parameters
+    ----------
+    ppd : Number (default: 128)
+        pixels per degree along the axis of grating
+
+    Returns
+    -------
+    dict[str, Any]
+        dict with the stimulus (key: "img"),
+        and additional keys containing stimulus parameters
+    """
+
+    # Parameters
+    res = {"visual_size": 11, "ppd": ppd}
+    radius = 5
+
+    segments = stimupy.waves.square_angular(**res, n_segments=8, rotation=-(360 / 16))
+    circle = stimupy.components.shapes.circle(**res, radius=radius, intensity_background=0)
+    pie = copy.deepcopy(segments)
+
+    pie.update(
+        img=np.where(circle["circle_mask"], segments["img"], intensity_background),
+        mask=np.where(circle["circle_mask"], segments["grating_mask"], 0),
+        radius=circle["radius"],
+    )
+
+    # Create stimuli for pies
+    grating = stimupy.stimuli.waves.sine_linear(**res, frequency=1, rotation=(360 / 8))
+    dotted = stimupy.stimuli.sbcs.dotted(
+        **res, n_dots=10, dot_radius=0.25, distance=0.1, target_shape=2
+    )
+    dotted = stimupy.utils.roll_dict(dotted, shift=-dotted["shape"][0] / 4, axes=0)
+    grid = stimupy.stimuli.hermanns.grid(**res, element_size=(0.5, 0.5, 0.07))
+    whites = stimupy.stimuli.whites.radial(**res, target_indices=(7, 14), frequency=2)
+    plaid = stimupy.stimuli.plaids.sine_waves(
+        grating_parameters1={**res, "frequency": 2},
+        grating_parameters2={**res, "frequency": 1, "rotation": 45},
+    )
+    mondrian = stimupy.mondrians.corrugated_mondrian(
+        **res, intensities=np.random.rand(10, 10), depths=(0, 0, 0, 0, 0, 0, 0, 1, -1, 0)
+    )
+    noise = stimupy.noises.naturals.pink(**res)
+
+    # Fill segments
+    pie["img"] = np.where(pie["mask"] == 1, intensity_background, pie["img"])
+    pie["img"] = np.where(pie["mask"] == 2, grating["img"], pie["img"])
+    pie["img"] = np.where(pie["mask"] == 3, dotted["img"], pie["img"])
+    pie["img"] = np.where(pie["mask"] == 4, grid["img"], pie["img"])
+    pie["img"] = np.where(pie["mask"] == 5, whites["img"], pie["img"])
+    pie["img"] = np.where(pie["mask"] == 6, plaid["img"], pie["img"])
+    pie["img"] = np.where(pie["mask"] == 7, mondrian["img"], pie["img"])
+    pie["img"] = np.where(pie["mask"] == 8, noise["img"], pie["img"])
+
+    # Create eye components
+    lens = stimupy.components.lines.ellipse(**res, radius=radius, line_width=0.02)
+    pupil = stimupy.components.shapes.ellipse(**res, radius=(radius - 1.8, radius - 0.6))
+    pupilmask = stimupy.components.shapes.rectangle(
+        **res, rectangle_size=(11, 3), rectangle_position=(0, 0)
+    )
+    pupil["img"] = pupil["img"] * pupilmask["rectangle_mask"]
+    pupil["ellipse_mask"] = (pupil["ellipse_mask"] * pupilmask["rectangle_mask"]).astype(int)
+    pupil = stimupy.utils.roll_dict(pupil, shift=-pupil["shape"][1] / 5, axes=1)
+    pupil["ellipse_mask"] = np.where(
+        (pupil["ellipse_mask"] * segments["grating_mask"] * circle["circle_mask"]) == 1,
+        pupil["ellipse_mask"],
+        0,
+    )
+
+    pie["img"] = np.where((segments["grating_mask"] * lens["line_mask"]) == 1, 0, pie["img"])
+    pie["img"] = np.where(pupil["ellipse_mask"] == 1, 0, pie["img"])
+
+    return pie
+
+
+def qrcode(ppd=128):
+    import qrcode
+    from PIL import Image, ImageDraw
+    from qrcode.image.styledpil import StyledPilImage
+
+    arr = np.array(logo(ppd, intensity_background=1)["img"])
+    arr *= 255
+
+    imsize = arr.shape
+    im_logo = Image.new("L", (imsize[1], imsize[0]))
+    im_logo.putdata(arr.flatten())
+
+    qr = qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_H)
+    qr.add_data("https://stimupy.readthedocs.io/")
+    img = qr.make_image(
+        image_factory=StyledPilImage,
+        module_drawer=qrcode.image.styles.moduledrawers.CircleModuleDrawer(),
+        # eye_drawer=qrcode.image.styles.moduledrawers.CircleModuleDrawer(),
+        embeded_image=im_logo,
+    )
+
+    ## Make "eyes" circular
+    eye_size = 70  # default
+    quiet_zone = 40  # default
+
+    eyes_mask = Image.new("L", img.size, 0)
+
+    eye_border = quiet_zone + eye_size
+
+    draw = ImageDraw.Draw(eyes_mask)
+    draw.rectangle((quiet_zone, quiet_zone, eye_border, eye_border), fill=255)
+    draw.rectangle(
+        (img.size[0] - eye_border, quiet_zone, img.size[0] - quiet_zone, eye_border), fill=255
+    )
+    draw.rectangle(
+        (quiet_zone, img.size[0] - eye_border, eye_border, img.size[0] - quiet_zone), fill=255
+    )
+
+    draw.rectangle((58, 58, 92, 92), fill=0)
+    draw.rectangle((img.size[0] - 58, 92, img.size[0] - 92, 58), fill=0)
+    draw.rectangle((55, img.size[0] - 92, 92, img.size[0] - 58), fill=0)
+
+    round_eyes = Image.new("L", img.size, 255)
+
+    draw = ImageDraw.Draw(round_eyes)
+    draw.ellipse((quiet_zone, quiet_zone, eye_border, eye_border), fill=None, outline=0, width=8)
+    draw.ellipse(
+        (img.size[0] - eye_border, quiet_zone, img.size[0] - quiet_zone, eye_border),
+        fill=None,
+        outline=0,
+        width=8,
+    )
+    draw.ellipse(
+        (quiet_zone, img.size[0] - eye_border, eye_border, img.size[0] - quiet_zone),
+        fill=None,
+        outline=0,
+        width=8,
+    )
+
+    final = Image.composite(round_eyes, img, eyes_mask)
+
+    return final
+
+
+def overview(**kwargs):
+    """Generate example stimuli from this module
+
+    Returns
+    -------
+    stims : dict
+        dict with all stimuli containing individual stimulus dicts.
+    """
+
+    # fmt: off
+    stimuli = {
+        "logo": logo(),
+    }
+    # fmt: on
+
+    return stimuli
+
+
+if __name__ == "__main__":
+    from stimupy.utils import plot_stimuli
+
+    stims = overview()
+    plot_stimuli(stims, mask=False, save=None)
diff --git a/stimupy/papers/example.py b/stimupy/papers/example.py
new file mode 100644
index 00000000..1400db18
--- /dev/null
+++ b/stimupy/papers/example.py
@@ -0,0 +1,138 @@
+"""Example paper module (citation information here)
+
+This set only serves as an example / template
+for how to set up a stimulus-set-module
+
+Attributes
+----------
+__all__ (list of str): list of all stimulus-functions,
+    these are exported by this module when executing
+        >>> from stimupy.papers.example import *
+
+References
+----------
+reference information here
+
+"""
+
+from stimupy.components import combine_masks, draw_regions, shapes
+
+# Define original size resolution parameters
+VISUAL_SIZE = (10, 12)
+PPD = 10
+
+__all__ = [
+    "my_bullseye",
+    "my_inverse_bullseye",
+]
+
+
+# Helper function:
+def bullseye_geometry(ppd=PPD):
+    """Helper function to create the bullseye geometry
+
+    This function itself is not a stimulus,
+    and will not be shown in `stimupy.papers.my_paper`
+
+    Parameters
+    ----------
+    ppd : int
+        Resolution of stimulus in pixels per degree. (default: 10)
+
+    """
+    # Create center (target) disc:
+    disc = shapes.disc(
+        visual_size=VISUAL_SIZE, ppd=ppd, radius=2, intensity_disc=0.5, intensity_background=0.5
+    )
+
+    # Create first ring, white:
+    ring_1 = shapes.ring(
+        visual_size=VISUAL_SIZE, ppd=ppd, radii=(2, 3), intensity_ring=1, intensity_background=0.5
+    )
+
+    # Create second ring, black:
+    ring_2 = shapes.ring(
+        visual_size=VISUAL_SIZE, ppd=ppd, radii=(3, 4), intensity_ring=0, intensity_background=0.5
+    )
+
+    bullseye_mask = combine_masks(disc["ring_mask"], ring_1["ring_mask"], ring_2["ring_mask"])
+
+    return bullseye_mask
+
+
+# New stimulus function:
+def my_bullseye(ppd=PPD):
+    """My bullseye stimulus: grey on white on black
+
+    Parameters
+    ----------
+    ppd : int
+        Resolution of stimulus in pixels per degree. (default: 32)
+
+    Returns
+    -------
+    dict of str
+        dict with the stimulus (key: "img")
+        and additional keys containing stimulus parameters
+
+    """
+
+    # Call geometry helper function
+    bullseye_mask = bullseye_geometry(ppd=ppd)
+
+    bullseye_img = draw_regions(
+        mask=bullseye_mask, intensities=[0.5, 1, 0], intensity_background=0.5
+    )
+
+    # Package into stim-dict, adding parameter information
+    stim = {
+        "img": bullseye_img,
+        "visual_size": VISUAL_SIZE,
+        "ppd": ppd,
+        "radii": (2, 3, 4),
+        "intensities": (0.5, 1, 0),
+        "intensity_background": 0.5,
+    }
+
+    # Output
+    return stim
+
+
+# Second stimulus function:
+def my_inverse_bullseye(ppd=PPD):
+    """My other bullseye stimulus: grey on black on white
+
+
+    Parameters
+    ----------
+    ppd : int
+        Resolution of stimulus in pixels per degree. (default: 10)
+
+    Returns
+    -------
+    dict of str
+        dict with the stimulus (key: "img")
+        and additional keys containing stimulus parameters
+
+    """
+
+    # Call geometry helper function
+    bullseye_mask = bullseye_geometry(ppd=ppd)
+
+    bullseye_img = draw_regions(
+        mask=bullseye_mask, intensities=[0.5, 0, 1], intensity_background=0.5
+    )
+
+    # Package into stim-dict, adding parameter information
+    stim = {
+        "img": bullseye_img,
+        "visual_size": VISUAL_SIZE,
+        "ppd": ppd,
+        "radii": (2, 3, 4),
+        "intensities": (0.5, 0, 1),
+        "intensity_background": 0.5,
+        "note": "Here is some additional information: I like this stimulus!",
+    }
+
+    # Output
+    return stim
diff --git a/stimupy/stimuli/__init__.py b/stimupy/stimuli/__init__.py
index cd9d1505..22787463 100644
--- a/stimupy/stimuli/__init__.py
+++ b/stimupy/stimuli/__init__.py
@@ -10,6 +10,7 @@
     "cubes",
     "delboeufs",
     "dungeons",
+    "edges",
     "gabors",
     "gratings",
     "hermanns",
diff --git a/stimupy/stimuli/checkerboards.py b/stimupy/stimuli/checkerboards.py
index 3be3ad01..9a966ba4 100644
--- a/stimupy/stimuli/checkerboards.py
+++ b/stimupy/stimuli/checkerboards.py
@@ -292,6 +292,9 @@ def checkerboard(
         "period": period,
         "rotation": rotation,
         "intensity_checks": intensity_checks,
+        "target_indices": target_indices,
+        "extend_targets": extend_targets,
+        "round_phase_width": round_phase_width,
     }
 
     # Add targets
@@ -414,6 +417,10 @@ def contrast_contrast(
 
     stim["img"] = img
     stim["target_mask"] = mask.astype(int)
+    stim["target_shape"] = target_shape
+    stim["alpha"] = alpha
+    stim["tau"] = tau
+    stim["round_phase_width"] = round_phase_width
     return stim
 
 
diff --git a/stimupy/stimuli/delboeufs.py b/stimupy/stimuli/delboeufs.py
index 9dc9f7e1..079f4b44 100644
--- a/stimupy/stimuli/delboeufs.py
+++ b/stimupy/stimuli/delboeufs.py
@@ -85,6 +85,9 @@ def delboeuf(
     inner["outer_radius"] = outer_radius
     inner["target_radius"] = target_radius
     inner["target_mask"] = inner["ring_mask"]
+    inner["outer_line_width"] = outer["line_width"]
+    inner["intensity_outer_line"] = intensity_outer_line
+    inner["intensity_target"] = intensity_target
     del inner["ring_mask"]
     return inner
 
diff --git a/stimupy/stimuli/dungeons.py b/stimupy/stimuli/dungeons.py
index 505702ee..30e40ca0 100644
--- a/stimupy/stimuli/dungeons.py
+++ b/stimupy/stimuli/dungeons.py
@@ -149,7 +149,7 @@ def resolve_dungeon_params(
         "visual_size": visual_size,
         "ppd": ppd,
         "shape": shape,
-        "n_cells": (n_cells1 + 1, n_cells2 + 1),
+        "n_cells": (n_cells1 + 0.5, n_cells2 + 0.5),
         "cell_size": (cell_size1 / 2, cell_size2 / 2),
     }
 
diff --git a/stimupy/stimuli/edges.py b/stimupy/stimuli/edges.py
new file mode 100644
index 00000000..89dde9ea
--- /dev/null
+++ b/stimupy/stimuli/edges.py
@@ -0,0 +1,2 @@
+from stimupy.components.edges import *  # noqa: F401
+from stimupy.components.edges import overview  # noqa: F401
diff --git a/stimupy/stimuli/gabors.py b/stimupy/stimuli/gabors.py
index 7ff7611d..dbfc1967 100644
--- a/stimupy/stimuli/gabors.py
+++ b/stimupy/stimuli/gabors.py
@@ -13,9 +13,9 @@ def gabor(
     bar_width=None,
     period="ignore",
     rotation=0.0,
-    phase_shift=None,
+    phase_shift=0,
     intensity_bars=(0.0, 1.0),
-    origin=None,
+    origin="center",
     round_phase_width=False,
     sigma=None,
 ):
diff --git a/stimupy/stimuli/mondrians.py b/stimupy/stimuli/mondrians.py
index 127426e9..7d6ac9da 100644
--- a/stimupy/stimuli/mondrians.py
+++ b/stimupy/stimuli/mondrians.py
@@ -199,8 +199,11 @@ def corrugated_mondrian(
     if nrows is None:
         nrows = len(intensities)
 
-    if ncols is None:
-        ncols = len(intensities[0])
+    try:
+        if ncols is None:
+            ncols = len(intensities[0])
+    except Exception():
+        ncols = nrows
 
     if isinstance(depths, (float, int)):
         depths = (depths,) * nrows
diff --git a/stimupy/utils/__init__.py b/stimupy/utils/__init__.py
index 44d9dd35..c7a78f7b 100644
--- a/stimupy/utils/__init__.py
+++ b/stimupy/utils/__init__.py
@@ -1,4 +1,8 @@
+from .color_conversions import *
+from .contrast_conversions import *
+from .export import *
 from .filters import *
+from .masks import *
 from .pad import *
 from .plotting import *
 from .resolution import *
diff --git a/stimupy/utils/export.py b/stimupy/utils/export.py
index 4b6069bb..a7870aa6 100644
--- a/stimupy/utils/export.py
+++ b/stimupy/utils/export.py
@@ -1,19 +1,150 @@
+import copy
 import json
+import pickle
 from hashlib import md5
 
 import numpy as np
 from PIL import Image
+from scipy.io import savemat
 
 __all__ = [
-    "arrs_to_checksum",
+    "array_to_checksum",
+    "array_to_image",
+    "array_to_npy",
+    "array_to_mat",
+    "array_to_pickle",
+    "arrays_to_checksum",
     "to_json",
-    "write_array_to_image",
+    "to_mat",
+    "to_pickle",
 ]
 
 
-def arrs_to_checksum(stim, keys=["img", "mask"]):
+def array_to_checksum(arr):
+    """Hash (md5) values, and save only the hex
+
+    Parameters
+    ----------
+    arr : np.ndarray
+        Array to be hashed.
+
+    Returns
+    ----------
+    hex
     """
-    Hash (md5) values, and save only the hex
+    return md5(np.ascontiguousarray(arr.round(8))).hexdigest()
+
+
+def array_to_image(arr, filename, norm=True):
+    """Save a 2D numpy array as a grayscale image file.
+
+    Parameters
+    ----------
+    arr : np.ndarray
+        Array to be saved. Values will be cropped to [0,255].
+    filename : str
+        full path to the file to be created.
+    norm : bool
+        if True (default), multiply array by 255
+    """
+    if filename[-4:] != ".png" and filename[-4:] != ".jpg":
+        filename += ".png"
+
+    if isinstance(arr, (np.ndarray, list)):
+        arr = np.array(arr)
+
+        if norm:
+            arr = arr * 255
+
+        if Image:
+            imsize = arr.shape
+            im = Image.new("L", (imsize[1], imsize[0]))
+            im.putdata(arr.flatten())
+            im.save(filename)
+    else:
+        raise ValueError("arr should be a np.ndarray")
+
+
+def array_to_npy(arr, filename):
+    """Save a numpy array to npy-file.
+
+    Parameters
+    ----------
+    arr : np.ndarray
+        Array to be saved.
+    filename : str
+        full path to the file to be creaated.
+    """
+    if isinstance(arr, (np.ndarray, list)):
+        np.save(filename, arr)
+    else:
+        raise ValueError("arr should be a np.ndarray")
+
+
+def array_to_mat(arr, filename):
+    """Save a numpy array to a mat-file.
+
+    Parameters
+    ----------
+    arr : np.ndarray
+        Array to be saved.
+    filename : str
+        full path to the file to be creaated.
+    """
+    if filename[-4:] != ".mat":
+        filename += ".mat"
+
+    if isinstance(arr, (np.ndarray, list)):
+        savemat(filename, {"arr": arr})
+    else:
+        raise ValueError("arr should be a np.ndarray")
+
+
+def array_to_pickle(arr, filename):
+    """Save a numpy array to a pickle-file.
+
+    Parameters
+    ----------
+    arr : np.ndarray
+        Array to be saved.
+    filename : str
+        full path to the file to be creaated.
+    """
+    if filename[-7:] != ".pickle":
+        filename += ".pickle"
+
+    if isinstance(arr, (np.ndarray, list)):
+        with open(filename, "wb") as handle:
+            pickle.dump({"arr": arr}, handle, protocol=pickle.HIGHEST_PROTOCOL)
+    else:
+        raise ValueError("arr should be a np.ndarray")
+
+
+def array_to_json(arr, filename):
+    """Save a numpy array to a (pretty) JSON.
+
+    Parameters
+    ----------
+    arr : np.ndarray
+        Array to be saved.
+    filename : str
+        full path to the file to be creaated.
+    """
+    if filename[-5:] != ".json":
+        filename += ".json"
+
+    if isinstance(arr, np.ndarray):
+        with open(filename, "w", encoding="utf-8") as f:
+            json.dump(arr.tolist(), f, ensure_ascii=False, indent=4)
+    elif isinstance(arr, list):
+        with open(filename, "w", encoding="utf-8") as f:
+            json.dump(arr, f, ensure_ascii=False, indent=4)
+    else:
+        raise ValueError("arr should be a np.ndarray")
+
+
+def arrays_to_checksum(stim, keys=["img", "mask"]):
+    """Hash (md5) values of arrays specified in keys, and save only the hex
 
     Parameters
     ----------
@@ -33,14 +164,13 @@ def arrs_to_checksum(stim, keys=["img", "mask"]):
         ]
 
     for key in keys:
-        stim[key] = md5(np.ascontiguousarray(stim[key].round(8))).hexdigest()
+        stim[key] = array_to_checksum(stim[key])
 
     return stim
 
 
 def to_json(stim, filename):
-    """
-    Stimulus-dict(s) as (pretty) JSON
+    """Save stimulus-dict(s) as (pretty) JSON
 
     Parameters
     ----------
@@ -50,25 +180,67 @@ def to_json(stim, filename):
         full path to the file to be creaated.
 
     """
+    if filename[-5:] != ".json":
+        filename += ".json"
+
     # stimulus-dict(s) as (pretty) JSON
-    with open(filename, "w", encoding="utf-8") as f:
-        json.dump(stim, f, ensure_ascii=False, indent=4)
+    if isinstance(stim, dict):
+        stim2 = copy.deepcopy(stim)
 
+        for key in stim2.keys():
+            # np.ndarrays are not serializable; change to list
+            if isinstance(stim2[key], np.ndarray):
+                stim2[key] = stim2[key].tolist()
+
+        with open(filename, "w", encoding="utf-8") as f:
+            json.dump(stim2, f, ensure_ascii=False, indent=4)
+    else:
+        raise ValueError("stim should be a dict")
+
+
+def to_mat(stim, filename):
+    """Save stimulus-dict(s) as mat-file
+
+    Parameters
+    ----------
+    stim : dict
+        stimulus dictionary containing keys
+    filename : str
+        full path to the file to be creaated.
 
-def write_array_to_image(filename, arr):
     """
-    Save a 2D numpy array as a grayscale image file.
+    if filename[-4:] != ".mat":
+        filename += ".mat"
+
+    if isinstance(stim, dict):
+        savemat(filename, stim)
+    else:
+        raise ValueError("stim should be a dict")
+
+
+def to_pickle(stim, filename):
+    """Save stimulus-dict(s) as pickle-file
 
     Parameters
     ----------
+    stim : dict
+        stimulus dictionary containing keys
     filename : str
         full path to the file to be creaated.
-    arr : np.ndarray
-        2D numpy array
-        The data to be stored in the image. Values will be cropped to [0,255].
+
     """
-    if Image:
-        imsize = arr.shape
-        im = Image.new("L", (imsize[1], imsize[0]))
-        im.putdata(arr.flatten())
-        im.save(filename)
+    if filename[-7:] != ".pickle":
+        filename += ".pickle"
+
+    if isinstance(stim, dict):
+        stim2 = copy.deepcopy(stim)
+
+        for key in stim2.keys():
+            # certain classes can cause problems for pickles; change to list
+            if key in ["visual_size", "ppd", "shape"]:
+                stim2[key] = list(stim2[key])
+
+        with open(filename, "wb") as handle:
+            pickle.dump(stim2, handle, protocol=pickle.HIGHEST_PROTOCOL)
+    else:
+        raise ValueError("stim should be a dict")
diff --git a/stimupy/utils/plotting.py b/stimupy/utils/plotting.py
index 25213b4e..52feadac 100644
--- a/stimupy/utils/plotting.py
+++ b/stimupy/utils/plotting.py
@@ -98,7 +98,7 @@ def plot_stim(
             x, y = resolution.visual_size_to_axes(
                 stim[units], shape=stim["img"].shape, origin=origin
             )
-            extent = [x.min(), x.max(), y.max(), y.max()]
+            extent = [x.min(), x.max(), y.min(), y.max()]
         elif len(stim[units]) == 4:
             # provided 4 values for units, so assume proper formatting
             extent = stim[units]
diff --git a/stimupy/utils/utils.py b/stimupy/utils/utils.py
index b804d1c9..0adb8423 100644
--- a/stimupy/utils/utils.py
+++ b/stimupy/utils/utils.py
@@ -1,4 +1,5 @@
 import copy
+import itertools
 
 import numpy as np
 import scipy.special as sp
@@ -17,6 +18,8 @@
     "flip_dict",
     "roll_dict",
     "strip_dict",
+    "permutate_params",
+    "create_stimspace_stimuli",
 ]
 
 
@@ -435,3 +438,38 @@ def strip_dict(
         if name in dct.keys():
             new_dict[name] = dct[name]
     return new_dict
+
+
+def permutate_params(params):
+    if not isinstance(params, dict):
+        raise ValueError("params needs to be a dict with all stimulus parameters")
+
+    keys, values = zip(*params.items())
+    permutations_dicts = [dict(zip(keys, v)) for v in itertools.product(*values)]
+    return permutations_dicts
+
+
+def create_stimspace_stimuli(stimulus_function, permutations_dicts, title_params=None):
+    if not callable(stimulus_function):
+        raise ValueError("stimulus_function needs to be a function")
+    if isinstance(title_params, str):
+        title_params = [
+            title_params,
+        ]
+
+    stimuli = {}
+    for i, p in enumerate(permutations_dicts):
+        if title_params is None:
+            key = str(i)
+        else:
+            key = ""
+            for tname in title_params:
+                if isinstance(p[tname], (float, int)):
+                    ptname = np.round(p[tname], 3)
+                    key += f"{tname}={ptname} "
+                else:
+                    key += f"{tname}={p[tname]} "
+        stimuli[key] = stimulus_function(
+            **p,
+        )
+    return stimuli


Test patch:
diff --git a/tests/papers/gen_ground_truth.py b/tests/papers/gen_ground_truth.py
index f224c7d2..6d14e884 100644
--- a/tests/papers/gen_ground_truth.py
+++ b/tests/papers/gen_ground_truth.py
@@ -28,7 +28,7 @@
         stim.clear()
         stim["img"] = img
         stim["mask"] = mask
-        export.arrs_to_checksum(stim, ["img", "mask"])
+        export.arrays_to_checksum(stim, ["img", "mask"])
 
     # Save all as .JSON
     export.to_json(stims, f"{d}/{paper}.json")
diff --git a/tests/papers/paper_tests_template.py b/tests/papers/paper_tests_template.py
new file mode 100644
index 00000000..aaba6055
--- /dev/null
+++ b/tests/papers/paper_tests_template.py
@@ -0,0 +1,20 @@
+import json
+import os.path
+
+import pytest
+
+import stimupy.papers.paper_key
+from stimupy.papers.paper_key import __all__ as stimlist
+from stimupy.utils import export
+
+data_dir = os.path.dirname(__file__)
+jsonfile = os.path.join(data_dir, "<paper_key>.json")
+loaded = json.load(open(jsonfile))
+
+
+@pytest.mark.parametrize("stim_name", stimlist)
+def test_stim(stim_name):
+    func = getattr(stimupy.papers.paper_key, stim_name)
+    stim = export.arrays_to_checksum(func(), keys=["img", "target_mask"])
+    assert stim["img"] == loaded[stim_name]["img"], "imgs are different"
+    assert stim["target_mask"] == loaded[stim_name]["mask"], "masks are different"
diff --git a/tests/papers/test_RHS2007.py b/tests/papers/test_RHS2007.py
index 923eb12c..a8cefec8 100644
--- a/tests/papers/test_RHS2007.py
+++ b/tests/papers/test_RHS2007.py
@@ -15,6 +15,6 @@
 @pytest.mark.parametrize("stim_name", stimlist)
 def test_stim(stim_name):
     func = getattr(stimupy.papers.RHS2007, stim_name)
-    stim = export.arrs_to_checksum(func(), keys=["img", "target_mask"])
+    stim = export.arrays_to_checksum(func(), keys=["img", "target_mask"])
     assert stim["img"] == loaded[stim_name]["img"], "imgs are different"
     assert stim["target_mask"] == loaded[stim_name]["mask"], "masks are different"
diff --git a/tests/papers/test_bindmann2004.py b/tests/papers/test_bindmann2004.py
index 5409ce0f..25d3583a 100644
--- a/tests/papers/test_bindmann2004.py
+++ b/tests/papers/test_bindmann2004.py
@@ -15,6 +15,6 @@
 @pytest.mark.parametrize("stim_name", stimlist)
 def test_stim(stim_name):
     func = getattr(stimupy.papers.bindmann2004, stim_name)
-    stim = export.arrs_to_checksum(func(), keys=["img", "target_mask"])
+    stim = export.arrays_to_checksum(func(), keys=["img", "target_mask"])
     assert stim["img"] == loaded[stim_name]["img"], "imgs are different"
     assert stim["target_mask"] == loaded[stim_name]["mask"], "masks are different"
diff --git a/tests/papers/test_domijan2015.py b/tests/papers/test_domijan2015.py
index 0b4b6fd5..ab059218 100644
--- a/tests/papers/test_domijan2015.py
+++ b/tests/papers/test_domijan2015.py
@@ -17,7 +17,7 @@
 @pytest.mark.parametrize("stim_name", stimlist)
 def test_stim(stim_name):
     func = getattr(stimupy.papers.domijan2015, stim_name)
-    stim = export.arrs_to_checksum(func(), keys=["img", "target_mask"])
+    stim = export.arrays_to_checksum(func(), keys=["img", "target_mask"])
     assert stim["img"] == loaded[stim_name]["img"], "imgs are different"
     assert stim["target_mask"] == loaded[stim_name]["mask"], "masks are different"
 
diff --git a/tests/papers/test_modelfest.py b/tests/papers/test_modelfest.py
index 389b9d0f..6931fa02 100644
--- a/tests/papers/test_modelfest.py
+++ b/tests/papers/test_modelfest.py
@@ -15,5 +15,5 @@
 @pytest.mark.parametrize("stim_name", stimlist)
 def test_stim(stim_name):
     func = getattr(stimupy.papers.modelfest, stim_name)
-    stim = export.arrs_to_checksum(func(), keys=["img"])
+    stim = export.arrays_to_checksum(func(), keys=["img"])
     assert stim["img"] == loaded[stim_name]["img"], "imgs are different"
diff --git a/tests/papers/test_murray2020.py b/tests/papers/test_murray2020.py
index c7314e1e..f3690445 100644
--- a/tests/papers/test_murray2020.py
+++ b/tests/papers/test_murray2020.py
@@ -15,7 +15,7 @@
 @pytest.mark.parametrize("stim_name", stimlist)
 def test_checksum(stim_name):
     func = getattr(stimupy.papers.murray2020, stim_name)
-    stim = export.arrs_to_checksum(func(), keys=["img", "target_mask"])
+    stim = export.arrays_to_checksum(func(), keys=["img", "target_mask"])
     assert stim["img"] == loaded[stim_name]["img"], "imgs are different"
     assert stim["target_mask"] == loaded[stim_name]["mask"], "masks are different"
 
diff --git a/tests/papers/test_white1981.py b/tests/papers/test_white1981.py
index 14f1ab48..1cefcc02 100644
--- a/tests/papers/test_white1981.py
+++ b/tests/papers/test_white1981.py
@@ -15,6 +15,6 @@
 @pytest.mark.parametrize("stim_name", stimlist)
 def test_stim(stim_name):
     func = getattr(stimupy.papers.white1981, stim_name)
-    stim = export.arrs_to_checksum(func(), keys=["img", "target_mask"])
+    stim = export.arrays_to_checksum(func(), keys=["img", "target_mask"])
     assert stim["img"] == loaded[stim_name]["img"], "imgs are different"
     assert stim["target_mask"] == loaded[stim_name]["mask"], "masks are different"
diff --git a/tests/papers/test_white1985.py b/tests/papers/test_white1985.py
index 39192975..23ef34e1 100644
--- a/tests/papers/test_white1985.py
+++ b/tests/papers/test_white1985.py
@@ -15,6 +15,6 @@
 @pytest.mark.parametrize("stim_name", stimlist)
 def test_stim(stim_name):
     func = getattr(stimupy.papers.white1985, stim_name)
-    stim = export.arrs_to_checksum(func(), keys=["img", "target_mask"])
+    stim = export.arrays_to_checksum(func(), keys=["img", "target_mask"])
     assert stim["img"] == loaded[stim_name]["img"], "imgs are different"
     assert stim["target_mask"] == loaded[stim_name]["mask"], "masks are different"