Skip to main content
Skip table of contents

Drawing dots at 1440 Hz in Python

This demo is the Python equivalent to our MATLAB demo Drawing dots at 1440 Hz. We use PsychoPy and the libdpx wrapper tools to display twelve white dots around the center of the screen, cycling at a rate of 1440 Hz. The demo ends after 5 seconds.

To achieve this display rate, the PROPixx uses a “sequencer” to break up a single 1920 x 1080, 120 Hz RGB video signal into 12 frames displayed in sequence.

First, the 1920 x 1080 image is divided into four 960 x 540 images, or “quadrants.”. Next, each 8-bit RGB colour channel is converted to grayscale and displayed sequentially. The order of appearance is given in the illustration below:

Quad12x mode

To create our stimuli, we determine target locations as if they were full resolution, full screen. The helper script reformatForQUAD12x reassigns and rescales the target positon to the correct quadrant. Depending on the frame, we draw our targets as either full red, full green or full blue.

A screen running in 120 Hz mode will display the twelve dots on the same frame, in the correct quadrant and with the proper colour profile. Here is a demonstration, albeit slowed down:

A screen running in 120 Hz mode will display the twelve dots on the same frame, in the correct quadrant and with the correct colour profile. Here is a demonstration, albeit slowed down.

The PROPixx in 1440 Hz mode will display the four quadrants in sequence, starting with red and repeating for green and blue. The image will be on full screen and grayscale. Note: This animation is slowed down for demonstration purposes. At true 1440 Hz, the dots appear simultaneously in a ring.

PY
"""
This demonstration invokes the PROPixx QUAD12x sequencer to present a series of dots
at 1440 Hz. The dot positions are initially constructed based on a full-sized image. 
They are drawn in order from 1-12; the colour of the dot is chosen based on their position in the order,
and a helper function scales the dots and adds an offset for the appropriate quadrant. The final 
image is of a ring of white dots positioned around the center of the display. They are in fact illuminating 
sequentially but to the eye they appear static.

History
Feb 10 2025 - LEF & JT - Written & tested
"""

from psychopy import visual  # Import visual module from PsychoPy for stimulus presentation
from psychopy.hardware import keyboard  # Import keyboard module for user input handling
from pypixxlib import _libdpx as dp  # Import DPX library for PROPixx hardware communication
import numpy as np  # Import NumPy for numerical operations


### HELPER FUNCTIONS 
def reformatForQUAD12x(shapeStim, win, quadrant):
    """ 
    Rescales the shapeStim stimulus to 960 x 540 and applies a position offset 
    based on the quadrant argument.
    
    Parameters:
    shapeStim (PsychoPy visual stimulus): The stimulus to be resized and repositioned
    win (PsychoPy Window): The PsychoPy window where the stimulus is displayed
    quadrant (int): Quadrant number (1-4) determining position offset
    
    Returns:
    shapeStim (PsychoPy visual stimulus): Updated stimulus with new size and position
    """
    
    # Ensure the window is 1920 x 1080, required for proper sequencing
    if not(win.size[0] == 1920) or not(win.size[1] == 1080):
        print('Warning! Window is not 1920 x 1080, window size must be 1920 x 1080 for sequencer to work correctly.')
        return
        
    # Rescale stimulus size by half to match resolution reduction
    for i in range(0, len(shapeStim.size)):
        shapeStim.size[i] = shapeStim.size[i] / 2

    # Define position offsets for each quadrant
    x = win.size[0] / 4  # Quarter of screen width
    y = win.size[1] / 4  # Quarter of screen height
    offsets = np.tile(([-x, y], [x, y], [-x, -y], [x, -y]), (3, 1))  # Offsets for 4 quadrants
    
    # Apply position offsets
    newPos = [0, 0]
    newPos[0] = shapeStim.pos[0] + offsets[quadrant][0]
    newPos[1] = shapeStim.pos[1] + offsets[quadrant][1]  
    shapeStim.pos = newPos   
        
    return shapeStim

### EXPERIMENT START 
# Start PROPixx connection and enable sequencer 
dp.DPxOpen()
isReady = dp.DPxIsReady()
if isReady:
    dp.DPxSetPPxDlpSeqPgrm('GREY Quad 1440Hz')  # Set sequencing mode
    dp.DPxWriteRegCache()
else:
    print('Warning! DPx call failed, check connection to hardware')
    
## Create a 1920 x 1080 PsychoPy window
win = visual.Window(
        screen=0,  # Change to 1 for second monitor display
        monitor=None,
        size=[1920, 1080],
        fullscr=True,  # Fullscreen mode
        pos=[0, 0],
        color=[-1, -1, -1],  # Black background
        units="pix"   # Pixel-based coordinate system
)

# Define stimulus parameters
dotRadius = 30  # Radius of stimulus dot
targetRadius = 200  # Distance of stimuli from center
targetIntensity = 1 # Max intensity
center = (0, 0)  # Center of display
locations = np.empty(shape=(12, 2))  # Array to store stimulus positions

# Define 12 evenly spaced positions around a circle
position = np.linspace(-180, 150, 12)  # Generate angles for circular placement
for k in range(0, 12):
    angle = position[k]
    locations[k, 0] = center[0] + targetRadius * np.cos(-angle * np.pi / 180)  # X-coordinate
    locations[k, 1] = center[1] + targetRadius * np.sin(-angle * np.pi / 180)  # Y-coordinate


### PRESENTATION LOOP
# Initialize time step counter
t = 0

while t < (120*5):  # Run for 5s
    for i in range(0, 12): 
        
        # Assign color based on stimulus index
        if i < 4:
            colourChannel = [targetIntensity, -1, -1]  # Red
        elif i > 7:
            colourChannel = [-1, -1, targetIntensity]  # Blue
        else:
            colourChannel = [-1, targetIntensity, -1]  # Green
            
        # Create circular stimulus
        stimulus = visual.Circle(win=win,
                                 pos = locations[i],
                                 size=[dotRadius, dotRadius],                                              
                                 fillColor=colourChannel)
        
        # Reformat stimulus position based on quadrant
        stimulus = reformatForQUAD12x(stimulus, win, i)
        stimulus.draw()  # Draw stimulus to window
    
        # Flip window after all 12 quadrants are drawn
        if i == 11:
            win.flip()
        
        t += 1  # Increment frame counter
        
# Close the PsychoPy window
win.close() 

# Restore default PROPixx sequencing mode (Commented out for now)
dp.DPxSetPPxDlpSeqPgrm('RGB 120Hz')
dp.DPxWriteRegCache()
dp.DPxClose()
JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.