Fixing of overlapping dots with visual.dotStim

I checked out the source code for dotStim and it looks pretty basic in its generation of the location of dots

    def _newDotsXY(self, nDots):
        """Returns a uniform spread of dots, according to the fieldShape and fieldSize

        usage::

            dots = self._newDots(nDots)

        """
        if self.fieldShape=='circle':#make more dots than we need and only use those that are within circle
            while True:#repeat until we have enough
                new=numpy.random.uniform(-1, 1, [nDots*2,2])#fetch twice as many as needed
                inCircle= (numpy.hypot(new[:,0],new[:,1])<1)
                if sum(inCircle)>=nDots:
                    return new[inCircle,:][:nDots,:]*0.5
        else:
            return numpy.random.uniform(-0.5, 0.5, [nDots,2])

It doesn’t seem to handle instances where in the event if the spawn location of a dot occurs within the perimeter of another (when two dots overlap), regenerate another random location. I can’t quite figure out how to do this at the moment. Anyone able to lend a helping hand with that? On a side note, I believe it would be a pretty useful added function in future psychopy updates for users who require such a feature for their experiments. In cases where nDot is say set at 10, nobody would really hope for a physical presentation of nDot = 9.5 in cases of a partial overlap or nDot = 9 in a full overlap (or an even smaller value if two or more dots overlap).

This should also apply to ElementArrayStim.

There are scenarios where overlapping elements are desirable, and I think it would be difficult to code a generic solution that avoids overlap when it isn’t desired. It is probably best left to the user to control the generation of the coordinates in such a situation.

Here’s some (untested) code that might help get you started:

import functools

import numpy as np


def generate(
    n_dots,
    gen_func=None,
    test_func=None,
    max_iter=10000
):

    if gen_func is None:

        # default is to return a random location between -1 and 1 for both
        # dimensions
        gen_func = functools.partial(
            np.random.uniform,
            low=-1.0,
            high=1.0,
            size=2
        )

    if test_func is None:

        # default is to return True regardless of the established dots and the
        # current candidate
        test_func = lambda _, __: True

    dots = []

    iteration = 0

    while iteration < max_iter:

        candidate = gen_func()

        candidate_ok = test_func(candidate, dots)

        if candidate_ok:
            dots.append(candidate)

        if len(dots) == n_dots:
            break

        iteration += 1

    else:
        raise ValueError("Iteration limit reached")

    dots = np.array(dots)

    return dots

You need to provide it with a ‘test’ that returns True if the proposed dot is OK. For example, you could use something like the following if you wanted to make it such that the minimum dot distance was 0.5:

def test_func(candidate, dots):

    if len(dots) == 0:
        return True

    distances = np.sqrt(
        np.sum(
            (candidate[np.newaxis, :] - dots) ** 2,
            axis=1
        )
    )

    return np.all(distances > 0.5)

You could then generate dots via:

dots = generate(n_dots=10, test_func=test_func)

Thanks djmannion!

I ran the codes and it generates the new list nicely. My only problem now is not being able to get the dotStim to update the new positions subsequently. Below is a snippet of the crucial elements of the script that I am currently using:

targetStim = visual.DotStim(win, units='', nDots=NrOfTDots, coherence=0.5, fieldPos=(0.0, 0.0), fieldSize=(120.0, 120.0), 
               fieldShape='sqr', dotSize=12.0, dotLife=10000, dir=0.0, speed=0.00, rgb=None, color=(1.0,1.0,1.0), 
                colorSpace='rgb', opacity=1.0, contrast=1.0, depth=0, element=None, signalDots='same', 
                noiseDots='direction', name=None, autoLog=None)

print targetStim._dotsXY

for displaythis in range(60):
    targetStim.draw()
    win.flip()

win.flip()
targetStim._dotsXY = generate(n_dots=NrOfTDots, test_func=test_func) 
targetStim._update_dotsXY()
print targetStim._dotsXY

for displaythis in range(60):
    targetStim.draw()
    win.flip()

So basically there is a visible change between the lists of position from the 1st and 2nd print. However the updated list wasn’t translated during the second draw (ie. the dots were still in the same position as before the test_func was initiated). What went wrong?

I think it is actually using _verticesBase internally rather than _dotsXY. You could change the targetStim._dotsXY = line to be targetStim._verticesBase =, but you might be better off overriding the new dot creation method:

gen_func = functools.partial(
    np.random.uniform(low=0.5, high=+0.5, 2)
)

psychopy.visual.DotStim._newDotsXY = functools.partial(
    generate,
    gen_func=gen_func,
    test_func=test_func
)

You would need to do this before you create your targetStim.

Note however that the position constraints will currently only operate for the initially generated distribution; any replacements for ‘dead’ dots and some forms of dot motion will not have the same constraints.

This is a bit off the topic of the original post, but since you mentioned the case of small nDots (e.g., 10), beware of potential issues with coherence parameter: DotStim with small number of dots (+ ideas on future updates)

Say if I have two different dot stimuli present simultaneously, which vary between 312 and 382 dots each. Would randomly generating a new dot if two overlap slow my program down too much? is there a workaround that would work?

Also, would doing so hinder my ability to upload my experiment online, as I would be moving away from using the builder view?