I'm developing a metacognition experiment in PsychoPy (v. 1.90.1) and I need a visual analogue scale to measure confidence. However, I can't find a way to remove the numeric values (0 and 1) from the extremities of the Psychopy VAS.
Is there any way to hide them?
I need the word labels ("Not at all confident", "Extremely confident") but I would also like to have the answers recorded on a 0-100 scale (or an equivalent 0-1) as the analogue scale does (so switching to categorical wouldn't do).
Any suggestion?
Thanks in advance.
Sonia
You may also be interested in the new Slider, which is included in the current PsychoPy beta versions and will be part of the next release. Here is a Python 3 code example how to use it:
from psychopy.visual.window import Window
from psychopy.visual.slider import Slider
win = Window()
vas = Slider(win,
ticks=(1, 100),
labels=('Not at all confident', 'Extremely confident'),
granularity=1,
color='white')
while not vas.rating:
vas.draw()
win.flip()
print(f'Rating: {vas.rating}, RT: {vas.rt}')
Before re-use, you will have to call vas.reset().
Take a look at the documentation, particularly labels and scale. This is one solution:
# Set up window and scale
from psychopy import visual
win = visual.Window()
scale = visual.RatingScale(win,
labels=['Not at all confident', 'Extremely confident'], # End points
scale=None, # Suppress default
low=1, high=100, tickHeight=0)
# Show scale
while scale.noResponse:
scale.draw()
win.flip()
# Show response
print scale.getRating(), scale.getRT()
Might I extend #hoechenberger's answer to add support for
the randomised start on each trial (set markerPos to a random val in units of the tick marks)
keypress support (just use keys to adjust markerPos and when you're done assign that to rating)
custom step sizes (when you've understood (2) then this is probably obvious)
Python2.7 (no need to force Py3.6 here :wink: )
Here's the code:
from psychopy import visual
from psychopy import event
from numpy.random import random
stepSize = 2
win = visual.Window()
vas = visual.Slider(win,
ticks=(0, 1),
labels=('Not at all confident', 'Extremely confident'),
granularity=1,
color='white')
for thisTrialN in range(5):
vas.reset()
vas.markerPos = random() # randomise start
while not vas.rating:
# check keys
keys = event.getKeys()
if 'right' in keys:
vas.markerPos += stepSize
if 'left' in keys:
vas.markerPos -= stepSize
if 'return' in keys:
# confirm as a rating
vas.rating = vas.markerPos
# update the scale on screen
vas.draw()
win.flip()
print('Rating: {}, RT: {}'.format(vas.rating, vas.rt))
Related
I'm trying run essentially two animations (ref. following code):
class RelTrain(Scene):
def construct(self):
train = Rectangle(height=1, width=4)
train2 = Rectangle(height=2, width=2)
train.move_to(np.array([-10,0,0]))
train2.move_to(np.array([0,0,0]))
self.add(train, train2)
self.play(
train.move_to, np.array([10,0,0]),
train2.move_to, np.array([15,0,0]),
run_time=18,
rate_func=linear,
)
self.wait()
Essentially two rectangles are moving, but I do not want them to begin movement simultaneously. I want train to start moving, and after 2 seconds (train would still be moving at this point since run_time=18), I want train2 to pop up on the screen and begin its motion. I'm not sure how this is done and would appreciate any help.
After playing for a while, I've figured how to do this with ManimCE (v0.3.0). This is not very well documented yet, but essentially you can use mobject updaters. I'm not sure if this is the best way to do this (it seems to me that is too much verbose and low level), but it works:
Code
import numpy as np
from manim import *
class DeplayedTrains(Scene):
def construct(self):
# create both trains
trains = (
Rectangle(height=1, width=4),
Rectangle(height=2, width=2),
)
# indicate start and end points
start_points_X, end_points_X = ((-5, 0), (5, 5))
# compute movement distances for both trains
distances = (
(end_points_X[0] - start_points_X[0]),
(end_points_X[1] - start_points_X[1]),
)
# place trains at start points and add to the scene
for train, start_point in zip(trains, start_points_X):
train.move_to(np.array([start_point, 0, 0]))
self.add(train)
# deifine durations of movements for both trains, get FPS from config
durations, fps = ((5, 3), config["frame_rate"])
# create updaters
updaters = (
# add to the current position in X the difference for each frame,
# given the distance and duration defined
lambda mobj, dt: mobj.set_x(mobj.get_x() + (distances[0] / fps / durations[0])),
lambda mobj, dt: mobj.set_x(mobj.get_x() + (distances[1] / fps / durations[1])),
)
# add updaters to trains objects, movement begins
trains[0].add_updater(updaters[0])
# wait 2 seconds
self.wait(2)
# start the movement of the second train and wait 3 seconds
trains[1].add_updater(updaters[1])
self.wait(3)
# remove the updaters
trains[0].clear_updaters() # you can also call trains[0].remove_updater(updaters[0])
trains[1].clear_updaters()
Output
For those looking to making animations start at different times while still partially temporally overlapping, take a look at LaggedStart and similar things:
https://docs.manim.community/en/stable/reference/manim.animation.composition.LaggedStart.html#manim.animation.composition.LaggedStart
https://docs.manim.community/en/stable/reference/manim.animation.composition.html
Unfortunately they are not documented, but it took me a while to realize they even existed.
I wrote a code to display live feed of analog data. The code uses pyfirmata to define pins and pull readings. I've set the funcanimation to pull all 12 channels when the port is open. Currently, matplotlib checkbutton is used to show/hide live feed of the channels.
I'd like to manipulate the matplotlib checkbutton so that only the channels that are checked are actually read instead of just being hidden.
The matplotlib widget module is a little too sophisticated for me to break down to a level where I can modify it. What I'd like to do is write a true/false status on each index depending on its visibility then put a nested if statements in the funcanimation to read only the visible lines. I'd appreciate if anyone could share me a sample code to allow me to do that.
Here is a segment of my code:
##check buttons
lines = [ln0, ln1, ln2, ln3, ln4, ln5, ln6, ln7, ln8, ln9, ln10, ln11]
labels = [str(ln0.get_label()) for ln0 in lines]
visibility = [ln0.get_visible() for ln0 in lines]
check = CheckButtons(ax1, labels, visibility)
for i, c in enumerate(colour):
check.labels[i].set_color(c)
def func(label):
index = labels.index(label)
lines[index].set_visible(not lines[index].get_visible())
check.on_clicked(func)
## define pins
a0 = due.get_pin('a:0:i')
a1 = due.get_pin('a:1:i')
a2 = due.get_pin('a:2:i')
a3 = ...
##funcanimation
def rt(i):
t.append(datetime.now())
if due.is_open == True:
T0.append(round(a0.read()*3.3/0.005, 1))
T1.append(round(a1.read()*3.3/0.005, 1))
...
Here is the graph and checkbuttons when run:
click here
Thanks,
I figured it out. There is a get_status function embedded in the matplotlib widget which returns a tuple of trues and falses to indicate the status of check buttons. I used this to write a nested if statements in the funcanimation so that only checked ones are read.
I'm using Python to generate images using dashed lines for stippling. The period of the dashing is constant, what changes is dash/space ratio. This produces something like this:
However in that image the dashing has a uniform origin and this creates unsightly vertical gutters. So I tried to randomize the origin to remove the gutters. This sort of works but there is an obvious pattern:
Wondering where this comes from I made a very simple test case with stacked dashed straight lines:
dash ratio: 50%
dash period 20px
origin shift from -10px to +10px using random.uniform(-10.,+10.)(*) (after an initial random.seed()
And with added randomness:
So there is still pattern. What I don't understand is that to get a visible gutter you need to have 6 or 7 consecutive values falling in the same range (says, half the total range), which should be a 1/64 probability but seems to happen a lot more often in the 200 lines generated.
Am I misunderstanding something? Is it just our human brain which is seeing patterns where there is none? Could there be a better way to generate something more "visually random" (python 2.7, and preferably without installing anything)?
(*) partial pixels are valid in that context
Annex: the code I use (this is a Gimp script):
#!/usr/bin/env python
# -*- coding: iso-8859-15 -*-
# Python script for Gimp (requires Gimp 2.10)
# Run on a 400x400 image to see something without having to wait too much
# Menu entry is in "Test" submenu of image menubar
import random,traceback
from gimpfu import *
def constant(minShift,maxShift):
return 0
def triangle(minShift,maxShift):
return random.triangular(minShift,maxShift)
def uniform(minShift,maxShift):
return random.uniform(minShift,maxShift)
def gauss(minShift,maxShift):
return random.gauss((minShift+maxShift)/2,(maxShift-minShift)/2)
variants=[('Constant',constant),('Triangle',triangle),('Uniform',uniform),('Gauss',gauss)]
def generate(image,name,generator):
random.seed()
layer=gimp.Layer(image, name, image.width, image.height, RGB_IMAGE,100, LAYER_MODE_NORMAL)
image.add_layer(layer,0)
layer.fill(FILL_WHITE)
path=pdb.gimp_vectors_new(image,name)
# Generate path, horizontal lines are 2px apart,
# Start on left has a random offset, end is on the right edge right edge
for i in range(1,image.height, 2):
shift=generator(-10.,10.)
points=[shift,i]*3+[image.width,i]*3
pdb.gimp_vectors_stroke_new_from_points(path,0, len(points),points,False)
pdb.gimp_image_add_vectors(image, path, 0)
# Stroke the path
pdb.gimp_context_set_foreground(gimpcolor.RGB(0, 0, 0, 255))
pdb.gimp_context_set_stroke_method(STROKE_LINE)
pdb.gimp_context_set_line_cap_style(0)
pdb.gimp_context_set_line_join_style(0)
pdb.gimp_context_set_line_miter_limit(0.)
pdb.gimp_context_set_line_width(2)
pdb.gimp_context_set_line_dash_pattern(2,[5,5])
pdb.gimp_drawable_edit_stroke_item(layer,path)
def randomTest(image):
image.undo_group_start()
gimp.context_push()
try:
for name,generator in variants:
generate(image,name,generator)
except Exception as e:
print e.args[0]
pdb.gimp_message(e.args[0])
traceback.print_exc()
gimp.context_pop()
image.undo_group_end()
return;
### Registration
desc="Python random test"
register(
"randomize-test",desc,'','','','',desc,"*",
[(PF_IMAGE, "image", "Input image", None),],[],
randomTest,menu="<Image>/Test",
)
main()
Think of it like this: a gutter is perceptible until it is obstructed (or almost so). This only happens when two successive lines are almost completely out of phase (with the black segments in the first line lying nearly above the white segments in the next). Such extreme situations only happens about one out of every 10 rows, hence the visible gutters which seem to extend around 10 rows before being obstructed.
Looked at another way -- if you print out the image, there really are longish white channels through which you can easily draw a line with a pen. Why should your mind not perceive them?
To get better visual randomness, find a way to make successive lines dependent rather than independent in such a way that the almost-out-of-phase behavior appears more often.
There's at least one obvious reason why we see a pattern in the "random" picture : the 400x400 pixels are just the same 20x400 pixels repeated 20 times.
So every apparent movement is repeated 20 times in parallel, which really helps the brain analyzing the picture.
Actually, the same 10px wide pattern is repeated 40 times, alternating between black and white:
You could randomize the dash period separately for each line (e.g. between 12 and 28):
Here's the corresponding code :
import numpy as np
import random
from matplotlib import pyplot as plt
%matplotlib inline
plt.rcParams['figure.figsize'] = [13, 13]
N = 400
def random_pixels(width, height):
return np.random.rand(height, width) < 0.5
def display(table):
plt.imshow(table, cmap='Greys', interpolation='none')
plt.show()
display(random_pixels(N, N))
def stripes(width, height, stripe_width):
table = np.zeros((height, width))
cycles = width // (stripe_width * 2) + 1
pattern = np.concatenate([np.zeros(stripe_width), np.ones(stripe_width)])
for i in range(height):
table[i] = np.tile(pattern, cycles)[:width]
return table
display(stripes(N, N, 10))
def shifted_stripes(width, height, stripe_width):
table = np.zeros((height, width))
period = stripe_width * 2
cycles = width // period + 1
pattern = np.concatenate([np.zeros(stripe_width), np.ones(stripe_width)])
for i in range(height):
table[i] = np.roll(np.tile(pattern, cycles), random.randrange(0, period))[:width]
return table
display(shifted_stripes(N, N, 10))
def flexible_stripes(width, height, average_width, delta):
table = np.zeros((height, width))
for i in range(height):
stripe_width = random.randint(average_width - delta, average_width + delta)
period = stripe_width * 2
cycles = width // period + 1
pattern = np.concatenate([np.zeros(stripe_width), np.ones(stripe_width)])
table[i] = np.roll(np.tile(pattern, cycles), random.randrange(0, period))[:width]
return table
display(flexible_stripes(N, N, 10, 4))
Posting my final solution as an answer, but please upvote others.
John Coleman has a point when he says:
To get better visual randomness, find a way to make successive lines dependent rather than independent in such a way that the almost-out-of-phase behavior appears more often.
So, finally, the best way to avoid gutters is to forego randomness and have a very fixed scheme of shifts, and one that works well is a 4-phase 0,25%,75%,50% cycle:
OK, there is still slight diamond pattern, but it is much less visible than the patterns introduced by the random schemes I tried.
This is slightly counter-intuitive, but as you add random elements together the randomness gets less. If I follow correctly the range of each element is 10px - 30px. So the total size of 10 elements is 100px to 300px, but the distribution is not even across that range. The extremes are very unlikely and on average it will be pretty close to 200px, so that fundamental 20px pattern will emerge. Your random distribution needs to avoid this.
EDIT: I see I slightly misunderstood, and all dashes are are 20px with a random offset. So, I think looking at any 1 vertical set of dashes would appear random, but that same random set is repeated across the page, giving the pattern.
The pyautogui scroll amount value 1 is too small, 2 is to big for a specific task I want to do. Is there a way to scroll inbetween? I tried 1.5, but it didn't work.
I'm on OSX 10.13 and I can certainly scroll with more precision than what pyautogui is doing, when using the trackpad.
This is an issue that has been annoying me, so I took a look at the pyautogui source code and was able to solve the problem. This will probably be quite a long answer; I'll try to explain every step in detail. Note that this only works for Mac. (scroll to the bottom if you want the answer, not the explanation)
First, here is the source code for the scroll function:
def _scroll(clicks, x=None, y=None):
_vscroll(clicks, x, y)
def _vscroll(clicks, x=None, y=None):
_moveTo(x, y)
clicks = int(clicks)
for _ in range(abs(clicks) // 10):
scrollWheelEvent = Quartz.CGEventCreateScrollWheelEvent(
None, # no source
Quartz.kCGScrollEventUnitLine, # units
1, # wheelCount (number of dimensions)
10 if clicks >= 0 else -10) # vertical movement
Quartz.CGEventPost(Quartz.kCGHIDEventTap, scrollWheelEvent)
scrollWheelEvent = Quartz.CGEventCreateScrollWheelEvent(
None, # no source
Quartz.kCGScrollEventUnitLine, # units
1, # wheelCount (number of dimensions)
clicks % 10 if clicks >= 0 else -1 * (-clicks % 10)) # vertical movement
Quartz.CGEventPost(Quartz.kCGHIDEventTap, scrollWheelEvent)
Let's break this down:
1.
def _scroll(clicks, x=None, y=None):
_vscroll(clicks, x, y)
This is just a wrapper for the _vscroll function, simple enough.
2.
The main thing to realise is that pyautogui, for Mac, uses Quartz Core Graphics, all it does is provide a simpler, more readable wrapper for the Quartz code.
With the scroll function, what it is doing is creating a scroll event:
scrollWheelEvent = Quartz.CGEventCreateScrollWheelEvent
And then posting it:
Quartz.CGEventPost(Quartz.kCGHIDEventTap, scrollWheelEvent)
Ignore the details of the posting, we won't be changing any of that.
To me, it seems as if this code repeats itself, and I have no clue why any of the code after the for loop is included. I deleted this from my source code and everything works; If anyone knows why this code is included, please comment below and correct me.
3.
So we are left with the following code (ignoring the mouse moveTo, which has nothing to do with the scrolling itself):
clicks = int(clicks)
for _ in range(abs(clicks) // 10):
scrollWheelEvent = Quartz.CGEventCreateScrollWheelEvent(
None, # no source
Quartz.kCGScrollEventUnitLine, # units
1, # wheelCount (number of dimensions)
10 if clicks >= 0 else -10) # vertical movement
Quartz.CGEventPost(Quartz.kCGHIDEventTap, scrollWheelEvent)
The format of a CGEventCreateScrollWheelEvent is the following:
Quartz.CGEventCreateScrollWheelEvent(source, units, wheelCount, scroll distance)
The source in this case is None, don't worry about that, and we are only dealing with 1 wheel, so wheelCount is 1.
What the source code is doing, therefore, is scrolling a distance of ±10 Quartz.kCGScrollEventUnitLine, which are your computers units for one 'scroll'. It repeats this in a for loop for however many times you specify because the system can bug if too many scroll units are sent at once.
Therefore, the minimum one can scroll on pyautogui is one iteration of this loop, which sends one computer unit. The problem is that these units are too big for fine scrolling.
SOLUTION
We need to change the minimum value we can send. Currently it is 1 Quartz.kCGScrollEventUnitLine, but we can change these to base units by replacing them with a zero. I also see no need to floor divide clicks (in range(abs(clicks) // 10)) and then send 10 scroll units.
We can change these two parts, and remove the unnecessary repetition:
def _scroll(clicks, x=None, y=None):
_vscroll(clicks, x, y)
def _vscroll(clicks, x=None, y=None):
_moveTo(x, y)
clicks = int(clicks)
for _ in range(abs(clicks)): # <------------------------------------
scrollWheelEvent = Quartz.CGEventCreateScrollWheelEvent(
None, # no source
0, # units <------------------------------------------------
1, # wheelCount (number of dimensions)
1 if clicks >= 0 else -1) # vertical movement <--------------
Quartz.CGEventPost(Quartz.kCGHIDEventTap, scrollWheelEvent)
If you don't feel comfortable editing the source code itself, you can use these functions in your code directly, skipping out the need for pyautogui. Just have pyobjc installed (which you'll have anyway if you use pyautogui), remove _moveTo(x, y) and the keyword arguments, and use the following imports:
from Quartz.CoreGraphics import CGEventCreateScrollWheelEvent, CGEventPost, kCGHIDEventTap
I realise this answer is a bit late, but I came looking for answers to this problem and saw your question; When I solved the problem I thought I would share the knowledge.
I really struggled with this one, so I thought I'd post my solution for Windows.
After a quick pip install pywin32, I got access to the necessary win32api & win32con, among others.
NOTE: The last time I checked, pywin32 was only supported for:
Python :: 2.7
Python :: 3.5
Python :: 3.6
Python :: 3.7
import time
import win32api
import win32con
def scroll(clicks=0, delta_x=0, delta_y=0, delay_between_ticks=0):
"""
Source: https://learn.microsoft.com/en-gb/windows/win32/api/winuser/nf-winuser-mouse_event?redirectedfrom=MSDN
void mouse_event(
DWORD dwFlags,
DWORD dx,
DWORD dy,
DWORD dwData,
ULONG_PTR dwExtraInfo
);
If dwFlags contains MOUSEEVENTF_WHEEL,
then dwData specifies the amount of wheel movement.
A positive value indicates that the wheel was rotated forward, away from the user;
A negative value indicates that the wheel was rotated backward, toward the user.
One wheel click is defined as WHEEL_DELTA, which is 120.
:param delay_between_ticks:
:param delta_y:
:param delta_x:
:param clicks:
:return:
"""
if clicks > 0:
increment = win32con.WHEEL_DELTA
else:
increment = win32con.WHEEL_DELTA * -1
for _ in range(abs(clicks)):
win32api.mouse_event(win32con.MOUSEEVENTF_WHEEL, delta_x, delta_y, increment, 0)
time.sleep(delay_between_ticks)
Then, after defining
click_point = x_position, y_position
and then using
pyautogui.moveTo(x=click_point[0], y=click_point[1], duration=0.25)
to make sure that my mouse is in the correct location. I just call the above scroll function:
scroll(-4, 0.1)
to scroll down 4 ticks with a 100ms delay between ticks.
I'm trying to make a random walk in 2d, and plot the 2d walk.
I've been able to make the walk, but the plot is not exactly what I wanted.
Would it be possible to see the walk live in python ? Or just add a label to every point so that you know which point came first and which point came second etc. ?
import numpy as np
import matplotlib.pyplot as plt
import random
def randomWalkb(length):
steps = []
x,y = 0,0
walkx,walky = [x],[y]
for i in range(length):
new = random.randint(1,4)
if new == 1:
x += 1
elif new == 2:
y += 1
elif new ==3 :
x += -1
else :
y += -1
walkx.append(x)
walky.append(y)
return [walkx,walky]
walk = randomWalkb(25)
print walk
plt.plot(walk[0],walk[1],'b+', label= 'Random walk')
plt.axis([-10,10,-10,10])
plt.show()
Edit I copied my own code wrong, now it is compiling if you have the right packages installed.
The built-in turtle module could be used to draw the path at a perceptible rate.
import turtle
turtle.speed('slowest')
walk = randomWalkb(25)
for x, y in zip(*walk):
#multiply by 10, since 1 pixel differences are hard to see
turtle.goto(x*10,y*10)
turtle.exitonclick()
Sample result:
I would visualize the time-information using a color, i.e. try to plot
plt.plot(walk[0],walk[1],label= 'Random walk')
plt.scatter(walk[0],walk[1],s=50,c=range(26))
See the animation tutorial at http://jakevdp.github.io/blog/2012/08/18/matplotlib-animation-tutorial/
In this case the animate function should perform one step of your walk and set the x-y data appropriately. If you don't care to save the animation or don't have the codec installed, just ignore the anim.save call.