Skip to content

Commit

Permalink
Angular grating uses resolve_grating_params
Browse files Browse the repository at this point in the history
  • Loading branch information
JorisVincent committed Dec 13, 2022
1 parent a245ff2 commit e19c695
Showing 1 changed file with 27 additions and 95 deletions.
122 changes: 27 additions & 95 deletions stimuli/components/angular.py
Expand Up @@ -4,7 +4,7 @@
import numpy as np

from stimuli.components.circular import ring
from stimuli.components.components import image_base
from stimuli.components.components import image_base, resolve_grating_params
from stimuli.utils import resolution

__all__ = [
Expand Down Expand Up @@ -189,91 +189,13 @@ def angular_segments(
img += bool_mask["mask"] * intensity
mask += bool_mask["mask"] * (idx + 1)

return {"img": img, "mask": mask, "angles": angles, "visual_size": visual_size, "ppd": ppd}


def resolve_angular_params(
shape=None,
visual_size=None,
ppd=None,
frequency=None,
n_segments=None,
segment_width=None,
):
"""Resolve (if possible) spatial parameters for angular grating, i.e., set of segments
Angular grating (circle segments) component takes the regular resolution parameters
(shape, ppd, visual_size). In addition, there has to be an additional specification
of the number of segments, and their width. This can be done in two ways:
a segment_width (in degrees) and n_segments, and/or by specifying the spatial frequency
of a angular grating (in cycles per degree)
The total shape (in pixels) and visual size (in degrees) has to match the
specification of the segments and their widths.
Thus, not all 6 parameters have to be specified, as long as the both the resolution
and the distribution of segments can be resolved.
Note: all segments in a grating have the same width
Parameters
----------
shape : Sequence[Number, Number], Number, or None (default)
shape [height, width] of image, in pixels
visual_size : Sequence[Number, Number], Number, or None (default)
visual size [height, width] of image, in degrees
ppd : Sequence[Number, Number], Number, or None (default)
pixels per degree [vertical, horizontal]
frequency : Number, or None (default)
spatial frequency of angular grating, in cycles per degree
n_segments : int, or None (default)
number of segments
segment_width : Number, or None (default)
width of a single segment, in degrees
Returns
-------
dict[str, Any]
dictionary with all six resolution & size parameters resolved.
"""

# Resolve resolution
resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd)

# Try to resolve number and width(s) of segments
# segment_width = degrees_per_segment = 1 / segments_per_degree = 1 / (2*frequency)
if segment_width is not None:
segment_per_degree = 1 / segment_width
if frequency is not None and segment_per_degree != 2 * frequency:
raise ValueError(
f"segment_width {segment_width} and frequency {frequency} don't match"
)
elif frequency is not None:
segment_per_degree = 2 * frequency
else: # both are None:
segment_per_degree = None

# Logic here is that segment_width expresses "degrees per segment",
# which we can invert to segments_per_degree, analogous to ppd:
# n_segments = segments_per_degree * n_degrees
# is analogous to
# pix = ppd * n_degrees
# Thus we can resolve the number and spacing of segments also as a resolution
try:
n_segments, _, segment_per_degree = resolution.resolve_1D(
length=n_segments, visual_angle=360, ppd=segment_per_degree
)
segment_width = 1 / segment_per_degree
frequency = segment_per_degree / 2
except Exception as e:
raise Exception("Could not resolve grating frequency, segment_width, n_segments") from e

return {
"shape": shape,
"img": img,
"mask": mask,
"angles": angles,
"visual_size": visual_size,
"ppd": ppd,
"frequency": frequency,
"segment_width": segment_width,
"n_segments": n_segments,
"shape": shape,
}


Expand Down Expand Up @@ -318,30 +240,40 @@ def grating(
and additional keys containing stimulus parameters
"""

# Resolve grating
params = resolve_angular_params(shape, visual_size, ppd, frequency, n_segments, segment_width)
# Resolve resolution
shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd)

# Clean-up params for passing through
stim_params = copy.deepcopy(params)
n_segments = stim_params.pop("n_segments", None)
segment_width = stim_params.pop("segment_width", None)
stim_params.pop("frequency", None)
# Resolve grating
params = resolve_grating_params(
visual_angle=360,
ppd=1,
frequency=frequency,
n_phases=n_segments,
phase_width=segment_width,
period="ignore",
)

# Determine angles
angular_widths = itertools.repeat(segment_width, n_segments - 1)
angles = np.array([0] + [*itertools.accumulate(angular_widths)] + [360])
angles = [0] + params["edges"]
angles = sorted(np.unique(angles))

# Draw stim
stim = angular_segments(
angles,
angles=angles,
rotation=rotation,
**stim_params,
visual_size=visual_size,
ppd=ppd,
shape=shape,
intensities=intensities,
)

# Assemble output
return {**stim, **params}
return {
**stim,
"n_segments": params["n_phases"],
"frequency": params["frequency"],
"n_segments": params["n_phases"],
}


def pinwheel(
Expand Down

0 comments on commit e19c695

Please sign in to comment.