QGridLayout creates an incorrect layout in PyQt5 - pyqt5

I am having an issue with QGridLayout using PyQt5. In my UI, I have two pyqtgraph PlotWidget(). One of them contains 5 different plots and the other one has only one. I want the PlotWidget() with 5 plots to be 5 times that of the other one. The following code configures everything for this plot size proportion:
grid_layout.addLayout(vlayout_signals, 0, 0, 2, 1)
grid_layout.addWidget(self.p1, 0, 1, 5, 1) # self.p1 has 5 plots in it.
grid_layout.addWidget(self.p2, 5, 1, 1, 1) # self.p2 has only 1 plot in it.
The problem is python does exactly the opposite. The widget with one plot becomes 5 times bigger. I don't know why. I wanted self.p1 to be at (row=0, col=1) and occupy 5 rows and 1 column, and self.p2 to be at (row=5, col=1) and only occupy 1 row and 1 column. For some reason, Python is not doing it (I'm using python 3.11).
Here is a minimal working example:
NFRAME = 200 * 6
import sys
import numpy as np
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QLabel, QMainWindow, QVBoxLayout, QHBoxLayout, QPushButton, QWidget, QGridLayout, QComboBox, QSlider
from PyQt5.QtGui import QColor
import pyqtgraph as pg
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
grid_layout = QGridLayout()
vlayout = QVBoxLayout()
self.p1 = pg.PlotWidget()
self.p2 = pg.PlotWidget()
self.p1.plotItem.addLegend()
self.p2.plotItem.addLegend()
x = np.linspace(0, 2 * np.pi * 2, NFRAME)
y1 = np.sin(2 * np.pi * 1 * x)
y2 = 10 * np.sin(2 * np.pi * 2 * x) + 50
y3 = 20 * np.sin(2 * np.pi * 3 * x) + 200
y4 = 30 * np.sin(2 * np.pi * 4 * x) + 300
y5 = 40 * np.sin(2 * np.pi * 5 * x) + 400
self.ph1_y1 = self.p1.plot(x, y1, pen=pg.mkPen(QColor(255, 0, 0)), name="y1")
self.ph1_y2 = self.p1.plot(x, y2, pen=pg.mkPen(QColor(255, 255, 0)), name="y2")
self.ph1_y3 = self.p1.plot(x, y3, pen=pg.mkPen(QColor(0, 255, 255)), name="y3")
self.ph1_y4 = self.p1.plot(x, y4, pen=pg.mkPen(QColor(255, 0, 255)), name="y4")
self.ph1_y5 = self.p1.plot(x, y5, pen=pg.mkPen(QColor(102, 102, 255)), name="y5")
self.ph2_y1 = self.p2.plot(x, 0 * x + 6)
signal_select = QComboBox(self)
signal_select.addItem("Item1")
signal_select.addItem("Item2")
signal_offset = QSlider(Qt.Orientation.Vertical)
signal_gain = QSlider(Qt.Orientation.Vertical)
hlayout_sliders = QHBoxLayout()
hlayout_sliders.addWidget(signal_offset)
hlayout_sliders.addWidget(signal_gain)
hlayout_sliders_labels = QHBoxLayout()
signal_offset_label = QLabel()
signal_gain_label = QLabel()
signal_offset_label.setText("Offset")
signal_offset_label.setAlignment(Qt.AlignCenter)
signal_gain_label.setText("Gain")
signal_gain_label.setAlignment(Qt.AlignCenter)
hlayout_sliders_labels.addWidget(signal_offset_label)
hlayout_sliders_labels.addWidget(signal_gain_label)
arduino_signal_reset = QPushButton("Reset")
vlayout_signals = QVBoxLayout()
vlayout_signals.addWidget(signal_select)
vlayout_signals.addLayout(hlayout_sliders)
vlayout_signals.addLayout(hlayout_sliders_labels)
vlayout_signals.addWidget(arduino_signal_reset)
grid_layout.addLayout(vlayout_signals, 0, 0, 2, 1)
grid_layout.addWidget(self.p1, 0, 1, 5, 1)
grid_layout.addWidget(self.p2, 5, 1, 1, 1)
widget = QWidget()
widget.setLayout(grid_layout)
vlayout.addWidget(widget)
self.setLayout(grid_layout)
self.setCentralWidget(widget)
self.setWindowTitle('My softwaree')
if __name__ == '__main__':
app = QApplication(sys.argv)
windowExample = MainWindow()
windowExample.show()
sys.exit(app.exec_())

TL;DR
Don't use spans to set the proportions between widgets, instead use setRowStretch(), or, better, nested layouts.
Explanation
A common misconception about QGridLayout is that using "gaps" in rows and columns would result in placing them at different positions, and, similarly, using bigger row/column spans would result in a bigger size available for the widget.
That is not how the grid layout works, since its rows and columns have no specific size, nor they are directly considered for size ratios.
...At least, in theory.
What happens in your case is related to the complex way both QGridLayout and PlotWidget behave and interact.
When a layout-managed widget is shown for the first time (and whenever it's resized), it queries its layout, which in turn makes lots of internal computations for the items it manages, including considering their size hints, constraints and policies.
Note that by "items" I mean widgets, spacers and even inner layouts (see QLayoutItem).
QGridLayout is quite complex, and its behavior is not always intuitive, especially when dealing with complex widgets and layouts.
Items in a grid layout are placed in "cells", and when computing geometries for those cells (and the items they occupy) the layout engine does, more or less, the following:
query each layout item (widget, spacer or nested layout) about their size hints, constraints and policies;
correlate those items with the cells they occupy;
compute each item final geometry, which is based on the item x and y, and width and height of the last row/column they occupy;
add the last row/height coordinates to the width/height of the item, minus the initial x/y;
compute the internal required geometry of the item, considering the contents of the area occupied by the item;
set the geometry of the items, and eventually do everything again in case those items react to their resizing by updating their hints (which happens for "size reactive" widgets like QLabel or those that implement heightForWidth());
While this generally works, it has an important drawback: if the chosen span doesn't include other items in the same rows or columns, the resulting widths and heights are possibly "nullified" (meaning that their possible size is the minimum), especially whenever any widget has an Expanding size policy, which is the case of PlotWidget, which inherits from QAbstractScrollArea (since it's based on QGraphicsView), that by default always tries to expand its contents whenever there is space left from other widgets that don't expand theirselves.
Note that things are actually even more complex, as other widgets that by default have Expanding size policies might not give the same result (ie. QScrollArea). This answer is already quite extended, and I won't go too much into the darkness of Qt size management... The problem is mainly caused by a wrong usage of spans: if you want to know more, I suggest you to take your time to patiently study the Qt sources.
Take this basic example:
from PyQt5.QtWidgets import *
class Test(QWidget):
def __init__(self):
super().__init__()
self.setStyleSheet('''
QLabel {
border: 1px solid red;
}
''')
layout = QGridLayout(self)
w1 = QLabel('Row span=2')
w1.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
w2 = QLabel('Row span=1')
w2.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
layout.addWidget(w1, 0, 0, 2, 1)
layout.addWidget(w2, 2, 0, 1, 1)
app = QApplication([])
example = Test()
example.show()
sys.exit(app.exec_())
Which will result conceptually like your case:
This behavior may seem invalid and certainly not perfect, but the point remains: as mentioned above, the purpose of spans is not to directly suggest a size for the item; using something in the wrong way often results in unexpected behavior.
This is one of the reasons for which I generally don't use (and often discourage) QGridLayout for complex UIs: most of the times that I decide for a QGridLayout, I end up with dismantling it and use nested layouts instead.
Solutions
If you want your widgets to have proportional sizes, then you have two choices. Either use QGridLayout properly (with item based rows/columns and proper stretch factors), or use nested layouts.
In your case, keeping a grid layout requires you to:
use two rows, with each plot widget taking just one row;
use setRowStretch() for both rows, with a value of 5 for the first, and 1 for the second;
use a row span of 2 for the left panel;
add a stretch to the bottom of the left panel layout, so that its contents are "pushed" to the top;
vlayout_signals.addStretch()
grid_layout.addLayout(vlayout_signals, 0, 0, 2, 1)
grid_layout.addWidget(self.p1, 0, 1)
grid_layout.addWidget(self.p2, 1, 1)
grid_layout.setRowStretch(0, 5)
grid_layout.setRowStretch(1, 1)
With a nested layout, instead:
use a HBoxLayout as main layout;
create a vertical layout for the plot widgets, and add them using the stretch argument;
add a stretch to the left panel as above;
main_layout = QHBoxLayout(self)
vlayout_signals.addStretch()
main_layout.addLayout(vlayout_signals)
plot_layout = QVBoxLayout()
main_layout.addLayout(plot_layout)
plot_layout.addWidget(self.p1, stretch=5)
plot_layout.addWidget(self.p2, stretch=1)
Note: all QLayouts have an optional QWidget argument that automatically installs the layout on the given widget, thus avoiding calling layout.setLayout(widget).
I would strongly suggest you this last solution, as it's more consistent with widgets that will probably have no size relations: the moment you need to add more plots, you don't have to care about the row span of the panel.
Interestingly enough, the only case for which a grid layout would have really made sense is the slider/label pair: instead of adding two unrelated horizontal layouts, you should have used a grid for them. You were "lucky" because those labels are relatively small and similar in widths, but the reality is that they are not properly aligned with their corresponding sliders. Try to add the following:
self.setStyleSheet('QSlider, QLabel {border: 1px solid red;}')
And that's the result:
If those label had very different lengths, their offset would have been very confusing.
Conclusions
While QGridLayout is certainly useful, it's important to consider that it can rarely be used and relied upon as "main layout" that directly manages items that are very different in their "visual" purpose; its row/column cells are just abstract references to item positions, not their resulting sizes, and spans must always be used only according to the other contents in the corresponding rows and columns.
Complex UIs almost always require nested layout managers that group their contents based on the hierarchy of their usage.
The rule of thumb is that a layout has to be considered for items that are grouped within the same hierarchy. You can certainly use QGridLayout as a main layout, but only as long as it manages items that can be considered siblings. For instance, you could consider a grid layout like this:
┌─────────────┬─────────────────────────┐
|┌───────────┐|┌───────────────────────┐|
|| combo ||| ||
||┌────┬────┐||| plot1 ||
||| | |||| ||
||| s1 | s2 |||└───────────────────────┘|
||| | |||┌───────────────────────┐|
||├────┼────┤||| ||
||| l1 | l2 |||| ||
||└─────────┘||| ||
|| btn ||| ||
|| ^ ||| plot2 ||
|| ║ ||| ||
|| stretch ||| ||
|| ║ ||| ||
|| v ||| ||
|└───────────┘|└───────────────────────┘|
├─────────────┴─────────────────────────┤
| something else |
└───────────────────────────────────────┘
Which would be written as follows:
main_layout = QGridLayout()
panel_layout = QVBoxLayout()
main_layout.addLayout(panel_layout)
panel_layout.addWidget(combo)
slider_layout = QGridLayout()
panel_layout.addLayout(slider_layout)
slider_layout.addWidget(s1)
slider_layout.addWidget(s2, 0, 1)
slider_layout.addWidget(l1, 1, 0)
slider_layout.addWidget(l2, 1, 0)
panel_layout.addWidget(btn)
panel_layout.addStretch()
plot_layout = QVBoxLayout()
main_layout.addLayout(plot_layout, 0, 1)
plot_layout.addWidget(plot1)
plot_layout.addWidget(plot2)
main_layout.addWidget(else, 1, 0, 1, 2)
I suggest you to take your time to experiment with Qt Designer, in order to get accustomed with layout managers and different widget types, policies or restraints.

You can try adjusting the stretch factors on the grid rows.
For example something like this might be what you are looking for.
grid_layout.addLayout(vlayout_signals, 0, 0, 2, 1)
grid_layout.addWidget(self.p1, 0, 1, 5, 1)
grid_layout.addWidget(self.p2, 5, 1, 1, 1)
for row in range(6):
grid_layout.setRowStretch(row, 1)
Note
The above solution will not work in all situations. For a better explanation see #musicmante's answer.

Related

Resize border of qgraphicstextitem

I am adding a QGraphicTextItem to a scene using pyqt6.
I cannot resize the widget border when text is resized.
I have looked at a few way of resizing, but none work.
The text does change to a bigger font via the context menu.
The entire class is shown below.
class FreeTextGraphicsItem(QtWidgets.QGraphicsTextItem):
def __init__(self, x, y, text_):
super(FreeTextGraphicsItem, self).__init__(None)
self.x = x
self.y = y
self.text = text_
self.setFlags(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable |
QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsFocusable |
QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
self.font = QtGui.QFont(self.settings['font'], 9, QtGui.QFont.Weight.Normal)
self.setFont(self.font)
self.setPlainText(self.text)
self.setPos(self.x, self.y)
def contextMenuEvent(self, event):
menu = QtWidgets.QMenu()
menu.addAction(_("Large font"))
action = menu.exec(QtGui.QCursor.pos())
if action is None:
return
if action.text() == "Large font":
self.font = QtGui.QFont(self.settings['font'], 12, QtGui.QFont.Weight.Normal)
frame = self.document().documentLayout().frameBoundingRect(self.document().rootFrame())
self.boundingRect().setRect(0, 0, frame.width(), frame.height())
def paint(self, painter, option, widget):
color = QtCore.Qt.GlobalColor.white
painter.setBrush(QtGui.QBrush(color, style=QtCore.Qt.BrushStyle.SolidPattern))
painter.drawRect(self.boundingRect())
painter.setFont(self.font)
fm = painter.fontMetrics()
painter.setPen(QtGui.QColor(QtCore.Qt.GlobalColor.black))
lines = self.text.split('\\n')
for row in range(0, len(lines)):
painter.drawText(5, fm.height() * (row + 1), lines[row])
You're not using the features of QGraphicsTextItem.
In fact, you're completely ignoring and overriding most of its aspects:
x and y are existing and dynamic properties of all QGraphicsItems and should never be overwritten;
the same for font of QGraphicsTextItem;
calling setRect() on the bounding rectangle is useless, as boundingRect() is a *property getter" and is returned internally by the item based on its contents (in this case, the text set with setPlainText());
the text drawing is completely overridden, and not reliable nor consistent with the text set for the item, considering that you're painting the text with split lines, while the original text has escaped new lines;
If your main purpose is to draw a border around the item, then you should only do that, and then rely on the existing capabilities of the item.
class FreeTextGraphicsItem(QtWidgets.QGraphicsTextItem):
def __init__(self, x, y, text_):
super().__init__(text_.replace('\\n', '\n'))
self.setPos(x, y)
self.setFlags(
QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable
| QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsFocusable
| QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
)
font = QtGui.QFont(self.settings['font'], 9, QtGui.QFont.Weight.Normal)
self.setFont(font)
self.setDefaulTextColor(QtGui.QColor(QtCore.Qt.GlobalColor.white))
def contextMenuEvent(self, event):
menu = QtWidgets.QMenu()
largeFontAction = menu.addAction(_("Large font"))
action = menu.exec(event.screenPos())
if action == largeFontAction:
font = QtGui.QFont(
self.settings['font'], 12, QtGui.QFont.Weight.Normal)
self.setFont(font)
def paint(self, painter, option, widget=None):
painter.save()
painter.setBrush(QtCore.Qt.GlobalColor.white)
painter.drawRect(self.boundingRect())
painter.restore()
super().paint(painter, option, widget)
Note: comparing actions with their text is pointless, other than conceptually wrong; not only you can have a more reliable object-based comparison using the action (as shown above), but that comparison can also become invalid: a menu could contain items that have the same names, and you're also probably using the _ for translations, so the text might not match at all.

Adding multiple labels to a branch in a phylogenetic tree using geom_label

I am very new to R, so I am sorry if this question is obvious. I would like to add multiple labels to branches in a phylogenetic tree, but I can only figure out how to add one label per branch. I am using the following code:
treetext = "(Japon[&&NHX:S=2],(((Al,Luteo),(Loam,(Niet,Cal))),(((Car,Bar),(Aph,Long[&&NHX:S=1],((Yam,Stig),((Zey,Semp),(A,(Hap,(This,That))))))));"
mytree <- read.nhx(textConnection(treetext))
ggtree(mytree) + geom_tiplab() +
geom_label(aes(x=branch, label=S))
I can add multiple symbols to a branch using the code below, but is so labor-intensive that I may as well do it by hand:
ggtree(mytree) +
geom_tiplab()+
geom_nodepoint(aes(subset = node == 32, x = x - .5),
size = 5, colour = "black", shape = 15) +
geom_nodepoint(aes(subset = node == 32, x = x - 2),
size = 5, colour = "gray", shape = 15)
A solution using the "ape" package would be:
library(ape)
mytree <- rtree(7) # A random tree
labels1 <- letters[1:6]
labels2 <- LETTERS[1:6]
plot(mytree)
# Setting location of label with `adj`
nodelabels(labels1, adj = c(1, -1), frame = 'none')
# We could also use `pos =` 1: below node; 3: above node
nodelabels(labels2, pos = 1, frame = 'n')
You might want to tweak the adj parameter to set the location as you desire it.
As I couldn't parse the treetext object you provided as an example (unbalanced braces), and I'm not familiar with how read.nhx() stores node labels, you might need a little R code to extract the labels elements; you can use a bare nodelabels() to plot the numbers of the nodes on the tree to be sure that your vectors are in the correct sequence.
If you wanted labels to appear on edges rather than at nodes, the function is ape::edgelabels().

Why is there a space between the bars and the axis in ggplot2 bar graphs, and how do I get rid of it?

I've been building a bar graph in R, and I noticed a problem. whenever the graph is made, it has a very small gap between the bars and the axis that causes a line of the background image to appear (Link). How can I get rid of this?
Code:
album_cover <- image_read("https://i.scdn.co/image/ab67616d0000b273922a12ba0b5a66f034dc9959")
ggplot(data=album_df, aes(x=rev(factor(track_names, track_names)), y=-1 * track_length)) +
ggtitle("Songs vs length")+
annotation_custom(rasterGrob(album_cover,
width = unit(1,"npc"),
height = unit(1,"npc")),
-Inf, Inf, -Inf, Inf)+
#geom_image(image = "https://i.scdn.co/image/ab67616d0000b273922a12ba0b5a66f034dc9959", size = Inf) +
geom_bar(stat="identity", position = "identity", color = 'NA', alpha = 0.9, width = 1, fill = 'white') +
scale_y_continuous(expand = c(0, 0), limits = c(-1 * max_track, 0)) +
scale_x_discrete(expand = c(0, 0)) +
theme(axis.title.x=element_blank(),
axis.title.y=element_blank(),
axis.text.x=element_blank(),
axis.ticks.x=element_blank()
) +
coord_flip()
Interesting issue. I've tried many things, including modification of many of the theme elements. It works with theme_void(), but then the issue resurfaces as you add back in the plot elements (namely the song titles on the axis, for some reason).
What finally did work is just squishing your image to be ever so slightly less than 1. In this case, just changing from 1 to 0.999 fixes the issue and you no longer have the strip of the image hanging out on the right. For this, I made up my own data, but I'm using the same image:
df <- data.frame(
track_names=paste0('Song',1:8),
track_length=c(3.5,7.5,5,3,7,10,6,7.4)
)
album_cover <- image_read2("https://i.scdn.co/image/ab67616d0000b273922a12ba0b5a66f034dc9959")
ggplot(data=df, aes(x=track_names, y=-1*track_length)) +
annotation_custom(rasterGrob(album_cover,
width=unit(0.999,'npc'), height=unit(1,'npc')),
xmin=-Inf, xmax=Inf, ymin=-Inf, ymax=Inf) +
geom_col(alpha=0.9, width=1, fill='white', color=NA) +
scale_y_continuous(expand=c(0,0)) +
scale_x_discrete(expand=c(0,0)) +
ggtitle('Songs vs Length') +
coord_flip()
Note, the same code above gives the following image below when width=unit(1, 'npc'),... in the rasterGrob() function (note the line at the right side of the image):

How to change colors in stat_summary()

I am trying to plot two columns of raw data (I have used melt to combine them into one data frame) and then add separate error bars for each. However, I want to make the raw data for each column one pair of colors and the error bars another set of colors, but I can't seem to get it to work. The plot I am getting is at the link below. I want to have different color pairs for the raw data and for the error bars. A simple reproducible example is coded below, for illustrative purposes.
dat2.m<-data.frame(obs=c(2,4,6,8,12,16,2,4,6),variable=c("raw","raw","raw","ip","raw","ip","raw","ip","ip"),value=runif(9,0,10))
c <- ggplot(dat2.m, aes(x=obs, y=value, color=variable,fill=variable,size = 0.02)) +geom_jitter(size=1.25) + scale_colour_manual(values = c("blue","Red"))
c<- c+stat_summary(fun.data="median_hilow",fun.args=(conf.int=0.95),aes(color=variable), position="dodge",geom="errorbar", size=0.5,lty=1)
print(c)
[1]: http://i.stack.imgur.com/A5KHk.jpg
For the record: I think that this is a really, really bad idea. Unless you have a use case where this is crucial, I think you should re-examine your plan.
However, you can get around it by adding a new set of variables, padded with a space at the end. You will want/need to play around with the legends, but this should work (though it is definitely ugly):
dat2.m<- data.frame(obs=c(2,4,6,8,12,16,2,4,6),variable=c("raw","raw","raw","ip","raw","ip","raw","ip","ip"),value=runif(9,0,10))
c <- ggplot(dat2.m, aes(x=obs, y=value, color=variable,fill=variable,size = 0.02)) +geom_jitter(size=1.25) + scale_colour_manual(values = c("blue","Red","green","purple"))
c<- c+stat_summary(fun.data="median_hilow",fun.args=(conf.int=0.95),aes(color=paste(variable," ")), position="dodge",geom="errorbar", size=0.5,lty=1)
print(c)
One way around this would be to use repetitive calls to geom_point and stat_summary. Use the data argument of those functions to feed subsets of your dataset into each call, and set the color attribute outside of aes(). It's repetitive and somewhat defeats the compactness of ggplot, but it'd do.
c <- ggplot(dat2.m, aes(x = obs, y = value, size = 0.02)) +
geom_jitter(data = subset(dat2.m, variable == 'raw'), color = 'blue', size=1.25) +
geom_jitter(data = subset(dat2.m, variable == 'ip'), color = 'red', size=1.25) +
stat_summary(data = subset(dat2.m, variable == 'raw'), fun.data="median_hilow", fun.args=(conf.int=0.95), color = 'pink', position="dodge",geom="errorbar", size=0.5,lty=1) +
stat_summary(data = subset(dat2.m, variable == 'ip'), fun.data="median_hilow", fun.args=(conf.int=0.95), color = 'green', position="dodge",geom="errorbar", size=0.5,lty=1)
print(c)

Stimuli changes with every frame being displayed.

I have a bit of code (displayed below) that is supposed to display the stimulus for 10 frames. We need pretty exact display times, so using number of frames is a must instead of core.wait(xx) as the display time won't be as precise.
Instead of drawing the stimuli, and leaving it for another 9 frames - the stimuli is re-drawn for every frame.
# Import what is needed
import numpy as np
from psychopy import visual, event, core, logging
from math import sin, cos
import random, math
win = visual.Window(size=(1366, 768), fullscr=True, screen=0, allowGUI=False, allowStencil=False,
monitor='testMonitor', color=[0,0,0], colorSpace='rgb',
blendMode='avg', useFBO=True,
units='deg')
### Definitions of libraries
'''Parameters :
numpy - python package of numerical computations
visual - where all visual stimulus live
event - code to deal with mouse + keyboard input
core - general function for timing & closing the program
logging - provides function for logging error and other messages to one file
random - options for creating arrays of random numbers
sin & cos - for geometry and trigonometry
math - mathematical operations '''
# this is supposed to record all frames
win.setRecordFrameIntervals(True)
win._refreshThreshold=1/65.0+0.004 #i've got 65Hz monitor and want to allow 4ms tolerance
#set the log module to report warnings to the std output window (default is errors only)
logging.console.setLevel(logging.WARNING)
nIntervals=5
# Create space variables and a window
lineSpaceX = 0.55
lineSpaceY = 0.55
patch_orientation = 45 # zero is vertical, going anti-clockwise
surround_orientation = 90
#Jitter values
g_posJitter = 0.05 #gaussian positional jitter
r_posJitter = 0.05 #random positional jitter
g_oriJitter = 5 #gaussian orientation jitter
r_oriJitter = 5 #random orientation jitter
#create a 1-Dimentional array
line = np.array(range(38)) #with values from (0-37) #possibly not needed 01/04/16 DK
#Region where the rectangular patch would appear
#x_rand=random.randint(1,22) #random.randint(Return random integers from low (inclusive) to high (exclusive).
#y_rand=random.randint(1,25)
x_rand=random.randint(6,13) #random.randint(Return random integers from low (inclusive) to high (inclusive).
y_rand=random.randint(6,16)
#rectangular patch dimensions
width=15
height=12
message = visual.TextStim(win,pos=(0.0,-12.0),text='...Press SPACE to continue...')
fixation = visual.TextStim(win, pos=(0.0,0.0), text='X')
# Initialize clock to record response time
rt_clock = core.Clock()
#Nested loop to draw anti-aliased lines on grid
#create a function for this
def myStim():
for x in xrange(1,33): #32x32 grid. When x is 33 will not execute loop - will stop
for y in xrange(1,33): #When y is 33 will not execute loop - will stop
##Define x & y value (Gaussian distribution-positional jitter)
x_pos = (x-32/2-1/2 )*lineSpaceX + random.gauss(0,g_posJitter) #random.gauss(mean,s.d); -1/2 is to center even-numbered stimuli; 32x32 grid
y_pos = (y-32/2-1/2 )*lineSpaceY + random.gauss(0,g_posJitter)
if (x >= x_rand and x < x_rand+width) and (y >= y_rand and y < y_rand+height): # note only "=" on one side
Line_Orientation = random.gauss(patch_orientation,g_oriJitter) #random.gauss(mean,s.d) - Gaussian func.
else:
Line_Orientation = random.gauss(surround_orientation,g_oriJitter) #random.gauss(mean,s.d) - Gaussian func.
#Line_Orientation = random.gauss(Line_Orientation,g_oriJitter) #random.gauss(mean,s.d) - Gaussian func.
#stimOri = random.uniform(xOri - r_oriJitter, xOri + r_oriJitter) #random.uniform(A,B) - Uniform func.
visual.Line(win, units = "deg", start=(0,0), end=(0.0,0.35), pos=(x_pos,y_pos), ori=Line_Orientation, autoLog=False).draw() #Gaussian func.
for frameN in range (10):
myStim()
win.flip()
print x_rand, y_rand
print keys, rt #display response and reaction time on screen output window
I have tried to use the following code to keep it displayed (by not clearing the buffer). But it just draws over it several times.
for frameN in range(10):
myStim()
win.flip(clearBuffer=False)
I realize that the problem could be because I have .draw() in the function that I have defined def myStim():. However, if I don't include the .draw() within the function - I won't be able to display the stimuli.
Thanks in advance for any help.
If I understand correctly, the problem you are facing is that you have to re-draw the stimulus on every flip, but your current drawing function also recreates the entire (random) stimulus, so:
the stimulus changes on each draw between flips, although you need it to stay constant, and
you get a (on some systems quite massive) performance penalty by re-creating the entire stimulus over and over again.
What you want instead is: create the stimulus once, in its entirety, before presentation; and then have this pre-generated stimulus drawn on every flip.
Since your stimulus consists of a fairly large number of visual elements, I would suggest using a class to store the stimulus in one place.
Essentially, you would replace your myStim() function with this class (note that I stripped out most comments, re-aligned the code a bit, and simplified the if statement):
class MyStim(object):
def __init__(self):
self.lines = []
for x in xrange(1, 33):
for y in xrange(1, 33):
x_pos = ((x - 32 / 2 - 1 / 2) * lineSpaceX +
random.gauss(0, g_posJitter))
y_pos = ((y - 32 / 2 - 1 / 2) * lineSpaceY +
random.gauss(0, g_posJitter))
if ((x_rand <= x < x_rand + width) and
(y_rand <= y < y_rand + height)):
Line_Orientation = random.gauss(patch_orientation,
g_oriJitter)
else:
Line_Orientation = random.gauss(surround_orientation,
g_oriJitter)
current_line = visual.Line(
win, units="deg", start=(0, 0), end=(0.0, 0.35),
pos=(x_pos, y_pos), ori=Line_Orientation,
autoLog=False
)
self.lines.append(current_line)
def draw(self):
[line.draw() for line in self.lines]
What this code does on instantiation is in principle identical to your myStim() function: it creates a set of (random) lines. But instead of drawing them onto the screen right away, they are all collected in the list self.lines, and will remain there until we actually need them.
The draw() method traverses through this list, element by element (that is, line by line), and calls every line's draw() method. Note that the stimuli do not have to be re-created every time we want to draw the whole set, but instead we just draw the already pre-created lines!
To get this working in practice, you first need to instantiate the MyStim class:
myStim = MyStim()
Then, whenever you want to present the stimulus, all you have to do is
myStim.draw()
win.flip()
Here is the entire, modified code that should get you started:
import numpy as np
from psychopy import visual, event, core, logging
from math import sin, cos
import random, math
win = visual.Window(size=(1366, 768), fullscr=True, screen=0, allowGUI=False, allowStencil=False,
monitor='testMonitor', color=[0,0,0], colorSpace='rgb',
blendMode='avg', useFBO=True,
units='deg')
# this is supposed to record all frames
win.setRecordFrameIntervals(True)
win._refreshThreshold=1/65.0+0.004 #i've got 65Hz monitor and want to allow 4ms tolerance
#set the log module to report warnings to the std output window (default is errors only)
logging.console.setLevel(logging.WARNING)
nIntervals=5
# Create space variables and a window
lineSpaceX = 0.55
lineSpaceY = 0.55
patch_orientation = 45 # zero is vertical, going anti-clockwise
surround_orientation = 90
#Jitter values
g_posJitter = 0.05 #gaussian positional jitter
r_posJitter = 0.05 #random positional jitter
g_oriJitter = 5 #gaussian orientation jitter
r_oriJitter = 5 #random orientation jitter
x_rand=random.randint(6,13) #random.randint(Return random integers from low (inclusive) to high (inclusive).
y_rand=random.randint(6,16)
#rectangular patch dimensions
width=15
height=12
message = visual.TextStim(win,pos=(0.0,-12.0),text='...Press SPACE to continue...')
fixation = visual.TextStim(win, pos=(0.0,0.0), text='X')
# Initialize clock to record response time
rt_clock = core.Clock()
class MyStim(object):
def __init__(self):
self.lines = []
for x in xrange(1, 33):
for y in xrange(1, 33):
x_pos = ((x - 32 / 2 - 1 / 2) * lineSpaceX +
random.gauss(0, g_posJitter))
y_pos = ((y - 32 / 2 - 1 / 2) * lineSpaceY +
random.gauss(0, g_posJitter))
if ((x_rand <= x < x_rand + width) and
(y_rand <= y < y_rand + height)):
Line_Orientation = random.gauss(patch_orientation,
g_oriJitter)
else:
Line_Orientation = random.gauss(surround_orientation,
g_oriJitter)
current_line = visual.Line(
win, units="deg", start=(0, 0), end=(0.0, 0.35),
pos=(x_pos, y_pos), ori=Line_Orientation,
autoLog=False
)
self.lines.append(current_line)
def draw(self):
[line.draw() for line in self.lines]
myStim = MyStim()
for frameN in range(10):
myStim.draw()
win.flip()
# Clear the screen
win.flip()
print x_rand, y_rand
core.quit()
Please do note that even with this approach, I am dropping frames on a 3-year-old laptop computer with relatively weak integrated graphics chip. But I suspect a modern, fast GPU would be able to handle this amount of visual objects just fine. In the worst case, you could pre-create a large set of stimuli, save them as a bitmap file via win.saveMovieFrames(), and present them as a pre-loaded SimpleImageStim during your actual study.