Skip to content

Commit

Permalink
docs(contribute): instructions and examples for contributing paper set
Browse files Browse the repository at this point in the history
  • Loading branch information
JorisVincent committed May 10, 2023
1 parent 2cc2cb0 commit 17ff040
Show file tree
Hide file tree
Showing 4 changed files with 274 additions and 4 deletions.
126 changes: 123 additions & 3 deletions docs/contributing/sets.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,133 @@
# 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
- Create a new file in `stimupy/papers`
- Add only stimuli that use `stimupy` functionality
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.
- Also create a corresponding `tests/papers_my_paper.py` file
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
Expand Down
1 change: 0 additions & 1 deletion docs/topic_guides/sets_papers.md

This file was deleted.

131 changes: 131 additions & 0 deletions stimupy/papers/example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""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=.5, intensity_background=.5)

# Create first ring, white:
ring_1 = shapes.ring(visual_size=VISUAL_SIZE, ppd=ppd,
radii=(2, 3),
intensity_ring=1, intensity_background=.5)

# Create second ring, black:
ring_2 = shapes.ring(visual_size=VISUAL_SIZE, ppd=ppd,
radii=(3, 4),
intensity_ring=0, intensity_background=.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
20 changes: 20 additions & 0 deletions tests/papers/paper_tests_template.py
Original file line number Diff line number Diff line change
@@ -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"

0 comments on commit 17ff040

Please sign in to comment.