├── .gitattributes ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── COPYING ├── README.rst ├── TODO.rst ├── docs ├── Makefile ├── _static │ ├── adjusted_phase_and_frequency.png │ ├── ccbs.jpg │ ├── default_circular.png │ ├── default_gaussian.png │ ├── default_linear.png │ ├── default_moving_dots.png │ ├── free_recall.png │ ├── iat_mouse.png │ ├── moving_props.png │ ├── no_variables.png │ ├── purple_box.png │ ├── red_green_smaller_std_dev.png │ ├── scale_color_radius_change_moving_dots.png │ ├── smile_example.png │ ├── stern.png │ └── stroop.png ├── accessing_data.rst ├── advanced_smile.rst ├── android.rst ├── conf.py ├── examples │ ├── backtest.py │ ├── button_test.py │ ├── constant.py │ ├── dynamic_circles.py │ ├── dynamic_dot_box.py │ ├── face-smile.png │ ├── flip_test.py │ ├── free_recall │ │ ├── config.py │ │ ├── free_recall.py │ │ ├── freekey_instructions.rst │ │ ├── freerecall.rst │ │ ├── gen_stim.py │ │ └── pools │ │ │ ├── living.txt │ │ │ └── nonliving.txt │ ├── freekey_test.py │ ├── iat_mouse │ │ ├── config.py │ │ ├── gen_stim.py │ │ ├── iat_mouse.py │ │ ├── iat_mouse.rst │ │ ├── iat_mouse_instructions1.rst │ │ ├── iat_mouse_instructions2.rst │ │ ├── iat_mouse_instructions3.rst │ │ ├── iat_mouse_instructions4.rst │ │ ├── iat_mouse_instructions5.rst │ │ ├── iat_mouse_instructions6.rst │ │ ├── iat_mouse_instructions7.rst │ │ └── pools │ │ │ ├── flowers.txt │ │ │ ├── insects.txt │ │ │ ├── negatives.txt │ │ │ └── positives.txt │ ├── joytest.py │ ├── log_test.py │ ├── loop_example.py │ ├── mean_until_test_py.py │ ├── meta_exp.py │ ├── mouse_tracking.py │ ├── nested.py │ ├── ordered.py │ ├── placement.py │ ├── press_error_test.py │ ├── questionnaire_example.csv │ ├── questionnaire_test.py │ ├── rotation_test.py │ ├── scale_examples.py │ ├── slide.py │ ├── smile_turn.py │ ├── stern │ │ ├── config.py │ │ ├── gen_stim.py │ │ ├── stern.py │ │ ├── stern.rst │ │ └── stern_instructions.rst │ ├── stroop │ │ ├── config.py │ │ ├── gen_stim.py │ │ ├── stroop.py │ │ ├── stroop.rst │ │ └── stroop_instructions.rst │ ├── timing_prop_test.py │ ├── timing_test.py │ ├── unicode.py │ └── wait_test.py ├── grating.rst ├── index.rst ├── install.rst ├── make.bat ├── moving_dots.rst ├── real_examples.rst ├── seeking_help.rst ├── smile.rst ├── smile_states.rst ├── tutorial.rst └── tutorial_2.rst ├── license.txt ├── pyproject.toml ├── smile ├── __init__.py ├── audio.py ├── clock.py ├── common.py ├── crosshairs.svg ├── crosshairs_100x100.png ├── crosshairs_50x50.png ├── dag.py ├── demographics.py ├── dotbox.py ├── event.py ├── experiment.py ├── face-smile.png ├── freekey.py ├── grating.py ├── joystick.py ├── keyboard.py ├── kivy_overrides.py ├── lock.png ├── log.py ├── logo.png ├── lsl.py ├── main.py ├── math_distract.py ├── mouse.py ├── moving_dots.py ├── niusb_interface.py ├── pulse.py ├── questionnaire.py ├── ref.py ├── scale.py ├── socket_interface.py ├── startup.py ├── state.py ├── test_sound.wav ├── test_video.mp4 ├── unlock.png ├── utils.py ├── version.py └── video.py ├── tests ├── datadirtest │ └── test2 │ │ └── nothing_file.txt ├── test_audio.py ├── test_clean_attrs.py ├── test_crashlogger.py ├── test_datadir.py ├── test_dotbox.py ├── test_freekey.py ├── test_func.py ├── test_grating.py ├── test_keyboard.py ├── test_math_distract.py ├── test_mouse.py ├── test_moving_dot.py ├── test_niusb_interface.py ├── test_pulse.py ├── test_ref.py ├── test_socket_interface.py ├── test_state.py ├── test_timing.py ├── test_update_widget.py └── test_video.py └── versioning.py /.gitattributes: -------------------------------------------------------------------------------- 1 | smile/version.py filter=versioning 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG] - " 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots or Error Message Output** 24 | If applicable, add screenshots or a codeblock with the error message to help explain your problem. 25 | e.g. 26 | ``` 27 | Whole bunch of traceback information: 28 | ERROR: This is an error! 29 | ``` 30 | 31 | **Environment (please complete the following information):** 32 | - OS: [e.g. iOS] - Version: [e.g. 22] 33 | - Python Version: [e.g. 3.10.12] 34 | 35 | **Additional context** 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE] - " 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.orig 2 | *~ 3 | *.py[co] 4 | \#* 5 | \.\#* 6 | build 7 | _build 8 | data 9 | *.slog 10 | *.so 11 | build-arch* 12 | dist 13 | MANIFEST 14 | .vscode 15 | smile.egg-info 16 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | smile - State Machine Interface Library for Experiments 2 | 3 | Copyright (C) 2012-2017 4 | Per B. Sederberg 5 | 6 | Authors: Per B. Sederberg 7 | URL: https://github.com/compmem/smile 8 | 9 | This program is free software: you can redistribute it and/or modify 10 | it under the terms of the GNU General Public License as published by 11 | the Free Software Foundation, either version 3 of the License, or 12 | (at your option) any later version. 13 | 14 | This program is distributed in the hope that it will be useful, 15 | but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | GNU General Public License for more details. 18 | 19 | You should have received a copy of the GNU General Public License 20 | along with this program. It can be found in the file license.txt which 21 | is part of the smile package and online at 22 | . -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ====== 2 | SMILE 3 | ====== 4 | ----------------------------------------------- 5 | State Machine Interface Library for Experiments 6 | ----------------------------------------------- 7 | 8 | Overview 9 | ======== 10 | 11 | Writing experiments should be simple, the library should be small, and 12 | it should make you happy. 13 | 14 | Prepare to SMILE... 15 | 16 | 17 | Dependencies 18 | ============ 19 | 20 | - `python `_ (version 3) 21 | - `kivy `_ (version >= 1.8) 22 | - `pyo `_ (optional, for audio) 23 | 24 | 25 | For versioning 26 | ============== 27 | 28 | :: 29 | 30 | git config filter.versioning.smudge 'python versioning.py' 31 | git config filter.versioning.clean 'python versioning.py --clean' 32 | 33 | Add a file to .git called post-checkout with the following. 34 | 35 | :: 36 | 37 | #!/bin/sh 38 | cat version.py | python versioning.py --clean | python versioning.py > version.py 39 | 40 | For more information visit `Mass Communicating via the Wayback Machine`_ 41 | 42 | 43 | Documentation 44 | ============= 45 | 46 | For detailed documentation, please visit http://smile-docs.readthedocs.io/ 47 | -------------------------------------------------------------------------------- /TODO.rst: -------------------------------------------------------------------------------- 1 | ================ 2 | Smile To Do List 3 | ================ 4 | 5 | 6 | Approximately in order of importance: 7 | 8 | - (DONE) Subject processing (i.e., put the log files from a subj in 9 | their own directory.) 10 | 11 | - (DONE) Logging 12 | - Each State is responsible for creating a dictionary to log the 13 | most important aspect of that state and write that to a YAML file 14 | for each experiment. 15 | - Create a Log state that can pick and choose from other state logs 16 | to make custom event logs. 17 | 18 | - (DONE) Variables 19 | - For custom tracking of stuff throughout the experiment 20 | (e.g., counting up the num correct for a block) 21 | - Implemented as Set and Get states 22 | 23 | - Keyboard input 24 | - (DONE) Add event hooks 25 | - (DONE) Tracking individual keys 26 | - Track extended text input 27 | 28 | - Mouse input 29 | - (DONE) Button presses (what, when, and where [not yet where]) 30 | - Movement (A list of dicts for each movement during an active 31 | state) 32 | 33 | - (DONE) Conditional state to allow branching 34 | 35 | - (DONE) Loop state (to allow looping without unraveling into a big sequence) 36 | 37 | - (IN PROGRESS) Tests (especially to verify timing) 38 | 39 | - (DONE) Plot the DAG with pydot 40 | 41 | - Audio 42 | - (DONE/In PROGRESS) Playback 43 | - Recording 44 | 45 | - Movie playback 46 | - (DONE) without audio 47 | - sync with audio 48 | 49 | - (DONE) EEG sync pulsing 50 | 51 | - Animations (i.e., complex/dynamic visual stimuli) 52 | 53 | - Better comments 54 | 55 | - Docs (in code and with Sphinx) 56 | 57 | - (In Progress) Example experiments 58 | 59 | - (DONE) Upload to github 60 | 61 | - Find good smile logo/icon 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /docs/_static/adjusted_phase_and_frequency.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compmem/smile/e85c2da827bb161c2ac7fc43d2a2cac20e536364/docs/_static/adjusted_phase_and_frequency.png -------------------------------------------------------------------------------- /docs/_static/ccbs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compmem/smile/e85c2da827bb161c2ac7fc43d2a2cac20e536364/docs/_static/ccbs.jpg -------------------------------------------------------------------------------- /docs/_static/default_circular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compmem/smile/e85c2da827bb161c2ac7fc43d2a2cac20e536364/docs/_static/default_circular.png -------------------------------------------------------------------------------- /docs/_static/default_gaussian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compmem/smile/e85c2da827bb161c2ac7fc43d2a2cac20e536364/docs/_static/default_gaussian.png -------------------------------------------------------------------------------- /docs/_static/default_linear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compmem/smile/e85c2da827bb161c2ac7fc43d2a2cac20e536364/docs/_static/default_linear.png -------------------------------------------------------------------------------- /docs/_static/default_moving_dots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compmem/smile/e85c2da827bb161c2ac7fc43d2a2cac20e536364/docs/_static/default_moving_dots.png -------------------------------------------------------------------------------- /docs/_static/free_recall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compmem/smile/e85c2da827bb161c2ac7fc43d2a2cac20e536364/docs/_static/free_recall.png -------------------------------------------------------------------------------- /docs/_static/iat_mouse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compmem/smile/e85c2da827bb161c2ac7fc43d2a2cac20e536364/docs/_static/iat_mouse.png -------------------------------------------------------------------------------- /docs/_static/moving_props.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compmem/smile/e85c2da827bb161c2ac7fc43d2a2cac20e536364/docs/_static/moving_props.png -------------------------------------------------------------------------------- /docs/_static/no_variables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compmem/smile/e85c2da827bb161c2ac7fc43d2a2cac20e536364/docs/_static/no_variables.png -------------------------------------------------------------------------------- /docs/_static/purple_box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compmem/smile/e85c2da827bb161c2ac7fc43d2a2cac20e536364/docs/_static/purple_box.png -------------------------------------------------------------------------------- /docs/_static/red_green_smaller_std_dev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compmem/smile/e85c2da827bb161c2ac7fc43d2a2cac20e536364/docs/_static/red_green_smaller_std_dev.png -------------------------------------------------------------------------------- /docs/_static/scale_color_radius_change_moving_dots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compmem/smile/e85c2da827bb161c2ac7fc43d2a2cac20e536364/docs/_static/scale_color_radius_change_moving_dots.png -------------------------------------------------------------------------------- /docs/_static/smile_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compmem/smile/e85c2da827bb161c2ac7fc43d2a2cac20e536364/docs/_static/smile_example.png -------------------------------------------------------------------------------- /docs/_static/stern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compmem/smile/e85c2da827bb161c2ac7fc43d2a2cac20e536364/docs/_static/stern.png -------------------------------------------------------------------------------- /docs/_static/stroop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compmem/smile/e85c2da827bb161c2ac7fc43d2a2cac20e536364/docs/_static/stroop.png -------------------------------------------------------------------------------- /docs/accessing_data.rst: -------------------------------------------------------------------------------- 1 | ============================= 2 | Data accessing and Processing 3 | ============================= 4 | 5 | Saving your Data into SLOG Files 6 | ================================ 7 | 8 | In SMILE, each state will produce what is called a *.slog* file by default. 9 | This file is a specially compressed file composed of all the important data 10 | associated with the state. It not only logs every parameter, but also 11 | all of the variables listed in the *Logged Attributes* section of the 12 | docstrings. In most cases, every state will save out 2 rows of data to the 13 | *.slog* file. The first row is the field names, and the second row will be the 14 | data for each of those fields. 15 | 16 | The kind of state that write multiple lines out to its associated *.slog* file 17 | is the :py:class:`~smile.state.Log` state. Wherever the class is inserted into the experiment, the class will log the 18 | values of all of the keywords/argument pairs it is passed. If the *Log* 19 | exists within a loop, it will write out the values of the keyword/argument 20 | pairs during each iteration of the loop during experimental runtime. In this 21 | case, the *.slog* file will have the first row be the keywords, and the 22 | subsequent rows be all of the data for each *Log* during each iteration of the 23 | loop. 24 | 25 | .. note:: 26 | 27 | Every instance of the Log state in the experiment will save to a separate 28 | file. 29 | 30 | Below is an example of a *Log* state. 31 | 32 | .. code-block:: python 33 | 34 | with Loop(10) as trial: 35 | lb = Label(text=trial.i, duration=2) 36 | Log(trial.current, 37 | name="looping_log", 38 | label_appear_time=lb.appear_time['time']) 39 | 40 | This example will save 11 rows into a *.slog* file. If **trial.current** is the 41 | first argument for *Log*, then it will save out all of the information about 42 | the looping variable out in different columns. 43 | 44 | A Record state will record all of the references given. It will write a line 45 | to the *.slog* file every time one of the references changes. It will also log 46 | the time at which the given reference changed. 47 | 48 | Reading your SLOG files in python 49 | ================================= 50 | 51 | In order to slog through data, one of two things are first needed to be completed. 52 | The first is to pull the data into python by using the :py:class:`~smile.state.Log` 53 | method called :py:func:`~smile.state.log2dl`. This method converts the *.slog* file to a 54 | list of dictionaries so that you can perform any pythonic functions on it in 55 | order to analyze the data. *log2dl* has one required parameter, 56 | *log_filename*, which should be a string that starts out *log_* and ends with 57 | a user chosen *name* parameter of the *Log* in the user's experiment. 58 | 59 | If there are multiple files with the same name, they have trailing *_#* in the 60 | filename. *log2dl* will pull all of the files with the same base name, and 61 | concatenate them into a single list of dictionaries. 62 | 63 | The other way data can be access is by converting all of the *.slog* files 64 | to *.csv* files. This can be accomplished by running the :py:func:`~smile.log.Log2csv` 65 | method. This method will take two parameters, *log_filename* and *csv_filename*. 66 | *log_filename* works the same way as in *log2dl*, where a string that is *log_* 67 | plus the name which was provided in the *name* parameter of the *Log* is passed. 68 | If no *csv_filename* is given, then it will be saved as the same name as the 69 | *log_filename* plus *.csv*. From there, one can use their preferred method of 70 | data analysis. 71 | -------------------------------------------------------------------------------- /docs/android.rst: -------------------------------------------------------------------------------- 1 | ======================== 2 | SMILE on Android Devices 3 | ======================== 4 | 5 | Preparing your Experiment Directory 6 | =================================== 7 | 8 | 1. Inside your project directory, you need to rename your experiment file to 9 | "main.py". This allows a program called Buildozer to find the file that is needed 10 | to run your experiment. 11 | 12 | 2. You must then have a folder in your experiment directory that contains all of the 13 | SMILE .py files and call this folder "SMILE" 14 | 15 | 3. NOTE : You must add a line to your "main.py" exactly as shown below. If you don't 16 | include the spaces on either side of the equal sign, Buildozer will not compile your experiment for Android: 17 | 18 | :: 19 | 20 | __version__ = "1.0.0" 21 | 22 | .. note:: 23 | 24 | You may put whatever numbers you want in place of the 1.0.0 but they 25 | must be in quotations. 26 | 27 | Installing Buildozer (Your SMILE for Android best friend) 28 | ========================================================= 29 | 30 | Buildozer is a packager that allows Linux systems to create a file (.apk) that has 31 | everything you need to run your python program packed into that file. This program 32 | automates the entire process by downloading everything that is needed for the 33 | program Python-For-Android, including the SDK and NDK for Android. 34 | 35 | In the terminal, change directory to wherever you want to install Buildozer 36 | (probably just in your documents folder) and then run the following in your terminal 37 | window: 38 | 39 | :: 40 | 41 | >>>git clone https://github.com/kivy/buildozer.git 42 | >>>cd buildozer 43 | >>>sudo python2.7 setup.py install 44 | 45 | This installs Buildozer and then allows you to run Buildozer from any directory. 46 | 47 | Customizing your build specifications 48 | ===================================== 49 | First things first, we must have Buildozer generate a "buildozer.spec" file for our 50 | python program. Run the following line in your terminal in your project directory. 51 | 52 | :: 53 | 54 | >>>buildozer init 55 | 56 | You will see that Buildozer has created a file called "buildozer.spec". We need to make 57 | some changes in this file. Open the file with and we will start with editing the "title" line. 58 | 59 | :: 60 | 61 | title : Whatever you name your Experiment 62 | 63 | Easy. Next we need to edit the source line. This line tells Buildozer what files in 64 | the experiment directory to include when packing everything up into your .apk file. 65 | 66 | .. note:: 67 | 68 | We are adding file extensions to the end of this line. Use the format of 69 | ",extension". They must have a comma in between each new extension. The py, 70 | kv, jpg, png, atlas are the defaults and required to be included. 71 | 72 | :: 73 | 74 | source.include_exts = py,png,jpg,kv,atlas,mp3,mp4 75 | 76 | Next, we will edit the log_level line. If we set log_level to 2 then we are able to 77 | see all of the logs and the errors if Buildozer breaks. 78 | 79 | :: 80 | 81 | log_level = 2 82 | 83 | Finally, we need to edit the requirements line. This line tells Buildozer what 84 | packages to download and include into its packaged version of python. Kivy is 85 | required always for SMILE, but here you can include packages like "ffmpeg" for 86 | playing video through SMILE. 87 | 88 | :: 89 | 90 | requirements = kivy,ffmpeg 91 | 92 | Building your project with Buildozer 93 | ==================================== 94 | 95 | This next step takes a few minutes to run. Running the next line in your terminal 96 | will download all of the python-for-Android files and your required packages that 97 | can be downloaded, package all of your included files that match any of your 98 | include_exts, and download the SDK and NDK that are needed to compile your python 99 | code. In your terminal, run the following line: 100 | 101 | :: 102 | 103 | >>>buildozer android debug 104 | 105 | .. note:: 106 | 107 | This process can take a long time depending on how many packages your python 108 | program requires. 109 | 110 | You will notice the process is complete if the terminal sends you a message saying 111 | that the application file has been saved in the *bin* folder. 112 | 113 | Changing up the blacklist to allow SMILE to Run 114 | =============================================== 115 | 116 | Once Buildozer has built your .apk, it also filled your directory with some new 117 | folders. The important folder for this step is the *.buildozer* folder. We need to 118 | edit the *blacklist.txt* file to tell Buildozer to include *python._csv*, otherwise 119 | SMILE cannot run. First, navigate to the path below. 120 | 121 | :: 122 | 123 | >>>cd .buildozer/android/platform/python-for-android/dist/myapp/ 124 | 125 | In this folder, gedit **blacklist.txt**. Under the ``unused binaries python modules`` 126 | section of the file, put a *#* in front of the line that has **_csv.so** in it. What the 127 | new line should look like is presented below. 128 | 129 | :: 130 | 131 | #lib-dynload/_csv.so 132 | 133 | .. note:: 134 | 135 | If you do not comment out this line with *#* the **_csv.so** will not be 136 | included in your ".apk" and then SMILE will break. 137 | 138 | Setting up an Android phone as a Developer to install Homebrew apps 139 | =================================================================== 140 | 141 | Most Android devices have processes that are basically the same for setting up 142 | a phone in Developer mode. 143 | 144 | 1. Navigate to *Settings->About Phone* and tap the *Build Number* button 7 times. 145 | This sets up your phone for developer mode. This unlocks a new settings tab 146 | called *Developer Options*. 147 | 148 | 2. Navigate to *Settings->Developer Options* and Enable *USB debugging*. This allows 149 | your Linux machine to send the build version of your python experiment straight to 150 | your phone. 151 | 152 | 153 | Finally Adding your APK to your Phone 154 | ===================================== 155 | 156 | If you hook up via USB to your Linux machine, you will be able to automatically 157 | upload the .apk to your Android phone. With the following line sent into your 158 | terminal, you rebuild your program with the required python libraries. This line also 159 | sets your terminal to print out the logs from your phone. The line is as follows: 160 | 161 | :: 162 | 163 | >>>buildozer android debug deploy run logcat 164 | 165 | This will open the app on your phone, allowing you to see if it works! 166 | 167 | .. note:: 168 | 169 | If your phone isn't unlocked, the experiment will not run from the terminal. 170 | Make sure your phone isn't locked when you run the above line. 171 | 172 | .. note:: 173 | 174 | If it looks like the app breaks before running, press *Ctrl+C*. If you press 175 | this early enough, then you will be able to *Ctrl+F* and find *python*. This 176 | will let you find the lines that *Kivy* has sent to the log and help you 177 | find where and why your SMILE program broke. 178 | -------------------------------------------------------------------------------- /docs/examples/backtest.py: -------------------------------------------------------------------------------- 1 | #emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | #ex: set sts=4 ts=4 sw=4 et: 3 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 4 | # 5 | # See the COPYING file distributed along with the smile package for the 6 | # copyright and license terms. 7 | # 8 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 9 | 10 | # load all the states 11 | from smile.common import * 12 | 13 | # create an experiment 14 | exp = Experiment() 15 | 16 | Wait(1.0) 17 | 18 | BackgroundColor(color='red', duration=1.0) 19 | BackgroundColor(color='orange', duration=1.0) 20 | BackgroundColor(color='yellow', duration=1.0) 21 | BackgroundColor(color='green', duration=1.0) 22 | BackgroundColor(color='blue', duration=1.0) 23 | BackgroundColor(color='purple', duration=1.0) 24 | 25 | Wait(1.0) 26 | 27 | if __name__ == '__main__': 28 | exp.run() 29 | -------------------------------------------------------------------------------- /docs/examples/button_test.py: -------------------------------------------------------------------------------- 1 | #emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | #ex: set sts=4 ts=4 sw=4 et: 3 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 4 | # 5 | # See the COPYING file distributed along with the smile package for the 6 | # copyright and license terms. 7 | # 8 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 9 | 10 | from smile.common import * 11 | 12 | # set up an experiment 13 | exp = Experiment(show_splash=False) 14 | 15 | Wait(0.5) 16 | 17 | # use a grid to include a bunch of buttons 18 | with GridLayout(rows=10, left=exp.screen.left, top=exp.screen.height): 19 | with ButtonPress() as bp: 20 | MouseCursor() 21 | for i in range(200): 22 | Button(name=str(i), text=str(i)) 23 | 24 | # report which was pressed 25 | Debug(pressed=bp.pressed) 26 | 27 | Wait(0.5) 28 | 29 | if __name__ == '__main__': 30 | exp.run() 31 | -------------------------------------------------------------------------------- /docs/examples/constant.py: -------------------------------------------------------------------------------- 1 | #emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | #ex: set sts=4 ts=4 sw=4 et: 3 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 4 | # 5 | # See the COPYING file distributed along with the smile package for the 6 | # copyright and license terms. 7 | # 8 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 9 | 10 | 11 | # Load all the states 12 | from smile.common import * 13 | 14 | # Create an experiment 15 | exp = Experiment() 16 | 17 | # Initial wait 18 | Wait(1.0) 19 | 20 | # Define some items 21 | block = [str(i) for i in range(5)] 22 | 23 | with Parallel(): 24 | # Show placement and bounding boxes 25 | txt = Label(text='+', font_size=48) 26 | img = Image(source='face-smile.png', 27 | bottom=txt.top) 28 | rect = Rectangle(size=txt.size, 29 | color=('cyan', .3)) 30 | rect_img = Rectangle(size=img.size, center=img.center, 31 | color=('red', .3)) 32 | with UntilDone(): 33 | # Loop over items 34 | with Loop(block) as trial: 35 | # Show the label 36 | num = Label(text=trial.current, 37 | top=txt.bottom, 38 | duration=1.0, font_size=48) 39 | 40 | # Debug to show placement locations 41 | Debug(txt_size=txt.size, 42 | txt_bottom=txt.bottom, 43 | txt_top=txt.top, 44 | num_size=num.size, 45 | num_top=num.top, 46 | img_size=img.size, 47 | img_bottom=img.bottom) 48 | Wait(.5) 49 | 50 | Wait(2.0) 51 | 52 | # If this was run in a command line, run the experiment 53 | if __name__ == '__main__': 54 | exp.run() 55 | -------------------------------------------------------------------------------- /docs/examples/dynamic_circles.py: -------------------------------------------------------------------------------- 1 | #emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | #ex: set sts=4 ts=4 sw=4 et: 3 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 4 | # 5 | # See the COPYING file distributed along with the smile package for the 6 | # copyright and license terms. 7 | # 8 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 9 | 10 | # load all the states 11 | from smile.common import * 12 | 13 | # create an experiment 14 | exp = Experiment() 15 | 16 | # Initial wait 17 | Wait(1.0) 18 | 19 | # Placeholder for saving inserted circs 20 | exp.circs = [] 21 | 22 | # Accept input for 5 seconds 23 | Wait(5.) 24 | with Meanwhile(): 25 | # Put it all in a parallel so we can keep adding stuff 26 | with Parallel() as p: 27 | # show the cursor 28 | MouseCursor() 29 | with Loop() as l: 30 | # Wait for a mouse press 31 | mp = MousePress() 32 | # Insert everything within to the Parallel above 33 | with p.insert() as ins: 34 | # Add a circle with random color 35 | circ = Ellipse(center=mp.pos, color=(jitter(0, 1), 36 | jitter(0, 1), 37 | jitter(0, 1))) 38 | # Add a reference to the last MousePress that happened 39 | # to the list of MousePress References 40 | exp.circs = exp.circs + [ins.first] 41 | 42 | # Print out the locs we saved 43 | with Loop(exp.circs, save_log=False) as c: 44 | Debug(i=c.i, 45 | center=c.current.center, 46 | color=c.current.color) 47 | Wait(1.0) 48 | 49 | 50 | # If this is being run from the command line, run this experiment 51 | if __name__ == '__main__': 52 | exp.run() 53 | -------------------------------------------------------------------------------- /docs/examples/dynamic_dot_box.py: -------------------------------------------------------------------------------- 1 | #emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | #ex: set sts=4 ts=4 sw=4 et: 3 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 4 | # 5 | # See the COPYING file distributed along with the smile package for the 6 | # copyright and license terms. 7 | # 8 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 9 | 10 | 11 | # Load all the states 12 | from smile.common import * 13 | 14 | # Create an experiment 15 | exp = Experiment() 16 | 17 | with Loop(['cyan', 'blue', 'pink', 'green']) as col: 18 | 19 | with Parallel(): 20 | # Display some DynamicDotBoxes 21 | ddb1 = DynamicDotBox(color=col.current, 22 | center_x=exp.screen.center_x-200, 23 | num_dots=40, size=(400, 400)) 24 | ddb2 = DynamicDotBox(color=col.current, 25 | center_x=exp.screen.center_x+200, 26 | num_dots=80, size=(400, 400)) 27 | 28 | # Run the above state until a keypress happens. 29 | with UntilDone(): 30 | kp = KeyPress() 31 | 32 | Done(ddb1) 33 | # Show all the info about the states in the command line 34 | Debug(appear_time_1=ddb1.db.appear_time, 35 | appear_time_2=ddb2.db.appear_time, 36 | disappear_time_1=ddb1.db.disappear_time, 37 | disappear_time_2=ddb2.db.disappear_time, 38 | num_dots1=ddb1.db.num_dots, 39 | num_dots2=ddb2.db.num_dots, 40 | press_time = kp.press_time, 41 | dis_diff = ddb1.db.disappear_time['time'] - kp.press_time['time']) 42 | 43 | Wait(1.0) 44 | 45 | 46 | # If this was run in a command line, run the experiment 47 | if __name__ == '__main__': 48 | exp.run(trace=False) 49 | -------------------------------------------------------------------------------- /docs/examples/face-smile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compmem/smile/e85c2da827bb161c2ac7fc43d2a2cac20e536364/docs/examples/face-smile.png -------------------------------------------------------------------------------- /docs/examples/flip_test.py: -------------------------------------------------------------------------------- 1 | #emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | #ex: set sts=4 ts=4 sw=4 et: 3 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 4 | # 5 | # See the COPYING file distributed along with the smile package for the 6 | # copyright and license terms. 7 | # 8 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 9 | 10 | # load all the states 11 | from smile.common import * 12 | 13 | # create an experiment 14 | exp = Experiment() 15 | 16 | # define the trials for the experiment 17 | trials = [{'txt': str(i)} for i in range(50)] 18 | 19 | # save stim 20 | txt = Label(text=trials[0]['txt'], 21 | x=300, 22 | y=exp.screen.center_y, 23 | bold=True) 24 | with UntilDone(): 25 | Wait(1.0) 26 | with Loop(trials[1:]) as trial: 27 | Wait(.005) 28 | uw = UpdateWidget(txt, 29 | text=trial.current['txt'], 30 | x=txt.x+5, 31 | font_size=txt.font_size+1.0) 32 | 33 | Log(name='flip_test', 34 | txt=trial.current['txt'], 35 | appear=uw.appear_time) 36 | 37 | Wait(2.0) 38 | Label(text='Done!!!', font_size=42, duration=2.0) 39 | Wait(2.0) 40 | 41 | if __name__ == '__main__': 42 | exp.run() 43 | -------------------------------------------------------------------------------- /docs/examples/free_recall/config.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | # Get the directory where this script is located 4 | script_dir = Path(__file__).parent.absolute() 5 | 6 | # Define paths to the stimulus files, relative to the script's location 7 | POOLS_DIR = script_dir / "pools" 8 | LIVING_TXT_PATH = POOLS_DIR / "living.txt" 9 | NONLIVING_TXT_PATH = POOLS_DIR / "nonliving.txt" 10 | 11 | # Open and combine the stimulus files with error handling 12 | try: 13 | living_list = LIVING_TXT_PATH.read_text().splitlines() 14 | nonliving_list = NONLIVING_TXT_PATH.read_text().splitlines() 15 | except FileNotFoundError as e: 16 | print(f"Error: Stimulus file not found. {e}") 17 | exit(1) 18 | 19 | stim_list = living_list + nonliving_list 20 | 21 | # Open the instructions file, also relative to the script's location 22 | INSTRUCTIONS_PATH = script_dir / 'freekey_instructions.rst' 23 | try: 24 | INSTRUCT_TEXT = INSTRUCTIONS_PATH.read_text() 25 | except FileNotFoundError as e: 26 | print(f"Error: Instructions file not found. {e}") 27 | exit(1) 28 | 29 | # Define the Experimental Variables (Constants) 30 | INTER_STIMULUS_INTERVAL = 2 31 | INTER_BLOCK_INTERVAL = 2 32 | STIMULUS_DURATION = 2 33 | PRE_FREE_KEY_INTERVAL = 4 34 | FONT_SIZE = 40 35 | RST_FONT_SIZE = 30 36 | RST_WIDTH = 900 37 | MIN_FREE_KEY_DURATION = 20 38 | 39 | NUM_BLOCKS = 2 40 | NUM_PER_BLOCK = [10, 15, 20] 41 | -------------------------------------------------------------------------------- /docs/examples/free_recall/free_recall.py: -------------------------------------------------------------------------------- 1 | from smile.common import * 2 | from smile.freekey import FreeKey 3 | 4 | # execute both the configuration file and the 5 | # stimulus generation file 6 | from config import * 7 | from gen_stim import * 8 | 9 | # Initialize the Experiment 10 | exp = Experiment(debug=True) 11 | 12 | # Show the instructions to the participant 13 | RstDocument(text=INSTRUCT_TEXT, base_font_size=RST_FONT_SIZE, 14 | width=RST_WIDTH, height=exp.screen.height) 15 | with UntilDone(): 16 | # When a KeyPress is detected, the UntilDone 17 | # will cancel the RstDocument state 18 | KeyPress() 19 | # Start the experiment Loop 20 | with Loop(blocks) as block: 21 | Wait(INTER_BLOCK_INTERVAL) 22 | with Loop(block.current['study']) as study: 23 | # Present the Fixation Cross 24 | Label(text="+", duration=INTER_STIMULUS_INTERVAL, font_size=FONT_SIZE) 25 | 26 | # Present the study item and add debug information for current stimulus 27 | Debug(study.current) 28 | Label(text=study.current, duration=STIMULUS_DURATION, font_size=FONT_SIZE) 29 | 30 | Wait(PRE_FREE_KEY_INTERVAL) 31 | 32 | # Start FreeKey session 33 | fk = FreeKey(Label(text="XXXXXXX", font_size=FONT_SIZE), 34 | max_duration=block.current['duration']) 35 | # Log everything! 36 | Log(block.current, 37 | name="FreeKey", 38 | responses=fk.responses) 39 | # Run the experiment 40 | exp.run() 41 | -------------------------------------------------------------------------------- /docs/examples/free_recall/freekey_instructions.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | INSTRUCTIONS 3 | ============ 4 | 5 | You are about to be presented with sever blocks of words. Look at each word then 6 | try to remember them. After being presented with a number of words, you will 7 | wait a brief period then be asked to recall as many of those words as possible. 8 | 9 | :: 10 | 11 | When the **XXXXXX** appears on the screen, type out one word and then press the **ENTER** key. Try to remember as many words from the list as possible within the time limit provided. 12 | 13 | Once the time runs out, you will be presented with another block of words to 14 | try to remember. 15 | 16 | When you are ready to continue, press **ENTER**. 17 | -------------------------------------------------------------------------------- /docs/examples/free_recall/gen_stim.py: -------------------------------------------------------------------------------- 1 | import random 2 | from config import NUM_BLOCKS, stim_list, NUM_PER_BLOCK, MIN_FREE_KEY_DURATION 3 | 4 | # Shuffle the stimulus list for randomness 5 | random.shuffle(stim_list) 6 | 7 | blocks = [] 8 | for block_index in range(NUM_BLOCKS): 9 | study_items = [] 10 | 11 | # Select stimuli for the current block based on NUM_PER_BLOCK 12 | num_items_in_block = NUM_PER_BLOCK[block_index % len(NUM_PER_BLOCK)] 13 | for _ in range(num_items_in_block): 14 | study_items.append(stim_list.pop()) 15 | 16 | # Create a block dictionary with study items and duration 17 | block_duration = MIN_FREE_KEY_DURATION + \ 18 | 10 * (block_index % len(NUM_PER_BLOCK)) 19 | blocks.append({ 20 | "study": study_items, 21 | "duration": block_duration 22 | }) 23 | 24 | # Shuffle the blocks to ensure random order of blocks 25 | random.shuffle(blocks) 26 | -------------------------------------------------------------------------------- /docs/examples/free_recall/pools/living.txt: -------------------------------------------------------------------------------- 1 | ABDOMEN 2 | ACORN 3 | ACTRESS 4 | ADULT 5 | AGENT 6 | ALGAE 7 | ALMOND 8 | ANGEL 9 | ANIMAL 10 | ANKLE 11 | APPLE 12 | ARTIST 13 | ATHLETE 14 | AUDIENCE 15 | AUNT 16 | AUTHOR 17 | BABY 18 | BACKBONE 19 | BANANA 20 | BAND 21 | BARK 22 | BASS 23 | BEAN 24 | BEAR 25 | BEARD 26 | BEAST 27 | BEAUTY 28 | BEAVER 29 | BEETLE 30 | BELLY 31 | BERRY 32 | BIRD 33 | BLOSSOM 34 | BODY 35 | BOSS 36 | BOUQUET 37 | BROTHER 38 | BUSH 39 | CABBAGE 40 | CALF 41 | CAMEL 42 | CAPTAIN 43 | CARDINAL 44 | CARROT 45 | CATTLE 46 | CELL 47 | CHEEK 48 | CHERRY 49 | CHEST 50 | CHICKEN 51 | CHIEF 52 | CHILD 53 | CHOIR 54 | CHORUS 55 | CLAM 56 | CLOWN 57 | COACH 58 | COCOON 59 | COLONEL 60 | CONGRESS 61 | COOK 62 | CORAL 63 | CORN 64 | COTTON 65 | COUPLE 66 | COUSIN 67 | CREATURE 68 | CREW 69 | CROW 70 | CROWD 71 | CUSTOMER 72 | DAISY 73 | DAUGHTER 74 | DEER 75 | DENTIST 76 | DOCTOR 77 | DONKEY 78 | DOVE 79 | DRAGON 80 | DRIVER 81 | DUCK 82 | EAGLE 83 | ELBOW 84 | ELEPHANT 85 | EMPEROR 86 | EYE 87 | FAIRY 88 | FARMER 89 | FATHER 90 | FEMALE 91 | FINGER 92 | FISH 93 | FLEA 94 | FLESH 95 | FLOWER 96 | FOOT 97 | FOREHEAD 98 | FOREST 99 | FRUIT 100 | FUR 101 | GARDEN 102 | GARLIC 103 | GEESE 104 | GENIUS 105 | GIANT 106 | GIRL 107 | GOAT 108 | GODDESS 109 | GRAPE 110 | GRASS 111 | GROVE 112 | GUARD 113 | GUEST 114 | HAIR 115 | HAWK 116 | HAYSTACK 117 | HEART 118 | HEDGE 119 | HEEL 120 | HERB 121 | HERD 122 | HERO 123 | HIVE 124 | HORSE 125 | HOSTESS 126 | HOUND 127 | HUSBAND 128 | INFANT 129 | INSECT 130 | IVY 131 | JUDGE 132 | JUNGLE 133 | KIDNEY 134 | KING 135 | KITTEN 136 | KNEE 137 | KNIGHT 138 | KNUCKLE 139 | LADY 140 | LAMB 141 | LAWN 142 | LAWYER 143 | LEADER 144 | LEAF 145 | LEMON 146 | LEOPARD 147 | LETTUCE 148 | LILY 149 | LIMB 150 | LIME 151 | LION 152 | LIVER 153 | LIZARD 154 | LOVER 155 | LUNG 156 | MAGICIAN 157 | MAIDEN 158 | MAKER 159 | MAPLE 160 | MASTER 161 | MATE 162 | MAYOR 163 | MEADOW 164 | MEAT 165 | MEMBER 166 | MILDEW 167 | MOLE 168 | MONARCH 169 | MONK 170 | MONKEY 171 | MOSS 172 | MOTHER 173 | MOUSE 174 | MOUTH 175 | MULE 176 | MUSCLE 177 | MUSHROOM 178 | NAIL 179 | NECK 180 | NEPHEW 181 | NOSE 182 | NURSE 183 | OFFICER 184 | OLIVE 185 | ONION 186 | ORANGE 187 | ORCHARD 188 | ORCHID 189 | ORGAN 190 | OTTER 191 | OWNER 192 | OYSTER 193 | PAINTER 194 | PALM 195 | PARENT 196 | PARROT 197 | PARSLEY 198 | PARTNER 199 | PEACH 200 | PEANUT 201 | PEAR 202 | PECAN 203 | PEPPER 204 | PERSON 205 | PICKLE 206 | PIGEON 207 | PILOT 208 | PINE 209 | PLANT 210 | PLAYER 211 | POET 212 | PONY 213 | PORK 214 | POTATO 215 | PRAIRIE 216 | PRINCE 217 | PRINCESS 218 | PUMPKIN 219 | PUPIL 220 | PUPPY 221 | QUEEN 222 | RABBIT 223 | RADISH 224 | RAISIN 225 | RECRUIT 226 | REPTILE 227 | RICE 228 | RIDER 229 | ROBIN 230 | ROOT 231 | ROSE 232 | RULER 233 | SAILOR 234 | SALESMAN 235 | SALMON 236 | SARDINE 237 | SCALLOP 238 | SEAL 239 | SEED 240 | SERGEANT 241 | SERVANT 242 | SHEEP 243 | SHEPHERD 244 | SHERIFF 245 | SHOULDER 246 | SHRIMP 247 | SINGER 248 | SISTER 249 | SKIN 250 | SKULL 251 | SNAIL 252 | SNAKE 253 | SPARROW 254 | SPEAKER 255 | SPIDER 256 | SPINACH 257 | SQUIRREL 258 | STAFF 259 | STEM 260 | STOMACH 261 | STUDENT 262 | SUSPECT 263 | SWEETHEART 264 | TAIL 265 | TEACHER 266 | TEETH 267 | THROAT 268 | THUMB 269 | TIGER 270 | TISSUE 271 | TOAD 272 | TOMATO 273 | TONGUE 274 | TOOTH 275 | TORTOISE 276 | TOURIST 277 | TREE 278 | TROUT 279 | TRUNK 280 | TULIP 281 | TURKEY 282 | TURTLE 283 | TYPIST 284 | UNCLE 285 | VEIN 286 | VINE 287 | WAIST 288 | WAITRESS 289 | WALNUT 290 | WALRUS 291 | WEDDING 292 | WHEAT 293 | WIFE 294 | WITNESS 295 | WOLF 296 | WOMAN 297 | WORKER 298 | WORM 299 | BONE 300 | FEATHER -------------------------------------------------------------------------------- /docs/examples/free_recall/pools/nonliving.txt: -------------------------------------------------------------------------------- 1 | ACCOUNT 2 | ATLAS 3 | ATOM 4 | ATTIC 5 | AVENUE 6 | AWARD 7 | BACON 8 | BALL 9 | BALLOT 10 | BANDAGE 11 | BANNER 12 | BANQUET 13 | BARN 14 | BASEMENT 15 | BASKET 16 | BEACH 17 | BEAM 18 | BED 19 | BELT 20 | BEVERAGE 21 | BITE 22 | BOARD 23 | BOAT 24 | BOOK 25 | BOOT 26 | BOTTLE 27 | BRAKE 28 | BRASS 29 | BREAD 30 | BREAKFAST 31 | BREEZE 32 | BRICK 33 | BRIDGE 34 | BRONZE 35 | BROOM 36 | BUILDING 37 | BUSINESS 38 | CAKE 39 | CANAL 40 | CANE 41 | CANNON 42 | CANOE 43 | CANYON 44 | CARBON 45 | CARD 46 | CARTON 47 | CASTLE 48 | CEMENT 49 | CEREAL 50 | CHAPEL 51 | CHART 52 | CHEESE 53 | CHIMNEY 54 | CHOCOLATE 55 | CIRCLE 56 | CIRCUS 57 | CLOCK 58 | CLOSET 59 | CLUB 60 | COAL 61 | COAT 62 | COBWEB 63 | COFFEE 64 | COIN 65 | COLOGNE 66 | COLOR 67 | CONCRETE 68 | CONSCIENCE 69 | CONTRACT 70 | CORRIDOR 71 | COTTAGE 72 | COURAGE 73 | CRADLE 74 | CRANE 75 | CREAM 76 | CRICKET 77 | CRUMB 78 | CRYSTAL 79 | CURB 80 | CURTAIN 81 | DATE 82 | DECK 83 | DENT 84 | DESSERT 85 | DIAL 86 | DIAMOND 87 | DIME 88 | DOMAIN 89 | DOOR 90 | DOUGH 91 | DRAIN 92 | DRESS 93 | DUST 94 | EARTH 95 | EMPIRE 96 | ENVELOPE 97 | ESSAY 98 | FABRIC 99 | FACTORY 100 | FARM 101 | FASHION 102 | FEAST 103 | FIGURE 104 | FILM 105 | FLASK 106 | FLUID 107 | FOOD 108 | FOOTBALL 109 | FORK 110 | FORT 111 | FOSSIL 112 | FRAME 113 | FROST 114 | FUNCTION 115 | GARBAGE 116 | GATE 117 | GIFT 118 | GLOVE 119 | GOWN 120 | GRAPH 121 | GRAVY 122 | HAIL 123 | HALL 124 | HANDLE 125 | HARBOR 126 | HARVEST 127 | HEAVEN 128 | HILL 129 | HOOK 130 | HORN 131 | HOUSE 132 | HYMN 133 | IRON 134 | ISLAND 135 | JACKET 136 | JELLY 137 | JEWEL 138 | JUICE 139 | KITCHEN 140 | KNOB 141 | LABEL 142 | LACE 143 | LAMP 144 | LAND 145 | LANDSCAPE 146 | LANGUAGE 147 | LEATHER 148 | LENS 149 | LETTER 150 | LINEN 151 | LINT 152 | LIQUID 153 | LUMBER 154 | LUNCH 155 | MACHINE 156 | MAGAZINE 157 | MAGNET 158 | MAIL 159 | MANSION 160 | MARBLE 161 | MARKET 162 | MASSAGE 163 | MATCH 164 | MATERIAL 165 | MATTRESS 166 | MEAL 167 | MECHANIC 168 | MEDICINE 169 | METAL 170 | MILK 171 | MOLASSES 172 | MONEY 173 | MOON 174 | MOTOR 175 | MOUNTAIN 176 | MUFFIN 177 | NECKLACE 178 | NEST 179 | NUMBER 180 | OATMEAL 181 | OCEAN 182 | ODOR 183 | OFFICE 184 | OVEN 185 | PAGE 186 | PAIL 187 | PAINT 188 | PALACE 189 | PAPER 190 | PATH 191 | PEBBLE 192 | PEDAL 193 | PENCIL 194 | PHONE 195 | PILE 196 | PILLOW 197 | PIN 198 | PLATE 199 | POEM 200 | POINT 201 | POLE 202 | POOL 203 | PORTION 204 | POSTAGE 205 | POUCH 206 | PROGRAM 207 | PROJECT 208 | PROMISE 209 | PUDDING 210 | PUMP 211 | PURPLE 212 | PYRAMID 213 | QUART 214 | RACK 215 | RAIL 216 | RAIN 217 | RAINBOW 218 | RAKE 219 | RATTLE 220 | RESORT 221 | RESPONSE 222 | REST 223 | RESULT 224 | RING 225 | RIVER 226 | ROAD 227 | ROCKET 228 | ROOF 229 | ROPE 230 | RUBY 231 | RUST 232 | SADDLE 233 | SAFETY 234 | SAIL 235 | SALAD 236 | SAND 237 | SANDPAPER 238 | SANDWICH 239 | SAUCE 240 | SCRATCH 241 | SEAM 242 | SECTION 243 | SENATE 244 | SERVICE 245 | SHAMPOO 246 | SHELL 247 | SHIP 248 | SHORE 249 | SHOWER 250 | SINK 251 | SKETCH 252 | SKILLET 253 | SLEEVE 254 | SLIDE 255 | SLUMBER 256 | SMILE 257 | SOAP 258 | SOCK 259 | SOIL 260 | SONG 261 | SOUP 262 | SPIRIT 263 | SPOON 264 | SPOT 265 | SPRING 266 | STADIUM 267 | STAIRWAY 268 | STAPLE 269 | STAR 270 | STEAK 271 | STEAM 272 | STEW 273 | STICK 274 | STONE 275 | STORE 276 | STORM 277 | STORY 278 | STRAW 279 | STRING 280 | STUMP 281 | SUBJECT 282 | SUBWAY 283 | SUIT 284 | SUMMER 285 | SUNRISE 286 | SUNSHINE 287 | SUPPER 288 | SUPPLY 289 | SWAMP 290 | SWEAT 291 | SYRUP 292 | TABLET 293 | TAPE 294 | TASTE 295 | TEMPLE 296 | THIMBLE 297 | THREAD 298 | TIMBER 299 | TITLE 300 | TOAST 301 | TOILET 302 | TOWER 303 | TRACK 304 | TRAFFIC 305 | TRAIL 306 | TRAIN 307 | TRAY 308 | TREASURE 309 | TRIANGLE 310 | TROPHY 311 | TRUCK 312 | TRUMPET 313 | TUBE 314 | TUNNEL 315 | TWILIGHT 316 | UMBRELLA 317 | UNIFORM 318 | VACUUM 319 | VEHICLE 320 | VESSEL 321 | VIOLIN 322 | WALL 323 | WHEEL 324 | WIRE 325 | WISDOM 326 | WOOD 327 | WOOL 328 | WRINKLE 329 | YAWN 330 | YELLOW -------------------------------------------------------------------------------- /docs/examples/freekey_test.py: -------------------------------------------------------------------------------- 1 | #emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | #ex: set sts=4 ts=4 sw=4 et: 3 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 4 | # 5 | # See the COPYING file distributed along with the smile package for the 6 | # copyright and license terms. 7 | # 8 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 9 | 10 | 11 | # Load all the states 12 | from smile.common import * 13 | 14 | # Create the experiment 15 | exp = Experiment() 16 | 17 | # Prepare them 18 | Label(text="Get Ready...", font_size=40, duration=1.0) 19 | 20 | # Pause a moment 21 | Wait(.5) 22 | 23 | # Collect responses for 10 seconds 24 | fk = FreeKey(Label(text='??????', font_size=40), 25 | max_duration=10.0) 26 | 27 | # Show one way to log responses 28 | Log(fk.responses, name='free_key_test') 29 | 30 | # Debug the output to screen, too 31 | Debug(responses=fk.responses) 32 | 33 | # Wait sec 34 | Wait(1.0) 35 | 36 | # If this was run in a command line, run the experiment 37 | if __name__ == '__main__': 38 | exp.run() 39 | -------------------------------------------------------------------------------- /docs/examples/iat_mouse/config.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | # Get the directory where this script is located 4 | script_dir = Path(__file__).parent.absolute() 5 | 6 | # RST VARIABLES 7 | RST_FONT_SIZE = 50 8 | RST_WIDTH = 600 9 | 10 | # Read instruction files relative to the script location 11 | instruct1 = (script_dir / 'iat_mouse_instructions1.rst').read_text() 12 | instruct2 = (script_dir / 'iat_mouse_instructions2.rst').read_text() 13 | instruct3 = (script_dir / 'iat_mouse_instructions3.rst').read_text() 14 | instruct4 = (script_dir / 'iat_mouse_instructions4.rst').read_text() 15 | instruct5 = (script_dir / 'iat_mouse_instructions5.rst').read_text() 16 | instruct6 = (script_dir / 'iat_mouse_instructions6.rst').read_text() 17 | instruct7 = (script_dir / 'iat_mouse_instructions7.rst').read_text() 18 | 19 | # MOUSE MOVING VARIABLES 20 | WARNING_DURATION = 2.0 21 | MOUSE_MOVE_RADIUS = 100 22 | MOUSE_MOVE_INTERVAL = 0.400 23 | 24 | # BUTTON VARIABLES 25 | BUTTON_HEIGHT = 150 26 | BUTTON_WIDTH = 200 27 | 28 | # GENERAL VARIABLES 29 | FONT_SIZE = 40 30 | INTER_TRIAL_INTERVAL = 0.750 31 | -------------------------------------------------------------------------------- /docs/examples/iat_mouse/gen_stim.py: -------------------------------------------------------------------------------- 1 | import random as random 2 | from pathlib import Path 3 | from typing import List, Dict 4 | from config import instruct1, instruct2, instruct3, instruct4, instruct5, instruct6, instruct7 5 | 6 | # Define file paths using Pathlib for robustness across platforms 7 | BASE_DIR = Path(__file__).parent.absolute() 8 | POOLS_DIR = BASE_DIR / "pools" 9 | 10 | # Paths to the text files 11 | # Word lists from Greenwald et al. 1998 12 | FILE_INSECTS = POOLS_DIR / "insects.txt" 13 | FILE_FLOWERS = POOLS_DIR / "flowers.txt" 14 | FILE_POSITIVES = POOLS_DIR / "positives.txt" 15 | FILE_NEGATIVES = POOLS_DIR / "negatives.txt" 16 | 17 | 18 | def load_words_from_file(filename: Path) -> List[str]: 19 | """Helper function to load and split words from a file.""" 20 | with filename.open("r") as f: 21 | words = f.read().splitlines() 22 | return words 23 | 24 | 25 | # Load the word lists 26 | stim_list_insects = load_words_from_file(FILE_INSECTS) 27 | stim_list_flowers = load_words_from_file(FILE_FLOWERS) 28 | stim_list_positives = load_words_from_file(FILE_POSITIVES) 29 | stim_list_negatives = load_words_from_file(FILE_NEGATIVES) 30 | 31 | 32 | def generate_blocks(type: int) -> List[Dict[str, any]]: 33 | """ 34 | Generate the blocks for the experiment. 35 | 36 | Parameters: 37 | type (int): Indicates whether to return the critical compatible lists (type 1) 38 | or critical incompatible lists (type 2). 39 | 40 | Returns: 41 | List[Dict]: A list of dictionaries representing blocks of word pairs with instructions. 42 | """ 43 | 44 | # Sample 10 words from each list 45 | sample_insects = random.sample(stim_list_insects, 10) 46 | sample_flowers = random.sample(stim_list_flowers, 10) 47 | sample_positives = random.sample(stim_list_positives, 10) 48 | sample_negatives = random.sample(stim_list_negatives, 10) 49 | 50 | # Block 1: Flower (left) vs Insect (right) 51 | block1 = { 52 | "left_word": "flower", 53 | "right_word": "insect", 54 | "instruct": instruct1, 55 | "words": ([{"correct": "right", "center_word": I} for I in sample_insects] + 56 | [{"correct": "left", "center_word": F} for F in sample_flowers]) 57 | } 58 | 59 | # Block 2: Positive (left) vs Negative (right) 60 | block2 = { 61 | "left_word": "positive", 62 | "right_word": "negative", 63 | "instruct": instruct2, 64 | "words": ([{"correct": "left", "center_word": P} for P in sample_positives] + 65 | [{"correct": "right", "center_word": N} for N in sample_negatives]) 66 | } 67 | 68 | # Block 3: Flower Positive (left) vs Insect Negative (right) - 5 samples each 69 | block3 = { 70 | "left_word": "flower positive", 71 | "right_word": "insect negative", 72 | "instruct": instruct3, 73 | "words": ([{"correct": "right", "center_word": I} for I in random.sample(sample_insects[:], 5)] + 74 | [{"correct": "left", "center_word": F} for F in random.sample(sample_flowers[:], 5)] + 75 | [{"correct": "left", "center_word": P} for P in random.sample(sample_positives[:], 5)] + 76 | [{"correct": "right", "center_word": N} for N in random.sample(sample_negatives[:], 5)]) 77 | } 78 | 79 | # Block 4: Flower Positive (left) vs Insect Negative (right) - all 10 samples 80 | block4 = { 81 | "left_word": "flower positive", 82 | "right_word": "insect negative", 83 | "instruct": instruct4, 84 | "words": ([{"correct": "right", "center_word": I} for I in sample_insects] + 85 | [{"correct": "left", "center_word": F} for F in sample_flowers] + 86 | [{"correct": "left", "center_word": P} for P in sample_positives] + 87 | [{"correct": "right", "center_word": N} for N in sample_negatives]) 88 | } 89 | 90 | # Block 5: Insect (left) vs Flower (right) 91 | block5 = { 92 | "left_word": "insect", 93 | "right_word": "flower", 94 | "instruct": instruct5, 95 | "words": ([{"correct": "left", "center_word": I} for I in sample_insects] + 96 | [{"correct": "right", "center_word": F} for F in sample_flowers]) 97 | } 98 | 99 | # Block 6: Insect Positive (left) vs Flower Negative (right) - 5 samples each 100 | block6 = { 101 | "left_word": "insect positive", 102 | "right_word": "flower negative", 103 | "instruct": instruct6, 104 | "words": ([{"correct": "left", "center_word": I} for I in random.sample(sample_insects[:], 5)] + 105 | [{"correct": "right", "center_word": F} for F in random.sample(sample_flowers[:], 5)] + 106 | [{"correct": "left", "center_word": P} for P in random.sample(sample_positives[:], 5)] + 107 | [{"correct": "right", "center_word": N} for N in random.sample(sample_negatives[:], 5)]) 108 | } 109 | 110 | # Block 7: Insect Positive (left) vs Flower Negative (right) - all 10 samples 111 | block7 = { 112 | "left_word": "insect positive", 113 | "right_word": "flower negative", 114 | "instruct": instruct7, 115 | "words": ([{"correct": "left", "center_word": I} for I in sample_insects] + 116 | [{"correct": "right", "center_word": F} for F in sample_flowers] + 117 | [{"correct": "left", "center_word": P} for P in sample_positives] + 118 | [{"correct": "right", "center_word": N} for N in sample_negatives]) 119 | } 120 | 121 | # Shuffle the word blocks for randomization 122 | for block in [block1, block2, block3, block4, block5, block6, block7]: 123 | random.shuffle(block['words']) 124 | 125 | # Return the blocks based on the type parameter 126 | if type == 1: 127 | # Critical compatible blocks 128 | return [block1, block2, block3, block4, block5, block6, block7] 129 | else: 130 | # Critical incompatible blocks 131 | return [block5, block2, block6, block7, block1, block3, block4] 132 | 133 | 134 | BLOCKS = generate_blocks(1) 135 | -------------------------------------------------------------------------------- /docs/examples/iat_mouse/iat_mouse.py: -------------------------------------------------------------------------------- 1 | from smile.common import * 2 | from config import * 3 | from gen_stim import * 4 | 5 | # Start setting up the experiment 6 | exp = Experiment() 7 | 8 | 9 | # Setup the Block loop, where *block* is a 10 | # Reference to the variable you are looping over 11 | with Loop(BLOCKS) as block: 12 | # Show the instructions to the participant 13 | RstDocument(text=block.current['instruct'], base_font_size=RST_FONT_SIZE, 14 | width=RST_WIDTH, height=exp.screen.height) 15 | with UntilDone(): 16 | # When a KeyPress is detected, the UntilDone 17 | # will cancel the RstDocument state 18 | KeyPress() 19 | # Setup a loop over each Trial in a Block. *block.current* references the 20 | # current iteration of the loop, which is a dictionary that contains the list 21 | # words. *trial* will be our reference to the current word in our loop. 22 | with Loop(block.current['words']) as trial: 23 | # initialize our testing variable in Experiment Runtime 24 | # exp.something = something will create a Set state 25 | exp.mouse_test = False 26 | # The following is a ButtonPress state. This state works like KeyPress, 27 | # but instead waits for any of the buttons that are its children to be 28 | # press. 29 | with ButtonPress(correct_resp=trial.current['correct']) as bp: 30 | # block.current is a dictionary that has all of the information we 31 | # would need during each individual block, including the text that is 32 | # in these buttons, which differs from block to block 33 | Button(text=block.current['left_word'], name="left", left=0, 34 | top=exp.screen.top, width=BUTTON_WIDTH, height=BUTTON_HEIGHT, text_size=( 35 | 170, None), 36 | font_size=FONT_SIZE, halign='center') 37 | Button(text=block.current['right_word'], name="right", 38 | right=exp.screen.right, top=exp.screen.top, 39 | width=BUTTON_WIDTH, height=BUTTON_HEIGHT, text_size=( 40 | 170, None), 41 | font_size=FONT_SIZE, halign='center') 42 | # Required! To see the mouse on the screen 43 | MouseCursor() 44 | # while Those buttons are waiting to be pressed, go ahead and do the 45 | # children of this next state, the Meanwhile 46 | with Meanwhile(): 47 | # The start button that is required to be pressed before the trial 48 | # word is seen. 49 | with ButtonPress(): 50 | Button(text="Start", bottom=exp.screen.bottom, font_size=FONT_SIZE) 51 | # Do all of the children of a Parallel at the same time. 52 | with Parallel(): 53 | # display target word 54 | target_lb = Label( 55 | text=trial.current['center_word'], font_size=FONT_SIZE, bottom=exp.screen.bottom+100) 56 | # Record the movements of the mouse 57 | MouseRecord(name="MouseMovements") 58 | # Setup an invisible rectangle that is used to detect exactly 59 | # when the mouse starts to head toward an answer. 60 | rtgl = Rectangle(center=MousePos(), width=MOUSE_MOVE_RADIUS, 61 | height=MOUSE_MOVE_RADIUS, color=(0, 0, 0, 0)) 62 | with Serial(): 63 | # wait until the mouse leaves the rectangle from above 64 | wt = Wait(until=(MouseWithin(rtgl) == False)) 65 | # If they waited too long to start moving, tell the experiment 66 | # to display a warning message to the paricipant 67 | with If(wt.event_time['time'] - wt.start_time > MOUSE_MOVE_INTERVAL): 68 | exp.mouse_test = True 69 | with If(exp.mouse_test): 70 | Label(text="You are taking to long to move, Please speed up!", 71 | font_size=FONT_SIZE, color="RED", duration=WARNING_DURATION) 72 | # wait the interstimulus interval 73 | Wait(INTER_TRIAL_INTERVAL) 74 | # WRITE THE LOGS 75 | Log(name="IAT_MOUSE", 76 | left=block.current['left_word'], 77 | right=block.current['right_word'], 78 | word=trial.current, 79 | correct=bp.correct, 80 | reaction_time=bp.press_time['time']-target_lb.appear_time['time'], 81 | slow_to_react=exp.mouse_test) 82 | # the line required to run your experiment after all 83 | # of it is defined above 84 | exp.run() 85 | -------------------------------------------------------------------------------- /docs/examples/iat_mouse/iat_mouse_instructions1.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | INSTRUCTIONS 3 | ============ 4 | 5 | You are going to be presented with a list of words. At the top left of your 6 | screen, you will see a button marked **flower**. At the top right of your screen 7 | you will see a button marked **insect**. 8 | 9 | Each trial, you will need to click a button at the bottom of your screen marked 10 | **Start**. After you click that, a word will appear on the screen. Your task is 11 | to click on the button that corresponds to the word presented. 12 | 13 | 14 | :: 15 | 16 | If the word is an insect, please click the top right button! 17 | 18 | :: 19 | 20 | If the word is a flower, please click the top left button! 21 | 22 | It is **CRUCIAL** that you start moving the mouse as soon as you click the start 23 | button. If you take to long to start moving, a warning will appear on the screen 24 | to tell you to speed up! We want you to categorize these words as quickly as you 25 | possibly can. 26 | 27 | Press any key to Continue! 28 | -------------------------------------------------------------------------------- /docs/examples/iat_mouse/iat_mouse_instructions2.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | INSTRUCTIONS 3 | ============ 4 | 5 | You are going to be presented with a list of words. At the top left of your 6 | screen, you will see a button marked **positive**. At the top right of your screen 7 | you will see a button marked **negative**. 8 | 9 | Each trial, you will need to click a button at the bottom of your screen marked 10 | **Start**. After you click that, a word will appear on the screen. Your task is 11 | to click on the button that corresponds to the word presented. 12 | 13 | 14 | :: 15 | 16 | If the word is an negative associated noun, please click the top right button! 17 | 18 | :: 19 | 20 | If the word is a positive associated noun, please click the top left button! 21 | 22 | It is **CRUCIAL** that you start moving the mouse as soon as you click the start 23 | button. If you take to long to start moving, a warning will appear on the screen 24 | to tell you to speed up! We want you to categorize these words as quickly as you 25 | possibly can. 26 | 27 | Press any key to Continue! 28 | -------------------------------------------------------------------------------- /docs/examples/iat_mouse/iat_mouse_instructions3.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | TRAINING BLOCK INSTRUCTIONS 3 | =========================== 4 | 5 | You are going to be presented with a list of words. At the top left of your 6 | screen, you will see a button marked **flower positive**. At the top right of your screen 7 | you will see a button marked **insect negative**. 8 | 9 | Each trial, you will need to click a button at the bottom of your screen marked 10 | **Start**. After you click that, a word will appear on the screen. Your task is 11 | to click on the button that corresponds to the word presented. 12 | 13 | 14 | :: 15 | 16 | If the word is an insect or a negative associated noun, please click the top right button! 17 | 18 | :: 19 | 20 | If the word is a flower or a positive associated noun, please click the top left button! 21 | 22 | It is **CRUCIAL** that you start moving the mouse as soon as you click the start 23 | button. If you take to long to start moving, a warning will appear on the screen 24 | to tell you to speed up! We want you to categorize these words as quickly as you 25 | possibly can. 26 | 27 | Press any key to Continue! 28 | -------------------------------------------------------------------------------- /docs/examples/iat_mouse/iat_mouse_instructions4.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | CRITICAL BLOCK INSTRUCTIONS 3 | =========================== 4 | 5 | You are going to be presented with a list of words. At the top left of your 6 | screen, you will see a button marked **flower positive**. At the top right of your screen 7 | you will see a button marked **insect negative**. 8 | 9 | Each trial, you will need to click a button at the bottom of your screen marked 10 | **Start**. After you click that, a word will appear on the screen. Your task is 11 | to click on the button that corresponds to the word presented. 12 | 13 | 14 | :: 15 | 16 | If the word is an insect or a negative associated noun, please click the top right button! 17 | 18 | :: 19 | 20 | If the word is a flower or a positive associated noun, please click the top left button! 21 | 22 | It is **CRUCIAL** that you start moving the mouse as soon as you click the start 23 | button. If you take to long to start moving, a warning will appear on the screen 24 | to tell you to speed up! We want you to categorize these words as quickly as you 25 | possibly can. 26 | 27 | Press any key to Continue! 28 | -------------------------------------------------------------------------------- /docs/examples/iat_mouse/iat_mouse_instructions5.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | INSTRUCTIONS 3 | ============ 4 | 5 | You are going to be presented with a list of words. At the top left of your 6 | screen, you will see a button marked **insect**. At the top right of your screen 7 | you will see a button marked **flower**. 8 | 9 | Each trial, you will need to click a button at the bottom of your screen marked 10 | **Start**. After you click that, a word will appear on the screen. Your task is 11 | to click on the button that corresponds to the word presented. 12 | 13 | 14 | :: 15 | 16 | If the word is an flower, please click the top right button! 17 | 18 | :: 19 | 20 | If the word is a insect, please click the top left button! 21 | 22 | It is **CRUCIAL** that you start moving the mouse as soon as you click the start 23 | button. If you take to long to start moving, a warning will appear on the screen 24 | to tell you to speed up! We want you to categorize these words as quickly as you 25 | possibly can. 26 | 27 | Press any key to Continue! 28 | -------------------------------------------------------------------------------- /docs/examples/iat_mouse/iat_mouse_instructions6.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | TRAINING BLOCK INSTRUCTIONS 3 | =========================== 4 | 5 | You are going to be presented with a list of words. At the top left of your 6 | screen, you will see a button marked **insect positive**. At the top right of your screen 7 | you will see a button marked **flower negative**. 8 | 9 | Each trial, you will need to click a button at the bottom of your screen marked 10 | **Start**. After you click that, a word will appear on the screen. Your task is 11 | to click on the button that corresponds to the word presented. 12 | 13 | 14 | :: 15 | 16 | If the word is an flower or a negative associated noun, please click the top right button! 17 | 18 | :: 19 | 20 | If the word is a insect or a positive associated noun, please click the top left button! 21 | 22 | It is **CRUCIAL** that you start moving the mouse as soon as you click the start 23 | button. If you take to long to start moving, a warning will appear on the screen 24 | to tell you to speed up! We want you to categorize these words as quickly as you 25 | possibly can. 26 | 27 | Press any key to Continue! 28 | -------------------------------------------------------------------------------- /docs/examples/iat_mouse/iat_mouse_instructions7.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | CRITICAL BLOCK INSTRUCTIONS 3 | =========================== 4 | 5 | You are going to be presented with a list of words. At the top left of your 6 | screen, you will see a button marked **insect positive**. At the top right of your screen 7 | you will see a button marked **flower negative**. 8 | 9 | Each trial, you will need to click a button at the bottom of your screen marked 10 | **Start**. After you click that, a word will appear on the screen. Your task is 11 | to click on the button that corresponds to the word presented. 12 | 13 | 14 | :: 15 | 16 | If the word is an flower or a negative associated noun, please click the top right button! 17 | 18 | :: 19 | 20 | If the word is a insect or a positive associated noun, please click the top left button! 21 | 22 | It is **CRUCIAL** that you start moving the mouse as soon as you click the start 23 | button. If you take to long to start moving, a warning will appear on the screen 24 | to tell you to speed up! We want you to categorize these words as quickly as you 25 | possibly can. 26 | 27 | Press any key to Continue! 28 | -------------------------------------------------------------------------------- /docs/examples/iat_mouse/pools/flowers.txt: -------------------------------------------------------------------------------- 1 | aster 2 | clover 3 | hyacinth 4 | marigold 5 | poppy 6 | azalea 7 | crocus 8 | iris 9 | orchid 10 | rose 11 | bluebell 12 | daffodil 13 | lilac 14 | pansy 15 | tulip 16 | buttercup 17 | daisy 18 | lily 19 | peony 20 | violet 21 | carnation 22 | gladiola 23 | magnolia 24 | petunia 25 | zinnia 26 | -------------------------------------------------------------------------------- /docs/examples/iat_mouse/pools/insects.txt: -------------------------------------------------------------------------------- 1 | ant 2 | caterpiller 3 | flea 4 | locust 5 | spider 6 | bedbug 7 | centipede 8 | fly 9 | maggot 10 | tarantula 11 | bee 12 | cockroach 13 | gnat 14 | mosquito 15 | termite 16 | beetle 17 | cricket 18 | hornet 19 | moth 20 | wasp 21 | blackfly 22 | dragonfly 23 | horsefly 24 | roach 25 | weevil 26 | -------------------------------------------------------------------------------- /docs/examples/iat_mouse/pools/negatives.txt: -------------------------------------------------------------------------------- 1 | abuse 2 | crash 3 | filth 4 | murder 5 | sickness 6 | accident 7 | death 8 | grief 9 | poison 10 | stink 11 | assault 12 | disaster 13 | hatred 14 | pollute 15 | tragedy 16 | bomb 17 | divorce 18 | jail 19 | poverty 20 | ugly 21 | cancer 22 | evil 23 | kill 24 | rotten 25 | vomit 26 | agony 27 | prison 28 | -------------------------------------------------------------------------------- /docs/examples/iat_mouse/pools/positives.txt: -------------------------------------------------------------------------------- 1 | caress 2 | freedom 3 | health 4 | love 5 | peace 6 | cheer 7 | friend 8 | heaven 9 | loyal 10 | pleasure 11 | diamond 12 | gentle 13 | honest 14 | lucky 15 | rainbow 16 | diploma 17 | gift 18 | honor 19 | miracle 20 | sunrise 21 | family 22 | happy 23 | laughter 24 | paradise 25 | vacation 26 | -------------------------------------------------------------------------------- /docs/examples/joytest.py: -------------------------------------------------------------------------------- 1 | #emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | #ex: set sts=4 ts=4 sw=4 et: 3 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 4 | # 5 | # See the COPYING file distributed along with the smile package for the 6 | # copyright and license terms. 7 | # 8 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 9 | 10 | from smile.common import * 11 | from smile.joystick import JoyPress, JoyAxesToPolar, JoyAxis, JoyButton 12 | import math 13 | 14 | 15 | # set up an experiment 16 | exp = Experiment() 17 | 18 | Wait(0.5) 19 | 20 | # Create references to the joystick vals 21 | radius0, theta0 = JoyAxesToPolar(0, 1, scaled=True) 22 | radius1, theta1 = JoyAxesToPolar(3, 4, scaled=True) 23 | 24 | # button info 25 | base_color = (.1, .1, .1, 1.0) 26 | pressed_color = (.1, .1, .8, 1.0) 27 | 28 | with Parallel(): 29 | buttons = {} 30 | buttons[0] = Rectangle(color=base_color, 31 | left_bottom=(exp.screen.center_x-100*2, 32 | exp.screen.center_y)) 33 | buttons[1] = Rectangle(color=base_color, 34 | left_bottom=buttons[0].right_bottom) 35 | buttons[2] = Rectangle(color=base_color, 36 | left_bottom=buttons[1].right_bottom) 37 | buttons[3] = Rectangle(color=base_color, 38 | left_bottom=buttons[2].right_bottom) 39 | buttons[4] = Rectangle(color=base_color, 40 | left_top=buttons[0].left_bottom) 41 | buttons[5] = Rectangle(color=base_color, 42 | left_bottom=buttons[4].right_bottom) 43 | buttons[6] = Rectangle(color=base_color, 44 | left_bottom=buttons[5].right_bottom) 45 | buttons[7] = Rectangle(color=base_color, 46 | left_bottom=buttons[6].right_bottom) 47 | for i in buttons.keys(): 48 | Label(text=str(i), center=buttons[i].center) 49 | 50 | el = Ellipse(width=200, height=200, color='green', bottom=buttons[0].top) 51 | pnt = Ellipse(width=50, height=50, color='white', center=el.center) 52 | lbl_radius0 = Label(bottom=el.top, text='0.0') 53 | lbl_theta0 = Label(bottom=lbl_radius0.top, text='0.0') 54 | with UntilDone(): 55 | KeyPress() 56 | with Meanwhile(): 57 | with Parallel(): 58 | # I'm not sure why this won't work in a loop 59 | #for i in buttons.keys(): 60 | # buttons[i].animate(color=lambda t, initial: 61 | # Ref.cond(JoyButton(i), pressed_color, base_color)) 62 | buttons[0].animate(color=lambda t, initial: 63 | Ref.cond(JoyButton(0), pressed_color, base_color)) 64 | buttons[1].animate(color=lambda t, initial: 65 | Ref.cond(JoyButton(1), pressed_color, base_color)) 66 | buttons[2].animate(color=lambda t, initial: 67 | Ref.cond(JoyButton(2), pressed_color, base_color)) 68 | buttons[3].animate(color=lambda t, initial: 69 | Ref.cond(JoyButton(3), pressed_color, base_color)) 70 | buttons[4].animate(color=lambda t, initial: 71 | Ref.cond(JoyButton(4), pressed_color, base_color)) 72 | buttons[5].animate(color=lambda t, initial: 73 | Ref.cond(JoyButton(5), pressed_color, base_color)) 74 | buttons[6].animate(color=lambda t, initial: 75 | Ref.cond(JoyButton(6), pressed_color, base_color)) 76 | buttons[7].animate(color=lambda t, initial: 77 | Ref.cond(JoyButton(7), pressed_color, base_color)) 78 | 79 | # axis tracking 80 | lbl_radius0.animate(text=lambda t, initial: Ref(str, radius0)) 81 | lbl_theta0.animate(text=lambda t, initial: Ref(str, theta0)) 82 | 83 | pnt.animate(center=lambda t, initial: 84 | (el.center_x + JoyAxis(0)*el.width/2, 85 | el.center_y - JoyAxis(1)*el.width/2)) 86 | Wait(.5) 87 | 88 | if __name__ == '__main__': 89 | 90 | exp.run() 91 | -------------------------------------------------------------------------------- /docs/examples/log_test.py: -------------------------------------------------------------------------------- 1 | #emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | #ex: set sts=4 ts=4 sw=4 et: 3 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 4 | # 5 | # See the COPYING file distributed along with the smile package for the 6 | # copyright and license terms. 7 | # 8 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 9 | 10 | # load all the states 11 | from smile.common import * 12 | 13 | # create an experiment 14 | exp = Experiment() 15 | 16 | # initial wait 17 | Wait(1.0) 18 | 19 | # Wait for a bunch of different times 20 | trials = [{'val':str(i), 'val2':str(i+1)} for i in range(5)] 21 | with Loop(trials) as trial: 22 | s = Label(text=trial.current['val'], duration=.5) 23 | l = Log(trial.current, 24 | appear_time=s.appear_time) 25 | Wait(1.0) 26 | 27 | if __name__ == '__main__': 28 | exp.run() 29 | 30 | #print l.log_filename 31 | #print s.log_filename 32 | #from smile.log import LogReader 33 | 34 | #lr = LogReader(l.log_filename) 35 | #print lr.field_names 36 | #print 37 | 38 | -------------------------------------------------------------------------------- /docs/examples/loop_example.py: -------------------------------------------------------------------------------- 1 | # emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | # ex: set sts=4 ts=4 sw=4 et: 3 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 4 | # 5 | # See the COPYING file distributed along with the smile package for the 6 | # copyright and license terms. 7 | # 8 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 9 | 10 | # load all the states 11 | from smile.common import * 12 | 13 | # create an experiment 14 | exp = Experiment() 15 | 16 | # set up my list 17 | trials = [str(i) for i in range(5)] 18 | GOOD_RT = .5 19 | RESP_KEYS = ['J', 'K'] 20 | 21 | Wait(1.0) 22 | 23 | with Loop(trials) as trial: 24 | t = Label(text=trial.current, font_size=24) 25 | with UntilDone(): 26 | # Ensure `t.appear_time` is available by waiting until it exists 27 | Wait(until=t.appear_time) 28 | key = KeyPress(keys=RESP_KEYS, 29 | base_time=t.appear_time['time']) 30 | 31 | # set whether it was a good RT 32 | exp.good = key.rt < GOOD_RT 33 | with If(exp.good): 34 | Label(text='Awesome!', font_size=30, duration=1.0) 35 | with Else(): 36 | Label(text='Bummer!', font_size=30, duration=1.0) 37 | 38 | Log(stim_txt=trial.current, 39 | stim_on=t.appear_time, 40 | resp=key.pressed, 41 | rt=key.rt, 42 | good=exp.good, 43 | trial_num=trial.i) 44 | 45 | Wait(1.0) 46 | 47 | Wait(1.0) 48 | 49 | 50 | if __name__ == '__main__': 51 | exp.run() 52 | -------------------------------------------------------------------------------- /docs/examples/mean_until_test_py.py: -------------------------------------------------------------------------------- 1 | #emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | #ex: set sts=4 ts=4 sw=4 et: 3 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 4 | # 5 | # See the COPYING file distributed along with the smile package for the 6 | # copyright and license terms. 7 | # 8 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 9 | 10 | # load all the states 11 | from smile.common import * 12 | 13 | # create an experiment 14 | exp = Experiment() 15 | 16 | # Parallel equivalent to Meanwhile 17 | with Loop(3): 18 | with Parallel(): 19 | lblp = Label(text="This is on the screen for exactly 2 seconds", 20 | duration=2., blocking=True) 21 | with Serial(blocking=False): 22 | Wait(until=lblp.appear_time) 23 | Label(text='This is on the screen for exactly 1 second', 24 | top=lblp.bottom-20, duration=1.) 25 | Wait(1.) 26 | 27 | # Meanwhile approach 28 | with Loop(3): 29 | lblmw = Label(text="This is on the screen for exactly 2 seconds", 30 | duration=2.) 31 | with Meanwhile(): 32 | Wait(until=lblmw.appear_time) 33 | Label(text='This is on the screen for exactly 1 second', 34 | top=lblmw.bottom-20, duration=1.) 35 | Wait(1.) 36 | 37 | # UntilDone 38 | with Loop(3): 39 | lblut = Label(text="This should be on the screen for exactly 2 second", 40 | duration=2.) 41 | with UntilDone(): 42 | Wait(until=lblut.appear_time) 43 | Label(text='This is on the screen for exactly 1 second', 44 | top=lblut.bottom-20, duration=1.) 45 | Wait(1.) 46 | 47 | 48 | if __name__ == '__main__': 49 | exp.run() 50 | 51 | -------------------------------------------------------------------------------- /docs/examples/meta_exp.py: -------------------------------------------------------------------------------- 1 | #emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | #ex: set sts=4 ts=4 sw=4 et: 3 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 4 | # 5 | # See the COPYING file distributed along with the smile package for the 6 | # copyright and license terms. 7 | # 8 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 9 | 10 | 11 | # Load all the states 12 | from smile.common import * 13 | 14 | # Create an experiment 15 | exp = Experiment() 16 | 17 | # Load this code 18 | code = open(__file__, 'r').read() 19 | code = code[code.find('# load'):] 20 | 21 | # Show the image and code 22 | with Parallel(): 23 | # Pass in a source image, and 24 | # display it on the right side of 25 | # the screen 26 | Image(source='face-smile.png', 27 | center_x=exp.screen.center_x+exp.screen.width/4) 28 | 29 | # Pass in text, display the code on the left side 30 | # of the screen. 31 | CodeInput(left_top=exp.screen.left_top, 32 | width=exp.screen.width/2, 33 | height=exp.screen.height, 34 | text=code) 35 | 36 | with UntilDone(): 37 | # Take a screenshot after a keypress 38 | kp = KeyPress() 39 | Screenshot(filename='meta_exp.png') 40 | Wait(1.0) 41 | 42 | # If this was run in a command line, run the experiment 43 | if __name__ == '__main__': 44 | exp.run() 45 | -------------------------------------------------------------------------------- /docs/examples/mouse_tracking.py: -------------------------------------------------------------------------------- 1 | #emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | #ex: set sts=4 ts=4 sw=4 et: 3 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 4 | # 5 | # See the COPYING file distributed along with the smile package for the 6 | # copyright and license terms. 7 | # 8 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 9 | 10 | 11 | # Load all the states 12 | from smile.common import * 13 | 14 | # Set up an experiment 15 | exp = Experiment() 16 | 17 | 18 | # Initial Wait 19 | Wait(1.0) 20 | 21 | 22 | # This Parallel will show 2 rectangles and wait until 23 | # your cursor is in the one that is on the bottom 24 | with Parallel(): 25 | # Place a rectangle in the bottom-middle of the screen 26 | rect = Rectangle(center_bottom=exp.screen.center_bottom, 27 | color='white') 28 | # Place a rectangle in the top of the screen 29 | r2 = Rectangle(bottom=rect.top, color='purple') 30 | # Show the Mouse, required to show the mouse during 31 | # any experiment 32 | MouseCursor() 33 | # Do the above state Until the below states leave 34 | with UntilDone(): 35 | # Wait UNTIL the mouse is within the rect 36 | Wait(until=MouseWithin(rect)) 37 | # While we are waiting for the mouse, animate the other rectangle 38 | with Meanwhile(): 39 | r2.animate(center_x=lambda t, initial: MousePos()[0]) 40 | 41 | 42 | # This Parallel will show 2 rectangles, choice_A and chioce_B, 43 | # one at the top left and one at the top right. It will also show and 44 | # record the mouse position. 45 | with Parallel(): 46 | choice_A = Rectangle(left_top=(exp.screen.left + 100, exp.screen.top - 100)) 47 | choice_B = Rectangle(right_top=(exp.screen.right - 100, exp.screen.top - 100)) 48 | mrec = Record(mouse_pos=MousePos()) 49 | MouseCursor() 50 | # The above state will run until the mouse goes within either rectangle 51 | # and display which rectangle it was in. 52 | with UntilDone(): 53 | # Create MouseWithin Ref A 54 | mwa = MouseWithin(choice_A) 55 | # Create MouseWithin Ref B 56 | mwb = MouseWithin(choice_B) 57 | # Wait Until wither Ref is true 58 | w = Wait(until= mwa | mwb) 59 | # If the first mouse within is true 60 | with If(mwa): 61 | Debug(choice='A', 62 | rt=w.event_time['time'] - choice_A.appear_time['time']) 63 | # Else the second mouse within is true 64 | with Else(): 65 | Debug(choice='B', 66 | rt=w.event_time['time'] - choice_B.appear_time['time']) 67 | 68 | 69 | # If this was run in a command line, run the experiment 70 | if __name__ == '__main__': 71 | 72 | exp.run() 73 | -------------------------------------------------------------------------------- /docs/examples/nested.py: -------------------------------------------------------------------------------- 1 | #emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | #ex: set sts=4 ts=4 sw=4 et: 3 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 4 | # 5 | # See the COPYING file distributed along with the smile package for the 6 | # copyright and license terms. 7 | # 8 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 9 | 10 | 11 | # load all the states 12 | from smile.common import * 13 | 14 | # create an experiment 15 | exp = Experiment() 16 | 17 | # initial wait 18 | Wait(1.0) 19 | 20 | # Present two stim with embedded If 21 | with Loop(range(10)) as trial: 22 | # Show a Label with X or O depending on 23 | # which iteration of the loop it is. 24 | # In Parallel, display a : and then a ! 25 | # at the same time as the X and O. 26 | with Parallel(): 27 | with If((trial.current % 2) == 0): 28 | Label(text=' X', duration=2.0) 29 | with Else(): 30 | Label(text='O ', duration=2.0) 31 | with Serial(): 32 | Label(text=' : ', duration=1.0) 33 | Label(text=' ! ', duration=1.0) 34 | 35 | Wait(1.0) 36 | 37 | # If this was run in a command line, run the experiment 38 | if __name__ == '__main__': 39 | exp.run() 40 | -------------------------------------------------------------------------------- /docs/examples/ordered.py: -------------------------------------------------------------------------------- 1 | #emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | #ex: set sts=4 ts=4 sw=4 et: 3 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 4 | # 5 | # See the COPYING file distributed along with the smile package for the 6 | # copyright and license terms. 7 | # 8 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 9 | 10 | 11 | # Load all the states 12 | from smile.common import * 13 | 14 | # Create an experiment 15 | exp = Experiment() 16 | 17 | Wait(1.0) 18 | 19 | # Create stims 20 | with Parallel(): 21 | stimB = Label(text="B", x=exp.screen.center_x + 25, 22 | font_size=64, 23 | color='blue') 24 | stimF = Label(text="F", x=exp.screen.center_x - 25, 25 | font_size=64, 26 | color='orange') 27 | 28 | # Do the above state until the below states leave 29 | with UntilDone(): 30 | Wait(1.0) 31 | with Parallel(): 32 | stimB.slide(x=exp.screen.center_x - 25, duration=3.0) 33 | stimF.slide(x=exp.screen.center_x + 25, duration=3.0) 34 | 35 | Wait(1.0) 36 | 37 | # If this was run in a command line, run the experiment 38 | if __name__ == '__main__': 39 | exp.run() 40 | -------------------------------------------------------------------------------- /docs/examples/placement.py: -------------------------------------------------------------------------------- 1 | #emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | #ex: set sts=4 ts=4 sw=4 et: 3 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 4 | # 5 | # See the COPYING file distributed along with the smile package for the 6 | # copyright and license terms. 7 | # 8 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 9 | 10 | # load all the states 11 | from smile.common import * 12 | 13 | # create an experiment 14 | exp = Experiment() 15 | 16 | # initial wait 17 | Wait(1.0) 18 | 19 | # show above and below center 20 | Wait(2.) 21 | with Meanwhile(): 22 | with Parallel(): 23 | a = Label(text='Above', center_bottom=exp.screen.center) 24 | b = Label(text='Below', center_top=exp.screen.center) 25 | c = Label(text='Way Below', center_top=(b.center_x, b.bottom-100)) 26 | 27 | # Show Labels next to each other 28 | Wait(3.) 29 | with Meanwhile(): 30 | with Parallel(): 31 | a = Label(text='Middle', center_x=exp.screen.center_x) 32 | b = Label(text='Right', left=a.right+10 ) 33 | c = Label(text='Left', right=a.left-10) 34 | 35 | # Use a Layout to plot things in the layout, 36 | # not the experiment window 37 | Wait(3.) 38 | with Meanwhile(): 39 | with FloatLayout(center_x=exp.screen.center_x + 50, width=300, height=200) as fl: 40 | with Parallel(): 41 | a = Label(text='Above', center_bottom=fl.center) 42 | b = Label(text='Below', center_top=fl.center) 43 | c = Label(text='Way Below',center_x=fl.center_x, bottom=fl.bottom) 44 | 45 | 46 | Wait(1.0) 47 | 48 | # If this was run in a command line, run the experiment 49 | if __name__ == '__main__': 50 | exp.run() 51 | -------------------------------------------------------------------------------- /docs/examples/press_error_test.py: -------------------------------------------------------------------------------- 1 | #emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | #ex: set sts=4 ts=4 sw=4 et: 3 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 4 | # 5 | # See the COPYING file distributed along with the smile package for the 6 | # copyright and license terms. 7 | # 8 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 9 | 10 | # load all the states 11 | from smile.common import * 12 | 13 | # create an experiment 14 | exp = Experiment(background_color='black') 15 | 16 | exp.err_text = '0.0' 17 | with Loop(): 18 | # flash some text 19 | text = Label(text=exp.err_text, duration=.005, font_size=48) 20 | Wait(until=text.disappear_time) 21 | ResetClock(text.disappear_time['time']) 22 | Wait(.005) 23 | with UntilDone(): 24 | with Loop(100): 25 | # accept either a mouse or key press 26 | with Parallel(): 27 | mp = MousePress(blocking=False) 28 | kp = KeyPress(blocking=False) 29 | with If(mp.press_time): 30 | exp.err_text = Ref(str, mp.press_time['error']) 31 | with Elif(kp.press_time): 32 | exp.err_text = Ref(str, kp.press_time['error']) 33 | with Else(): 34 | Debug(kpt=kp.press_time, mpt=mp.press_time) 35 | 36 | Wait(1.0) 37 | 38 | if __name__ == '__main__': 39 | exp.run() 40 | -------------------------------------------------------------------------------- /docs/examples/questionnaire_example.csv: -------------------------------------------------------------------------------- 1 | type,ans,question,multiline 2 | TITLE,,SMILE QUESTIONNAIRE, 3 | CA,Yes,HOW DO YOU DO, 4 | ,no,, 5 | ,maybeso,, 6 | CA,no,How IS YOU DODODOD, 7 | ,no,, 8 | ,no,, 9 | ,yes,, 10 | MC,HELL,What best?, 11 | ,SATAN,, 12 | ,BEBEKING,, 13 | ,JESUS,, 14 | TI,,Tell me about your dat,3 15 | CT,Less,Pick on the scale what you want to do with your life, 16 | ,Mid,, 17 | ,More,, 18 | TITLE,,THE REST, 19 | TI,,Tell me about your dat,3 20 | CT,Less,Pick on the scale what you want to do with your life, 21 | ,Mid,, 22 | ,More,, 23 | TI,,Tell me about your dat,3 24 | CT,Less,Pick on the scale what you want to do with your life, 25 | ,Mid,, 26 | ,More,, 27 | -------------------------------------------------------------------------------- /docs/examples/questionnaire_test.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from smile.common import * 3 | from smile.questionnaire import csv2loq 4 | 5 | # Get the directory of the current script 6 | script_dir = Path(__file__).parent.absolute() 7 | 8 | # Define the path to the questionnaire CSV file 9 | csv_file_path = script_dir / "questionnaire_example.csv" 10 | 11 | # Initialize the Experiment with given resolution 12 | exp = Experiment() 13 | 14 | with Parallel(): 15 | # Load the questionnaire from the CSV file, using Ref to pass the path dynamically 16 | with UntilDone(): 17 | tt = Questionnaire(loq=Ref(csv2loq, str(csv_file_path)), 18 | height=exp.screen.height, 19 | width=exp.screen.width, 20 | x=0, y=0) 21 | MouseCursor() 22 | 23 | # Log the questionnaire results 24 | Log(tt.results) 25 | 26 | # Run the experiment 27 | exp.run() 28 | -------------------------------------------------------------------------------- /docs/examples/rotation_test.py: -------------------------------------------------------------------------------- 1 | #emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | #ex: set sts=4 ts=4 sw=4 et: 3 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 4 | # 5 | # See the COPYING file distributed along with the smile package for the 6 | # copyright and license terms. 7 | # 8 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 9 | 10 | 11 | # Load all the states 12 | from smile.common import * 13 | 14 | # Create an experiment 15 | exp = Experiment() 16 | 17 | with Parallel(): 18 | # Add the Image and Rectangle 19 | # (with zero alpha so they are not visible) 20 | # and start the Image rotated at 359 degrees 21 | img = Image(source='face-smile.png', 22 | color=(1, 1, 1, 0), 23 | rotate=359) 24 | rect = Rectangle(color=('red', 0.0), 25 | size=img.size) 26 | 27 | # Create some circles on top of each other, 28 | # this shows that you can use pythonic syntax 29 | # to create states 30 | circs = [Ellipse(color=col, size=(50, 50)) 31 | for col in ['red', 'green', 'blue', 'purple']] 32 | 33 | # Do the above state until the below states are finished 34 | with UntilDone(): 35 | Wait(.5) 36 | with Parallel(): 37 | # Move the circles out to the side 38 | [circs[i].slide(center_x=img.right+offset, duration=2.0) 39 | for i, offset in enumerate([0, 50, 100, 150])] 40 | with Parallel(): 41 | # Spin the circles 42 | [circs[i].slide(rotate=360, duration=dur) 43 | for i, dur in enumerate([2.0, 2.5, 3.0, 3.5])] 44 | 45 | # have the smile image fade in 46 | img.slide(rotate=0, duration=4.0, color=(1, 1, 1, 1)) 47 | with Parallel(): 48 | # Fade out and spin back the circles 49 | [circs[i].slide(rotate=0, duration=dur, color=(0, 0, 0, 0)) 50 | for i, dur in enumerate([3.5, 3.0, 2.5, 2.0])] 51 | 52 | # Fade in the rectangle 53 | rect.slide(color=('red', .3), duration=2) 54 | 55 | # Show that you can simply set the rotate_origin 56 | rect.rotate_origin = rect.left_bottom 57 | img.rotate_origin = img.center 58 | 59 | # And slide the origin 60 | rect.slide(rotate_origin=rect.center, duration=2.0, rotate=90) 61 | 62 | # Rotate the image and rect 63 | with Parallel(): 64 | img.slide(rotate=360, duration=4.0, center=rect.left_bottom) 65 | with Serial(): 66 | rect.slide(rotate=45, duration=2.0, bottom=exp.screen.bottom) 67 | rect.slide(rotate=45, duration=2.0, center=exp.screen.center) 68 | 69 | # Show you can rotate the image by setting a value 70 | Wait(1.0) 71 | img.rotate = 270 72 | Wait(.5) 73 | img.rotate = 180 74 | Wait(.5) 75 | img.rotate = 90 76 | Wait(.5) 77 | img.rotate = 0 78 | Wait(2.0) 79 | 80 | # Show two rectangles with different rotation props 81 | with Parallel(): 82 | r1 = Rectangle(left_bottom=exp.screen.center, 83 | rotate=45, 84 | rotate_origin=exp.screen.center, 85 | size=(200, 100), 86 | color=('yellow', .5)) 87 | r2 = Rectangle(left_bottom=exp.screen.center, 88 | rotate=45, 89 | rotate_origin=r1.right_top, 90 | size=(200, 100), 91 | color=('blue', .5)) 92 | with UntilDone(): 93 | # Show where we're heading 94 | Wait(.5) 95 | r1.rotate_origin = r1.right_top 96 | Wait(1.) 97 | r1.rotate_origin = r1.left_bottom 98 | Wait(1.0) 99 | Debug(lb=r1.left_bottom, rt=r1.right_top, ro=r1.rotate_origin) 100 | # slide there 101 | r1.slide(rotate_origin=r1.right_top, 102 | duration=2.0) 103 | Wait(1.0) 104 | Debug(lb=r1.left_bottom, rt=r1.right_top, ro=r1.rotate_origin) 105 | 106 | # If this was run in a command line, run the experiment 107 | if __name__ == '__main__': 108 | exp.run() 109 | -------------------------------------------------------------------------------- /docs/examples/scale_examples.py: -------------------------------------------------------------------------------- 1 | from smile.common import * 2 | from kivy.metrics import sp, dp 3 | from kivy import platform 4 | from smile.ref import NotAvailable 5 | from smile.scale import scale as s 6 | 7 | 8 | # This is an example on how to use the scale function in smile. You can use the 9 | # scale function to make sure that your stimuli are the same size regardless of 10 | # screen density. You can also use scale to scale up or down your stimuli to 11 | # fill or fit into whatever size monitor you would like, as long as you set a 12 | # scale_box. scale_box is set in pixel space. If you keep all of your simuli 13 | # within the scale_box and you set scale_up/down to True, all of your simuli 14 | # will appear on any screen you run the experiment on. 15 | 16 | # Sometimes you might want to scale down only if the screen is too small to fit 17 | # your stimuli. In this case you set a *scale_box* and the scale_up/down flags. 18 | # If scale_down is True, **scale** will scale your numbers such that the dp on 19 | # your scale box is able to fit within the screen. If the screen is bigger than 20 | # the scale box, then your sizing numbers will only be scaled by **dp**, meaning 21 | # your stimuli will look the same on all monitors that are bigger than the 22 | # scale box. 23 | 24 | # NOTE If you want to test out what your code would look like on a different 25 | # monitor (that is smaller than the one you already have up), you can run the 26 | # following line in the command line: 27 | # python your_code.py -r **width**x**height** 28 | # Where width and height are replaced with the resolution of the monitor you 29 | # want to test. 30 | 31 | RADIUS = 100 32 | 33 | 34 | # Set a scale_box to [600, 500] which is [width, height] and scale_up/scale_down 35 | # to make sure that no matter what the monitor size is, everything will be scale 36 | # to fit in side a scale_box. 37 | exp = Experiment(scale_box=[600,500], scale_up=True, scale_down=True) 38 | 39 | # As an example, if you run this file on one computer, and then another one 40 | # with a different monitor size, the first number in this debug should be 41 | # different on both machines, but the second number will be 10. 42 | Debug(s=s(10), b=10) 43 | 44 | # Font size is a perfect example of something you would want to scale with 45 | # monitor size and screen density. 46 | Label(text="These are some instructions", font_size=s(20)) 47 | with UntilDone(): 48 | if platform == "android": 49 | with ButtonPress(): 50 | Button(size=exp.screen.size, background_color=(0, 0, 0, 0)) 51 | else: 52 | KeyPress() 53 | Wait(1.) 54 | 55 | 56 | with Loop([20, 10, 5, 1]) as lp: 57 | # MovingDots allows us to to play with many different sizing and positioning 58 | # parameters. The scale of the dots, the radius of the apature, the speed of the 59 | # dots, all of these things are perfect for using scale. 60 | with Parallel(): 61 | md1 = MovingDots(scale=s(lp.current), radius=s(RADIUS), speed=s(100)) 62 | # Please note the md1.top + s(50). If you want to do relative positioning, 63 | # then you must use scale if you want the relative distance between things 64 | # to be equal. 65 | Label(text="20: "+Ref(str,s(lp.current)), y=md1.top + s(50), font_size=s(40)) 66 | with UntilDone(): 67 | if platform == "android": 68 | with ButtonPress(): 69 | Button(size=exp.screen.size, background_color=(0, 0, 0, 0)) 70 | else: 71 | KeyPress() 72 | Wait(1.) 73 | 74 | exp.run() 75 | -------------------------------------------------------------------------------- /docs/examples/slide.py: -------------------------------------------------------------------------------- 1 | #emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | #ex: set sts=4 ts=4 sw=4 et: 3 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 4 | # 5 | # See the COPYING file distributed along with the smile package for the 6 | # copyright and license terms. 7 | # 8 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 9 | 10 | # load all the states 11 | from smile.common import * 12 | 13 | # create an experiment 14 | exp = Experiment() 15 | 16 | # initial wait 17 | Wait(.25) 18 | 19 | # Put up a circle and then slide it 20 | circ = Ellipse(color=(jitter(0, 1), 21 | jitter(0, 1), 22 | jitter(0, 1))) 23 | with UntilDone(): 24 | Wait(until=circ.appeared) 25 | with Loop(5): 26 | # slide to new loc and color 27 | exp.new_col = (jitter(0, 1), 28 | jitter(0, 1), 29 | jitter(0, 1)) 30 | exp.new_loc = (jitter(0, exp.screen.width), 31 | jitter(0, exp.screen.height)) 32 | cu = circ.slide(duration=1.5, 33 | color=exp.new_col, 34 | center=exp.new_loc) 35 | 36 | Wait(.25) 37 | 38 | if __name__ == '__main__': 39 | exp.run() 40 | -------------------------------------------------------------------------------- /docs/examples/smile_turn.py: -------------------------------------------------------------------------------- 1 | #emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | #ex: set sts=4 ts=4 sw=4 et: 3 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 4 | # 5 | # See the COPYING file distributed along with the smile package for the 6 | # copyright and license terms. 7 | # 8 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 9 | 10 | # Load all the states 11 | from smile.common import * 12 | 13 | # Create an experiment 14 | exp = Experiment() 15 | 16 | 17 | with Parallel(): 18 | # Set initially to white (need to use Rectangle b/c it can "slide" 19 | bg = Rectangle(color='white', size=exp.screen.size) 20 | 21 | # Put items in a scatter for rotation 22 | with ScatterLayout(do_rotation=False, 23 | do_translation=False, 24 | do_scale=False, 25 | center=exp.screen.center, 26 | rotation=17) as sl: 27 | # Must put at 0,0 in the scatter 28 | img = Image(source='face-smile.png', 29 | color=(1, 1, 1, 0), 30 | x=0, y=0) 31 | 32 | # Must put at 0,0 33 | rect = Rectangle(color=('red', .2), 34 | size=sl.size, 35 | x=0, y=0) 36 | 37 | lbl = Label(text='SMILE!', font_size=12, color=('white', 0.0)) 38 | 39 | 40 | # Run the above state until that which is within the UntilDone leaves 41 | with UntilDone(): 42 | Wait(1.0) 43 | # Change the size of the ScatterLayout to the same 44 | # size as the Image 45 | sl.size = img.size 46 | # Change the size of the rectangle to the same size of 47 | # the ScatterLayout over 1 second 48 | rect.slide(size=sl.size, duration=1.0) 49 | Debug(width=sl.width, x=sl.x, center1=exp.screen.center, 50 | center2=sl.center, isize=img.size, itop=img.top) 51 | # Change the color of the background rectangle, 52 | # the color of the Image, the color and position of 53 | # the Label, and rotate the ScatterLayout using the 54 | # rotation parameter 55 | with Parallel(): 56 | bg.slide(color='black', duration=4.0) 57 | img.slide(color=(1., 1., 1., 1.), duration=4.0) 58 | lbl.slide(color=('white', 1.0), 59 | bottom=exp.screen.top-100, 60 | font_size=64, 61 | duration=5.0) 62 | sl.slide(rotation=360, 63 | duration=6.0) 64 | Wait(2.0) 65 | 66 | # If this was run in a command line, run the experiment 67 | if __name__ == '__main__': 68 | exp.run() 69 | -------------------------------------------------------------------------------- /docs/examples/stern/config.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | from pathlib import Path 4 | 5 | # config vars 6 | NUM_TRIALS = 2 7 | # The trials, shuffled, for the stimulus generation. 8 | NUM_ITEMS = [2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4] 9 | random.shuffle(NUM_ITEMS) 10 | ITEMS = string.ascii_lowercase 11 | 12 | # Get the directory where this script is located 13 | script_dir = Path(__file__).parent.absolute() 14 | # instructions written in another document 15 | INSTRUCT_TEXT = open(script_dir / 'stern_instructions.rst', 'r').read() 16 | 17 | RST_FONT_SIZE = 30 18 | RST_WIDTH = 900 19 | STUDY_DURATION = 1.2 20 | STUDY_ISI = .4 21 | RETENTION_INTERVAL = 1.0 22 | 23 | # KeyPress stuff 24 | RESP_KEYS = ['J', 'K'] 25 | RESP_DELAY = .2 26 | ORIENT_DURATION = 1.0 27 | ORIENT_ISI = .5 28 | ITI = 1.0 29 | FONT_SIZE = 30 30 | -------------------------------------------------------------------------------- /docs/examples/stern/gen_stim.py: -------------------------------------------------------------------------------- 1 | import random 2 | from config import NUM_ITEMS, NUM_TRIALS, ITEMS 3 | # generate Sternberg trial 4 | def stern_trial(nitems=2, lure_trial=False,): 5 | if lure_trial: 6 | condition = 'lure' 7 | items = random.sample(ITEMS,nitems+1) 8 | else: 9 | condition = 'target' 10 | items = random.sample(ITEMS,nitems) 11 | # append a test item 12 | items.append(random.sample(items,1)[0]) 13 | trial = {'nitems':nitems, 14 | 'study_items':items[:-1], 15 | 'test_item':items[-1], 16 | 'condition':condition,} 17 | return trial 18 | 19 | trials = [] 20 | for i in NUM_ITEMS: 21 | # add target trials 22 | trials.extend([stern_trial(i,lure_trial=False) for t in range(NUM_TRIALS)]) 23 | # add lure trials 24 | trials.extend([stern_trial(i,lure_trial=True) for t in range(NUM_TRIALS)]) 25 | 26 | # shuffle and number 27 | random.shuffle(trials) 28 | for t in range(len(trials)): 29 | trials[t]['trial_num'] = t 30 | -------------------------------------------------------------------------------- /docs/examples/stern/stern.py: -------------------------------------------------------------------------------- 1 | # load all the states 2 | from smile.common import * 3 | 4 | # execute both the configuration file and the 5 | # stimulus generation file 6 | from config import * 7 | from gen_stim import * 8 | 9 | # Define the experiment 10 | exp = Experiment() 11 | # Present the instructions to the participant 12 | init_text = RstDocument(text=INSTRUCT_TEXT, width=RST_WIDTH, 13 | font_size=RST_FONT_SIZE, top=exp.screen.top, height=exp.screen.height) 14 | with UntilDone(): 15 | # Once the KeyPress is detected, the UntilDone 16 | # cancels the RstDocument 17 | keypress = KeyPress() 18 | # loop over study block 19 | with Loop(trials) as trial: 20 | # Setup the list of study times. 21 | exp.study_times = [] 22 | # orient stim 23 | orient = Label(text='+', duration=ORIENT_DURATION, font_size=FONT_SIZE) 24 | Wait(ORIENT_ISI) 25 | # loop over study items 26 | with Loop(trial.current['study_items']) as item: 27 | # present the letter 28 | ss = Label(text=item.current, duration=STUDY_DURATION, 29 | font_size=FONT_SIZE) 30 | # wait some jittered amount 31 | Wait(STUDY_ISI) 32 | # append the time 33 | exp.study_times += [ss.appear_time['time']] 34 | # Retention interval 35 | Wait(RETENTION_INTERVAL - STUDY_ISI) 36 | # present the letter 37 | test_stim = Label( 38 | text=trial.current['test_item'], bold=True, font_size=FONT_SIZE) 39 | with UntilDone(): 40 | # wait some before accepting input 41 | Wait(RESP_DELAY) 42 | # Wait until the test_stim label has the appear_time attribute 43 | Wait(until=test_stim.appear_time) 44 | # After the KeyPress is detected, the UntilDone 45 | # cancels the Label test_stim and allows the 46 | # experiment to continue. 47 | ks = KeyPress(keys=RESP_KEYS, 48 | base_time=test_stim.appear_time['time']) 49 | # Log the trial 50 | Log(trial.current, 51 | name="Stern", 52 | resp=ks.pressed, 53 | rt=ks.rt, 54 | orient_time=orient.appear_time['time'], 55 | study_times=exp.study_times, 56 | test_time=test_stim.appear_time['time'], 57 | correct=(((trial.current['condition'] == 'target') & 58 | (ks.pressed == RESP_KEYS[0])) | 59 | ((trial.current['condition'] == 'lure') & 60 | (ks.pressed == RESP_KEYS[1])))) 61 | Wait(ITI) 62 | # run that exp! 63 | exp.run() 64 | -------------------------------------------------------------------------------- /docs/examples/stern/stern_instructions.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | INSTRUCTIONS 3 | ============ 4 | 5 | You will be presented with blocks of lists of letters. During each block, you 6 | will see a number of letters (2 or 3 or 4) one after the next. Then you will be 7 | presented with another letter that is bold. You will need to identify whether or 8 | not you have seen this bold letter during this block. 9 | 10 | :: 11 | 12 | If you have seen this letter in the current block, Press J. 13 | 14 | :: 15 | 16 | If you haven't seen this letter in the current block, Press K. 17 | 18 | When you are ready to start the experiment, Press any key! 19 | -------------------------------------------------------------------------------- /docs/examples/stroop/config.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | # Get the directory where this script is located 3 | script_dir = Path(__file__).parent.absolute() 4 | # Read in the instructions 5 | INSTRUCT_TEXT = open(script_dir / 'stroop_instructions.rst', 'r').read() 6 | NUMBER_OF_BLOCKS = 1 7 | NUMBER_OF_TRIALS_PER_BLOCK = 2 8 | RECORD_DURATION = 2 9 | INTER_BLOCK_DURATION = 2 10 | INTER_STIMULUS_INTERVAL = 2 11 | RST_FONT_SIZE = 30 12 | RST_WIDTH = 900 13 | -------------------------------------------------------------------------------- /docs/examples/stroop/gen_stim.py: -------------------------------------------------------------------------------- 1 | from random import shuffle, randint, choice 2 | from typing import List, Dict 3 | from config import NUMBER_OF_BLOCKS, NUMBER_OF_TRIALS_PER_BLOCK 4 | 5 | 6 | def randomize_color(word: str, index: int) -> str: 7 | """ 8 | Randomly select a color from the available colors that does not match the given word. 9 | 10 | Args: 11 | word (str): The word representing the color. 12 | index (int): The index used for color randomization. 13 | 14 | Returns: 15 | str: A mismatched color. 16 | """ 17 | word_color_map = { 18 | 'red': ["BLUE", "GREEN", "ORANGE"], 19 | 'blue': ["RED", "GREEN", "ORANGE"], 20 | 'green': ["RED", "BLUE", "ORANGE"], 21 | 'orange': ["RED", "BLUE", "GREEN"] 22 | } 23 | return word_color_map[word][index % 3] 24 | 25 | 26 | def create_trial(word: str, matched: bool = True, color: str = None) -> Dict[str, str]: 27 | """ 28 | Creates a trial dictionary with word, color, and matched status. 29 | 30 | Args: 31 | word (str): The word for the trial. 32 | matched (bool): Whether the word matches the color. 33 | color (str): Optional color to use for the trial, only for mismatched trials. 34 | 35 | Returns: 36 | dict: A dictionary representing a single trial. 37 | """ 38 | trial_color = color if not matched else word.upper() 39 | return { 40 | 'word': word, 41 | 'color': trial_color, 42 | 'matched': matched 43 | } 44 | 45 | 46 | def generate_block(number_of_trials_per_block: int) -> List[Dict[str, str]]: 47 | """ 48 | Generate a block of trials with a specified length. 49 | 50 | Args: 51 | number_of_trials_per_block (int): The desired number of trials in the block. 52 | 53 | Returns: 54 | List[Dict[str, str]]: A list of trial dictionaries. 55 | """ 56 | block = [] 57 | words = ["red", "blue", "green", "orange"] 58 | 59 | num_matched_trials = number_of_trials_per_block // 2 60 | 61 | # Add matched trials 62 | for _ in range(num_matched_trials): # Half of the trials are matched 63 | block.append(create_trial(choice(words), matched=True)) 64 | 65 | # Add mismatched trials 66 | # Half of the trials are mismatched 67 | for _ in range(number_of_trials_per_block - num_matched_trials): 68 | random_word = choice(words) 69 | block.append(create_trial(random_word, matched=False, 70 | color=randomize_color(random_word, randint(0, 2)))) 71 | 72 | shuffle(block) # Shuffle the block for randomization 73 | return block 74 | 75 | 76 | def generate_blocks(num_of_blocks: int, number_of_trials_per_block: int) -> List[List[Dict[str, str]]]: 77 | """ 78 | Generate a list of blocks, where each block contains a number of trials. 79 | 80 | Each trial is represented as a dictionary with string keys and values. 81 | 82 | Args: 83 | num_of_blocks (int): The number of blocks to generate. 84 | number_of_trials_per_block (int): The number of trials in each block. 85 | 86 | Returns: 87 | List[List[Dict[str, str]]]: A list of blocks, where each block is a list of trials. 88 | Each trial is represented as a dictionary with string keys and values. 89 | """ 90 | blocks = [] 91 | 92 | for _ in range(num_of_blocks): 93 | # Generate a block with the specified number of trials 94 | block = generate_block(number_of_trials_per_block) 95 | blocks.append(block) 96 | 97 | shuffle(blocks) # Shuffle the list of blocks to randomize their order 98 | return blocks 99 | 100 | 101 | BLOCKS = generate_blocks(NUMBER_OF_BLOCKS, NUMBER_OF_TRIALS_PER_BLOCK) 102 | 103 | 104 | # Example usage 105 | if __name__ == "__main__": 106 | BLOCKS = generate_blocks(NUMBER_OF_BLOCKS, NUMBER_OF_TRIALS_PER_BLOCK) 107 | print(len(BLOCKS)) 108 | print(BLOCKS) 109 | -------------------------------------------------------------------------------- /docs/examples/stroop/stroop.py: -------------------------------------------------------------------------------- 1 | from smile.common import * 2 | from smile.audio import RecordSoundFile 3 | 4 | # execute both the configuration file and the 5 | # stimulus generation file 6 | from config import * 7 | from gen_stim import * 8 | 9 | # Define the Experiment Variable 10 | exp = Experiment() 11 | 12 | # Show the instructions as an RstDocument Viewer on the screen 13 | init_text = RstDocument(text=INSTRUCT_TEXT, font_size=RST_FONT_SIZE, 14 | width=RST_WIDTH, top=exp.screen.top, height=exp.screen.height) 15 | with UntilDone(): 16 | # Once you press any key, the UntilDone will cancel the RstDocument, 17 | # allowing the rest of the experiment to continue running. 18 | keypress = KeyPress() 19 | 20 | # Initialize the block counter, only used because we need 21 | # unique names for the .wav files later. 22 | exp.block_number = 0 23 | 24 | # Initialize the Loop as "with Loop(list_like) as reference_variable_name:" 25 | with Loop(BLOCKS) as block: 26 | # Initialize the trial counter, only used because we need 27 | # unique names for the .wav files later. 28 | exp.trial_number = 0 29 | 30 | # Initialize the Loop as "with Loop(list_like) as reference_variable_name:" 31 | with Loop(block.current) as trial: 32 | inter_stim = Label(text='+', font_size=80, 33 | duration=INTER_BLOCK_DURATION) 34 | # Display the word, with the appropriate colored text 35 | t = Label(text=trial.current['word'], 36 | font_size=48, color=trial.current['color']) 37 | with UntilDone(): 38 | # The Label will stay on the screen for as long as 39 | # the RecordSoundFile state is active. The filename 40 | # for this state is different for each trial in each block. 41 | rec = RecordSoundFile(filename="b_" + Ref(str, exp.block_number) + "_t_" + Ref(str, exp.trial_number), 42 | duration=RECORD_DURATION) 43 | # Log the color and word that was presented on the screen, 44 | # as well as the block and trial number 45 | Log(name='Stroop', stim_word=trial.current['word'], stim_color=trial.current['color'], 46 | block_num=exp.block_number, trial_num=exp.trial_number) 47 | Wait(INTER_STIMULUS_INTERVAL) 48 | # Increase the trial_number 49 | exp.trial_number += 1 50 | # Increase the block_number 51 | exp.block_number += 1 52 | # Run the experiment! 53 | exp.run() 54 | -------------------------------------------------------------------------------- /docs/examples/stroop/stroop_instructions.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | How to stroop! 3 | ============== 4 | 5 | You are going to be presented a few lists of words that are colors. When a word 6 | appears on the screen, you are to say the text color's name as clearly as 7 | possible. You want to minimize the time it takes you to respond, but not the 8 | time it takes you to say the whole word. 9 | 10 | EXAMPLE 11 | ------- 12 | 13 | :: 14 | 15 | If the word is "Green" but the text color is "Orange" you would say "Orange" as soon as you can. 16 | 17 | :: 18 | 19 | If the word is "Green" and the text color is "Green" you would say "Green" as soon as you can. 20 | 21 | REMINDER : Say the Color of the text out loud, not the printed word on the screen! 22 | 23 | Press any key to continue! Good Luck! 24 | -------------------------------------------------------------------------------- /docs/examples/timing_prop_test.py: -------------------------------------------------------------------------------- 1 | #emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | #ex: set sts=4 ts=4 sw=4 et: 3 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 4 | # 5 | # See the COPYING file distributed along with the smile package for the 6 | # copyright and license terms. 7 | # 8 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 9 | 10 | # load all the states 11 | from smile.common import * 12 | 13 | # create an experiment 14 | exp = Experiment(background_color='black') 15 | 16 | # set the dur and isi for each trial 17 | trials = [{'dur': d, 'isi': i} 18 | for d, i in zip([.005, .010, .020, .050, .100, .200, .500, 1.0], 19 | [.005, .010, .020, .050, .100, .200, .500, 1.0])] 20 | 21 | # add in a bunch of fast switches 22 | trials = [{'dur': .005, 'isi': .005}] * 10 + trials 23 | 24 | # double length, reverse, and repeat 25 | trials = trials*2 26 | trials_copy = trials[:] 27 | trials_copy.reverse() 28 | trials.extend(trials_copy) 29 | 30 | rect = Rectangle(color=(0, 0, 0, 1.0), 31 | size=(exp.screen.width*.9, 32 | exp.screen.height*.9)) 33 | with UntilDone(): 34 | Wait(1.0) 35 | with Loop(trials) as trial: 36 | # wait the isi 37 | reset = Wait(trial.current['isi']) 38 | 39 | # turn it on 40 | rect.color = (1,1,1,1) #'white' 41 | uw = UpdateWidget(rect, color=(1,1,1,1)) 42 | 43 | #Wait(until=uw.appear_time) 44 | #Debug(on_time = uw.appear_time) 45 | 46 | #Wait(until=bg.on_screen) 47 | 48 | # reset clock to ensure flip-level timing 49 | #ResetClock(bg.appear_time['time']-exp.flip_interval/4.) 50 | #ResetClock(rect.appear_time['time']-.004) 51 | 52 | # wait the dur 53 | Wait(trial.current['dur']) 54 | 55 | rect.color = (0,0,0,1) #'black' 56 | 57 | # log the on and off times 58 | #Done(rect) 59 | Log(on=rect.appear_time, 60 | #off=rect.disappear_time, 61 | dur=trial.current['dur'], 62 | isi=trial.current['isi']) 63 | 64 | Wait(1.0) 65 | 66 | if __name__ == '__main__': 67 | exp.run() 68 | -------------------------------------------------------------------------------- /docs/examples/timing_test.py: -------------------------------------------------------------------------------- 1 | #emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | #ex: set sts=4 ts=4 sw=4 et: 3 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 4 | # 5 | # See the COPYING file distributed along with the smile package for the 6 | # copyright and license terms. 7 | # 8 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 9 | 10 | 11 | # Load all the states 12 | from smile.common import * 13 | import random 14 | 15 | # Create an experiment 16 | exp = Experiment(background_color='black') 17 | 18 | # Set the dur and isi for each trial 19 | trials = [{'dur':d,'isi':i} 20 | for d,i in zip([.005,.010,1.0/60.0,.020,.050,.100,.200,.500,1.0], 21 | [.005,.010,1.0/60.0,.020,.050,.100,.200,.500,1.0])] 22 | 23 | # Add in a bunch of fast switches 24 | trials = [{'dur':.005,'isi':.005}]*10 + trials 25 | 26 | # Double length, reverse, and repeat 50 times 27 | trials = trials*2 28 | trials_copy = trials[:] 29 | trials_copy.reverse() 30 | trials.extend(trials_copy) 31 | trials = trials*50 32 | 33 | # Initial Wait 34 | Wait(1.0) 35 | 36 | # Main Loop to loop through all the trials created above 37 | with Loop(trials) as trial: 38 | # Wait the isi 39 | reset = Wait(trial.current['isi']) 40 | 41 | # Turn it on 42 | bg = BackgroundColor(color=(1,1,1,1.0)) 43 | with UntilDone(): 44 | Wait(until=bg.on_screen) 45 | 46 | # Reset clock to ensure flip-level timing 47 | ResetClock(bg.appear_time['time']) 48 | 49 | # Wait the dur 50 | Wait(trial.current['dur']) 51 | 52 | # Log the on and off times 53 | Wait(until=bg.disappear_time) 54 | ResetClock(bg.disappear_time['time']) 55 | Log(name="flipping", 56 | on=bg.appear_time, 57 | off=bg.disappear_time, 58 | dur=trial.current['dur'], 59 | isi=trial.current['isi']) 60 | 61 | Wait(1.0) 62 | 63 | # If this was run in a command line, run the experiment 64 | if __name__ == '__main__': 65 | exp.run() 66 | -------------------------------------------------------------------------------- /docs/examples/unicode.py: -------------------------------------------------------------------------------- 1 | #emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | #ex: set sts=4 ts=4 sw=4 et: 3 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 4 | # 5 | # See the COPYING file distributed along with the smile package for the 6 | # copyright and license terms. 7 | # 8 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 9 | 10 | # Load all the states 11 | from smile.common import * 12 | 13 | # Create an experiment 14 | exp = Experiment() 15 | 16 | # Initial wait 17 | Wait(1.0) 18 | 19 | # Show a label 20 | Label(text=chr(10025) + u" Unicode " + u"\u2729", 21 | font_size=64, font_name='DejaVuSans') 22 | with UntilDone(): 23 | KeyPress() 24 | 25 | Wait(1.0) 26 | 27 | # If this was run in a command line, run the experiment 28 | if __name__ == '__main__': 29 | exp.run() 30 | -------------------------------------------------------------------------------- /docs/examples/wait_test.py: -------------------------------------------------------------------------------- 1 | #emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | #ex: set sts=4 ts=4 sw=4 et: 3 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 4 | # 5 | # See the COPYING file distributed along with the smile package for the 6 | # copyright and license terms. 7 | # 8 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 9 | 10 | # load all the states 11 | from smile.common import * 12 | 13 | # create an experiment 14 | exp = Experiment() 15 | 16 | # initial wait 17 | Wait(1.0) 18 | 19 | # Wait for a bunch of different times 20 | times = [.001,.002,.005,.010,.020,.050] #,.1,.2,.5,1,2,5.] 21 | times_copy = times[:] 22 | times_copy.reverse() 23 | times.extend(times_copy) 24 | 25 | with Loop(times) as time: 26 | w = Wait(time.current) 27 | db = Debug(cur_time=time.current) 28 | ResetClock(db.leave_time) 29 | 30 | Wait(1.0) 31 | 32 | if __name__ == '__main__': 33 | import cProfile 34 | cProfile.run('exp.run()','waitstats') 35 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | What is SMILE? 2 | ============== 3 | 4 | .. image:: _static/smile_example.png 5 | :width: 375 6 | :height: 241 7 | :align: right 8 | 9 | SMILE is the State Machine Interface Library for Experiments. The goal when 10 | developing SMILE was to create an east-to-use experiment building library 11 | that allows for millisecond-accurate timing. SMILE was written so that the end 12 | user doesn't have to worry about the intricacies of timing, event handling, or 13 | logging data. 14 | 15 | Inspired by the concept of a state machine, SMILE works by storing the current 16 | status of releveant input events, then initiating an action depending on a 17 | predetermined set of rules. With the support of the versatile **Python** 18 | programming language and **Kivy**, a module create for video game development, 19 | SMILE is powerful and flexible while still being simple to use. 20 | 21 | What does a SMILE experiment look like? 22 | ======================================= 23 | 24 | Below is hello.py, an example of what the simplest SMILE experiment looks like: 25 | 26 | .. code-block:: python 27 | 28 | from smile.common import * 29 | 30 | exp = Experiment() 31 | 32 | Label(text="Hello, World!", duration=5) 33 | 34 | exp.run() 35 | 36 | In order to run this experiment from a computer that has SMILE installed, you 37 | would use your favorite OS's command prompt and run the following line: 38 | 39 | :: 40 | 41 | >> python hello.py -s SubjectID 42 | 43 | This program creates a full-screen window with a black background and the words 44 | **Hello, World!** in white text in the center--just like that, we are SMILEing! 45 | 46 | Now let us go through our SMILE experiment line by line and see what each of 47 | them does. 48 | 49 | **First** is the line *exp = Experiment()*. This line is the initialization line 50 | for SMILE. This tells SMILE that it should prepare to see states being declared. 51 | 52 | **Second** is the line *Label(text="Hello, World!", duration=5)*. **Label** is a 53 | SMILE visual state that displays text onto the screen. Certain SMILE states take 54 | a *duration*, and we are setting this state's duration to **5**. This means the 55 | state will remain active on the screen for 5 seconds. 56 | 57 | **Third** is the line *exp.run()*. This line signals to SMILE that you have finished 58 | building your experiment and that it is ready to run. SMILE will then run your 59 | experiment from start to finish and exit the experiment window when it has 60 | finished. 61 | 62 | Whats Next? 63 | =========== 64 | 65 | To help you get ready to SMILE, the first section of this documentation is the 66 | SMILE installation and the installation of its dependencies. After that is a 67 | section that delves deeper into SMILE and how to write more complicated 68 | experiments. 69 | 70 | .. toctree:: 71 | :maxdepth: 1 72 | 73 | install 74 | tutorial 75 | smile_states 76 | tutorial_2 77 | real_examples 78 | accessing_data 79 | advanced_smile 80 | seeking_help 81 | smile 82 | 83 | Funding Sources 84 | =============== 85 | 86 | Development of SMILE made possible by grants from: 87 | 88 | .. image:: _static/ccbs.jpg 89 | :width: 150 90 | :height: 150 91 | :align: left 92 | :target: https://cog.osu.edu 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /docs/real_examples.rst: -------------------------------------------------------------------------------- 1 | ================ 2 | Full Experiments 3 | ================ 4 | 5 | Below are a few links to full, recognizable experiments that were coded up in 6 | SMILE. They include the idea behind the experiment, an explanation of the code, 7 | and they include a mini-analysis of the data collected. These real world examples 8 | will provide a better understanding into exactly how to code a 9 | SMILE experiment in real world conditions, rather than in bite-sized samples of 10 | code. 11 | 12 | .. _full_experiments: 13 | 14 | Experiments 15 | ----------- 16 | 17 | .. toctree:: 18 | :maxdepth: 2 19 | 20 | examples/stroop/stroop 21 | examples/stern/stern 22 | examples/free_recall/freerecall 23 | examples/iat_mouse/iat_mouse 24 | -------------------------------------------------------------------------------- /docs/seeking_help.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | Seeking Help? 3 | ============= 4 | 5 | .. _seeking-help: 6 | 7 | SMILE has a Google group where you can discuss SMILE with other people who are 8 | trying to learn how to use it, as well as see if anyone is having the same 9 | problems that you are. This group is located at `smile-users `_. 10 | 11 | SMILE also has a `GitHub `_ page where 12 | you can report any issues that you have. 13 | -------------------------------------------------------------------------------- /docs/smile.rst: -------------------------------------------------------------------------------- 1 | SMILE package 2 | ============= 3 | 4 | smile.audio module 5 | ------------------ 6 | 7 | .. automodule:: smile.audio 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | smile.clock module 13 | ------------------ 14 | 15 | .. automodule:: smile.clock 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | smile.dag module 21 | ---------------- 22 | 23 | .. automodule:: smile.dag 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | smile.experiment module 29 | ----------------------- 30 | 31 | .. automodule:: smile.experiment 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | 36 | smile.freekey module 37 | -------------------- 38 | 39 | .. automodule:: smile.freekey 40 | :members: 41 | :undoc-members: 42 | :show-inheritance: 43 | 44 | smile.keyboard module 45 | --------------------- 46 | 47 | .. automodule:: smile.keyboard 48 | :members: 49 | :undoc-members: 50 | :show-inheritance: 51 | 52 | smile.kivy_overrides module 53 | --------------------------- 54 | 55 | .. automodule:: smile.kivy_overrides 56 | :members: 57 | :undoc-members: 58 | :show-inheritance: 59 | 60 | smile.log module 61 | ---------------- 62 | 63 | .. automodule:: smile.log 64 | :members: 65 | :undoc-members: 66 | :show-inheritance: 67 | 68 | smile.mouse module 69 | ------------------ 70 | 71 | .. automodule:: smile.mouse 72 | :members: 73 | :undoc-members: 74 | :show-inheritance: 75 | 76 | smile.pulse module 77 | ------------------ 78 | 79 | .. automodule:: smile.pulse 80 | :members: 81 | :undoc-members: 82 | :show-inheritance: 83 | 84 | smile.ref module 85 | ---------------- 86 | 87 | .. automodule:: smile.ref 88 | :members: 89 | :undoc-members: 90 | :show-inheritance: 91 | 92 | smile.state module 93 | ------------------ 94 | 95 | .. automodule:: smile.state 96 | :members: 97 | :undoc-members: 98 | :show-inheritance: 99 | 100 | smile.utils module 101 | ------------------ 102 | 103 | .. automodule:: smile.utils 104 | :members: 105 | :undoc-members: 106 | :show-inheritance: 107 | 108 | smile.video module 109 | ------------------ 110 | 111 | .. automodule:: smile.video 112 | :members: 113 | :undoc-members: 114 | :show-inheritance: 115 | 116 | 117 | Module contents 118 | --------------- 119 | 120 | .. automodule:: smile 121 | :members: 122 | :undoc-members: 123 | :show-inheritance: 124 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "smile" 7 | version = "1.0.0" 8 | description = "State Machine Interface Library for Experiments" 9 | authors = [{ name = "Per B. Sederberg", email = "psederberg@gmail.com" }] 10 | maintainers = [{ name = "Per B. Sederberg", email = "psederberg@gmail.com" }] 11 | readme = { file = "README.rst", content-type = "text/x-rst" } 12 | license = { file = "license.txt" } 13 | requires-python = ">=3.6" 14 | dependencies = ["kivy", "packaging"] 15 | classifiers = [ 16 | "Development Status :: 4 - Beta", 17 | "Intended Audience :: Researchers", 18 | "Programming Language :: Python :: 3", 19 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 20 | ] 21 | 22 | keywords = ["state machine", "experiments"] 23 | 24 | [project.optional-dependencies] 25 | audio = ["pyo"] 26 | 27 | [project.urls] 28 | Documentation = "https://smile-docs.readthedocs.io/en/latest/#" 29 | Source = "https://github.com/compmem/smile" 30 | Tracker = "https://github.com/compmem/smile/issues" 31 | 32 | [tool.setuptools.package-data] 33 | smile = [ 34 | "face-smile.png", 35 | "logo.png", 36 | "test_sound.wav", 37 | "test_video.mp4", 38 | "lock.png", 39 | "unlock.png", 40 | "crosshairs*", 41 | ] 42 | -------------------------------------------------------------------------------- /smile/__init__.py: -------------------------------------------------------------------------------- 1 | #emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | #ex: set sts=4 ts=4 sw=4 et: 3 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 4 | # 5 | # See the COPYING file distributed along with the smile package for the 6 | # copyright and license terms. 7 | # 8 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 9 | 10 | -------------------------------------------------------------------------------- /smile/clock.py: -------------------------------------------------------------------------------- 1 | from . import kivy_overrides 2 | import kivy.clock 3 | 4 | _get_time = kivy.clock._default_time 5 | _kivy_clock = kivy.clock.Clock 6 | 7 | class _ClockEvent(object): 8 | def __init__(self, clock, func, event_time, repeat_interval): 9 | self.clock = clock 10 | self.func = func 11 | self.event_time = event_time 12 | self.repeat_interval = repeat_interval 13 | 14 | class Clock(object): 15 | def __init__(self): 16 | self._events = [] 17 | 18 | def now(self): 19 | return _get_time() 20 | 21 | def tick(self): 22 | #TODO: limit time spent in each tick? 23 | now = self.now() 24 | while len(self._events): 25 | event = self._events[0] 26 | if event.event_time is None or now >= event.event_time: 27 | del self._events[0] 28 | if event.repeat_interval is not None: 29 | if event.event_time is None: 30 | event.event_time = now + event.repeat_interval 31 | else: 32 | event.event_time += event.repeat_interval 33 | self._schedule(event) 34 | event.func() 35 | else: 36 | break 37 | 38 | def usleep(self, usec): 39 | _kivy_clock.usleep(usec) 40 | 41 | def _schedule(self, event): 42 | for n, cmp_event in enumerate(self._events): 43 | if ((event.event_time is None and 44 | cmp_event.event_time is not None) or 45 | (event.event_time is not None and 46 | cmp_event.event_time is not None and 47 | event.event_time < cmp_event.event_time)): 48 | self._events.insert(n, event) 49 | break 50 | else: 51 | self._events.append(event) 52 | 53 | def schedule(self, func, event_delay=None, event_time=None, 54 | repeat_interval=None): 55 | if event_delay is not None: 56 | event_time = self.now() + event_delay 57 | self._schedule(_ClockEvent(self, func, event_time, repeat_interval)) 58 | return func 59 | 60 | def unschedule(self, func): 61 | self._events = [event for event in self._events if event.func != func] 62 | 63 | clock = Clock() 64 | -------------------------------------------------------------------------------- /smile/common.py: -------------------------------------------------------------------------------- 1 | #emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | #ex: set sts=4 ts=4 sw=4 et: 3 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 4 | # 5 | # See the COPYING file distributed along with the smile package for the 6 | # copyright and license terms. 7 | # 8 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 9 | 10 | # SMILE components 11 | from .experiment import Experiment 12 | from .state import ( 13 | Parallel, 14 | Meanwhile, 15 | UntilDone, 16 | Serial, 17 | Subroutine, 18 | If, 19 | Elif, 20 | Else, 21 | Loop, 22 | Done, 23 | Wait, 24 | When, 25 | While, 26 | Record, 27 | Log, 28 | Func, 29 | ResetClock, 30 | Debug, 31 | PrintTraceback) 32 | from .keyboard import Key, KeyPress, KeyRecord 33 | from .mouse import ( 34 | MouseWithin, 35 | MousePos, 36 | MouseButton, 37 | MouseCursor, 38 | MouseRecord, 39 | MousePress) 40 | from .video import ( 41 | Screenshot, 42 | Bezier, 43 | Mesh, 44 | Point, 45 | Triangle, 46 | Quad, 47 | Rectangle, 48 | BorderImage, 49 | Ellipse, 50 | Line, 51 | Image, 52 | Label, 53 | RstDocument, 54 | Button, 55 | ButtonPress, 56 | Slider, 57 | TextInput, 58 | ToggleButton, 59 | ProgressBar, 60 | CodeInput, 61 | CheckBox, 62 | Video, 63 | Camera, 64 | FileChooserListView, 65 | AnchorLayout, 66 | BoxLayout, 67 | FloatLayout, 68 | GridLayout, 69 | PageLayout, 70 | RelativeLayout, 71 | Scatter, 72 | ScatterLayout, 73 | StackLayout, 74 | ScrollView, 75 | BackgroundColor, 76 | UpdateWidget, 77 | Animate, 78 | BlockingFlips, 79 | NonBlockingFlips) 80 | from .dotbox import DotBox, DynamicDotBox 81 | from .moving_dots import MovingDots 82 | from .grating import Grating 83 | from .ref import Ref, val, jitter, shuffle 84 | from .audio import Beep, SoundFile, RecordSoundFile 85 | from .freekey import FreeKey 86 | from .questionnaire import Questionnaire 87 | from .scale import scale 88 | -------------------------------------------------------------------------------- /smile/crosshairs.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 38 | 40 | 41 | 43 | image/svg+xml 44 | 46 | 47 | 48 | 49 | 50 | 54 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /smile/crosshairs_100x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compmem/smile/e85c2da827bb161c2ac7fc43d2a2cac20e536364/smile/crosshairs_100x100.png -------------------------------------------------------------------------------- /smile/crosshairs_50x50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compmem/smile/e85c2da827bb161c2ac7fc43d2a2cac20e536364/smile/crosshairs_50x50.png -------------------------------------------------------------------------------- /smile/dag.py: -------------------------------------------------------------------------------- 1 | #emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | #ex: set sts=4 ts=4 sw=4 et: 3 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 4 | # 5 | # See the COPYING file distributed along with the smile package for the 6 | # copyright and license terms. 7 | # 8 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 9 | 10 | import pydot 11 | from .state import ParentState, Serial 12 | from .utils import get_class_name 13 | 14 | class DAG(object): 15 | """ 16 | Directed Acyclic Graph (DAG) of an experiment. 17 | """ 18 | def __init__(self, base_state, fontname="Verdana"): 19 | """ 20 | """ 21 | # process the base state 22 | if get_class_name(base_state)[0] == 'Experiment': 23 | # must grab the root state 24 | base_state = base_state._root_state 25 | 26 | # create the starting graph 27 | self.graph = pydot.Dot(graph_type='digraph', 28 | fontname=fontname, 29 | compound='true') 30 | 31 | # save edges 32 | self.edges = [] 33 | 34 | # add all the states 35 | self._add_cluster(self.graph, base_state) 36 | 37 | # add those edges 38 | for e in self.edges: 39 | self.graph.add_edge(e) 40 | 41 | def _add_cluster(self, graph, cluster): 42 | # get class name for state 43 | name, cl_uname = get_class_name(cluster) 44 | 45 | # strip Builder 46 | name = name[:-7] 47 | 48 | # create the cluster 49 | clust = pydot.Cluster(cl_uname, label=name) 50 | 51 | # loop over children of cluster 52 | nodes = [] 53 | for i, c in enumerate(cluster._children): 54 | if issubclass(c.__class__, ParentState): 55 | # call recursively 56 | uname,first_uname,last_uname = self._add_cluster(clust, c) 57 | 58 | # save in node list 59 | if uname is not None: 60 | nodes.append({'uname':uname, 61 | 'first_uname': first_uname, 62 | 'last_uname': last_uname}) 63 | else: 64 | # get class name for state 65 | name, uname = get_class_name(c) 66 | 67 | # strip Builder 68 | name = name[:-7] 69 | 70 | # add the child node 71 | clust.add_node(pydot.Node(uname, label=name)) 72 | 73 | # save in node list (no children for first/last) 74 | nodes.append({'uname':uname, 75 | 'first_uname': None, 76 | 'last_uname': None}) 77 | 78 | # add edges if necessary 79 | if issubclass(cluster.__class__, Serial) and i>0: 80 | # set defaults 81 | ledge = nodes[i-1]['uname'] 82 | redge = nodes[i]['uname'] 83 | ltail = None 84 | lhead = None 85 | if nodes[i-1]['last_uname']: 86 | # coming from cluster 87 | ledge = nodes[i-1]['last_uname'] 88 | ltail = nodes[i-1]['uname'].get_name() 89 | if nodes[i]['first_uname']: 90 | # going to cluster 91 | redge = nodes[i]['first_uname'] 92 | lhead = nodes[i]['uname'].get_name() 93 | 94 | if ltail and lhead: 95 | self.edges.append(pydot.Edge(ledge, redge, 96 | ltail=ltail, 97 | lhead=lhead)) 98 | elif ltail: 99 | self.edges.append(pydot.Edge(ledge, redge, 100 | ltail=ltail)) 101 | elif lhead: 102 | self.edges.append(pydot.Edge(ledge, redge, 103 | lhead=lhead)) 104 | else: 105 | self.edges.append(pydot.Edge(ledge, redge)) 106 | 107 | if len(nodes) > 0: 108 | # insert the cluster to the graph 109 | graph.add_subgraph(clust) 110 | 111 | if nodes[0]['first_uname']: 112 | first_uname = nodes[0]['first_uname'] 113 | else: 114 | first_uname = nodes[0]['uname'] 115 | if nodes[-1]['last_uname']: 116 | last_uname = nodes[-1]['last_uname'] 117 | else: 118 | last_uname = nodes[-1]['uname'] 119 | else: 120 | clust, first_uname, last_uname = None, None, None 121 | 122 | # return the cluster uname for connections 123 | return clust, first_uname, last_uname 124 | 125 | def write(self, filename, prog='dot', format='pdf'): 126 | self.graph.write(filename, prog=prog, format=format) 127 | 128 | def view_png(self): 129 | from IPython.display import Image, display 130 | plt = Image(self.graph.create_png()) 131 | return display(plt) 132 | 133 | def view_svg(self): 134 | from IPython.display import SVG, display 135 | plt = SVG(self.graph.create_svg()) 136 | return display(plt) 137 | -------------------------------------------------------------------------------- /smile/demographics.py: -------------------------------------------------------------------------------- 1 | from .common import * 2 | 3 | # Standard Demographics Logging 4 | 5 | @Subroutine 6 | def Demographics(self): 7 | buttons = [] 8 | buttons2 = [] 9 | buttons3 = [] 10 | with Parallel(): 11 | # Gotta have a cursor 12 | MouseCursor() 13 | 14 | # background rectangle 15 | rc = Rectangle(color='GRAY', top=self._exp.screen.top, 16 | height=self._exp.screen.height, 17 | width=600) 18 | # Age 19 | lbAge = Label(text='Age : ', left=rc.left+50, 20 | top=self._exp.screen.top-20) 21 | txi1 = TextInput(left=lbAge.right + 10, height=lbAge.height+10, 22 | center_y=lbAge.center_y, input_filter='int') 23 | 24 | # Gender 25 | lbSex = Label(text='Gender : ', left=rc.left+50, top=lbAge.bottom-40) 26 | with ButtonPress() as bp1: 27 | lb1 = Label(text='Male', left=lbSex.right+10, 28 | center_y=lbSex.center_y) 29 | buttons.append(ToggleButton(name='Male', left=lb1.right+5, 30 | center_y=lb1.center_y, 31 | group='sex', 32 | width=20, height=20)) 33 | lb2 = Label(text='Female', left=buttons[0].right+50, 34 | center_y=lb1.center_y) 35 | buttons.append(ToggleButton(name='Female', 36 | center_y=lb2.center_y, 37 | group='sex', 38 | width=20, height=20, 39 | left=lb2.right+5),) 40 | lb3 = Label(text='Other', left=buttons[1].right+50, 41 | center_y=lb1.center_y) 42 | buttons.append(ToggleButton(name='Other', width=20, 43 | height=20, 44 | center_y=lb1.center_y, 45 | left=lb3.right+5, 46 | group='sex')) 47 | 48 | # Hispanic Or Latino 49 | lbHis = Label(text='Are you Hispanic or Latino? (Having origins in Cuban, Mecian, Puerto Rican, South or Central American, or other Spanish culture, regardless of race.)', 50 | left=rc.left+50, top=lbSex.bottom-40, 51 | text_size=(rc.width*.75, None)) 52 | with ButtonPress() as bp2: 53 | lbYes = Label(text='Yes', left=rc.left+50, top=lbHis.bottom-20) 54 | buttons2.append(ToggleButton(name='yes', 55 | left=lbYes.right+5, 56 | center_y=lbYes.center_y, 57 | width=20, height=20, 58 | group='latino')) 59 | lbNo = Label(text='No', left=buttons2[0].right+50, 60 | top=lbHis.bottom-20) 61 | buttons2.append(ToggleButton(name='no', 62 | left=lbNo.right+5, 63 | center_y=lbNo.center_y, 64 | width=20, height=20, 65 | group='latino')) 66 | 67 | # Racial Origin 68 | lbRace = Label(text='Racial Origin (Mark all that apply)', 69 | left=rc.left+50, top=lbHis.bottom-80) 70 | #Black African-American 71 | buttons3.append(ToggleButton(name='black_or_aftican-american', 72 | left=rc.left+50, 73 | top=lbRace.bottom-20, 74 | width=20, height=20)) 75 | Label(text='Black or African-American', left=buttons3[0].right+5, 76 | center_y=buttons3[0].center_y) 77 | 78 | # Middle Eastern 79 | buttons3.append(ToggleButton(name='middle_eastern', 80 | left=rc.left+50, 81 | top=buttons3[0].bottom-10, 82 | width=20, height=20)) 83 | Label(text='Middle Eastern', left=buttons3[1].right+5, 84 | center_y=buttons3[1].center_y) 85 | 86 | # North African 87 | buttons3.append(ToggleButton(name='north_african', 88 | left=rc.left+50, 89 | top=buttons3[1].bottom-10, 90 | width=20, height=20)) 91 | Label(text='North African', left=buttons3[2].right+5, 92 | center_y=buttons3[2].center_y) 93 | 94 | # Asian 95 | buttons3.append(ToggleButton(name='asian', 96 | left=rc.left+50, 97 | top=buttons3[2].bottom-10, 98 | width=20, height=20)) 99 | Label(text='Asian', left=buttons3[3].right+5, 100 | center_y=buttons3[3].center_y) 101 | 102 | # White 103 | buttons3.append(ToggleButton(name='white', 104 | left=rc.left+50, 105 | top=buttons3[3].bottom-10, 106 | width=20, height=20)) 107 | Label(text='White', left=buttons3[4].right+5, 108 | center_y=buttons3[4].center_y) 109 | 110 | # Native American, American Indian, or Alaskan Native 111 | buttons3.append(ToggleButton(name='native_american_or_alaskan', 112 | left=rc.left+50, 113 | top=buttons3[4].bottom-10, 114 | width=20, height=20)) 115 | Label(text='Native American, American Indian, or Alaskan Native', 116 | left=buttons3[5].right+5, center_y=buttons3[5].center_y) 117 | 118 | # Native Hawaiian, or other Pacific Islander 119 | buttons3.append(ToggleButton(name='native_hawaiian_or_islander', 120 | left=rc.left+50, 121 | top=buttons3[5].bottom-10, 122 | width=20, height=20)) 123 | Label(text='Native Hawaiian, or other Pacific Islander', 124 | left=buttons3[6].right+5, center_y=buttons3[6].center_y) 125 | 126 | with UntilDone(): 127 | with ButtonPress() as bp3: 128 | Button(name='enter', text='CONTINUE', 129 | bottom=self._exp.screen.bottom+10, 130 | height=40) 131 | Log(name='demographics', 132 | subj_id=self._exp.subject, 133 | age=txi1.text, 134 | male=buttons[0].state=='down', 135 | female=buttons[1].state=='down', 136 | other=buttons[2].state=='down', 137 | latino=buttons2[0].state=='down', 138 | non_latino=buttons2[1].state=='down', 139 | black_african_american=buttons3[0].state=='down', 140 | middle_eastern=buttons3[1].state=='down', 141 | north_african=buttons3[2].state=='down', 142 | asian=buttons3[3].state=='down', 143 | white=buttons3[4].state=='down', 144 | native_american=buttons3[5].state=='down', 145 | native_hawaiian_or_islander=buttons3[6].state=='down',) 146 | -------------------------------------------------------------------------------- /smile/dotbox.py: -------------------------------------------------------------------------------- 1 | #emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | #ex: set sts=4 ts=4 sw=4 et: 3 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 4 | # 5 | # See the COPYING file distributed along with the smile package for the 6 | # copyright and license terms. 7 | # 8 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 9 | 10 | from .video import WidgetState, BlockingFlips, NonBlockingFlips 11 | from .state import Wait, Meanwhile, Parallel, Loop, Subroutine 12 | from .state import If, Else 13 | from .ref import jitter 14 | 15 | from kivy.uix.widget import Widget 16 | from kivy.properties import NumericProperty, ListProperty 17 | from kivy.graphics import Point, Color, Rectangle 18 | 19 | import random 20 | 21 | 22 | @WidgetState.wrap 23 | class DotBox(Widget): 24 | """Display a box filled with random square dots. 25 | 26 | Parameters 27 | ---------- 28 | num_dots : integer 29 | Number of dots to draw 30 | pointsize : integer 31 | Radius of dot (see `Point`) 32 | color : tuple or string 33 | Color of dots 34 | backcolor : tuple or string 35 | Color of background rectangle 36 | 37 | """ 38 | color = ListProperty([1, 1, 1, 1]) 39 | backcolor = ListProperty([0, 0, 0, 0]) 40 | #num_dots = NumericProperty(10, force_dispatch=True) 41 | num_dots = NumericProperty(10) 42 | pointsize = NumericProperty(5) 43 | 44 | def __init__(self, **kwargs): 45 | super(type(self), self).__init__(**kwargs) 46 | 47 | self._color = None 48 | self._backcolor = None 49 | self._points = None 50 | 51 | self.bind(color=self._update_color, 52 | backcolor=self._update_backcolor, 53 | pos=self._update, 54 | size=self._update, 55 | num_dots=self._update_locs) 56 | self._update_locs() 57 | 58 | def _update_color(self, *pargs): 59 | self._color.rgba = self.color 60 | 61 | def _update_backcolor(self, *pargs): 62 | self._backcolor.rgba = self.backcolor 63 | 64 | def _update_locs(self, *pargs): 65 | self._locs = [random.random() 66 | for i in range(int(self.num_dots)*2)] 67 | self._update() 68 | 69 | def _update_pointsize(self, *pargs): 70 | self._points.pointsize = self.pointsize 71 | 72 | def _update(self, *pargs): 73 | # calc new point locations 74 | bases = (self.x+self.pointsize, self.y+self.pointsize) 75 | scales = (self.width-(self.pointsize*2), 76 | self.height-(self.pointsize*2)) 77 | points = [bases[i % 2]+scales[i % 2]*loc 78 | for i, loc in enumerate(self._locs)] 79 | 80 | # draw them 81 | self.canvas.clear() 82 | with self.canvas: 83 | # set the back color 84 | self._backcolor = Color(*self.backcolor) 85 | 86 | # draw the background 87 | Rectangle(size=self.size, 88 | pos=self.pos) 89 | 90 | # set the color 91 | self._color = Color(*self.color) 92 | 93 | # draw the points 94 | self._points = Point(points=points, pointsize=self.pointsize) 95 | 96 | 97 | @Subroutine 98 | def DynamicDotBox(self, duration=None, 99 | update_interval=jitter(1/20., (1/10.)-(1/20.)), 100 | **dotbox_args): 101 | """Display random dots that update at an interval. 102 | 103 | Parameters 104 | ---------- 105 | duration : float or None 106 | Duration to show the random dots. 107 | update_interval : float 108 | How often to update the random dots. Default is to jitter 109 | between 10 and 20 Hz. 110 | dotbox_args : kwargs 111 | See the DotBox for any kwargs options to control the DotBox 112 | 113 | Note: You can access the dotbox via the `db` attribute of the 114 | subroutine. 115 | 116 | Examples 117 | -------- 118 | Display a dynamic dot box with 40 dots for 3 seconds: 119 | 120 | :: 121 | DynamicDotBox(size=(500, 500), num_dots=40, duration=3.0) 122 | 123 | 124 | Display two dynamic dot boxes side-by-side until a key press: 125 | :: 126 | 127 | with Parallel(): 128 | ddb1 = DynamicDotBox(center_x=exp.screen.center_x-200, 129 | num_dots=40, size=(400, 400)) 130 | ddb2 = DynamicDotBox(center_x=exp.screen.center_x+200, 131 | num_dots=80, size=(400, 400)) 132 | with UntilDone(): 133 | kp = KeyPress() 134 | 135 | Log(appear_time=ddb1.db.appear_time) 136 | 137 | """ 138 | # show the dotbox 139 | with Parallel(): 140 | db = DotBox(duration=duration, **dotbox_args) 141 | self.db = db 142 | with Meanwhile(): 143 | # redraw the dots 144 | self.start_dots = db.num_dots 145 | with Loop() as l: 146 | Wait(duration=update_interval) 147 | # hack to make 1.8 work 148 | with If((l.i % 2)==0): 149 | self.ndots = self.start_dots + .01 150 | with Else(): 151 | self.ndots = self.start_dots 152 | #db.update(save_log=False, **dotbox_args) 153 | db.update(save_log=False, num_dots=self.ndots) 154 | -------------------------------------------------------------------------------- /smile/event.py: -------------------------------------------------------------------------------- 1 | # emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | # ex: set sts=4 ts=4 sw=4 et: 3 | # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 4 | # 5 | # See the COPYING file distributed along with the smile package for the 6 | # copyright and license terms. 7 | # 8 | # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 9 | 10 | 11 | def event_time(time, time_error=0.0): # TODO: make this a class! 12 | return {'time': time, 'error': time_error} 13 | -------------------------------------------------------------------------------- /smile/face-smile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compmem/smile/e85c2da827bb161c2ac7fc43d2a2cac20e536364/smile/face-smile.png -------------------------------------------------------------------------------- /smile/freekey.py: -------------------------------------------------------------------------------- 1 | #emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | #ex: set sts=4 ts=4 sw=4 et: 3 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 4 | # 5 | # See the COPYING file distributed along with the smile package for the 6 | # copyright and license terms. 7 | # 8 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 9 | 10 | 11 | from .keyboard import KeyPress 12 | from .state import Loop, If, Elif, Else, Subroutine 13 | from .state import UntilDone, Wait, ResetClock 14 | from .ref import Ref 15 | from .clock import clock 16 | from .video import Label 17 | 18 | # set the allowable keys (A-Z) 19 | asciiplus = [str(chr(i)) for i in range(65,65+26)] 20 | asciiplus += ['ENTER','BACKSPACE','SPACEBAR'] 21 | asciiplus += ['%d'%i for i in range(10)] 22 | 23 | @Subroutine 24 | def FreeKey(self, lbl, max_duration=10.0, max_resp=100, base_time=None): 25 | """ 26 | Perform free recall typed responses. 27 | 28 | Parameters 29 | ---------- 30 | lbl : Label state 31 | The text that will appear on the screen to indicate to the 32 | participant that they are to type a response. This text 33 | will disappear from the screen when a response is 34 | begun and return when ready for the next response. It's a 35 | good idea to use something like a label with '???????'. 36 | max_duration : {10.0, float} 37 | The amount of time in seconds that the participant is given 38 | to respond. 39 | max_resp : {100, int} 40 | Maximum number of responses that the participant is allowed 41 | to enter. 42 | base_time : float 43 | Manually set a time reference for the start of the state. This 44 | will be used to calculate reaction times. 45 | 46 | Example 47 | -------- 48 | FreeKey(Label('???????'), max_duration=15.0) 49 | The message '??????' will appear on the screen, 50 | and participants will be given 15 seconds to enter a response, 51 | replacing that text. They can enter as many responses as possible 52 | in the 15 second time period. 53 | 54 | Log Parameters 55 | --------------- 56 | All parameters above and below are available to be accessed and 57 | manipulated within the experiment code, and will be automatically 58 | recorded in the state.yaml and state.csv files. Refer to State class 59 | docstring for additional logged parameters. 60 | 61 | responses : list 62 | List of typed responses, each with the following information: 63 | first_key_time, first_key_rt, enter_key_time, enter_key_rt, 64 | response, response_num. 65 | The time is the actual time, the rt is the time since the 66 | base_time. 67 | """ 68 | # I'd like the lbl to be up until the below is done. How? 69 | # is it just that I would cancel it at the end here? 70 | #lbl = Label(text=txt, font_size=40) 71 | self.claim_child(lbl) 72 | with UntilDone(): 73 | 74 | # container for responses 75 | self.responses = [] 76 | 77 | # info for each response 78 | self.fk_num_resp = 0 79 | self.fk_first_key_time = 0 80 | self.fk_first_key_rt = 0 81 | self.fk_cur_resp = '' 82 | 83 | # save the starting text and base time 84 | self.fk_start_text = lbl.text 85 | 86 | # handle starting values 87 | self.max_duration = max_duration 88 | self.max_resp = max_resp 89 | 90 | # handle the base time 91 | with If(base_time): 92 | # use the passed value 93 | self.base_time = base_time 94 | with Else(): 95 | # make sure it's available 96 | #Debug(fk_on_screen=lbl.on_screen) 97 | Wait(until=lbl.on_screen) 98 | #Debug(fk_on_screen=lbl.on_screen) 99 | 100 | # use the label's appear time 101 | self.base_time = lbl.appear_time['time'] 102 | 103 | # reset timing to the desired base_time 104 | ResetClock(self.base_time) 105 | 106 | # collect responses for the desired max_duration or max_resp 107 | with Loop(): 108 | # accept a key response, time is based on label's ontime 109 | kp = KeyPress(keys=asciiplus, base_time=self.base_time) 110 | 111 | # process the key 112 | with If(kp.pressed == 'BACKSPACE'): 113 | # if there is text, remove a char 114 | with If(self.fk_cur_resp != ''): 115 | self.fk_cur_resp = self.fk_cur_resp[:-1] 116 | lbl.text = self.fk_cur_resp 117 | with Elif(kp.pressed == 'ENTER'): 118 | # if there is text, log as a response 119 | # increment the response counter 120 | self.fk_num_resp += 1 121 | 122 | # append the response to the list 123 | self.responses += [Ref(dict, 124 | response=self.fk_cur_resp, 125 | response_num=self.fk_num_resp, 126 | first_key_time=self.fk_first_key_time, 127 | first_key_rt=self.fk_first_key_rt, 128 | enter_key_time=kp.press_time, 129 | enter_key_rt=kp.rt)] 130 | 131 | # set starting text back and reset text 132 | self.fk_cur_resp = '' 133 | with If(self.fk_num_resp < self.max_resp): 134 | # gonna keep going 135 | lbl.text = self.fk_start_text 136 | with Else(): 137 | # new key, so append it 138 | # if it's first key, save the time 139 | with If(self.fk_cur_resp == ''): 140 | self.fk_first_key_rt = kp.rt 141 | self.fk_first_key_time = kp.press_time 142 | 143 | # append the text 144 | with If(kp.pressed == 'SPACEBAR'): 145 | # handle the space 146 | self.fk_cur_resp += ' ' 147 | with Else(): 148 | # just append the letter 149 | self.fk_cur_resp += kp.pressed 150 | 151 | # update the label 152 | lbl.text = self.fk_cur_resp 153 | 154 | with UntilDone(): 155 | Wait(max_duration, until=(self.fk_num_resp >= max_resp)) 156 | 157 | # ran out of time, see if there is an unfinished response 158 | with If(self.fk_cur_resp != ''): 159 | # there is something, so log it, too 160 | # increment the response counter 161 | self.fk_num_resp += 1 162 | 163 | # append the response to the list, but with no Enter key time 164 | self.responses += [Ref(dict, 165 | response=self.fk_cur_resp, 166 | response_num=self.fk_num_resp, 167 | first_key_time=self.fk_first_key_time, 168 | first_key_rt=self.fk_first_key_rt, 169 | enter_key_time=None, 170 | enter_key_rt=None)] 171 | -------------------------------------------------------------------------------- /smile/kivy_overrides.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import argparse 3 | import os 4 | from packaging import version 5 | 6 | if "kivy_overrides" not in sys.modules.keys() and \ 7 | any([name.startswith("kivy") for name in sys.modules.keys() if 8 | name != "kivy_overrides"]): 9 | raise ImportError("smile must be imported before kivy") 10 | 11 | # Prevent kivy from reading command line options... 12 | sys_argv = sys.argv[1:] 13 | sys.argv = sys.argv[:1] 14 | 15 | # perform the command line processing 16 | # set up the arg parser 17 | parser = argparse.ArgumentParser(description='Run a SMILE experiment.') 18 | parser.add_argument("-s", "--subject", 19 | help="unique subject id", 20 | default='test000') 21 | parser.add_argument("-e", "--experiment", 22 | help="experiment file", 23 | default='') 24 | parser.add_argument("-f", "--fullscreen", 25 | help="disable fullscreen", 26 | action='store_true') 27 | parser.add_argument("-r", "--resolution", 28 | help="screen / window resolution (e.g. '600x800')") 29 | parser.add_argument("-i", "--info", 30 | help="additional run info", 31 | default='') 32 | parser.add_argument("-c", "--csv", 33 | help="perform automatic conversion of SMILE logs to csv", 34 | action='store_true') 35 | parser.add_argument("-m", "--monitor", 36 | help="bring up the config screen first", 37 | action='store_true') 38 | # do the parsing 39 | #args = parser.parse_args(sys_argv) 40 | args, unknown = parser.parse_known_args(sys_argv) 41 | 42 | from kivy.utils import platform 43 | 44 | # set kivy config values 45 | from kivy.config import Config 46 | Config.set("kivy", "exit_on_escape", 0) 47 | if platform in ('win', 'linux', 'macosx'): 48 | Config.set("kivy", "desktop", 1) 49 | else: 50 | Config.set("kivy", "desktop", 0) 51 | 52 | # prevent throttling 53 | Config.set("graphics", "maxfps", 0) 54 | 55 | # preload fullscreen 56 | if args.fullscreen: 57 | # disable fullscreen 58 | Config.set("graphics", "borderless", 0) 59 | Config.set("graphics", "fullscreen", False) 60 | else: 61 | Config.set("graphics", "borderless", 1) 62 | Config.set("graphics", "fullscreen", "auto") 63 | 64 | # handle resolution 65 | if args.resolution: 66 | width, height = map(int, args.resolution.split("x")) 67 | Config.set("graphics", "width", width) 68 | Config.set("graphics", "height", height) 69 | 70 | # disable fullscreen to allow custom resolution 71 | Config.set("graphics", "borderless", 0) 72 | Config.set("graphics", "fullscreen", False) 73 | 74 | # prevent right-click multitouch with mouse 75 | Config.set("input", "mouse", "mouse,disable_multitouch") 76 | 77 | # hide the cursor (currently doesn't work in 1.9.0, but fixed in 1.9.1) 78 | Config.set('graphics', 'show_cursor', 0) 79 | 80 | # we don't want to be able to resize 81 | Config.set('graphics', 'resizable', 0) 82 | 83 | # ensure vsync 84 | Config.set('graphics', 'vsync', 1) 85 | 86 | density = Config.getdefault("SMILE", "DENSITY", "0.0") 87 | if density != "0.0": 88 | os.environ['KIVY_METRICS_DENSITY'] = density 89 | 90 | # handle supported kivy versions 91 | import kivy 92 | from kivy.logger import Logger 93 | 94 | KIVY_VERSION = kivy.__version__ 95 | MIN_VERSION = "1.8.0" 96 | MAX_VERSION = "2.3.0" 97 | kivy_ver = version.parse(KIVY_VERSION) 98 | min_ver = version.parse(MIN_VERSION) 99 | max_ver = version.parse(MAX_VERSION) 100 | if not (min_ver <= kivy_ver <= max_ver): 101 | Logger.warning(f"SMILE: Kivy Version {KIVY_VERSION} is outside " + 102 | f"the tested range ({MIN_VERSION} -- {MAX_VERSION}).") 103 | 104 | 105 | # provide custom event loop 106 | import kivy.base 107 | 108 | 109 | class SmileEventLoop(kivy.base.EventLoopBase): 110 | def __init__(self): 111 | super(SmileEventLoop, self).__init__() 112 | self._idle_callback = None 113 | 114 | def set_idle_callback(self, callback): 115 | self._idle_callback = callback 116 | 117 | def idle(self): 118 | if self._idle_callback: 119 | self._idle_callback(self) 120 | 121 | # don't loop if we don't have listeners ! 122 | if (len(self.event_listeners) == 0) and (self.status != 'closed'): 123 | kivy.base.Logger.error('SMILE: No event listeners have been created') 124 | kivy.base.Logger.error('SMILE: Application will leave') 125 | self.exit() 126 | return False 127 | 128 | return self.quit 129 | 130 | 131 | kivy.base.EventLoop = SmileEventLoop() 132 | 133 | Config.adddefaultsection("SMILE") 134 | 135 | 136 | def _get_config(): 137 | frame_rate = float(Config.getdefault("SMILE", "FRAMERATE", 60.)) 138 | locked = Config.getdefaultint("SMILE", "LOCKEDSUBJID", 0) 139 | font_name = Config.getdefault("SMILE", "FONTNAME", "Roboto") 140 | font_size = float(Config.getdefault("SMILE", "FONTSIZE", 45.)) 141 | fullscreen = Config.getdefault("SMILE", "FULLSCREEN", "auto") 142 | density = Config.getdefault("SMILE", "DENSITY", "1.0") 143 | if platform == "android" or platform == "ios": 144 | data_dir = Config.getdefault("SMILE", "DEFAULTDATADIR", 145 | "/sdcard/SMILE/data") 146 | else: 147 | data_dir = Config.getdefault("SMILE", "DEFAULTDATADIR", 148 | os.path.join(".", "data")) 149 | 150 | return_dict = {"fullscreen": fullscreen, "locked": locked, 151 | "density": density, 152 | "font_size": font_size, "font_name": font_name, 153 | "frame_rate": frame_rate, "default_data_dir": data_dir} 154 | 155 | return return_dict 156 | 157 | 158 | def _set_config(fullscreen=None, 159 | locked=None, 160 | framerate=None, 161 | fontname=None, 162 | fontsize=None, 163 | data_dir=None, 164 | density=None): 165 | if fullscreen is not None: 166 | Config.set("SMILE", "FULLSCREEN", fullscreen) 167 | if locked is not None: 168 | Config.set("SMILE", "LOCKEDSUBJID", locked) 169 | if framerate is not None: 170 | Config.set("SMILE", "FRAMERATE", float(framerate)) 171 | if fontname is not None: 172 | Config.set("SMILE", "FONTNAME", fontname) 173 | if fontsize is not None: 174 | Config.set("SMILE", "FONTSIZE", fontsize) 175 | if data_dir is not None: 176 | Config.set("SMILE", "DEFAULTDATADIR", data_dir) 177 | if density is not None: 178 | Config.set("SMILE", "DENSITY", density) 179 | Config.write() 180 | -------------------------------------------------------------------------------- /smile/lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compmem/smile/e85c2da827bb161c2ac7fc43d2a2cac20e536364/smile/lock.png -------------------------------------------------------------------------------- /smile/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compmem/smile/e85c2da827bb161c2ac7fc43d2a2cac20e536364/smile/logo.png -------------------------------------------------------------------------------- /smile/lsl.py: -------------------------------------------------------------------------------- 1 | #emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | #ex: set sts=4 ts=4 sw=4 et: 3 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 4 | # 5 | # See the COPYING file distributed along with the smile package for the 6 | # copyright and license terms. 7 | # 8 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 9 | 10 | from .state import CallbackState, Wait, Parallel, Loop 11 | from .video import Label 12 | from .clock import clock 13 | 14 | try: 15 | from pylsl import StreamInfo, StreamOutlet 16 | _got_pylsl = True 17 | except ImportError: 18 | print("Unable to import pylsl!") 19 | _got_pylsl = False 20 | 21 | 22 | _lsl_outlets = {} 23 | 24 | def init_lsl_outlet(server_name, server_type, nchans, 25 | suggested_freq, channel_format, unique_id="SMILE_LSL_OUT",): 26 | """Sends a Marker to a specified LSL. 27 | 28 | A *LSLPush* state will use a pre-initialized LSL outlet to send out a 29 | marker to the Lab Streaming Layer. 30 | 31 | Parameters 32 | ---------- 33 | server_name : string 34 | Name of the stream. Describes the device (or product series) that this 35 | stream makes available (for use by programs, experimenters or data 36 | analysts). 37 | server_type : string 38 | Content type of the stream. By convention LSL uses the content types 39 | defined in the XDF file format specification where applicable 40 | (https://github.com/sccn/xdf). The content type is the preferred way to 41 | find streams (as opposed to searching by name). This should be set to 42 | "Markers" when sending sync triggers. 43 | nchans : integer 44 | Number of channels per sample. This stays constant for the lifetime of 45 | the stream. This number should be 1 for sending a sync marker. 46 | suggested_freq : integer 47 | The sampling rate (in Hz) as advertised by the data source, regular 48 | (otherwise set to IRREGULAR_RATE). 49 | channel_format : string 50 | Format/type of each channel. If your channels have different formats, 51 | consider supplying multiple streams or use the largest type that can 52 | hold them all (such as cf_double64). It is also allowed to pass this as 53 | a string, without the cf_ prefix, e.g., 'float32' 54 | unique_id : string (defaut = "SMILE_LSL_OUT", optional) 55 | Unique identifier of the device or source of the data, if available 56 | (such as the serial number). This is critical for system robustness 57 | since it allows recipients to recover from failure even after the 58 | serving app, device or computer crashes (just by finding a stream with 59 | the same source id on the network again). Therefore, it is highly 60 | recommended to always try to provide whatever information can uniquely 61 | identify the data source itself. 62 | 63 | Returns a StreamOutlet class that is to be used in conjunction with the 64 | *LSLPush* state. 65 | 66 | """ 67 | if _got_pylsl: 68 | global _lsl_outlets 69 | 70 | s = "_" 71 | unique_identifier = s.join([server_name, server_type, str(nchans), 72 | str(suggested_freq), str(channel_format), 73 | str(unique_id)]) 74 | 75 | if unique_identifier in _lsl_outlets.keys(): 76 | return _lsl_outlets[unique_identifier] 77 | else: 78 | info = StreamInfo(server_name, server_type, nchans, suggested_freq, 79 | channel_format, unique_id) 80 | _lsl_outlets[unique_identifier] = StreamOutlet(info) 81 | return _lsl_outlets[unique_identifier] 82 | else: 83 | print("Unable to setup LSL server! No sync pulses will be made.") 84 | return None 85 | 86 | 87 | class LSLPush(CallbackState): 88 | """Sends a Marker to a specified LSL. 89 | 90 | A *LSLPush* state will use a pre-initialized LSL outlet to send out a 91 | marker to the Lab Streaming Layer. 92 | 93 | Parameters 94 | ---------- 95 | server : StreamOutlet 96 | The server that you would like to push data out to. This can easily be 97 | setup by running the *init_lsl_outlet* function defined in **lsl.py**. 98 | val : object 99 | The value of the marker you would like to send to the LSL. Can either 100 | be an object or a list of objects, depending on the nchans defined for 101 | the server. 102 | 103 | Logged Attributes 104 | ----------------- 105 | All parameters above and below are available to be accessed and 106 | manipulated within the experiment code, and will be automatically 107 | recorded in the state-specific log. Refer to State class 108 | docstring for additional logged parameters. 109 | 110 | push_time : Float 111 | The time in which the pulse has finished sending to the LSL. This value 112 | is in seconds and based on experiment time. Experiment time is the 113 | number of seconds since the start of the experiment. 114 | 115 | NOTE 116 | --------------------- 117 | It is highly recommended to send a TEST PULSE at the beginning of your 118 | experiment. Some systems use the first pulse as a signal to start listening 119 | to a particular server closely, so you need to send a pulse at the start of 120 | your experiment in order to *wake up* the LSL listener. 121 | 122 | """ 123 | 124 | 125 | def __init__(self, server, val, **kwargs): 126 | 127 | super(LSLPush, self).__init__(parent=kwargs.pop("parent", None), 128 | repeat_interval=kwargs.pop("repeat_interval", None), 129 | duration=kwargs.pop("duration", 0.0), 130 | save_log=kwargs.pop("save_log", True), 131 | name=kwargs.pop("name", None), 132 | blocking=kwargs.pop("blocking", True)) 133 | self._init_server = server 134 | self._init_push_val = val 135 | self._push_time = None 136 | 137 | self._log_attrs.extend(['push_val', 'push_time']) 138 | 139 | 140 | def _callback(self): 141 | if self._server is not None: 142 | if type(self._push_val) != list: 143 | self._server.push_sample([self._push_val]) 144 | else: 145 | self._server.push_sample(self._push_val) 146 | self._push_time = clock.now() 147 | -------------------------------------------------------------------------------- /smile/math_distract.py: -------------------------------------------------------------------------------- 1 | from .video import Label 2 | from .state import Subroutine, If, Else, UntilDone, Loop, Wait, Log 3 | from .audio import Beep 4 | from .keyboard import KeyPress 5 | from .ref import Ref 6 | from .scale import scale as s 7 | import random 8 | 9 | def _gen_questions(num_vars, max_num, min_num, max_probs, plus_and_minus, ans_mod, ans_prob, keys): 10 | #List Gen 11 | trials=[] 12 | for i in range(max_probs): 13 | factors=[] 14 | #generate the Factors of the Equation 15 | for b in range(num_vars): 16 | factors.append(random.randint(min_num, max_num+1)) 17 | opperators=[] 18 | random.shuffle(factors) 19 | total = factors[0] 20 | #Generate num_factors - 1 number of operators 21 | if plus_and_minus: 22 | for x in range(num_vars-1): 23 | if(random.randrange(2)==0): 24 | total = total + factors[x+1] 25 | opperators.append(' + ') 26 | else: 27 | total = total - factors[x+1] 28 | opperators.append(' - ') 29 | else: 30 | for x in range(num_vars-1): 31 | total = total + factors[x+1] 32 | opperators.append(' + ') 33 | 34 | 35 | last_prob = 0 36 | rand_per = random.random() 37 | for i in range(len(ans_prob)): 38 | if rand_per > last_prob and rand_per < last_prob+ans_prob[i]: 39 | new_total = total + ans_mod[i] 40 | last_prob += ans_prob[i] 41 | condition=True 42 | if new_total != total: 43 | condition = False 44 | 45 | 46 | # Build the Equation String 47 | temp = str(factors[0]) 48 | for y in range(num_vars-1): 49 | temp = temp + opperators[y] + str(factors[y+1]) 50 | temp = temp + ' = ' + str(new_total) 51 | # Append the list of trials with the currently 52 | # generated stimuli 53 | trials.append({ 54 | 'text':temp, 55 | 'condition':condition, 56 | 'correct_key':keys[str(condition)] 57 | }) 58 | random.shuffle(trials) 59 | return trials 60 | 61 | @Subroutine 62 | def MathDistract(self, 63 | num_vars=2, 64 | min_num=1, 65 | max_num=9, 66 | max_probs=50, 67 | duration=30, 68 | keys={'True':'F','False':'J'}, 69 | plus_and_minus=False, 70 | font_size=70, 71 | correct_beep_dur=.5, 72 | correct_beep_freq=400, 73 | correct_beep_rf=0.5, 74 | incorrect_beep_dur=.5, 75 | incorrect_beep_freq=200, 76 | incorrect_beep_rf=0.5, 77 | ans_mod=[0,1,-1,10,-10], 78 | ans_prob=[.5,.125,.125,.125,.125], 79 | visual_feedback=True): 80 | """ 81 | Math distractor for specified period of time. Logs to a subroutine_0.slog 82 | INPUT ARGS: 83 | duration - set this param for non-self-paced distractor; 84 | buzzer sounds when time's up; you get at least 85 | minDuration/problemTimeLimit problems. 86 | num_vars - Number of variables in the problem. 87 | max_num - Max possible number for each variable. 88 | min_num - Min possible number for each varialbe. 89 | max_probs - Max number of problems. 90 | plus_and_minus - True will have a chance to have both plus and minus. False will have only Plus 91 | min_duration - Minimum duration of distractor. 92 | text_size - Vertical height of the text. 93 | correct_beep_dur - Duration of correct beep. 94 | correct_beep_freq - Frequency of correct beep. 95 | correct_beep_rf - Rise/Fall of correct beep. 96 | incorrect_beep_dur - Duration of incorrect beep. 97 | incorrect_beep_freq - Frequency of incorrect beep. 98 | incorrect_beep_rf - Rise/Fall of incorrect beep 99 | keys - dictionary of keys for true/false problems. e.g., tfKeys = ('True':'T','False':'F') 100 | ans_mod - For True/False problems, the possible values to add to correct answer. 101 | ans_prob - The probability of each modifer on ansMod (must add to 1). 102 | visual_feedback - Whether to provide visual feedback to indicate correctness. 103 | 104 | """ 105 | 106 | self.responses = [] 107 | 108 | trials = Ref(_gen_questions,num_vars, max_num, min_num, max_probs, plus_and_minus, ans_mod, ans_prob, keys) 109 | with Loop(trials) as trial: 110 | Label(text='+', duration=1.0, font_size=s(font_size), color='white') 111 | curtext = Label(text=trial.current['text'], font_size=s(font_size), color='white') 112 | with UntilDone(): 113 | Wait(until=curtext.appear_time) 114 | kp = KeyPress(keys=(keys['True'],keys['False']), 115 | correct_resp=trial.current['correct_key'], 116 | base_time=curtext.appear_time['time']) 117 | # If visual_feedback is True, display a the words Correct! or Incorrect! in red or green 118 | # for the remainder of the trial duration. 119 | with If(visual_feedback): 120 | with If(kp.correct): 121 | Label(text='Correct!', center_x=self.exp.screen.center_x, center_y=self.exp.screen.center_y/2.0, 122 | font_size=s(font_size), duration=0.5, color='green') 123 | with Else(): 124 | Label(text='Incorrect!', center_x=self.exp.screen.center_x, center_y=self.exp.screen.center_y/2.0, 125 | font_size=s(font_size), duration=0.5, color='red') 126 | # If visual_feedback is False, queue a beep to be played, at the specified frequencies, 127 | # durations, and volumes depending on if they got it correct or incorrect. 128 | with Else(): 129 | with If(kp.correct): 130 | Beep(duration=correct_beep_dur, freq=correct_beep_freq, volume=correct_beep_rf) 131 | with Else(): 132 | Beep(duration=incorrect_beep_dur, freq=incorrect_beep_freq, volume=incorrect_beep_rf) 133 | Log(trial.current, 134 | name='math_distract', 135 | correct=kp.correct, 136 | resp=kp.pressed, 137 | rt=kp.rt, 138 | trial_appear=curtext.appear_time, 139 | press_time=kp.press_time) 140 | with UntilDone(): 141 | Wait(duration) 142 | -------------------------------------------------------------------------------- /smile/scale.py: -------------------------------------------------------------------------------- 1 | # emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | # ex: set sts=4 ts=4 sw=4 et: 3 | # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 4 | # 5 | # See the COPYING file distributed along with the smile package for the 6 | # copyright and license terms. 7 | # 8 | # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 9 | 10 | from .ref import Ref 11 | from kivy.metrics import dp 12 | 13 | 14 | class Scale(object): 15 | """ 16 | Utility to help scale depending on various screen sizes. 17 | 18 | By default, using **scale** will pass any number through kivy's built in dp 19 | function which converts your number into density pixels. If your display 20 | device has a density of 1, then the number will not change, but if your 21 | density is 2, your number will be doubled inorder to maintain the same 22 | size independent of density. The other thing this function can do is scale 23 | up and scale down to differening screen sizes. In order to tell smile to do 24 | this, you must set the scale_up and/or scale_down flags, and define a 25 | scale_box, when initializing your Experiment: 26 | 27 | :: 28 | 29 | exp = Experiment(scale_up=True, scale_down=True, scale_box=(600, 500)) 30 | 31 | This tells smile to expand or shrink all numbers passed through scale by a 32 | scaling factor that would allow the scale_box to fit within whatever 33 | monitor resolution you are using. 34 | 35 | Wrap all raw positional and size values with scale like so: 36 | 37 | :: 38 | 39 | scale(100) 40 | 41 | As an example: lets say you were wanting to write an experiment that would be 42 | run on a large monitor, 1600x900, but also a phone, 1920x1080, with a pixel 43 | density that was 4 times as much your monitor. Using just dp, the 600x600 44 | stimulus you would try to display would be 2400x2400 on the phone. That 45 | stimulus would be way to big to fit on a 1920x1080 screen, so you need to 46 | set scale_down to True, and set your scale box to be something appropriate. 47 | 48 | For this example, lets say our scale_box is 800x800 so that we have 200 49 | pixels of extra room around the stimulus. The limiting factor here is the 50 | height of the phone screen, so we calculate our scaling factor based on 51 | that. dp(800) would end up being 3200 density pixels. To calculate the 52 | scaling factor, we take the height of the screen and divide it by 53 | the dp'd height of the scale_box, which would be 0.3375. So now, after 54 | converting all of our numbers to density pixels, we multiply that by a 55 | scaling factor of 0.3375. 56 | 57 | :: 58 | 59 | 2400*0.3375 = 810 60 | 61 | Our stimulus would be 810x810 density pixels in size, and fit perfectly in 62 | the center of our phone device. But the end user doesn't have to worry about 63 | any of this calculations if they set the scale_box to 800x800, scale_down 64 | to True, and wrapped the height and width of the stimulus with scale as 65 | scale(600). 66 | 67 | """ 68 | def __init__(self): 69 | self._scale_factor = 1.0 70 | self._scale_factor_ref = Ref.getattr(self, "_scale_factor") 71 | self.scale_down = False 72 | self.scale_up = False 73 | self._scale_box = None 74 | 75 | def _set_scale_box(self, scale_box=None, 76 | scale_up=False, scale_down=False): 77 | self._scale_box = scale_box 78 | self.scale_up = scale_up 79 | self.scale_down = scale_down 80 | 81 | def _calc_scale_factor(self, width, height): 82 | # If they offered a scale box 83 | if self._scale_box is not None: 84 | # calc the scale factors 85 | width_scale_factor = width / dp(self._scale_box[0]) 86 | height_scale_factor = height / dp(self._scale_box[1]) 87 | 88 | if not self.scale_down: 89 | # don't let them scale down 90 | width_scale_factor = max(width_scale_factor, 1.0) 91 | height_scale_factor = max(height_scale_factor, 1.0) 92 | 93 | if not self.scale_up: 94 | # don't let them scale up 95 | width_scale_factor = min(width_scale_factor, 1.0) 96 | height_scale_factor = min(height_scale_factor, 1.0) 97 | 98 | self._scale_factor = min(width_scale_factor, 99 | height_scale_factor) 100 | else: 101 | self._scale_factor = 1.0 102 | 103 | def __call__(self, val): 104 | # Wrap the value in a ref. scale_factor is 1.0 by default, but we are 105 | # always trying to scale a dp value, not a raw pixel value. 106 | return Ref(dp, val) * self._scale_factor_ref 107 | 108 | 109 | # get global instance 110 | scale = Scale() 111 | -------------------------------------------------------------------------------- /smile/socket_interface.py: -------------------------------------------------------------------------------- 1 | #emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | #ex: set sts=4 ts=4 sw=4 et: 3 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 4 | # 5 | # See the COPYING file distributed along with the smile package for the 6 | # copyright and license terms. 7 | # 8 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 9 | 10 | from .state import CallbackState 11 | from .clock import clock 12 | 13 | import sys 14 | import socket 15 | 16 | _sockets = {} 17 | 18 | 19 | def init_socket_outlet(uniq_ID, server, port): 20 | """Set up a socket that allows for TCP messaging. 21 | 22 | A *SocketPush* state will use a pre-initialized Socket to send out a 23 | marker to the specific server and port. Once created, the socket will be 24 | added to a dictionary of sockets (_sockets) where the key is a string 25 | joined by an *_*. EX **markersocket_127.0.0.1_1234**. 26 | 27 | Parameters 28 | ---------- 29 | unique_id : string 30 | Unique identifier for this socket. Used only to differentiate this 31 | socket from other sockets initialized with this function. 32 | server : string 33 | Host name. For a local TCP process, use the string "localhost" 34 | port : int 35 | A port number for the server. 36 | 37 | Returns a Socket that is to be used in conjunction with the *SocketPush* 38 | state. Will return None if the connection could not be established. 39 | 40 | """ 41 | global _sockets 42 | 43 | unique_identifier = "_".join([uniq_ID, server, str(port)]) 44 | 45 | if unique_identifier in _sockets.keys(): 46 | return _sockets[unique_identifier] 47 | 48 | else: 49 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 50 | 51 | try: 52 | sock.connect((server, port)) 53 | _sockets[unique_identifier] = sock 54 | return _sockets[unique_identifier] 55 | 56 | except : 57 | sys.stdout.write("[ERROR ] [SOCKET ] Unable to establish a" + 58 | " connection for the socket at server" + 59 | " %s and the port %i\n" % (server, port)) 60 | return None 61 | 62 | 63 | class SocketPush(CallbackState): 64 | """Push a message to the provided socket. 65 | 66 | A state that uses a preinitialized Socket to send a message to a specific 67 | server and port. 68 | 69 | socket : socket 70 | Preinitialized socket class using *init_socket_outlet* 71 | msg : string 72 | A string message that is to be passed to the specific socket. 73 | 74 | Logged Attributes 75 | ----------------- 76 | All parameters above and below are available to be accessed and 77 | manipulated within the experiment code, and will be automatically 78 | recorded in the state-specific log. Refer to State class 79 | docstring for additional logged parameters. 80 | 81 | send_time : Float 82 | The time in which the message has finished being sent to the server. 83 | This value is in seconds and based on experiment time. Experiment time 84 | is the number of seconds since the start of the experiment. 85 | msg : string 86 | The message sent to the server. 87 | server_name : string 88 | The server the message is being sent to. 89 | port : int 90 | The port the message is being sent to in the server. 91 | rtn : int 92 | Number of bytes sent by this SocketPush. Check this value to make sure 93 | your entire message was sent. 94 | 95 | """ 96 | 97 | def __init__(self, socket, msg, **kwargs): 98 | super(SocketPush, self).__init__(parent=kwargs.pop("parent", None), 99 | repeat_interval=kwargs.pop("repeat_interval", None), 100 | duration=kwargs.pop("duration", 0.0), 101 | save_log=kwargs.pop("save_log", True), 102 | name=kwargs.pop("name", None), 103 | blocking=kwargs.pop("blocking", True)) 104 | self._init_socket = socket 105 | self._init_msg = msg 106 | self._rtn = None 107 | self._send_time = None 108 | 109 | self._log_attrs.extend(['rtn', 'msg', 'send_time', "port", "server"]) 110 | 111 | def _enter(self): 112 | if self._socket is not None: 113 | self._server = self._socket.getsockname()[0] 114 | self._port = self._socket.getsockname()[1] 115 | else: 116 | self._server = None 117 | self._port = None 118 | 119 | def _callback(self): 120 | if self._socket is not None: 121 | self._rtn = self._socket.send(self._msg) 122 | self._send_time = clock.now() 123 | -------------------------------------------------------------------------------- /smile/test_sound.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compmem/smile/e85c2da827bb161c2ac7fc43d2a2cac20e536364/smile/test_sound.wav -------------------------------------------------------------------------------- /smile/test_video.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compmem/smile/e85c2da827bb161c2ac7fc43d2a2cac20e536364/smile/test_video.mp4 -------------------------------------------------------------------------------- /smile/unlock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compmem/smile/e85c2da827bb161c2ac7fc43d2a2cac20e536364/smile/unlock.png -------------------------------------------------------------------------------- /smile/utils.py: -------------------------------------------------------------------------------- 1 | #emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 2 | #ex: set sts=4 ts=4 sw=4 et: 3 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 4 | # 5 | # See the COPYING file distributed along with the smile package for the 6 | # copyright and license terms. 7 | # 8 | ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## 9 | 10 | import math 11 | import wave 12 | import struct 13 | import random 14 | from itertools import * 15 | from io import StringIO 16 | 17 | def rindex(lst, item): 18 | try: 19 | return dropwhile(lambda x: lst[x] != item, reversed(range(len(lst)))).next() 20 | except StopIteration: 21 | raise ValueError("rindex(lst, item): item not in list") 22 | 23 | def get_class_name(obj): 24 | name = str(obj.__class__)[8:-2].split('.')[-1] 25 | mem_id = str(id(obj)) 26 | uname = name + "_" + mem_id 27 | return name, uname 28 | 29 | 30 | # 31 | # some audio utils adapted from: 32 | # http://zacharydenton.com/generate-audio-with-python/ 33 | # 34 | 35 | def grouper(n, iterable, fillvalue=None): 36 | "grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx" 37 | args = [iter(iterable)] * n 38 | return izip_longest(fillvalue=fillvalue, *args) 39 | 40 | def sine_wave(frequency=440.0, framerate=44100, amplitude=0.5, 41 | skip_frame=0): 42 | ''' 43 | Generate a sine wave at a given frequency of infinite length. 44 | ''' 45 | if amplitude > 1.0: amplitude = 1.0 46 | if amplitude < 0.0: amplitude = 0.0 47 | for i in count(skip_frame): 48 | sine = math.sin(2.0 * math.pi * float(frequency) * 49 | (float(i) / float(framerate))) 50 | yield float(amplitude) * sine 51 | 52 | def square_wave(frequency=440.0, framerate=44100, amplitude=0.5): 53 | for s in sine_wave(frequency, framerate, amplitude): 54 | if s > 0: 55 | yield amplitude 56 | elif s < 0: 57 | yield -amplitude 58 | else: 59 | yield 0.0 60 | 61 | def damped_wave(frequency=440.0, framerate=44100, amplitude=0.5, length=44100): 62 | if amplitude > 1.0: amplitude = 1.0 63 | if amplitude < 0.0: amplitude = 0.0 64 | return (math.exp(-(float(i%length)/float(framerate))) * s 65 | for i, s in enumerate(sine_wave(frequency, framerate, amplitude))) 66 | 67 | def white_noise(amplitude=0.5): 68 | ''' 69 | Generate random samples. 70 | ''' 71 | return (float(amplitude) * random.uniform(-1, 1) for i in count(0)) 72 | 73 | def compute_samples(channels, nsamples=None): 74 | ''' 75 | create a generator which computes the samples. 76 | 77 | essentially it creates a sequence of the sum of each function in the channel 78 | at each sample in the file for each channel. 79 | ''' 80 | return islice(izip(*(imap(sum, izip(*channel)) for channel in channels)), nsamples) 81 | 82 | def write_wavefile(f, samples, nframes=None, nchannels=2, 83 | sampwidth=2, framerate=44100, bufsize=2048): 84 | "Write samples to a wavefile." 85 | 86 | # next line breaks it, leaving at None 87 | #if nframes is None: 88 | # nframes = -1 89 | 90 | w = wave.open(f, 'w') 91 | w.setparams((nchannels, sampwidth, framerate, nframes, 'NONE', 'not compressed')) 92 | 93 | max_amplitude = float(int((2 ** (sampwidth * 8)) / 2) - 1) 94 | 95 | # split the samples into chunks (to reduce memory consumption and improve performance) 96 | for chunk in grouper(bufsize, samples): 97 | frames = ''.join(''.join(struct.pack('h', int(max_amplitude * sample)) 98 | for sample in channels) for channels in chunk 99 | if channels is not None) 100 | w.writeframesraw(frames) 101 | 102 | w.close() 103 | 104 | def write_wavebuf(samples, nframes=None, nchannels=2, 105 | sampwidth=2, framerate=44100, bufsize=2048): 106 | """Write samples to a wave buffer.""" 107 | buf = StringIO() 108 | write_wavefile(buf, samples) 109 | buf.seek(0) 110 | return buf 111 | 112 | -------------------------------------------------------------------------------- /smile/version.py: -------------------------------------------------------------------------------- 1 | __version__ = "" 2 | __author__ = "" 3 | __email__ = "" 4 | __date__ = "" 5 | -------------------------------------------------------------------------------- /tests/datadirtest/test2/nothing_file.txt: -------------------------------------------------------------------------------- 1 | nothinghere 2 | -------------------------------------------------------------------------------- /tests/test_audio.py: -------------------------------------------------------------------------------- 1 | from smile.experiment import Experiment 2 | from smile.state import Parallel, Wait, Serial, Meanwhile, Loop, Debug 3 | from smile.audio import Beep, SoundFile, init_audio_server 4 | import os 5 | 6 | init_audio_server(sr=44100, nchnls=1, buffersize=256, duplex=1, 7 | audio='portaudio', jackname='pyo', 8 | input_device=None, output_device=None) 9 | 10 | exp = Experiment(debug=True) 11 | 12 | Wait(1.0) 13 | with Loop([440, 500, 600, 880]) as l: 14 | Beep(freq=l.current, volume=0.4, duration=1.0) 15 | Debug(freq=l.current) 16 | Beep(freq=[440, 500, 600], volume=0.1, duration=1.0) 17 | Debug(freq=[440, 500, 600]) 18 | 19 | Beep(freq=880, volume=0.1, duration=1.0) 20 | Debug(freq=880) 21 | 22 | with Parallel(): 23 | Beep(freq=440, volume=0.1, duration=2.0) 24 | Debug(freq=440) 25 | 26 | with Serial(): 27 | Wait(1.0) 28 | Beep(freq=880, volume=0.1, duration=2.0) 29 | Debug(freq=880) 30 | 31 | Wait(1.0) 32 | with Meanwhile(): 33 | Beep(freq=500, volume=0.1) 34 | Debug(freq=500) 35 | 36 | Beep(freq=900, volume=0.1, duration=1.0) 37 | Debug(freq=900) 38 | 39 | SoundFile(os.path.join("..", "smile", "test_sound.wav")) 40 | Debug(freq="TEST") 41 | 42 | SoundFile(os.path.join("..", "smile", "test_sound.wav"), stop=1.0) 43 | Debug(freq="TEST stop early") 44 | Wait(1.0) 45 | SoundFile(os.path.join("..", "smile", "test_sound.wav"), 46 | loop=True, duration=3.0) 47 | Debug(freq="TEST loop, duration=3") 48 | Wait(1.0) 49 | SoundFile(os.path.join("..", "smile", "test_sound.wav"), start=0.5) 50 | Debug(freq="TEST Start later in") 51 | Wait(1.0) 52 | 53 | exp.run() 54 | -------------------------------------------------------------------------------- /tests/test_clean_attrs.py: -------------------------------------------------------------------------------- 1 | from smile.common import Experiment, Wait, Label, Image, Debug, Ref 2 | from os.path import join 3 | import sys 4 | 5 | fp = join('C:\\Users\\caboodle513\\code\\beegica\\smile\\smile', 'face-smile.png') 6 | 7 | exp = Experiment() 8 | Debug(fp=fp, clean_fp=Ref(exp.clean_path, fp)) 9 | Image(source=fp, duration=1.0) 10 | exp.run() 11 | -------------------------------------------------------------------------------- /tests/test_crashlogger.py: -------------------------------------------------------------------------------- 1 | from smile.common import Experiment, Func 2 | from smile.startup import InputSubject 3 | 4 | 5 | def do_it(): 6 | 1/0 7 | 8 | exp = Experiment(local_crashlog=True, cmd_traceback=False) 9 | InputSubject() 10 | Func(do_it) 11 | 12 | exp.run() 13 | -------------------------------------------------------------------------------- /tests/test_datadir.py: -------------------------------------------------------------------------------- 1 | from smile.experiment import Experiment 2 | from smile.state import Wait 3 | from smile.video import Image 4 | from smile.startup import InputSubject 5 | 6 | import os 7 | 8 | 9 | exp = Experiment(background_color="#330000", data_dir='datadirtest', 10 | name='DIRTEST') 11 | Wait(2.0) 12 | Image(source=os.path.join("..", "smile", "face-smile.png"), duration=1.0) 13 | Wait(1.0) 14 | exp.run() 15 | 16 | 17 | 18 | # WD test. 19 | #resource_add_path('datadirtest') 20 | exp = Experiment(background_color="#330000", 21 | data_dir=os.path.join('datadirtest', 22 | 'test2'), 23 | name='DIRTEST2') 24 | InputSubject() 25 | Wait(2.0) 26 | Image(source=os.path.join("..", "smile", "face-smile.png"), duration=1.0) 27 | Wait(1.0) 28 | exp.run() 29 | -------------------------------------------------------------------------------- /tests/test_dotbox.py: -------------------------------------------------------------------------------- 1 | from smile.experiment import Experiment 2 | from smile.state import Wait, UntilDone, Serial, Parallel 3 | from smile.dotbot import DotBox 4 | 5 | 6 | exp = Experiment(background_color="#330000") 7 | 8 | Wait(1.0) 9 | 10 | DotBox(duration=2.0, backcolor='blue') 11 | 12 | db = DotBox(color='red') 13 | with UntilDone(): 14 | db.slide(center=exp.screen.right_top, duration=2.0) 15 | db.slide(center=exp.screen.left_top, duration=2.0) 16 | db.slide(center=exp.screen.center, duration=1.0) 17 | 18 | db2 = DotBox(color='red', backcolor=(.1, .1, .1, .5)) 19 | with UntilDone(): 20 | with Parallel(): 21 | with Serial(): 22 | db2.slide(color='blue', duration=1.0) 23 | db2.slide(color='olive', duration=1.0) 24 | db2.slide(color='orange', duration=1.0) 25 | db2.slide(pointsize=20, duration=1.0) 26 | db2.slide(size=(400, 400), duration=4.0) 27 | 28 | db3 = DotBox(color='green', backcolor='purple', size=(400, 400)) 29 | with UntilDone(): 30 | db3.slide(num_dots=50, duration=3.0) 31 | 32 | Wait(2.0) 33 | exp.run(trace=False) 34 | -------------------------------------------------------------------------------- /tests/test_freekey.py: -------------------------------------------------------------------------------- 1 | from smile.experiment import Experiment 2 | from smile.state import Wait, Debug 3 | from smile.video import Label 4 | from smile.freekey import FreeKey 5 | 6 | exp = Experiment() 7 | 8 | Wait(.5) 9 | 10 | fk = FreeKey(Label(text='XXXXXX', font_size=40), max_resp=1) 11 | Debug(responses=fk.responses) 12 | 13 | Label(text='Done', font_size=32, duration=2.0) 14 | 15 | fk2 = FreeKey(Label(text='??????', font_size=30)) 16 | Debug(responses=fk2.responses) 17 | 18 | Wait(1.0) 19 | 20 | exp.run() 21 | -------------------------------------------------------------------------------- /tests/test_func.py: -------------------------------------------------------------------------------- 1 | from smile.experiment import Experiment 2 | from smile.state import Func 3 | 4 | exp = Experiment() 5 | 6 | Func(print, "Printing..........Success!") 7 | 8 | exp.run() 9 | -------------------------------------------------------------------------------- /tests/test_grating.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | from smile.experiment import Experiment 5 | from smile.state import UntilDone, Wait, Parallel, Serial 6 | from smile.keyboard import KeyPress 7 | from smile.video import Label 8 | from smile.grating import Grating 9 | import math 10 | 11 | exp = Experiment(background_color="#4F33FF") 12 | 13 | g = Grating(width=250, height=250, contrast=0.1) 14 | with UntilDone(): 15 | KeyPress() 16 | g.update(bottom=exp.screen.center) 17 | KeyPress() 18 | 19 | g = Grating(width=500, height=500, envelope='Gaussian', frequency=75, 20 | phase=11.0, color_one='blue', color_two='red', contrast=0.25) 21 | with UntilDone(): 22 | KeyPress() 23 | g.update(bottom=exp.screen.center) 24 | KeyPress() 25 | 26 | with Parallel(): 27 | g = Grating(width=256, height=256, frequency=20, 28 | envelope='Circular', std_dev=7.5, 29 | contrast=0.75, 30 | color_one='green', color_two='orange') 31 | lbl = Label(text='Grating!', bottom=g.top) 32 | with UntilDone(): 33 | # kp = KeyPress() 34 | with Parallel(): 35 | g.slide(phase=-8 * math.pi, frequency=10., 36 | bottom=exp.screen.bottom, 37 | duration=6.) 38 | g.slide(rotate=90, duration=2.0) 39 | with Serial(): 40 | Wait(2.0) 41 | lbl.slide(top=g.bottom, duration=4.) 42 | 43 | with Parallel(): 44 | g = Grating(width=1000, height=1000, frequency=10, envelope='Linear', 45 | std_dev=20, contrast=0.4, 46 | color_one='blue', color_two='red') 47 | lbl = Label(text='Grating!', bottom=g.top) 48 | with UntilDone(): 49 | kp = KeyPress() 50 | with Parallel(): 51 | g.slide(phase=-8 * math.pi, frequency=10., 52 | left=exp.screen.left, 53 | duration=6.) 54 | g.slide(rotate=90, duration=2.0) 55 | with Serial(): 56 | Wait(2.0) 57 | lbl.slide(top=g.bottom, duration=4.) 58 | 59 | exp.run(trace=False) 60 | -------------------------------------------------------------------------------- /tests/test_keyboard.py: -------------------------------------------------------------------------------- 1 | from smile.experiment import Experiment 2 | from smile.state import Wait, Debug, Loop, UntilDone, Log, Meanwhile 3 | from smile.keyboard import Key, KeyRecord, KeyPress 4 | 5 | 6 | exp = Experiment() 7 | 8 | with Meanwhile(): 9 | KeyRecord(name="record_all_key_presses") 10 | 11 | Debug(name='Press T+G+D or SHIFT+Q+R') 12 | Wait(until=((Key("T") & Key("G") & Key("D")) | 13 | (Key("SHIFT") & Key("Q") & Key("R")))) 14 | Debug(name='Key Press Test') 15 | 16 | exp.last_pressed = '' 17 | 18 | with Loop(conditional=(exp.last_pressed != 'K')): 19 | kp = KeyPress(keys=['J', 'K'], correct_resp='K') 20 | Debug(pressed=kp.pressed, rt=kp.rt, correct=kp.correct) 21 | exp.last_pressed = kp.pressed 22 | Log(pressed=kp.pressed, rt=kp.rt) 23 | 24 | KeyRecord() 25 | with UntilDone(): 26 | kp = KeyPress(keys=['J', 'K'], correct_resp='K') 27 | Debug(pressed=kp.pressed, rt=kp.rt, correct=kp.correct) 28 | Wait(1.0) 29 | 30 | kp = KeyPress() 31 | Debug(pressed=kp.pressed, rt=kp.rt, correct=kp.correct) 32 | Wait(1.0) 33 | 34 | kp = KeyPress(duration=2.0) 35 | Debug(pressed=kp.pressed, rt=kp.rt, correct=kp.correct) 36 | Wait(1.0) 37 | 38 | exp.run() 39 | -------------------------------------------------------------------------------- /tests/test_math_distract.py: -------------------------------------------------------------------------------- 1 | from smile.common import * 2 | from smile.math_distract import MathDistract 3 | 4 | 5 | 6 | 7 | exp = Experiment() 8 | 9 | Wait(1.0) 10 | MathDistract() 11 | Label(text="You are done!", duration=2.0) 12 | 13 | exp.run() 14 | -------------------------------------------------------------------------------- /tests/test_mouse.py: -------------------------------------------------------------------------------- 1 | from smile.experiment import Experiment 2 | from smile.state import Wait, Debug, Loop, Log, Parallel, UntilDone 3 | 4 | from smile.mouse import MouseRecord, MouseCursor, MousePress, MouseWithin 5 | from smile.video import Label 6 | 7 | 8 | def print_dt(state, *args): 9 | print(args) 10 | 11 | 12 | exp = Experiment(show_splash=True) 13 | 14 | 15 | with Parallel(): 16 | MouseRecord() 17 | MouseCursor() 18 | with UntilDone(): 19 | Wait(2.0) 20 | MouseCursor("smile/face-smile.png", (125, 125), duration=5.0) 21 | 22 | with Parallel(): 23 | lbl1 = Label(text="Click me", blocking=False) 24 | with Loop(10): 25 | Debug(mw=MouseWithin(lbl1)) 26 | Wait(1.0) 27 | 28 | Debug(name='Mouse Press Test') 29 | 30 | exp.last_pressed = '' 31 | 32 | with Loop(conditional=(exp.last_pressed != 'RIGHT')): 33 | kp = MousePress(buttons=['LEFT', 'RIGHT'], correct_resp='RIGHT') 34 | Debug(pressed=kp.pressed, rt=kp.rt, correct=kp.correct) 35 | exp.last_pressed = kp.pressed 36 | Log(pressed=kp.pressed, rt=kp.rt) 37 | 38 | kp = MousePress(buttons=['LEFT', 'RIGHT'], correct_resp='RIGHT') 39 | Debug(pressed=kp.pressed, rt=kp.rt, correct=kp.correct) 40 | Wait(1.0) 41 | 42 | kp = MousePress() 43 | Debug(pressed=kp.pressed, rt=kp.rt, correct=kp.correct) 44 | Wait(1.0) 45 | 46 | kp = MousePress(duration=2.0) 47 | Debug(pressed=kp.pressed, rt=kp.rt, correct=kp.correct) 48 | Wait(1.0) 49 | 50 | exp.run() 51 | -------------------------------------------------------------------------------- /tests/test_moving_dot.py: -------------------------------------------------------------------------------- 1 | from smile.experiment import Experiment 2 | from smile.state import UntilDone, Meanwhile, Wait, Loop, Debug 3 | from smile.keyboard import KeyPress 4 | from smile.moving_dots import MovingDots 5 | 6 | # A testing set to show all the different things you can change when 7 | # setting motion_props during run-time. 8 | testing_set = [[{"coherence": 0.1, "direction": 0, 9 | "direction_variance": 0, "speed": 400}, 10 | {"coherence": 0.5, "direction": 180, 11 | "direction_variance": 0, "speed": 50}], 12 | [{"coherence": 0.5, "direction": 0, 13 | "direction_variance": 0, "speed": 50, 14 | "lifespan": .6, "lifespan_variance": .5}, 15 | {"coherence": 0.1, "direction": 180, 16 | "direction_variance": 10, "speed": 400, 17 | "speed_variance": 200}]] 18 | exp = Experiment(background_color=("purple", .3)) 19 | 20 | Wait(.5) 21 | 22 | g = MovingDots(radius=300, 23 | scale=10, 24 | num_dots=4, 25 | motion_props=[{"coherence": 0.25, "direction": 0, 26 | "direction_variance": 0}, 27 | {"coherence": 0.25, "direction": 90, 28 | "direction_variance": 0}, 29 | {"coherence": 0.25, "direction": 180, 30 | "direction_variance": 0}, 31 | {"coherence": 0.25, "direction": 270, 32 | "direction_variance": 0}]) 33 | with UntilDone(): 34 | KeyPress() 35 | with Meanwhile(): 36 | g.slide(color='red', duration=4.0) 37 | Wait(.25) 38 | 39 | with Loop(4): 40 | MovingDots() 41 | with UntilDone(): 42 | KeyPress() 43 | 44 | md = MovingDots(radius=300, scale=3, num_dots=200, 45 | motion_props=[{"coherence": 0.25, "direction": 0, 46 | "direction_variance": 0}, 47 | {"coherence": 0.25, "direction": 180, 48 | "direction_variance": 0},]) 49 | with UntilDone(): 50 | with Loop(testing_set) as ts: 51 | Wait(3.) 52 | md.motion_props = ts.current 53 | Wait(5.) 54 | 55 | Debug(rate=g.widget.refresh_rate) 56 | exp.run(trace=False) 57 | -------------------------------------------------------------------------------- /tests/test_niusb_interface.py: -------------------------------------------------------------------------------- 1 | 2 | from smile.experiment import Experiment 3 | from smile.state import Wait, Parallel, Loop, Debug 4 | from smile.video import Label 5 | from smile.niusb_interface import NIPulse, init_task 6 | 7 | exp = Experiment() 8 | 9 | # Initialize the outlet 10 | task1 = init_task(task_name="Task1", min_val=0.0, max_val=1.0, 11 | chan_path='Dev1/ao0', chan_des="mychan1") 12 | task2 = init_task(task_name="Task2", min_val=0.0, max_val=1.0, 13 | chan_path='Dev1/ao1', chan_des="mychan2") 14 | 15 | NIPulse(task1, push_vals=[1.0], width=0.10,) 16 | NIPulse(task2, push_vals=[.5], width=0.10,) 17 | 18 | # Wait for the experiment to start! 19 | Wait(2.) 20 | 21 | with Parallel(): 22 | 23 | Label(text="We will now push 10 markers.", blocking=False) 24 | with Loop(10, blocking=False): 25 | 26 | ni1 = NIPulse(task1, push_vals=[1.0], width=0.10,) 27 | Wait(1.0) 28 | ni2 = NIPulse(task2, push_vals=[.5], width=0.10,) 29 | 30 | Wait(until=ni2.pulse_off) 31 | Wait(1.) 32 | Debug(a=ni1.pulse_on, b=ni2.pulse_off) 33 | 34 | exp.run() 35 | -------------------------------------------------------------------------------- /tests/test_pulse.py: -------------------------------------------------------------------------------- 1 | 2 | from smile.experiment import Experiment 3 | from smile.state import Debug, Wait, Log, Loop 4 | from smile.pulse import Pulse 5 | 6 | # set up default experiment 7 | exp = Experiment() 8 | 9 | with Loop(15): 10 | pulse = Pulse(code='S1') 11 | Wait(duration=1.0, jitter=1.0) 12 | Log(name='pulse', 13 | pulse_on=pulse.pulse_on, 14 | pulse_code=pulse.code, 15 | pulse_off=pulse.pulse_off) 16 | 17 | # print something 18 | Debug(width=exp.screen.width, height=exp.screen.height) 19 | 20 | # run the exp 21 | exp.run(trace=False) 22 | -------------------------------------------------------------------------------- /tests/test_ref.py: -------------------------------------------------------------------------------- 1 | from smile.ref import Ref, val, shuffle 2 | import math 3 | x = [0.0] 4 | r = Ref(math.cos, Ref.getitem(x, 0)) 5 | a = Ref.object(7.) 6 | print(7//3, val(a // 3.), 7/3, val(a / 3.)) 7 | print(x[0], val(r)) 8 | x[0] += .5 9 | print(x[0], val(r)) 10 | 11 | r = Ref.object(str)(Ref.object(x)[0]) 12 | x[0] += .75 13 | print(x[0], val(r)) 14 | 15 | r = (Ref.object(x)[0] > 0) & (Ref.object(x)[0] < 0) 16 | print(x[0], val(r)) 17 | r = (Ref.object(x)[0] > 0) & (Ref.object(x)[0] >= 0) 18 | x[0] -= 10.0 19 | print(x[0], val(r)) 20 | 21 | y = [7] 22 | ry = Ref.object(y) 23 | print(y[0], val(ry[0] % 2), val(2 % ry[0])) 24 | y[0] = 8 25 | print(y[0], val(ry[0] % 2), val(2 % ry[0])) 26 | 27 | class Jubba(object): 28 | def __init__(self, val): 29 | self.x = val 30 | 31 | def __getitem__(self, index): 32 | return Ref.getattr(self, index) 33 | 34 | a = Jubba(5) 35 | b = Ref.getattr(a, 'x') 36 | br = Ref.object(a).x 37 | print(val(b), val(br)) 38 | a.x += 42.0 39 | print(val(b), val(br)) 40 | 41 | c = {'y': 6} 42 | d = Ref.getitem(c, 'y') 43 | 44 | e = [4, 3, 2] 45 | f = Ref.getitem(e, 2) 46 | 47 | g = b+d+f 48 | print(val(g)) 49 | 50 | a.x = 6 51 | print(val(g)) 52 | 53 | c['y'] = 7 54 | print(val(g)) 55 | 56 | e[2] = 3 57 | print(val(g)) 58 | 59 | x = Jubba([]) 60 | y = Ref.getitem(x, 'x') 61 | print(val(y)) 62 | y = y + [b] 63 | print(val(y)) 64 | y = y + [d] 65 | y = y + [f] 66 | print(val(y)) 67 | -------------------------------------------------------------------------------- /tests/test_socket_interface.py: -------------------------------------------------------------------------------- 1 | from smile.experiment import Experiment 2 | from smile.state import Wait, Parallel, Loop 3 | from smile.video import Label 4 | from smile.socket_interface import init_socket_outlet, SocketPush 5 | # Initialize the outlet 6 | OUTLET = init_socket_outlet(uniq_ID='SocketMarket', server='localhost', 7 | port=1234) 8 | 9 | exp = Experiment() 10 | 11 | # Signal the beginning of the experiment. 12 | SocketPush(socket=OUTLET, msg="300") 13 | 14 | # Wait for the experiment to start! 15 | Wait(2.) 16 | 17 | with Parallel(): 18 | 19 | Label(text="We will now push 10 markers.", blocking=False) 20 | with Loop(10, blocking=False): 21 | 22 | # Create the push state 23 | push_out = SocketPush(socket=OUTLET, msg="55") 24 | 25 | # Log send_time if you want easy access 26 | # Log(name="MARKERS", 27 | # push_time=push_out.send_time) 28 | 29 | Wait(1.) 30 | 31 | exp.run() 32 | -------------------------------------------------------------------------------- /tests/test_state.py: -------------------------------------------------------------------------------- 1 | from smile.experiment import Experiment 2 | from smile.state import Subroutine, Debug, Meanwhile, Loop, UntilDone, Wait, \ 3 | Func, Serial, Parallel, While, If, Elif, Else, Log, \ 4 | PrintTraceback, _DelayedValueTest, Done, When, \ 5 | Record 6 | from smile.ref import val 7 | from smile.audio import * 8 | 9 | 10 | def print_actual_duration(target): 11 | print(val(target.end_time - target.start_time)) 12 | 13 | 14 | def print_periodic(): 15 | print("PERIODIC!") 16 | 17 | 18 | @Subroutine 19 | def DoTheThing(self, a, b, c=7, d="ssdfsd"): 20 | PrintTraceback(name="inside DoTheThing") 21 | self.foo = c * 2 22 | Wait(1.0) 23 | Debug(a=a, b=b, c=c, d=d, foo=self.foo, 24 | screen_size=self.exp.screen.size, name="inside DoTheThing") 25 | 26 | 27 | @Subroutine 28 | def DoTheOtherThing(self): 29 | Debug(name="before the yield") 30 | with Serial(): 31 | yield 32 | with Meanwhile(): 33 | PrintTraceback(name="during the yield") 34 | Debug(name="after the yield") 35 | 36 | 37 | exp = Experiment(debug=True) 38 | 39 | Debug(width=exp.screen.width, height=exp.screen.height) 40 | 41 | with Loop(5) as loop: 42 | Log(a=1, b=2, c=loop.i, name="aaa") 43 | Log({"q": loop.i, "w": loop.i}, x=4, y=2, z=1, name="bbb") 44 | Log([{"q": loop.i, "w": n} for n in range(5)], x=4, y=2, z=1, name="ccc") 45 | 46 | exp.for_the_thing = 3 47 | dtt = DoTheThing(3, 4, name="first") 48 | Debug(foo=dtt.foo, name="outside DoTheThing") 49 | dtt = DoTheThing(3, 4, d="bbbbbbb", c=exp.for_the_thing) 50 | Debug(foo=dtt.foo, name="outside DoTheThing") 51 | 52 | with DoTheOtherThing(): 53 | PrintTraceback(name="top of body") 54 | Wait(2.0) 55 | PrintTraceback(name="bottom of body") 56 | 57 | Wait(1.0) 58 | dvt = _DelayedValueTest(1.0, 42) 59 | Done(dvt) 60 | Debug(dvt_out=dvt.value_out) 61 | 62 | exp.bar = False 63 | with Parallel(): 64 | with Serial(): 65 | Wait(2.0) 66 | # force variable assignment 67 | # to wait until correct time 68 | Func(lambda: None) 69 | exp.bar = True 70 | Wait(2.0) 71 | # force variable assignment to wait until correct time 72 | Func(lambda: None) 73 | exp.bar = False 74 | Wait(1.0) 75 | When(exp.bar, Debug(name="when test")) 76 | with While(exp.bar): 77 | with Loop(): 78 | Wait(0.2) 79 | Debug(name="while test") 80 | with Loop(blocking=False): 81 | Wait(0.5) 82 | Debug(name="non-blocking test") 83 | 84 | exp.foo = 1 85 | Record(foo=exp.foo) 86 | with UntilDone(): 87 | Debug(name="FOO!") 88 | Wait(1.0) 89 | Debug(name="FOO!") 90 | exp.foo = 2 91 | Debug(name="FOO!") 92 | Wait(1.0) 93 | Debug(name="FOO!") 94 | exp.foo = 3 95 | Debug(name="FOO!") 96 | Wait(1.0) 97 | Debug(name="FOO!") 98 | 99 | with Parallel(): 100 | with Serial(): 101 | Debug(name="FOO!") 102 | Wait(1.0) 103 | Debug(name="FOO!") 104 | exp.foo = 4 105 | Debug(name="FOO!") 106 | Wait(1.0) 107 | Debug(name="FOO!") 108 | exp.foo = 5 109 | Debug(name="FOO!") 110 | Wait(1.0) 111 | Debug(name="FOO!") 112 | with Serial(): 113 | Debug(name="FOO!!!") 114 | Wait(until=exp.foo == 5, name="wait until") 115 | Debug(name="foo=5!") 116 | 117 | with Loop(10) as loop: 118 | with If(loop.current > 6): 119 | Debug(name="True") 120 | with Elif(loop.current > 4): 121 | Debug(name="Trueish") 122 | with Elif(loop.current > 2): 123 | Debug(name="Falsish") 124 | with Else(): 125 | Debug(name="False") 126 | 127 | # with implied parents 128 | block = [{'val': i} for i in range(3)] 129 | exp.not_done = True 130 | with Loop(conditional=exp.not_done) as outer: 131 | Debug(i=outer.i) 132 | with Loop(block, shuffle=True) as trial: 133 | Debug(current_val=trial.current['val']) 134 | Wait(1.0) 135 | If(trial.current['val'] == block[-1], 136 | Wait(2.0)) 137 | with If(outer.i >= 3): 138 | exp.not_done = False 139 | 140 | block = range(3) 141 | with Loop(block) as trial: 142 | Debug(current=trial.current) 143 | Wait(1.0) 144 | If(trial.current == block[-1], 145 | Wait(2.)) 146 | 147 | 148 | If(True, 149 | Debug(name="True"), 150 | Debug(name="False")) 151 | Wait(1.0) 152 | If(False, 153 | Debug(name="True"), 154 | Debug(name="False")) 155 | Wait(2.0) 156 | If(False, Debug(name="ACK!!!")) # won't do anything 157 | Debug(name="two") 158 | Wait(3.0) 159 | with Parallel(): 160 | with Serial(): 161 | Wait(1.0) 162 | Debug(name='three') 163 | Debug(name='four') 164 | Wait(2.0) 165 | 166 | block = [{'text': 'a'}, {'text': 'b'}, {'text': 'c'}] 167 | with Loop(block) as trial: 168 | Debug(current_text=trial.current['text']) 169 | Wait(1.0) 170 | 171 | Debug(name='before meanwhile 1') 172 | Wait(1.0) 173 | with Meanwhile(name="First Meanwhile") as mw: 174 | Wait(15.0) 175 | Debug(name='after meanwhile 1') 176 | Func(print_actual_duration, mw) 177 | 178 | Debug(name='before meanwhile 2') 179 | Wait(5.0) 180 | with Meanwhile() as mw: 181 | PrintTraceback() 182 | Wait(1.0) 183 | Debug(name='after meanwhile 2') 184 | Func(print_actual_duration, mw) 185 | 186 | Debug(name='before untildone 1') 187 | Wait(15.0) 188 | with UntilDone(name="UntilDone #1") as ud: 189 | Wait(1.0) 190 | PrintTraceback() 191 | Debug(name='after untildone 1') 192 | Func(print_actual_duration, ud) 193 | 194 | Debug(name='before untildone 2') 195 | Wait(1.0) 196 | with UntilDone() as ud: 197 | Wait(5.0) 198 | Debug(name='after untildone 2') 199 | Func(print_actual_duration, ud) 200 | 201 | with Serial() as s: 202 | with Meanwhile(): 203 | Wait(100.0) 204 | Wait(1.0) 205 | Func(print_actual_duration, s) 206 | with Serial() as s: 207 | with UntilDone(): 208 | Wait(1.0) 209 | Wait(100.0) 210 | Func(print_actual_duration, s) 211 | 212 | Debug(name='before parallel') 213 | with Parallel() as p: 214 | Debug(name='in parallel') 215 | with Loop(5) as lv: 216 | with p.insert(): 217 | with Serial(): 218 | Wait(lv.i) 219 | Debug(name='in insert after n second wait', n=lv.i) 220 | with Serial(): 221 | Wait(2.0) 222 | Debug(name='in insert after 2s wait', n=lv.i) 223 | Debug(name='in insert', n=lv.i) 224 | p.insert(Debug(name='in insert #2', n=lv.i)) 225 | Debug(name='after parallel') 226 | 227 | Wait(2.0) 228 | 229 | exp.run(trace=False) 230 | -------------------------------------------------------------------------------- /tests/test_timing.py: -------------------------------------------------------------------------------- 1 | # load all the states 2 | from smile.common import * 3 | from smile.startup import InputSubject 4 | 5 | # set the dur and isi for each trial 6 | trials = [{'dur': d, 'isi': i} 7 | for d, i in zip([.005, .010, .020, .050, .100, .200, .500, 1.0], 8 | [.005, .010, .020, .050, .100, .200, .500, 1.0])] 9 | 10 | # add in a bunch of fast switches 11 | trials = [{'dur': .005, 'isi': .005}]*10 + trials 12 | 13 | # double length, reverse, and repeat 14 | trials = trials*100 15 | trials_copy = trials[:] 16 | trials_copy.reverse() 17 | trials.extend(trials_copy) 18 | print(trials) 19 | 20 | # create an experiment 21 | exp = Experiment(background_color='black') 22 | 23 | InputSubject() 24 | 25 | Wait(1.0) 26 | 27 | with Loop(trials) as trial: 28 | bg = Rectangle(color='WHITE', size=exp.screen.size, 29 | duration=trial.current['dur']) 30 | Wait(until=bg.disappear_time) 31 | Wait(trial.current['isi']) 32 | on2 = Rectangle(color='WHITE', size=exp.screen.size, 33 | duration=trial.current['dur']) 34 | Wait(until=on2.disappear_time) 35 | Wait(trial.current['isi']) 36 | # log the on and off times 37 | Log(flush=False, 38 | name="timing", 39 | on1=bg.appear_time, 40 | off1=bg.disappear_time, 41 | on2=on2.appear_time, 42 | off2=on2.disappear_time, 43 | dur=trial.current['dur'], 44 | isi=trial.current['isi'], 45 | ) 46 | 47 | Wait(1.0) 48 | 49 | if __name__ == '__main__': 50 | exp.run() 51 | -------------------------------------------------------------------------------- /tests/test_update_widget.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compmem/smile/e85c2da827bb161c2ac7fc43d2a2cac20e536364/tests/test_update_widget.py -------------------------------------------------------------------------------- /versioning.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ Git Versioning Script 4 | 5 | Will transform stdin to expand some keywords with git version/author/date information. 6 | 7 | Specify --clean to remove this information before commit. 8 | 9 | Setup: 10 | 11 | 1. Copy versioning.py into your git repository 12 | 13 | 2. Run: 14 | 15 | git config filter.versioning.smudge 'python versioning.py' 16 | git config filter.versioning.clean 'python versioning.py --clean' 17 | echo 'version.py filter=versioning' >> .gitattributes 18 | git add versioning.py 19 | 20 | 21 | 3. add a version.py file with this contents: 22 | 23 | __version__ = "" 24 | __author__ = "" 25 | __email__ = "" 26 | __date__ = "" 27 | 28 | """ 29 | 30 | import sys 31 | import subprocess 32 | import re 33 | 34 | 35 | def main(): 36 | clean = False 37 | if len(sys.argv) > 1: 38 | if sys.argv[1] == '--clean': 39 | clean = True 40 | 41 | # initialise empty here. Otherwise: forkbomb through the git calls. 42 | subst_list = { 43 | "version": "", 44 | "date": "", 45 | "author": "", 46 | "email": "" 47 | } 48 | for line in sys.stdin: 49 | if not clean: 50 | subst_list = { 51 | # '--dirty' could be added to the following, too, but is not supported everywhere 52 | "version": subprocess.check_output(['git', 'describe', '--always', '--tags', '--long']), 53 | "date": subprocess.check_output(['git', 'log', '--pretty=format:"%ad"', '-1']), 54 | "author": subprocess.check_output(['git', 'log', '--pretty=format:"%an"', '-1']), 55 | "email": subprocess.check_output(['git', 'log', '--pretty=format:"%ae"', '-1']) 56 | } 57 | for k, v in iter(subst_list.items()): 58 | v = re.sub(r'[\n\r\t"\']', "", v) 59 | rexp = "__%s__\s*=[\s'\"]+" % k 60 | line = re.sub(rexp, "__%s__ = \"%s\"\n" % (k, v), line) 61 | sys.stdout.write(line) 62 | else: 63 | for k in subst_list: 64 | rexp = "__%s__\s*=.*" % k 65 | line = re.sub(rexp, "__%s__ = \"\"" % k, line) 66 | sys.stdout.write(line) 67 | 68 | 69 | if __name__ == "__main__": 70 | main() 71 | --------------------------------------------------------------------------------