From e19c695e5f7924ebf4a78cd8d878f73e8a17b1e6 Mon Sep 17 00:00:00 2001 From: Joris Vincent Date: Mon, 12 Dec 2022 15:20:02 +0100 Subject: [PATCH] Angular grating uses resolve_grating_params --- stimuli/components/angular.py | 122 ++++++++-------------------------- 1 file changed, 27 insertions(+), 95 deletions(-) diff --git a/stimuli/components/angular.py b/stimuli/components/angular.py index ee233930..0399c8a3 100644 --- a/stimuli/components/angular.py +++ b/stimuli/components/angular.py @@ -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__ = [ @@ -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, } @@ -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(