Contribrute a stimulus (set)#

stimupy contains both a wide range of generalized stimulus-functions that can generate many parameterizated stimulus images, and several complete sets of stimuli that all come from the same source. Development, organization and maintenance 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 paper 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.

  • 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.

    stim = stimupy.papers.RHS2007.WE_thick()
    
  • The output stim-dicts must contain at least the img, but should also contain relevant stimulus parameters.

  • The output stim-dicts 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, which at least documents the ppd argument and the returned dict as follows:

      """ <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 imported as stimupy.papers.example

"""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

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 maskss) to a hash of the img (and masks). The hashed imgs 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:

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:

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#

  1. Edit code/documentation

  2. Commit & Push changes to your fork

    • We use conventional commit 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: ....

  3. 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

  4. Changes will be reviewed by one of the maintainers