diff --git a/docs/getting_started/getting_started.md b/docs/getting_started/getting_started.md index 40686d8a..5d9d1496 100644 --- a/docs/getting_started/getting_started.md +++ b/docs/getting_started/getting_started.md @@ -1,9 +1,15 @@ # Getting started with `stimupy` +The following pages serve as **tutorial**, +walking you through the very basics of installing and using `stimupy`. + +Along the way, they also refer to some [topic guides](../topic_guides/), +which have more in-depth explanation on various concepts and design decisions. + ```{toctree} :numbered: installation first_stim -illusion +stimulus replicate ``` \ No newline at end of file diff --git a/docs/getting_started/replicate.md b/docs/getting_started/replicate.md index c0a77398..28bd150a 100644 --- a/docs/getting_started/replicate.md +++ b/docs/getting_started/replicate.md @@ -18,10 +18,10 @@ Since `stimupy`s functions are highly parameterizable, lots of variants of a stimulus can be created, and thus specific existing parameterizations can be recreated. -For a large selection of the generic `stimupy.illusions`, +For a large selection of the generic `stimupy.stimuli`, there are specific parameterizations in the published literature. Some of these are implement in stimupy as well, -under the corresponding [`stimupy.papers`](../reference/api/stimupy.papers). +under the corresponding [`stimupy.papers`](../reference/_api/stimupy.papers). ```{code-cell} :tags: [hide-cell] diff --git a/docs/getting_started/illusion.md b/docs/getting_started/stimulus.md similarity index 76% rename from docs/getting_started/illusion.md rename to docs/getting_started/stimulus.md index e7629cc8..3b5b70a4 100644 --- a/docs/getting_started/illusion.md +++ b/docs/getting_started/stimulus.md @@ -10,13 +10,11 @@ kernelspec: name: python3 --- -# Exploring an illusion +# Exploring more complex stimuli -The geometric components in [`stimupy.components`](../reference/api/stimupy.components) +The geometric components in [`stimupy.components`](../reference/_api/stimupy.components) form the basic building blocks for all stimuli implement in `stimupy`. More complex stimuli can be composed using the functions that generate components. -Included in `stimupy` is a large set of functionst to generate more complex stimuli, -which we term [`illusions`](../reference/api/stimupy.illusions). ## Simultaneous Brightness Contrast (SBC) @@ -26,8 +24,14 @@ import matplotlib.pyplot as plt from stimupy.utils import plot_stim ``` +Included in `stimupy` is a large set of functions +to generate more complex [`stimuli`](../reference/_api/stimupy.stimuli). +These are generally subdivided into specifically named submodules. +All of these can be accessed either through `stimupy.stimuli.`, +or by `from stimupy import `: + ```{code-cell} -from stimupy.illusions import sbcs +from stimupy import sbcs stim = sbcs.basic(visual_size=(6,8), ppd=10, target_size=(2,2)) @@ -45,8 +49,8 @@ plot_stim(component) plt.show() ``` -However, some of the stimulus parameters have different names in `illusions`. -In particular, **all** `illusions` have the concept of a `target` region(s): +However, some of the stimulus parameters have different names in `stimuli`. +In particular, **all** `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. Thus, the `sbcs.basic` function takes a @@ -81,9 +85,26 @@ plot_stim(two_sided_stim) plt.show() ``` -## White's illusion +## Todorovic illusion +```{code-cell} +from stimupy import todorovics + +stim_tod = todorovics.two_sided_rectangle( + visual_size=(10,20), + ppd=10, + target_size=3, + covers_size=1.5, + covers_offset=2 +) + +plot_stim(stim_tod) +plt.show() + +``` + +## White's effect stimulus ```{code-cell} -from stimupy.illusions import whites +from stimupy import whites stim_whites = whites.white_two_rows( visual_size=(10,12), diff --git a/stimupy/components/__init__.py b/stimupy/components/__init__.py index f526fef8..3a6ee69d 100644 --- a/stimupy/components/__init__.py +++ b/stimupy/components/__init__.py @@ -2,6 +2,7 @@ import numpy as np +from stimupy.components import * # angulars, edges, frames, gaussians, lines, radials, shapes, waves from stimupy.utils import resolution __all__ = [ @@ -210,9 +211,6 @@ def draw_regions(mask, intensities, intensity_background=0.5): return img -from stimupy.components import angulars, edges, frames, gaussians, lines, radials, shapes, waves - - def overview(skip=False): """Generate example stimuli from this module @@ -221,72 +219,36 @@ def overview(skip=False): dict[str, dict] Dict mapping names to individual stimulus dicts """ - - p = { - "visual_size": 10, - "ppd": 20, - } - - # fmt: off - stimuli = { - # angulars - "wedge": angulars.wedge(**p, width=30, radius=3), - "angular_grating": angulars.grating(**p, n_segments=8), - "pinwheel": angulars.pinwheel(**p, n_segments=8, radius=3), - # circulars - "rings (generalized)": radials.rings(**p, radii=[1, 2, 3]), - "disc": radials.disc(**p, radius=3), - "ring": radials.ring(**p, radii=(1, 3)), - "annulus (=ring)": radials.annulus(**p, radii=(1, 3)), - # edges - "step_edge": edges.step_edge(**p), - "gaussian_edge": edges.gaussian_edge(**p, sigma=1.5), - "cornsweet_edge": edges.cornsweet_edge(**p, ramp_width=3), - # frames - "frames": frames.frames(**p, radii=(1, 2, 3)), - # gaussians - "gaussian": gaussians.gaussian(**p, sigma=(1, 2)), - # lines - "line": lines.line(**p, line_length=3), - "dipole": lines.dipole(**p, line_length=3, line_gap=0.5), - "line_circle": lines.circle(**p, radius=3), - # shapes - "rectangle": shapes.rectangle(**p, rectangle_size=3), - "triangle": shapes.triangle(**p, triangle_size=3), - "cross": shapes.cross(**p, cross_size=3, cross_thickness=0.5), - "parallelogram": shapes.parallelogram(**p, parallelogram_size=(3, 3, 1)), - "ellipse": shapes.ellipse(**p, radius=(2, 3)), - "shape_wedge": shapes.wedge(**p, width=30, radius=3), - "shape_annulus": shapes.annulus(**p, radii=(1, 3)), - "shape_ring": shapes.ring(**p, radii=(1, 3)), - "shape_disc": shapes.disc(**p, radius=3), - } - # fmt: on - - # stimuli = {} - # for stimmodule_name in __all__: - # if stimmodule_name in ["overview", "plot_overview"]: - # pass - - # print(f"Generating stimuli from {stimmodule_name}") - # # Get a reference to the actual module - # stimmodule = globals()[stimmodule_name] - # try: - # stims = stimmodule.overview() - - # # Accumulate - # stimuli.update(stims) - # except NotImplementedError as e: - # if not skip: - # raise e - # # Skip stimuli that aren't implemented - # print("-- not implemented") - # pass + stimuli = {} + for stimmodule_name in __all__: + if stimmodule_name in [ + "overview", + "plot_overview", + "draw_regions", + "image_base", + "mask_elements", + ]: + continue + + print(f"Generating stimuli from {stimmodule_name}") + # Get a reference to the actual module + stimmodule = globals()[stimmodule_name] + try: + stims = stimmodule.overview() + + # Accumulate + stimuli.update(stims) + except NotImplementedError as e: + if not skip: + raise e + # Skip stimuli that aren't implemented + print("-- not implemented") + pass return stimuli -def plot_overview(mask=False, save=None, extent_key="shape"): +def plot_overview(mask=False, save=None, units="deg"): """Plot overview of examples in this module (and submodules) Parameters @@ -297,15 +259,16 @@ def plot_overview(mask=False, save=None, extent_key="shape"): save : None or str, optional If None (default), do not save the plot. If string is provided, save plot under this name. - extent_key : str, optional - Key to extent which will be used for plotting. - Default is "shape", using the image size in pixels as extent. + units : "px", "deg" (default), or str + what units to put on the axes, by default degrees visual angle ("deg"). + If a str other than "deg"(/"degrees") or "px"(/"pix"/"pixels") is passed, + it must be the key to a tuple in stim """ from stimupy.utils import plot_stimuli stims = overview(skip=True) - plot_stimuli(stims, mask=mask, extent_key=extent_key, save=save) + plot_stimuli(stims, mask=mask, units=units, save=save) if __name__ == "__main__": diff --git a/stimupy/components/angulars.py b/stimupy/components/angulars.py index dcd32481..9d1c5775 100644 --- a/stimupy/components/angulars.py +++ b/stimupy/components/angulars.py @@ -65,7 +65,7 @@ def wedge( rotation=0.0, inner_radius=0.0, intensity_wedge=1.0, - intensity_background=0.5, + intensity_background=0.0, origin="mean", ): """Draw a wedge, i.e., segment of a disc @@ -90,7 +90,7 @@ def wedge( intensity_wedge : float, optional intensity value of wedge, by default 1.0 intensity_background : float, optional - intensity value of background, by default 0.5 + intensity value of background, by default 0.0 origin : "corner", "mean" or "center" if "corner": set origin to upper left corner if "mean": set origin to hypothetical image center (default) diff --git a/stimupy/components/edges.py b/stimupy/components/edges.py index f614325a..bd4d769d 100644 --- a/stimupy/components/edges.py +++ b/stimupy/components/edges.py @@ -5,13 +5,13 @@ from stimupy.components import gaussians, image_base __all__ = [ - "step_edge", - "gaussian_edge", - "cornsweet_edge", + "step", + "gaussian", + "cornsweet", ] -def step_edge( +def step( visual_size=None, ppd=None, shape=None, @@ -67,7 +67,7 @@ def step_edge( return stim -def gaussian_edge( +def gaussian( visual_size=None, ppd=None, shape=None, @@ -105,7 +105,7 @@ def gaussian_edge( if sigma is None: raise ValueError("gaussian_edge() missing argument 'sigma' which is not 'None'") - stim = step_edge( + stim = step( visual_size=visual_size, ppd=ppd, shape=shape, @@ -129,7 +129,7 @@ def gaussian_edge( return stim -def cornsweet_edge( +def cornsweet( visual_size=None, ppd=None, shape=None, @@ -234,3 +234,35 @@ def cornsweet_edge( } return stim + + +def overview(**kwargs): + """Generate example stimuli from this module + + Returns + ------- + stims : dict + dict with all stimuli containing individual stimulus dicts. + """ + default_params = { + "visual_size": 10, + "ppd": 10, + } + default_params.update(kwargs) + + # fmt: off + stimuli = { + "edges_step": step(**default_params), + "edges_gaussian": gaussian(**default_params, sigma=3), + "edges_cornsweet": cornsweet(**default_params, ramp_width=3), + } + # 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/components/frames.py b/stimupy/components/frames.py index 8b00a5b6..ccb7cfef 100644 --- a/stimupy/components/frames.py +++ b/stimupy/components/frames.py @@ -128,7 +128,7 @@ def overview(**kwargs): # fmt: off stimuli = { - "frames": frames(**default_params, radii=(1, 2, 3)), + "frames_frames": frames(**default_params, radii=(1, 2, 3)), } # fmt: on diff --git a/stimupy/components/gaussians.py b/stimupy/components/gaussians.py index 8d199bc1..772bd138 100644 --- a/stimupy/components/gaussians.py +++ b/stimupy/components/gaussians.py @@ -96,18 +96,30 @@ def gaussian( return stim -if __name__ == "__main__": - from stimupy.utils.plotting import plot_stimuli +def overview(**kwargs): + """Generate example stimuli from this module - p = { - "visual_size": (10, 8), - "ppd": 50, - "rotation": 90, + Returns + ------- + stims : dict + dict with all stimuli containing individual stimulus dicts. + """ + default_params = { + "visual_size": 10, + "ppd": 10, } + default_params.update(kwargs) - stims = { - "gaussian1": gaussian(**p, sigma=2), - "gaussian2": gaussian(**p, sigma=(3, 2)), - } + # fmt: off + stimuli = { + "gaussians_gaussian": gaussian(**default_params, sigma=(3, 1.5), rotation=70),} + # fmt: on + + return stimuli + + +if __name__ == "__main__": + from stimupy.utils import plot_stimuli - plot_stimuli(stims, mask=True) + stims = overview() + plot_stimuli(stims, mask=False, save=None) diff --git a/stimupy/components/lines.py b/stimupy/components/lines.py index 90929389..91b58ce7 100644 --- a/stimupy/components/lines.py +++ b/stimupy/components/lines.py @@ -349,21 +349,39 @@ def circle( return stim -if __name__ == "__main__": - from stimupy.utils.plotting import plot_stimuli +def overview(**kwargs): + """Generate example stimuli from this module - p1 = { - "visual_size": (10, 5), + Returns + ------- + stims : dict + dict with all stimuli containing individual stimulus dicts. + """ + default_params = { + "visual_size": (10, 10), "ppd": 10, + } + default_params.update(kwargs) + + p = { "line_length": 2, "line_width": 0.01, "rotation": 30, } - stims = { - "line": line(**p1, origin="center", line_position=(-1, -1)), - "dipole": dipole(**p1, line_gap=1), - "circle": circle(visual_size=10, ppd=10, radius=3), - "ellipse": ellipse(visual_size=10, ppd=10, radius=(3, 4)), - } - plot_stimuli(stims, mask=True) + # fmt: off + stimuli = { + "lines_line": line(**default_params, **p, origin="center"), + "lines_dipole": dipole(**default_params, **p, line_gap=1), + "lines_circle": circle(**default_params, radius=3), + "lines_ellipse": ellipse(**default_params, radius=(3, 4)),} + # 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/components/radials.py b/stimupy/components/radials.py index 0b96b841..ba2cf193 100644 --- a/stimupy/components/radials.py +++ b/stimupy/components/radials.py @@ -110,7 +110,7 @@ def rings( and additional keys containing stimulus parameters """ if radii is None: - raise ValueError("disc_and_rings() missing argument 'radii' which is not 'None'") + raise ValueError("rings() missing argument 'radii' which is not 'None'") # Try to resolve resolution; try: @@ -154,7 +154,7 @@ def disc( shape=None, radius=None, intensity_disc=1.0, - intensity_background=0.5, + intensity_background=0.0, origin="mean", ): """Draw a central disc @@ -174,7 +174,7 @@ def disc( intensity_disc : Number intensity value of disc, by default 1.0 intensity_background : float (optional) - intensity value of background, by default 0.5 + intensity value of background, by default 0.0 origin : "corner", "mean" or "center" if "corner": set origin to upper left corner if "mean": set origin to hypothetical image center (default) @@ -216,7 +216,7 @@ def ring( shape=None, radii=None, intensity_ring=1.0, - intensity_background=0.5, + intensity_background=0.0, origin="mean", ): """Draw a ring (annulus) @@ -234,7 +234,7 @@ def ring( intensity_ring : Number intensity value of ring, by default 1.0 intensity_background : float (optional) - intensity value of background, by default 0.5 + intensity value of background, by default 0.0 origin : "corner", "mean" or "center" if "corner": set origin to upper left corner if "mean": set origin to hypothetical image center (default) @@ -300,9 +300,10 @@ def overview(**kwargs): # fmt: off stimuli = { - "disc": disc(**default_params, radius=3), - "disc_and_rings": rings(**default_params, radii=(1, 2, 3)), - "ring": ring(**default_params, radii=(1, 2)), + "radials_disc": disc(**default_params, radius=3), + "radials_rings": rings(**default_params, radii=(1, 2, 3)), + "radials_ring": ring(**default_params, radii=(1, 2)), + "radials_annulus": annulus(**default_params, radii=(1, 2)), } # fmt: on @@ -313,4 +314,4 @@ def overview(**kwargs): from stimupy.utils import plot_stimuli stims = overview() - plot_stimuli(stims, mask=True, save=None) + plot_stimuli(stims, mask=False, save=None) diff --git a/stimupy/components/shapes.py b/stimupy/components/shapes.py index ccf074ff..1940acca 100644 --- a/stimupy/components/shapes.py +++ b/stimupy/components/shapes.py @@ -595,23 +595,39 @@ def circle( return stim -if __name__ == "__main__": - from stimupy.utils.plotting import plot_stimuli +def overview(**kwargs): + """Generate example stimuli from this module - p = { - "visual_size": (10, 8), - "ppd": 50, - "rotation": 90, + Returns + ------- + stims : dict + dict with all stimuli containing individual stimulus dicts. + """ + default_params = { + "visual_size": (10, 10), + "ppd": 20, } + default_params.update(kwargs) - stims = { - "rectangle": rectangle(**p, rectangle_size=(4, 2.5)), - "triangle": triangle(**p, triangle_size=(4, 2.5)), - "cross": cross(**p, cross_size=(4, 2.5), cross_thickness=1, cross_arm_ratios=(1, 1)), - "parallelogram": parallelogram(**p, parallelogram_size=(5.2, 3.1, 0.9)), - "parallelogram2": parallelogram(shape=(100, 100), ppd=10, parallelogram_size=(10, 9, -1)), - "ellipse": ellipse(**p, radius=(4, 3)), - "circle": circle(visual_size=(10, 8), ppd=50, radius=3), - } + # fmt: off + stimuli = { + "shapes_rectangle": rectangle(**default_params, rectangle_size=(4, 2.5)), + "shapes_triangle": triangle(**default_params, triangle_size=(4, 2.5)), + "shapes_cross": cross(**default_params, cross_size=(4, 2.5), cross_thickness=1, cross_arm_ratios=(1, 1)), + "shapes_parallelogram": parallelogram(**default_params, parallelogram_size=(5.2, 3.1, 0.9)), + "shapes_ellipse": ellipse(**default_params, radius=(4, 3)), + "shapes_circle": circle(**default_params, radius=3), + "shapes_disc": disc(**default_params, radius=3), + "shapes_ring": ring(**default_params, radii=(1, 2)), + "shapes_annulus": annulus(**default_params, radii=(1, 2)), + "shapes_wedge": wedge(**default_params, width=30, radius=4),} + # fmt: on + + return stimuli + + +if __name__ == "__main__": + from stimupy.utils import plot_stimuli - plot_stimuli(stims, mask=False) + stims = overview() + plot_stimuli(stims, mask=False, save=None) diff --git a/stimupy/components/waves.py b/stimupy/components/waves.py index c8a242c7..e0354725 100644 --- a/stimupy/components/waves.py +++ b/stimupy/components/waves.py @@ -730,28 +730,28 @@ def overview(**kwargs): # fmt: off stimuli = { - "sine wave - horizontal": sine(**default_params, **grating_params, distance_metric="horizontal"), - "sine wave - vertical": sine(**default_params, **grating_params, distance_metric="vertical"), - "sine wave - oblique": sine(**default_params, **grating_params, distance_metric="oblique", rotation=30), - "sine wave - radial": sine(**default_params, **grating_params, distance_metric="radial"), - "sine wave - angular": sine(**default_params, **grating_params, distance_metric="angular"), - "sine wave - rectilinear": sine(**default_params, **grating_params, distance_metric="rectilinear"), - - "square wave - horizontal": square(**default_params, **grating_params, distance_metric="horizontal"), - "square wave - vertical": square(**default_params, **grating_params, distance_metric="vertical"), - "square wave - oblique": square(**default_params, **grating_params, distance_metric="oblique", rotation=30), - "square wave - radial": square(**default_params, **grating_params, distance_metric="radial"), - "square wave - angular": square(**default_params, **grating_params, distance_metric="angular"), - "square wave - rectilinear": square(**default_params, **grating_params, distance_metric="rectilinear"), - - "staircase - horizontal": staircase(**default_params, **grating_params, distance_metric="horizontal"), - "staircase - vertical": staircase(**default_params, **grating_params, distance_metric="vertical"), - "staircase - oblique": staircase(**default_params, **grating_params, distance_metric="oblique", rotation=30), - "staircase - radial": staircase(**default_params, **grating_params, distance_metric="radial"), - "staircase - angular": staircase(**default_params, **grating_params, distance_metric="angular"), - "staircase - rectilinear": staircase(**default_params, **grating_params, distance_metric="rectilinear"), - - "bessel": bessel(**default_params, frequency=0.5), + "waves_sine_horizontal": sine(**default_params, **grating_params, distance_metric="horizontal"), + "waves_sine_vertical": sine(**default_params, **grating_params, distance_metric="vertical"), + "waves_sine_oblique": sine(**default_params, **grating_params, distance_metric="oblique", rotation=30), + "waves_sine_radial": sine(**default_params, **grating_params, distance_metric="radial"), + "waves_sine_angular": sine(**default_params, **grating_params, distance_metric="angular"), + "waves_sine_rectilinear": sine(**default_params, **grating_params, distance_metric="rectilinear"), + + "waves_square_horizontal": square(**default_params, **grating_params, distance_metric="horizontal"), + "waves_square_vertical": square(**default_params, **grating_params, distance_metric="vertical"), + "waves_square_oblique": square(**default_params, **grating_params, distance_metric="oblique", rotation=30), + "waves_square_radial": square(**default_params, **grating_params, distance_metric="radial"), + "waves_square_angular": square(**default_params, **grating_params, distance_metric="angular"), + "waves_square_rectilinear": square(**default_params, **grating_params, distance_metric="rectilinear"), + + "waves_staircase_horizontal": staircase(**default_params, **grating_params, distance_metric="horizontal"), + "waves_staircase_vertical": staircase(**default_params, **grating_params, distance_metric="vertical"), + "waves_staircase_oblique": staircase(**default_params, **grating_params, distance_metric="oblique", rotation=30), + "waves_staircase_radial": staircase(**default_params, **grating_params, distance_metric="radial"), + "waves_staircase_angular": staircase(**default_params, **grating_params, distance_metric="angular"), + "waves_staircase_rectilinear": staircase(**default_params, **grating_params, distance_metric="rectilinear"), + + "waves_bessel": bessel(**default_params, frequency=0.5), } # fmt: on diff --git a/stimupy/noises/__init__.py b/stimupy/noises/__init__.py index 808dfc06..1f820b08 100644 --- a/stimupy/noises/__init__.py +++ b/stimupy/noises/__init__.py @@ -1,8 +1,13 @@ -from stimupy.noises.binaries import * -from stimupy.noises.narrowbands import * -from stimupy.noises.naturals import * -from stimupy.noises.utils import * -from stimupy.noises.whites import * +from stimupy.noises import binaries, narrowbands, naturals, whites + +__all__ = [ + "overview", + "plot_overview", + "binaries", + "narrowbands", + "naturals", + "whites", +] def overview(skip=False): @@ -13,52 +18,30 @@ def overview(skip=False): dict[str, dict] Dict mapping names to individual stimulus dicts """ - params = { - "visual_size": 10, - "ppd": 10, - "pseudo_noise": True, - } - - # fmt: off - stimuli = { - # Binary - "binary_noise": binary(visual_size=10, ppd=10), - # White - "white_noise": white(**params), - # One over frequency - "one_over_f": one_over_f(**params, exponent=0.5), - "pink_noise": pink(**params), - "brown_noise": brown(**params), - # Narrowband - "narrowband_1cpd": narrowband(**params, bandwidth=1, center_frequency=1.0), - "narrowband_3cpd": narrowband(**params, bandwidth=1, center_frequency=3.0), - } - # fmt: on - - # stimuli = {} - # for stimmodule_name in __all__: - # if stimmodule_name in ["overview", "plot_overview"]: - # pass + stimuli = {} + for stimmodule_name in __all__: + if stimmodule_name in ["overview", "plot_overview"]: + continue - # print(f"Generating stimuli from {stimmodule_name}") - # # Get a reference to the actual module - # stimmodule = globals()[stimmodule_name] - # try: - # stims = stimmodule.overview() + print(f"Generating stimuli from {stimmodule_name}") + # Get a reference to the actual module + stimmodule = globals()[stimmodule_name] + try: + stims = stimmodule.overview() - # # Accumulate - # stimuli.update(stims) - # except NotImplementedError as e: - # if not skip: - # raise e - # # Skip stimuli that aren't implemented - # print("-- not implemented") - # pass + # Accumulate + stimuli.update(stims) + except NotImplementedError as e: + if not skip: + raise e + # Skip stimuli that aren't implemented + print("-- not implemented") + pass return stimuli -def plot_overview(mask=False, save=None, extent_key="shape"): +def plot_overview(mask=False, save=None, units="deg"): """Plot overview of examples in this module (and submodules) Parameters @@ -69,15 +52,16 @@ def plot_overview(mask=False, save=None, extent_key="shape"): save : None or str, optional If None (default), do not save the plot. If string is provided, save plot under this name. - extent_key : str, optional - Key to extent which will be used for plotting. - Default is "shape", using the image size in pixels as extent. + units : "px", "deg" (default), or str + what units to put on the axes, by default degrees visual angle ("deg"). + If a str other than "deg"(/"degrees") or "px"(/"pix"/"pixels") is passed, + it must be the key to a tuple in stim """ from stimupy.utils import plot_stimuli stims = overview(skip=True) - plot_stimuli(stims, mask=mask, extent_key=extent_key, save=save) + plot_stimuli(stims, mask=mask, units=units, save=save) if __name__ == "__main__": diff --git a/stimupy/noises/binaries.py b/stimupy/noises/binaries.py index 75a00490..9c881eaa 100644 --- a/stimupy/noises/binaries.py +++ b/stimupy/noises/binaries.py @@ -55,15 +55,30 @@ def binary( return stim -if __name__ == "__main__": - from stimupy.utils import plot_stimuli +def overview(**kwargs): + """Generate example stimuli from this module - params = { + Returns + ------- + stims : dict + dict with all stimuli containing individual stimulus dicts. + """ + default_params = { "visual_size": 10, "ppd": 10, } + default_params.update(kwargs) - stims = { - "Binary noise": binary(**params), - } - plot_stimuli(stims, mask=True, save=None) + # fmt: off + stimuli = { + "binaries_binary": binary(**default_params),} + # 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/noises/narrowbands.py b/stimupy/noises/narrowbands.py index def1d0e6..e311bba9 100644 --- a/stimupy/noises/narrowbands.py +++ b/stimupy/noises/narrowbands.py @@ -90,17 +90,31 @@ def narrowband( return stim -if __name__ == "__main__": - from stimupy.utils import plot_stimuli +def overview(**kwargs): + """Generate example stimuli from this module - params = { + Returns + ------- + stims : dict + dict with all stimuli containing individual stimulus dicts. + """ + default_params = { "visual_size": 10, "ppd": 20, - "pseudo_noise": True, } + default_params.update(kwargs) - stims = { - "Narrowband noise - 3cpd": narrowband(**params, bandwidth=1, center_frequency=3.0), - "Narrowband noise - 9cpd": narrowband(**params, bandwidth=1, center_frequency=9.0), - } - plot_stimuli(stims, mask=True, save=None) + # fmt: off + stimuli = { + "narrowbands_narrowband3": narrowband(**default_params, center_frequency=3, bandwidth=1), + "narrowbands_narrowband9": narrowband(**default_params, center_frequency=9, bandwidth=1),} + # 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/noises/naturals.py b/stimupy/noises/naturals.py index 20cd5ec2..2dad2dce 100644 --- a/stimupy/noises/naturals.py +++ b/stimupy/noises/naturals.py @@ -165,18 +165,32 @@ def brown( return stim -if __name__ == "__main__": - from stimupy.utils import plot_stimuli +def overview(**kwargs): + """Generate example stimuli from this module - params = { + Returns + ------- + stims : dict + dict with all stimuli containing individual stimulus dicts. + """ + default_params = { "visual_size": 10, - "ppd": 20, - "pseudo_noise": True, + "ppd": 10, } + default_params.update(kwargs) - stims = { - "One over f": one_over_f(**params, exponent=0.5), - "Pink noise": pink(**params), - "Brown noise": brown(**params), - } - plot_stimuli(stims, mask=True, save=None) + # fmt: off + stimuli = { + "naturals_one-over-f": one_over_f(**default_params, exponent=0.5), + "naturals_pink": pink(**default_params), + "naturals_brown": brown(**default_params),} + # 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/noises/whites.py b/stimupy/noises/whites.py index 1a324fde..c87ebe6d 100644 --- a/stimupy/noises/whites.py +++ b/stimupy/noises/whites.py @@ -67,16 +67,30 @@ def white( return stim -if __name__ == "__main__": - from stimupy.utils import plot_stimuli +def overview(**kwargs): + """Generate example stimuli from this module - params = { + Returns + ------- + stims : dict + dict with all stimuli containing individual stimulus dicts. + """ + default_params = { "visual_size": 10, - "ppd": 20, - "pseudo_noise": True, + "ppd": 10, } + default_params.update(kwargs) - stims = { - "White noise": white(**params), - } - plot_stimuli(stims, mask=True, save=None) + # fmt: off + stimuli = { + "whites_white": white(**default_params),} + # 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/RHS2007.py b/stimupy/papers/RHS2007.py index 82b83c63..e3075076 100644 --- a/stimupy/papers/RHS2007.py +++ b/stimupy/papers/RHS2007.py @@ -1528,8 +1528,8 @@ def corrugated_mondrian(ppd=PPD, pad=True): params = { "visual_size": (5 * 2, 5 * 2 + 1), "ppd": ppd, - "mondrian_depths": (0.0, -1.0, 0.0, 1.0, 0.0), - "mondrian_intensities": values, + "depths": (0.0, -1.0, 0.0, 1.0, 0.0), + "intensities": values, "target_indices": ((1, 2), (3, 2)), "intensity_background": 0.5, } diff --git a/stimupy/papers/modelfest.py b/stimupy/papers/modelfest.py index 74903c91..16566846 100644 --- a/stimupy/papers/modelfest.py +++ b/stimupy/papers/modelfest.py @@ -39,7 +39,7 @@ from stimupy import checkerboards from stimupy.components import gaussians, lines, shapes -from stimupy.components.edges import gaussian_edge +from stimupy.components.edges import gaussian as gaussian_edge from stimupy.components.waves import bessel from stimupy.noises.binaries import binary as binary_noise from stimupy.stimuli.gabors import gabor @@ -2142,7 +2142,9 @@ def Disk40(ppd=PPD): https://doi.org/10.1117/12.348473 """ - stim = shapes.disc(visual_size=256 / PPD, ppd=ppd, radius=0.125, origin="center") + stim = shapes.disc( + visual_size=256 / PPD, ppd=ppd, radius=0.125, origin="center", intensity_background=0.5 + ) stim = roll_dict(stim, (-2, -2), axes=(0, 1)) v = 157 @@ -2347,5 +2349,5 @@ def compare_all(): from stimupy.utils import plot_stimuli stims = gen_all(skip=True) - plot_stimuli(stims, mask=False, extent_key="visual_size") + plot_stimuli(stims, mask=False, units="visual_size") # compare_all() diff --git a/stimupy/stimuli/__init__.py b/stimupy/stimuli/__init__.py index 0bc1e859..cd9d1505 100644 --- a/stimupy/stimuli/__init__.py +++ b/stimupy/stimuli/__init__.py @@ -57,7 +57,7 @@ def overview(skip=False): return stimuli -def plot_overview(mask=False, save=None, extent_key="shape"): +def plot_overview(mask=False, save=None, units="deg"): """Plot overview of examples in this module (and submodules) Parameters @@ -68,15 +68,16 @@ def plot_overview(mask=False, save=None, extent_key="shape"): save : None or str, optional If None (default), do not save the plot. If string is provided, save plot under this name. - extent_key : str, optional - Key to extent which will be used for plotting. - Default is "shape", using the image size in pixels as extent. + units : "px", "deg" (default), or str + what units to put on the axes, by default degrees visual angle ("deg"). + If a str other than "deg"(/"degrees") or "px"(/"pix"/"pixels") is passed, + it must be the key to a tuple in stim """ from stimupy.utils import plot_stimuli stims = overview(skip=True) - plot_stimuli(stims, mask=mask, extent_key=extent_key, save=save) + plot_stimuli(stims, mask=mask, units=units, save=save) if __name__ == "__main__": diff --git a/stimupy/stimuli/benarys.py b/stimupy/stimuli/benarys.py index f9df0580..46fd0b8f 100644 --- a/stimupy/stimuli/benarys.py +++ b/stimupy/stimuli/benarys.py @@ -265,14 +265,17 @@ def cross_triangles( raise ValueError("Target size is larger than cross thickness") # Calculate target placement for classical Benarys cross + theight, twidth = resolution.shape_from_visual_size_ppd(target_size, ppd) + cheight, cwidth = resolution.shape_from_visual_size_ppd(cross_thickness, ppd) + target_x = ( - (visual_size[1] - cross_thickness) / 2.0 - target_size[1], - (visual_size[1] + cross_thickness) / 2.0, + (shape[1] / 2 - cwidth / 2 - twidth) / ppd[1], + (shape[1] / 2 + cwidth / 2) / ppd[1], ) target_y = ( - (visual_size[0] - cross_thickness) / 2.0 - target_size[0], - (visual_size[0] - cross_thickness) / 2.0, + (shape[0] / 2 - cheight / 2 - theight) / ppd[0], + (shape[0] / 2 - cheight / 2) / ppd[0], ) stim = cross_generalized( @@ -696,6 +699,9 @@ def add_targets( mpatch = mpatch[:, ~np.all(mpatch == 0, axis=0)] theight_, twidth_ = tpatch.shape + if ty[i] + theight_ > img.shape[0] or tx[i] + twidth_ > img.shape[1]: + raise ValueError("At least one target does not fully fit into stimulus") + # Only change the target parts of the image: mlarge = np.zeros(img.shape) mlarge[ty[i] : ty[i] + theight_, tx[i] : tx[i] + twidth_] = mpatch @@ -749,12 +755,12 @@ def overview(**kwargs): # fmt: off stimuli = { - "cross": cross_generalized(**default_params, **params_benary, **target_pos), - "rectangles": cross_rectangles(**default_params, **params_benary), - "triangles": cross_triangles(**default_params, **params_benary), - "todorovic_general": todorovic_generalized(**default_params, **params_todo, **target_pos), - "todorovic_rectangles": todorovic_rectangles(**default_params, **params_todo), - "todorovic_triangles": todorovic_triangles(**default_params, **params_todo), + "benarys_cross_general": cross_generalized(**default_params, **params_benary, **target_pos), + "benarys_cross_rectangles": cross_rectangles(**default_params, **params_benary), + "benarys_cross_triangles": cross_triangles(**default_params, **params_benary), + "benarys_todorovic_general": todorovic_generalized(**default_params, **params_todo, **target_pos), + "benarys_todorovic_rectangles": todorovic_rectangles(**default_params, **params_todo), + "benarys_todorovic_triangles": todorovic_triangles(**default_params, **params_todo), } # fmt: on @@ -765,4 +771,4 @@ def overview(**kwargs): from stimupy.utils import plot_stimuli stims = overview() - plot_stimuli(stims, mask=True, save=None) + plot_stimuli(stims, mask=False, save=None) diff --git a/stimupy/stimuli/bullseyes.py b/stimupy/stimuli/bullseyes.py index 7c0da912..134e8ecb 100644 --- a/stimupy/stimuli/bullseyes.py +++ b/stimupy/stimuli/bullseyes.py @@ -450,10 +450,10 @@ def overview(**kwargs): # fmt: off stimuli = { - "circular": circular(**default_params, frequency=1.0, clip=True), - "circular, two sided": circular_two_sided(**default_params, frequency=1.0), - "rectangular": rectangular(**default_params, frequency=1.0, clip=True), - "rectangular, two sided": rectangular_two_sided(**default_params, frequency=1.0), + "bullseyes_circular": circular(**default_params, frequency=1.0, clip=True), + "bullseyes_circular_2sided": circular_two_sided(**default_params, frequency=1.0), + "bullseyes_rectangular": rectangular(**default_params, frequency=1.0, clip=True), + "bullseyes_rectangular_2sided": rectangular_two_sided(**default_params, frequency=1.0), } # fmt: on @@ -464,4 +464,4 @@ def overview(**kwargs): from stimupy.utils import plot_stimuli stims = overview() - plot_stimuli(stims, mask=True, save=None) + plot_stimuli(stims, mask=False, save=None) diff --git a/stimupy/stimuli/checkerboards.py b/stimupy/stimuli/checkerboards.py index 8d6c4587..ad1b3ab7 100644 --- a/stimupy/stimuli/checkerboards.py +++ b/stimupy/stimuli/checkerboards.py @@ -431,11 +431,9 @@ def overview(**kwargs): # fmt: off stimuli = { "checkerboard": checkerboard(**default_params, check_visual_size=(1, 1)), - "checkerboard rotated": checkerboard(**default_params, check_visual_size=(1, 1), rotation=45), - "checkerboard from frequency": checkerboard(**default_params, frequency=1), - "checkerboard from frequency, rotated": checkerboard(**default_params, frequency=1, rotation=45), - "checkerboard with targets": checkerboard(**default_params, check_visual_size=(1, 1), target_indices=[(3, 2), (5, 5)]), - "Checkerboard Contrast-Contrast illusion": contrast_contrast(**default_params, check_visual_size=(1, 1), target_shape=4, alpha=0.2), + "checkerboard_from_frequency": checkerboard(**default_params, frequency=1, rotation=45), + "checkerboard_with_targets": checkerboard(**default_params, check_visual_size=(1, 1), target_indices=[(3, 2), (5, 5)]), + "checkerboard_contrast_contrast": contrast_contrast(**default_params, check_visual_size=(1, 1), target_shape=4, alpha=0.2), } # fmt: on diff --git a/stimupy/stimuli/cornsweets.py b/stimupy/stimuli/cornsweets.py index ce06f9a7..ded13b54 100644 --- a/stimupy/stimuli/cornsweets.py +++ b/stimupy/stimuli/cornsweets.py @@ -1,6 +1,6 @@ import numpy as np -from stimupy.components.edges import cornsweet_edge +from stimupy.components.edges import cornsweet as cornsweet_edge __all__ = [ "cornsweet", @@ -97,7 +97,7 @@ def overview(**kwargs): # fmt: off stimuli = { - "(Craik-O'Brien-)Cornsweet edge/illusion": cornsweet(**default_params, ramp_width=3) + "cornsweet": cornsweet(**default_params, ramp_width=3) } # fmt: on diff --git a/stimupy/stimuli/cubes.py b/stimupy/stimuli/cubes.py index 60be4b88..eeca6e26 100644 --- a/stimupy/stimuli/cubes.py +++ b/stimupy/stimuli/cubes.py @@ -318,8 +318,8 @@ def overview(**kwargs): # fmt: off stimuli = { - "Cube stimulus": cube(**default_params, visual_size=10, n_cells=5, target_indices=(1,2)), - "Cube - varying cells": varying_cells(**default_params, cell_lengths=(2,4,2), target_indices=1), + "cubes_regular": cube(**default_params, visual_size=10, n_cells=5, target_indices=(1,2)), + "cubes_variable": varying_cells(**default_params, cell_lengths=(2,4,2), target_indices=1), } # fmt: on @@ -330,4 +330,4 @@ def overview(**kwargs): from stimupy.utils import plot_stimuli stims = overview() - plot_stimuli(stims, mask=True, save=None) + plot_stimuli(stims, mask=False, save=None) diff --git a/stimupy/stimuli/delboeufs.py b/stimupy/stimuli/delboeufs.py index b315618b..9dc9f7e1 100644 --- a/stimupy/stimuli/delboeufs.py +++ b/stimupy/stimuli/delboeufs.py @@ -194,8 +194,8 @@ def overview(**kwargs): # fmt: off stimuli = { - "Mueller-Lyer illusion": delboeuf(**default_params, target_radius=1, outer_radius=4), - "Mueller-Lyer, two-sided": two_sided(**default_params, target_radius=1, outer_radii=(2, 1.1)), + "delboeuf": delboeuf(**default_params, target_radius=1, outer_radius=4), + "delboeuf_2sided": two_sided(**default_params, target_radius=1, outer_radii=(2, 1.1)), } # fmt: on @@ -206,4 +206,4 @@ def overview(**kwargs): from stimupy.utils import plot_stimuli stims = overview() - plot_stimuli(stims, mask=True, save=None) + plot_stimuli(stims, mask=False, save=None) diff --git a/stimupy/stimuli/dungeons.py b/stimupy/stimuli/dungeons.py index f61042e1..505702ee 100644 --- a/stimupy/stimuli/dungeons.py +++ b/stimupy/stimuli/dungeons.py @@ -193,7 +193,7 @@ def overview(**kwargs): # fmt: off stimuli = { - "Dungeon illusion": dungeon(**default_params, n_cells=5) + "dungeon": dungeon(**default_params, n_cells=5) } # fmt: on @@ -204,4 +204,4 @@ def overview(**kwargs): from stimupy.utils import plot_stimuli stims = overview() - plot_stimuli(stims, mask=True, save=None) + plot_stimuli(stims, mask=False, save=None) diff --git a/stimupy/stimuli/gabors.py b/stimupy/stimuli/gabors.py index 8953a01b..239cd5ca 100644 --- a/stimupy/stimuli/gabors.py +++ b/stimupy/stimuli/gabors.py @@ -105,6 +105,7 @@ def overview(**kwargs): default_params = { "visual_size": 10, "ppd": 20, + "rotation": 45, } default_params.update(kwargs) @@ -115,3 +116,10 @@ def overview(**kwargs): # 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/stimuli/gratings.py b/stimupy/stimuli/gratings.py index 9be695c4..4f8ee4fa 100644 --- a/stimupy/stimuli/gratings.py +++ b/stimupy/stimuli/gratings.py @@ -605,18 +605,18 @@ def overview(**kwargs): # fmt: off stimuli = { - "On uniform background": on_uniform(**params, visual_size=20, grating_size=5, target_indices=2), - "On grating": on_grating(large_grating_params=large_grating, small_grating_params=small_grating), - "On grating, masked": on_grating_masked( + "grating_on_uniform": on_uniform(**params, visual_size=20, grating_size=5, target_indices=2), + "grating_on_grating": on_grating(large_grating_params=large_grating, small_grating_params=small_grating), + "grating_on_grating-masked": on_grating_masked( large_grating_params=large_grating, small_grating_params=small_grating, mask_size=(5, 5, 2), ), - "Phase shifted": phase_shifted( + "grating_phase_shifted": phase_shifted( **params, target_size=4, target_phase_shift=90 ), - "Grating Induction (sinewave)": grating_induction(**params, target_width=0.5), - "Grating Induction (blurred squarewave)": grating_induction_blur(**params, target_width=0.5, sigma=0.1), + "grating_induction": grating_induction(**params, target_width=0.5), + "grating_induction_blurred-squarewave": grating_induction_blur(**params, target_width=0.5, sigma=0.1), } # fmt: on diff --git a/stimupy/stimuli/hermanns.py b/stimupy/stimuli/hermanns.py index 6c7fdf34..8f56a22a 100644 --- a/stimupy/stimuli/hermanns.py +++ b/stimupy/stimuli/hermanns.py @@ -99,7 +99,7 @@ def overview(**kwargs): # fmt: off stimuli = { - "Hermann grid": grid(**default_params, element_size=(1.5, 1.5, 0.2)) + "hermann_grid": grid(**default_params, element_size=(1.5, 1.5, 0.2)) } # fmt: on @@ -110,4 +110,4 @@ def overview(**kwargs): from stimupy.utils import plot_stimuli stims = overview() - plot_stimuli(stims, mask=True, save=None) + plot_stimuli(stims, mask=False, save=None) diff --git a/stimupy/stimuli/mondrians.py b/stimupy/stimuli/mondrians.py index c9432cc4..6e3804ba 100644 --- a/stimupy/stimuli/mondrians.py +++ b/stimupy/stimuli/mondrians.py @@ -1,3 +1,5 @@ +import itertools + import numpy as np from stimupy.components.shapes import parallelogram @@ -13,9 +15,9 @@ def mondrian( visual_size=None, ppd=None, shape=None, - mondrian_positions=None, - mondrian_sizes=None, - mondrian_intensities=None, + positions=None, + sizes=None, + intensities=None, intensity_background=0.5, ): """Draw Mondrian of given size and intensity at given position @@ -28,12 +30,12 @@ def mondrian( pixels per degree [vertical, horizontal] shape : Sequence[Number, Number], Number, or None (default) shape [height, width] of image, in pixels - mondrian_positions : Sequence[tuple, ... ] or None (default) + positions : Sequence[tuple, ... ] or None (default) position (y, x) of each Mondrian in degrees visual angle - mondrian_sizes : Sequence[tuple, ... ] or None (default) + sizes : Sequence[tuple, ... ] or None (default) size (height, width, depth) of Mondrian parallelograms in degrees visual angle; if only one number is given, squares will be drawn - mondrian_intensities : Sequence[Number, ... ] or None (default) + intensities : Sequence[Number, ... ] or None (default) intensity values of each Mondrian, if only one number is given all will have the same intensity intensity_background : float @@ -46,12 +48,12 @@ def mondrian( mask with integer index for each Mondrian (key: "mondrian_mask"), and additional keys containing stimulus parameters """ - if mondrian_positions is None: - raise ValueError("mondrians() missing argument 'mondrian_positions' which is not 'None'") - if mondrian_sizes is None: - raise ValueError("mondrians() missing argument 'mondrian_sizes' which is not 'None'") - if mondrian_intensities is None: - raise ValueError("mondrians() missing argument 'mondrian_intensities' which is not 'None'") + if positions is None: + raise ValueError("mondrians() missing argument 'positions' which is not 'None'") + if sizes is None: + raise ValueError("mondrians() missing argument 'sizes' which is not 'None'") + if intensities is None: + raise ValueError("mondrians() missing argument 'intensities' which is not 'None'") # Resolve resolution shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd) @@ -61,35 +63,27 @@ def mondrian( img = np.ones(shape) * intensity_background mask = np.zeros(shape) - n_mondrians = len(mondrian_positions) - - if isinstance(mondrian_intensities, (float, int)): - mondrian_intensities = (mondrian_intensities,) * n_mondrians + n_mondrians = len(positions) + ints = itertools.cycle(intensities) - if isinstance(mondrian_sizes, (float, int)): - mondrian_sizes = ((mondrian_sizes, mondrian_sizes),) * n_mondrians + if isinstance(sizes, (float, int)): + sizes = ((sizes, sizes),) * n_mondrians - if any( - len(lst) != n_mondrians - for lst in [mondrian_positions, mondrian_sizes, mondrian_intensities] - ): - raise Exception( - "There need to be as many mondrian_positions as there are " - "mondrian_sizes and mondrian_intensities." - ) + if any(len(lst) != n_mondrians for lst in [positions, sizes]): + raise Exception("As many positions as sizes required.") - mondrian_positions_px = [] - mondrian_shapes = [] + epositions_px = [] + eshapes = [] for m in range(n_mondrians): try: - if len(mondrian_positions[m]) != 2: - raise ValueError("Mondrian position tuples should be (ypos, xpos)") + if len(positions[m]) != 2: + raise ValueError("Position tuples should be (ypos, xpos)") except Exception: - raise ValueError("Mondrian position tuples should be (ypos, xpos)") + raise ValueError("Position tuples should be (ypos, xpos)") - ypos, xpos = resolution.lengths_from_visual_angles_ppd(mondrian_positions[m], ppd[0]) - individual_shapes = resolution.lengths_from_visual_angles_ppd(mondrian_sizes[m], ppd[0]) + ypos, xpos = resolution.lengths_from_visual_angles_ppd(positions[m], ppd[0]) + individual_shapes = resolution.lengths_from_visual_angles_ppd(sizes[m], ppd[0]) try: if len(individual_shapes) == 2: @@ -98,7 +92,7 @@ def mondrian( depth, ] elif len(individual_shapes) == 3: - depth = mondrian_sizes[m][2] + depth = sizes[m][2] else: raise ValueError( "Mondrian size tuples should be (height, width) for " @@ -112,16 +106,14 @@ def mondrian( if depth < 0: xpos += int(depth * ppd[0]) - mondrian_positions_px.append(tuple([ypos, xpos])) - mondrian_shapes.append(tuple(individual_shapes)) + epositions_px.append(tuple([ypos, xpos])) + eshapes.append(tuple(individual_shapes)) # Create parallelogram patch = parallelogram( - visual_size=(mondrian_sizes[m][0], mondrian_sizes[m][1] + np.abs(depth)), + visual_size=(sizes[m][0], sizes[m][1] + np.abs(depth)), ppd=ppd, - parallelogram_size=(mondrian_sizes[m][0], mondrian_sizes[m][1], depth), - intensity_background=intensity_background, - intensity_parallelogram=mondrian_intensities[m], + parallelogram_size=(sizes[m][0], sizes[m][1], depth), ) # Place it into Mondrian mosaic @@ -133,7 +125,7 @@ def mondrian( mask_large = np.zeros(shape) mask_large[ypos : ypos + yshape, xpos : xpos + xshape] = patch["shape_mask"] - img[mask_large == 1] = mondrian_intensities[m] + img[mask_large == 1] = next(ints) mask[mask_large == 1] = m + 1 stim = { @@ -142,11 +134,9 @@ def mondrian( "ppd": ppd, "visual_size": visual_size, "shape": shape, - "mondrian_positions": tuple(mondrian_positions), - "mondrian_positions_px": tuple(mondrian_positions_px), - "mondrian_sizes": tuple(mondrian_sizes), - "mondrian_shapes": tuple(mondrian_shapes), - "mondrian_intensities": tuple(mondrian_intensities), + "positions": tuple(positions), + "sizes": tuple(sizes), + "intensities": tuple(intensities), "intensity_background": intensity_background, } return stim @@ -156,8 +146,10 @@ def corrugated_mondrian( visual_size=None, ppd=None, shape=None, - mondrian_depths=None, - mondrian_intensities=None, + nrows=None, + ncols=None, + depths=0, + intensities=(0, 1), target_indices=None, intensity_background=0.5, intensity_target=None, @@ -172,17 +164,17 @@ def corrugated_mondrian( pixels per degree [vertical, horizontal] shape : Sequence[Number, Number], Number, or None (default) shape [height, width] of image, in pixels - mondrian_depths : Sequence[Number, ... ], Number, or None (default) + depths : Sequence[Number, ... ], Number, or None (default) depth of Mondrian parallelograms per row - mondrian_intensities : nested tuples + intensities : nested tuples intensities of mondrians; as many tuples as there are rows and as many numbers in each tuple as there are columns target_indices : nested tuples indices of targets; as many tuples as there are targets with (y, x) indices intensity_background : float intensity value for background - intensity_target : float or None - target intensity value. If None, use values defined in mondrian_intensities + intensity_target : float + target intensity. If None, use values defined in intensities Returns ------ @@ -198,36 +190,35 @@ def corrugated_mondrian( Science, 262, 2042-2044. https://doi.org/10.1126/science.8266102 """ - if mondrian_depths is None: - raise ValueError( - "corrugated_mondrians() missing argument 'mondrian_depths' which is not 'None'" - ) - if mondrian_intensities is None: - raise ValueError( - "corrugated_mondrians() missing argument 'mondrian_intensities' which is not 'None'" - ) # Resolve resolution shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd) if len(np.unique(ppd)) > 1: raise ValueError("ppd should be equal in x and y direction") - nrows = len(mondrian_intensities) - ncols = len(mondrian_intensities[0]) - if isinstance(mondrian_depths, (float, int)): - mondrian_depths = (mondrian_depths,) * nrows + if nrows is None: + nrows = len(intensities) - if len(mondrian_depths) != nrows: - raise ValueError( - "Unclear number of Mondrians in y-direction, check elements " - "in mondrian_intensities and mondrian_depths" - ) + if ncols is None: + ncols = len(intensities[0]) + + if isinstance(depths, (float, int)): + depths = (depths,) * nrows + + if len(depths) != nrows: + raise ValueError("Unclear number of rows. Check nrows, intensities and depths.") + + ints = itertools.cycle(tuple(np.array(intensities).flatten())) height, width = visual_size - mdepths_px = resolution.lengths_from_visual_angles_ppd(mondrian_depths, ppd[0]) - max_depth = np.abs(np.array(mdepths_px)).max() - sum_depth = np.abs(np.array(mdepths_px).sum()) - red_depth = np.maximum(max_depth, sum_depth + max_depth) + mdepths_px = resolution.lengths_from_visual_angles_ppd(depths, ppd[0]) + + s1 = sum(i for i in mdepths_px) + s2 = sum(-i for i in mdepths_px if i < 0) + sum_depth = np.maximum(abs(s1), abs(s2)) + red_depth = np.cumsum(np.array(mdepths_px)).max() + print(sum_depth, red_depth) + red_depth = np.maximum(red_depth, sum_depth) mheight_px, mwidth_px = int(shape[0] / nrows), int((shape[1] - red_depth) / ncols) # Initial y coordinates @@ -242,43 +233,46 @@ def corrugated_mondrian( sizes = [] poses = [] - ints = [] + intenses = [] tlist = [] target_counter = 1 + counter = 0 for r in range(nrows): xst = xstarts[r] - if mondrian_depths[r] < 0: - xst -= int(mondrian_depths[r] * ppd[0]) + if depths[r] < 0: + xst -= int(depths[r] * ppd[0]) for c in range(ncols): if c != ncols - 1: - msize = (mheight_px / ppd[0], (mwidth_px + 1) / ppd[1], mondrian_depths[r]) + msize = (mheight_px / ppd[0], (mwidth_px + 1) / ppd[1], depths[r]) else: - msize = (mheight_px / ppd[0], mwidth_px / ppd[1], mondrian_depths[r]) + msize = (mheight_px / ppd[0], mwidth_px / ppd[1], depths[r]) mpos = (yst / ppd[0], xst / ppd[1]) - mint = mondrian_intensities[r][c] + mint = next(ints) sizes.append(msize) poses.append(mpos) - ints.append(mint) + intenses.append(mint) if (target_indices is not None) and (r, c) in target_indices: tlist.append(target_counter) xst += mwidth_px target_counter += 1 + counter += 1 yst += mheight_px stim = mondrian( visual_size=visual_size, ppd=ppd, shape=shape, - mondrian_positions=poses, - mondrian_sizes=sizes, - mondrian_intensities=ints, + positions=poses, + sizes=sizes, + intensities=intenses, intensity_background=intensity_background, ) + target_mask = np.zeros(shape) for t in range(len(tlist)): target_mask[stim["mondrian_mask"] == tlist[t]] = t + 1 @@ -290,8 +284,6 @@ def corrugated_mondrian( for t in range(len(tlist)): stim["img"] = np.where(target_mask == t + 1, intensity_target, stim["img"]) - if len(np.unique(stim["img"][target_mask != 0])) > 1: - raise Exception("targets are not equiluminant.") return stim @@ -304,41 +296,26 @@ def overview(**kwargs): dict with all stimuli containing individual stimulus dicts. """ default_params = { + "visual_size": 10, "ppd": 30, } default_params.update(kwargs) # fmt: off stimuli = { - "mondrian1": mondrian(**default_params, - visual_size=8, - mondrian_positions=((0, 0), (0, 4), (1, 3), (4, 4), (5, 1)), - mondrian_sizes=3, - mondrian_intensities=np.random.rand(5)), - "mondrian2": mondrian(**default_params, - visual_size=10, - mondrian_positions=((0, 0), (8, 4), (1, 6), (4, 4), (5, 1)), - mondrian_sizes=((3, 4, 1), (2, 2, 0), (5, 4, -1), (3, 4, 1), (5, 2, 0)), - mondrian_intensities=np.random.rand(5)), - "mondrian3": mondrian(**default_params, - visual_size=(2,6), - mondrian_positions=((0, 0), (0, 2)), - mondrian_sizes=((2, 2, 0), (2, 2, 0)), - mondrian_intensities=(0.2, 0.8)), - "mondrian4": mondrian(**default_params, - visual_size=(2,6), - mondrian_positions=((0, 0), (0, 2)), - mondrian_sizes=((2, 2, 1), (2, 2, 1)), - mondrian_intensities=(0.2, 0.8)), + "mondrian": mondrian(**default_params, + positions=((0, 0), (8, 4), (1, 6), (4, 4), (5, 1)), + sizes=((3, 4, 1), (2, 2, 0), (5, 4, -1), (3, 4, 1), (5, 2, 0)), + intensities=np.random.rand(5)), "corrugated_mondrian": corrugated_mondrian(**default_params, - visual_size=10, - mondrian_depths=(1, 0, -1, 0), - mondrian_intensities=( - (0.4, 0.75, 0.4, 0.75), - (0.75, 0.4, 0.75, 1.0), - (0.4, 0.75, 0.4, 0.75), - (0.0, 0.4, 0.0, 0.4)), - target_indices=((1, 1), (3, 1))) + depths=(1, 0, -1, 0), + intensities=np.random.rand(4, 4), + target_indices=((1, 1), (3, 1))), + "corrugated_mondrian2": corrugated_mondrian(**default_params, + nrows=5, + ncols=5, + depths=(1, -1, 0, -1, 1), + intensities=(0, 1)) } # fmt: on @@ -349,4 +326,4 @@ def overview(**kwargs): from stimupy.utils import plot_stimuli stims = overview() - plot_stimuli(stims, mask=True, save=None) + plot_stimuli(stims, mask=False, save=None) diff --git a/stimupy/stimuli/mueller_lyers.py b/stimupy/stimuli/mueller_lyers.py index 0a7f2f06..50a9035d 100644 --- a/stimupy/stimuli/mueller_lyers.py +++ b/stimupy/stimuli/mueller_lyers.py @@ -274,8 +274,8 @@ def overview(**kwargs): # fmt: off stimuli = { - "Mueller-Lyer illusion": mueller_lyer(**default_params, **stim_params), - "Mueller-Lyer, two-sided": two_sided(**default_params, **stim_params), + "mueller_lyer": mueller_lyer(**default_params, **stim_params), + "mueller_lyer_2sided": two_sided(**default_params, **stim_params), } # fmt: on @@ -286,4 +286,4 @@ def overview(**kwargs): from stimupy.utils import plot_stimuli stims = overview() - plot_stimuli(stims, mask=True, save=None) + plot_stimuli(stims, mask=False, save=None) diff --git a/stimupy/stimuli/pinwheels.py b/stimupy/stimuli/pinwheels.py index 8868f795..494f0bf8 100644 --- a/stimupy/stimuli/pinwheels.py +++ b/stimupy/stimuli/pinwheels.py @@ -183,7 +183,6 @@ def overview(**kwargs): # fmt: off stimuli = { "pinwheel": pinwheel(**default_params, n_segments=10, target_width=2, target_indices=3), - } # fmt: on diff --git a/stimupy/stimuli/ponzos.py b/stimupy/stimuli/ponzos.py index 99276a08..7f5bbb67 100644 --- a/stimupy/stimuli/ponzos.py +++ b/stimupy/stimuli/ponzos.py @@ -150,17 +150,13 @@ def overview(**kwargs): """ default_params = { "visual_size": 10, - "ppd": 30, + "ppd": 20, } default_params.update(kwargs) # fmt: off stimuli = { - "Ponzo illusion": ponzo(**default_params, - outer_lines_angle=10, - outer_lines_length=8, - target_lines_length=3, - target_distance=5) + "ponzo": ponzo(**default_params, outer_lines_angle=10, outer_lines_length=8, target_lines_length=3, target_distance=5) } # fmt: on diff --git a/stimupy/stimuli/rings.py b/stimupy/stimuli/rings.py index 72c75196..8843d286 100644 --- a/stimupy/stimuli/rings.py +++ b/stimupy/stimuli/rings.py @@ -333,10 +333,15 @@ def overview(**kwargs): # fmt: off stimuli = { - "circular": circular(**default_params, frequency=1.0, clip=True), - "circular, two sided": circular_two_sided(**default_params, frequency=1.0), - "rectangular": rectangular(**default_params, frequency=1.0, clip=True), - "rectangular, two sided": rectangular_two_sided(**default_params, frequency=1.0), + "rings_circular": circular(**default_params, frequency=1.0), + "rings_circular_with_targets": circular(**default_params, frequency=1.0, target_indices=3), + "rings_circular_clipped": circular(**default_params, frequency=1.0, clip=True), + "rings_circular_2sided": circular_two_sided(**default_params, frequency=1.0), + + "rings_rectangular": rectangular(**default_params, frequency=1.0), + "rings_rectangular_with_targets": rectangular(**default_params, frequency=1.0, target_indices=3), + "rings_rectangular_clipped": rectangular(**default_params, frequency=1.0, clip=True), + "rings_rectangular_2sided": rectangular_two_sided(**default_params, frequency=1.0), } # fmt: on @@ -347,4 +352,4 @@ def overview(**kwargs): from stimupy.utils import plot_stimuli stims = overview() - plot_stimuli(stims, mask=True, save=None) + plot_stimuli(stims, mask=False, save=None) diff --git a/stimupy/stimuli/sbcs.py b/stimupy/stimuli/sbcs.py index da19ecc3..17194911 100644 --- a/stimupy/stimuli/sbcs.py +++ b/stimupy/stimuli/sbcs.py @@ -669,13 +669,13 @@ def overview(**kwargs): # fmt: off stimuli = { - "generalized": generalized(**default_params, visual_size=10, target_size=3, target_position=(0, 2)), - "basic": basic(**default_params, visual_size=10, target_size=3), - "two_sided": two_sided(**default_params, visual_size=10, target_size=2), - "with_dots": with_dots(**default_params, n_dots=5, dot_radius=2, distance=0.5, target_shape=3), - "dotted": dotted(**default_params, n_dots=5, dot_radius=2, distance=0.5, target_shape=3), - "2sided_with_dots": two_sided_with_dots(**default_params, n_dots=5, dot_radius=2, distance=0.5, target_shape=3), - "2sided_dotted": two_sided_dotted(**default_params, n_dots=5, dot_radius=2, distance=0.5, target_shape=3), + "sbc_generalized": generalized(**default_params, visual_size=10, target_size=(3,4), target_position=(1, 2)), + "sbc_basic": basic(**default_params, visual_size=10, target_size=3), + "sbc_2sided": two_sided(**default_params, visual_size=10, target_size=2), + "sbc_with_dots": with_dots(**default_params, n_dots=5, dot_radius=2, distance=0.5, target_shape=3), + "sbc_dotted": dotted(**default_params, n_dots=5, dot_radius=2, distance=0.5, target_shape=3), + "sbc_2sided_with_dots": two_sided_with_dots(**default_params, n_dots=5, dot_radius=2, distance=0.5, target_shape=3), + "sbc_2sided_dotted": two_sided_dotted(**default_params, n_dots=5, dot_radius=2, distance=0.5, target_shape=3), } # fmt: on diff --git a/stimupy/stimuli/todorovics.py b/stimupy/stimuli/todorovics.py index df4bde70..0f1956f7 100644 --- a/stimupy/stimuli/todorovics.py +++ b/stimupy/stimuli/todorovics.py @@ -838,14 +838,14 @@ def overview(**kwargs): } # fmt: off stimuli = { - "rectangle": rectangle(**default_params, **rectangle_params, covers_offset=1.5), - "rectangle_general": rectangle_generalized(**default_params, **rectangle_params, target_position=3.5, covers_x=(2, 6), covers_y=(2, 6)), - "cross": cross(**default_params, **cross_params, covers_size=2), - "cross_general": cross_generalized(**default_params, **cross_params, covers_size=2, covers_x=(2, 6), covers_y=(2, 6)), - "equal": equal(**default_params, **cross_params,), - "two_sided_rectangle": two_sided_rectangle(**default_params, **rectangle_params, covers_offset=1), - "two_sided_cross": two_sided_cross(**default_params, **cross_params, covers_size=1), - "two_sided_equal": two_sided_equal(**default_params, **cross_params,), + "todorovic_rectangle": rectangle(**default_params, **rectangle_params, covers_offset=1.5), + "todorovic_rectangle_general": rectangle_generalized(**default_params, **rectangle_params, target_position=3.5, covers_x=(2, 6), covers_y=(2, 6)), + "todorovic_cross": cross(**default_params, **cross_params, covers_size=2), + "todorovic_cross_general": cross_generalized(**default_params, **cross_params, covers_size=2, covers_x=(2, 6), covers_y=(2, 6)), + "todorovic_equal": equal(**default_params, **cross_params,), + "todorovic_rectangle_2sided": two_sided_rectangle(**default_params, **rectangle_params, covers_offset=1), + "todorovic_cross_2sided": two_sided_cross(**default_params, **cross_params, covers_size=1), + "todorovic_equal_2sided": two_sided_equal(**default_params, **cross_params,), } # fmt: on diff --git a/stimupy/stimuli/wedding_cakes.py b/stimupy/stimuli/wedding_cakes.py index f674e447..813c0184 100644 --- a/stimupy/stimuli/wedding_cakes.py +++ b/stimupy/stimuli/wedding_cakes.py @@ -187,18 +187,19 @@ def overview(**kwargs): dict with all stimuli containing individual stimulus dicts. """ default_params = { - "visual_size": 10, - "ppd": 30, + "visual_size": 15, + "ppd": 10, } default_params.update(kwargs) # fmt: off stimuli = { - "Wedding cake": wedding_cake(**default_params, - L_size= (3, 3, 1), + "wedding_cake": wedding_cake(**default_params, + L_size= (4, 3, 1), target_height=1, - target_indices1=None, - target_indices2=((0, 1), (1, 1)),) + target_indices1=((2, 2), (2, 1)), + target_indices2=((2, -1), (2, 0)), + ) } # fmt: on @@ -209,4 +210,4 @@ def overview(**kwargs): from stimupy.utils import plot_stimuli stims = overview() - plot_stimuli(stims, mask=True, save=None) + plot_stimuli(stims, mask=False, save=None) diff --git a/stimupy/stimuli/whites.py b/stimupy/stimuli/whites.py index 3a782337..f88a5450 100644 --- a/stimupy/stimuli/whites.py +++ b/stimupy/stimuli/whites.py @@ -769,23 +769,24 @@ def overview(**kwargs): "visual_size": 10, "ppd": 30, "frequency": 0.5, + "intensity_bars": (1, 0), } default_params.update(kwargs) # fmt: off stimuli = { - "White's stimulus": white(**default_params, target_indices=(2, -3), target_height=2), - "White's, generalized": generalized( + "white": white(**default_params, target_indices=(2, -3), target_height=2), + "white_general": generalized( **default_params, target_indices=(1, 3, 5), target_center_offsets=(-1, -3, -1), target_heights=(2, 3, 2) ), - "White's, two rows": white_two_rows( + "white_two_rows": white_two_rows( **default_params, target_indices_top=(2, 4), target_indices_bottom=(-2, -4), target_height=1, target_center_offset=2, ), - "White's - Anderson": anderson( + "white_anderson": anderson( **default_params, target_indices_top=3, target_indices_bottom=-2, @@ -794,14 +795,14 @@ def overview(**kwargs): stripe_center_offset=1.5, stripe_height=2, ), - "White's - Howe": howe( + "white_howe": howe( **default_params, target_indices_top=3, target_indices_bottom=-2, target_center_offset=2, target_height=2, ), - "White's - Yazdanbakhsh": yazdanbakhsh( + "white_yazdanbakhsh": yazdanbakhsh( **default_params, target_indices_top=3, target_indices_bottom=-2, diff --git a/stimupy/utils/plotting.py b/stimupy/utils/plotting.py index f77b890e..ba387db7 100644 --- a/stimupy/utils/plotting.py +++ b/stimupy/utils/plotting.py @@ -4,16 +4,17 @@ import matplotlib.pyplot as plt import numpy as np +from stimupy.utils import resolution + __all__ = [ - "compare_plots", "plot_stim", "plot_stimuli", + "compare_plots", ] def compare_plots(plots): - """ - Plot multiple plots in one plot for comparing. + """Plot multiple plots in one plot for comparing. Parameters ---------- @@ -37,10 +38,12 @@ def plot_stim( vmin=0, vmax=1, save=None, - extent_key="shape", + units="deg", ): - """ - Utility function to plot stimulus array (key: "img") from stim dict and mask (optional) + """Plot a stimulus + + Plots the stimulus-array (key: "img") directly from stim dict. + Optionally also plots mask. Parameters ---------- @@ -60,9 +63,10 @@ def plot_stim( save : None or str, optional If None (default), do not save the plot. If string is provided, save plot under this name. - extent_key : str, optional - Key to extent which will be used for plotting. - Default is "shape", using the image size in pixels as extent. + units : "px", "deg" (default), or str + what units to put on the axes, by default degrees visual angle ("deg"). + If a str other than "deg"(/"degrees") or "px"(/"pix"/"pixels") is passed, + it must be the key to a tuple in stim Returns ------- @@ -70,22 +74,26 @@ def plot_stim( If ax was passed and plotting is None, returns updated Axis object. """ - print("Plotting:", stim_name) - single_plot = False if ax is None: ax = plt.gca() single_plot = True - if extent_key in stim.keys(): - if len(stim[extent_key]) == 2: - extent = [0, stim[extent_key][1], 0, stim[extent_key][0]] - elif len(stim[extent_key]) == 4: - extent = stim[extent_key] + # Figure out what units need to go on axes + if units in ["px", "pix", "pixels"]: + extent = [0, stim["img"].shape[1], 0, stim["img"].shape[0]] + elif units in ["deg", "degrees"]: + visual_size = resolution.validate_visual_size(stim["visual_size"]) + extent = [0, visual_size.width, 0, visual_size.height] + elif units in stim.keys(): + if len(stim[units]) == 2: + extent = [0, stim[units][1], 0, stim[units][0]] + elif len(stim[units]) == 4: + extent = stim[units] else: raise ValueError("extent should either contain 2 or 4 values") else: - warnings.warn("extent_key does not exist in dict, using pixel-extent") + warnings.warn("units does not exist in dict, using pixel-extent") extent = [0, stim["img"].shape[1], 0, stim["img"].shape[0]] if not mask: @@ -155,10 +163,13 @@ def plot_stimuli( vmin=0, vmax=1, save=None, - extent_key="shape", + units="deg", ): - """ - Utility function to plot multuple stimuli (key: "img") from stim dicts and mask (optional) + """Plot multiple stimuli + + Plots the stimulus-arrays (keys: "img") directly from stim dicts. + Arranges stimuli in a grid. + Optionally also plots masks. Parameters ---------- @@ -174,10 +185,10 @@ def plot_stimuli( save : None or str, optional If None (default), do not save the plot. If string is provided, save plot under this name. - extent_key : str, optional - Key to extent which will be used for plotting. - Default is "shape", using the image size in pixels as extent. - + units : "px", "deg" (default), or str + what units to put on the axes, by default degrees visual angle ("deg"). + If a str other than "deg"(/"degrees") or "px"(/"pix"/"pixels") is passed, + it must be the key to a tuple in stim """ # Plot each stimulus+mask @@ -193,7 +204,7 @@ def plot_stimuli( vmin=vmin, vmax=vmax, save=None, - extent_key=extent_key, + units=units, ) plt.tight_layout()