From 99bf3ceb532d5508d917e8fb1864660e4bd07ffd Mon Sep 17 00:00:00 2001 From: Younes Strittmatter Date: Sun, 13 Oct 2024 11:49:27 -0400 Subject: [PATCH] update: clear outputs of jupiter notebook --- hooks/post_gen_project.py | 8 + .../example_css/js_pych_bandit.css | 0 .../example_mains/js_psych_bandit.js | 122 +++++++++++++ .../example_workflows/js_psych_bandit.py | 133 ++++++++++++++ .../README_FIREBASE_js_psych_bandit.md | 165 ++++++++++++++++++ 5 files changed, 428 insertions(+) create mode 100644 {{ cookiecutter.__project_slug }}/example_css/js_pych_bandit.css create mode 100644 {{ cookiecutter.__project_slug }}/example_mains/js_psych_bandit.js create mode 100644 {{ cookiecutter.__project_slug }}/example_workflows/js_psych_bandit.py create mode 100644 {{ cookiecutter.__project_slug }}/readmes/README_FIREBASE_js_psych_bandit.md diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py index 6b5e614..57cb9e0 100644 --- a/hooks/post_gen_project.py +++ b/hooks/post_gen_project.py @@ -102,6 +102,7 @@ def create_autora_example_project(requirements_file): questions = [inquirer.List('project_type', message='What type of project do you want to create?', choices=['Blank', 'JsPsych - Stroop', 'JsPsych - RDK', + 'JsPsych - Bandit', 'SuperExperiment', 'SweetBean', 'Mathematical Model Discovery'] )] @@ -122,6 +123,11 @@ def create_autora_example_project(requirements_file): f.write(f'\nsweetbean') if answers['project_type'] == 'Mathematical Model Discovery': example_file = 'mathematical_model_discovery' + if answers['project_type'] == 'JsPsych - Bandit': + example_file = 'js_psych_bandit' + parent = os.path.join(os.getcwd(), 'testing_zone/css') + os.mkdir(parent) + shutil.move(f'example_css/js_psych_bandit.css', 'testing_zone/css/slot-machine.css') shutil.move(f'example_mains/{example_file}.js', 'testing_zone/src/design/main.js') shutil.move(f'example_workflows/{example_file}.py', 'researcher_hub/autora_workflow.py') @@ -133,6 +139,8 @@ def create_autora_example_project(requirements_file): shutil.rmtree(to_remove) to_remove = os.path.join(os.getcwd(), 'example_mains') shutil.rmtree(to_remove) + to_remove = os.path.join(os.getcwd(), 'example_css') + shutil.rmtree(to_remove) to_remove = os.path.join(os.getcwd(), 'readmes') shutil.rmtree(to_remove) diff --git a/{{ cookiecutter.__project_slug }}/example_css/js_pych_bandit.css b/{{ cookiecutter.__project_slug }}/example_css/js_pych_bandit.css new file mode 100644 index 0000000..e69de29 diff --git a/{{ cookiecutter.__project_slug }}/example_mains/js_psych_bandit.js b/{{ cookiecutter.__project_slug }}/example_mains/js_psych_bandit.js new file mode 100644 index 0000000..2cabcaa --- /dev/null +++ b/{{ cookiecutter.__project_slug }}/example_mains/js_psych_bandit.js @@ -0,0 +1,122 @@ +// To use the jsPsych package first install jspsych using `npm install jspsych` +// This example uses the rdk plugin. Install it via `npm install @jspsych-contrib/plugin-rdk` +// This example uses the html-keyboard-response plugin. Install it via `npm install @jspsych/plugin-html-keyboard-response` +// Here is documentation on how to program a jspsych experiment using npm: +// https://www.jspsych.org/7.3/tutorials/hello-world/#option-3-using-npm + +import {initJsPsych} from 'jspsych'; +import 'jspsych/css/jspsych.css' +import htmlKeyboardResponse from '@jspsych/plugin-html-keyboard-response'; +import jsPsychHtmlChoice from '@jspsych-contrib/plugin-html-choice'; +import fullscreen from '@jspsych/plugin-fullscreen'; +import '../css/slot-machine.css' + + +/** + * This is the main function where you program your experiment. Install jsPsych via node and + * use functions from there + * @param id this is a number between 0 and number of participants. You can use it for example to counterbalance between subjects + * @param condition this is a condition (4-32. Here we want to find out how the training length impacts the accuracy in a testing phase) + * @returns {Promise<*>} the accuracy in the post-trainging phase relative to the pre-training phase + */ +const main = async (id, condition) => { + + const timeline_variables = JSON.parse(condition) + const nr_trials = timeline_variables.length + let current_trial = 0 + + const jsPsych = initJsPsych({ + show_progress_bar: true, + auto_update_progress_bar: false, + on_finish: function () { + jsPsych.data.displayData(); + } + }); + + // create timeline (enter fullscreen) + const timeline = [{ + type: fullscreen, + fullscreen_mode: true + }]; + + + // create html divs + // let sm_blue = '
' + // let sm_red = '
' + + const sm_blue = (pos) => { + let _pos = 'left: 0' + if (pos === 'right') { + _pos = 'right: 0' + } + return `
` + } + const sm_red = (pos) => { + let _pos = 'left: 0' + if (pos === 'right') { + _pos = 'right: 0' + } + return `
` + } + let score = 0 + + const test = { + timeline: [ + { + type: jsPsychHtmlChoice, + html_array: () => { + return [ + sm_blue(jsPsych.timelineVariable('pos')[0]), + sm_red(jsPsych.timelineVariable('pos')[1]) + ] + }, + on_load: () => { + let content = document.getElementById('jspsych-content') + let score_div = document.createElement('div') + score_div.style.position = 'fixed' + score_div.style.left = '50vw' + score_div.style.bottom = '3vh' + score_div.style.transform = 'translateX(-50%)' + score_div.innerText = `Score: ${score} of ${nr_trials}` + content.appendChild(score_div) + }, + trial_duration: null, + values: jsPsych.timelineVariable('values'), + response_ends_trial: true, + time_after_response: 800, + on_finish: (data) => { + current_trial += 1 + score += data.value + let progress = current_trial / nr_trials + jsPsych.setProgressBar(progress) + } + }, + { + type: htmlKeyboardResponse, + stimulus: '', + trial_duration: 100, + on_load: () => { + let content = document.getElementById('jspsych-content') + let score_div = document.createElement('div') + score_div.style.position = 'fixed' + score_div.style.left = '50vw' + score_div.style.bottom = '3vh' + score_div.innerText = `Score: ${score} of ${nr_trials}` + score_div.style.transform = 'translateX(-50%)' + content.appendChild(score_div) + }, + }, + ], + timeline_variables: timeline_variables + } + + timeline.push(test) + + await jsPsych.run(timeline) + + const observation = jsPsych.data.get().filter({trial_type: "html-choice"}).select('response') + return JSON.stringify({condition: condition, observation: observation}) +} + + +export default main \ No newline at end of file diff --git a/{{ cookiecutter.__project_slug }}/example_workflows/js_psych_bandit.py b/{{ cookiecutter.__project_slug }}/example_workflows/js_psych_bandit.py new file mode 100644 index 0000000..468c17e --- /dev/null +++ b/{{ cookiecutter.__project_slug }}/example_workflows/js_psych_bandit.py @@ -0,0 +1,133 @@ +""" +Basic Workflow + Single condition Variable (0-1), Single Observation Variable(0-1) + Theorist: LinearRegression + Experimentalist: Random Sampling + Runner: Firebase Runner (no prolific recruitment) +""" +import json + +from autora.variable import VariableCollection, Variable +from autora.experimentalist.random import pool +from autora.experiment_runner.firebase_prolific import firebase_runner +from autora.state import StandardState, on_state, Delta + +import numpy as np +import pandas as pd +from sklearn.linear_model import LinearRegression + +# *** Set up variables *** # +# independent variable is coherence in percent (0 - 100) +# dependent variable is accuracy (0 - 1) +variables = VariableCollection( + independent_variables=[Variable(name="coherence", allowed_values=np.linspace(0, 1, 101))], + dependent_variables=[Variable(name="accuracy", value_range=(0, 1))]) + +# *** State *** # +# With the variables, we can set up a state. The state object represents the state of our +# closed loop experiment. + + +state = StandardState( + variables=variables, +) + +# *** Components/Agents *** # +# Components are functions that run on the state. The main components are: +# - theorist +# - experiment-runner +# - experimentalist +# See more about components here: https://autoresearch.github.io/autora/ + + +# ** Theorist ** # +# Here we use a linear regression as theorist, but you can use other theorists included in +# autora (for a list: https://autoresearch.github.io/autora/theorist/) + +theorist = LinearRegression() + +# To use the theorist on the state object, we wrap it with the on_state functionality and return a +# Delta object. +# Note: The if the input arguments of the theorist_on_state function are state-fields like +# experiment_data, variables, ... , then using this function on a state object will automatically +# use those state fields. +# The output of these functions is always a Delta object. The keyword argument in this case, tells +# the state object witch field to update. + + +@on_state() +def theorist_on_state(experiment_data, variables): + ivs = [iv.name for iv in variables.independent_variables] + dvs = [dv.name for dv in variables.dependent_variables] + x = experiment_data[ivs] + y = experiment_data[dvs] + return Delta(models=[theorist.fit(x, y)]) + +# ** Experimentalist ** # +# Here, we use a random pool and use the wrapper to create a on state function +# Note: The argument num_samples is not a state field. Instead, we will pass it in when calling +# the function + + +@on_state() +def experimentalist_on_state(variables, num_samples): + return Delta(conditions=pool(variables, num_samples)) + +# ** Experiment Runner ** # +# We will run our experiment on firebase and need credentials. You will find them here: +# (https://console.firebase.google.com/) +# -> project -> project settings -> service accounts -> generate new private key + +firebase_credentials = { + "type": "type", + "project_id": "project_id", + "private_key_id": "private_key_id", + "private_key": "private_key", + "client_email": "client_email", + "client_id": "client_id", + "auth_uri": "auth_uri", + "token_uri": "token_uri", + "auth_provider_x509_cert_url": "auth_provider_x509_cert_url", + "client_x509_cert_url": "client_x509_cert_url" +} + +# simple experiment runner that runs the experiment on firebase +experiment_runner = firebase_runner( + firebase_credentials=firebase_credentials, + time_out=100, + sleep_time=5) + + +# Again, we need to wrap the runner to use it on the state. Here, we send the raw conditions. +@on_state() +def runner_on_state(conditions): + data = experiment_runner(conditions) + # Here, parse the return value of the runner. The return value depends on the specific + # implementation of your online experiment (see testing_zone/src/design/main.js). + # In this example, the experiment runner returns a list of strings, that contain json formatted + # dictionaries. + # Example: + # data = ['{'coherence':.3, accuracy':.8}', ...] + result = [] + for item in data: + result.append(json.loads(item)) + return Delta(experiment_data=pd.DataFrame(result)) + + +# Now, we can run our components +for _ in range(3): + state = experimentalist_on_state(state, num_samples=2) # Collect 2 conditions per iteration + state = runner_on_state(state) + state = theorist_on_state(state) + + +# *** Report the data *** # +# If you changed the theorist, also change this part +def report_linear_fit(m: LinearRegression, precision=4): + s = f"y = {np.round(m.coef_[0].item(), precision)} x " \ + f"+ {np.round(m.intercept_.item(), 4)}" + return s + + +print(report_linear_fit(state.models[0])) +print(report_linear_fit(state.models[-1])) diff --git a/{{ cookiecutter.__project_slug }}/readmes/README_FIREBASE_js_psych_bandit.md b/{{ cookiecutter.__project_slug }}/readmes/README_FIREBASE_js_psych_bandit.md new file mode 100644 index 0000000..39414f2 --- /dev/null +++ b/{{ cookiecutter.__project_slug }}/readmes/README_FIREBASE_js_psych_bandit.md @@ -0,0 +1,165 @@ +## Create a firebase project in the browser + +### Google account + +You'll need a google account to use firebase. You can create one here: +https://www.google.com/account/about/ + +### Firebase Project + +While logged in into your google account head over to: +https://firebase.google.com/ + +- Click on `Get started` +- Click on the plus sign with `add project` +- name your project and click on `continue` +- For now, we don't use google analytics (you can leave it enabled if you want to use it in the future) +- Click 'Create project' + +### Adding a webapp to your project + +in your project console (in the firebase project), we now want to add an app to our project + +- Click on `` +- name the app (can be the same as your project) and check `Also set up Firebase Hosting` +- Click on `Register app` +- Click on `Next` +- Click on `Next` +- Click on `Continue to console` + +### Adding Firestore to your project + +in your project console in the left hand menu click on build and select Firestore Database + +- Click on `Create database` +- Leave `Start in production mode` selected and click on `Next` +- Select a Firestore location and click on `Enable` +- To see if everything is set up correctly, in the menu click on the gear symbol next to the Project overview and + select `Project settings` +- Under `Default GCP recource location` you should see the Firestore location, you've selected. + - If you don't see the location, select one now (click on the `pencil-symbol` and then on `Done` in the pop-up + window) + +### Set up node + +On your command line run: + +```shell +node -v +``` + +If an error appears or the version number is bellow 16.0, install node. You can download the and install the newest +version here: +https://nodejs.org/ + +### Link the local project to the firebase project + +Change the directory to the newly created folder + +```shell +cd testing_zone +``` + +Login to firebase + +```shell +firebase login +``` + +Initialize the project: + +```shell +firebase init +``` + +In your command line a dialog should appear. + +- Choose (by selecting and pressing space bar): + - Firestore: Configure security rules and indexes files for Firestore + - Hosting: Configure files for Firebase Hosting and (optionally) set up GitHub Action deploys + - Press `Enter` +- Use an existing project -> `Enter` +- Select the project you created when creating the project in the browser -> `Enter` +- For Firestore Rules, leave the default -> `Enter` +- For Firestore indexes, leave the default -> `Enter` +- ATTENTION: For the public directory, type in `build` and press `Enter` +- Configure as single page app, type `y` and press `Enter` +- No automatic builds and deploys with GitHub, type `n` and press `Enter` + +### Set the credentials +In the testing_zone folder there is a .env file, where you need to replace the default values with credentials from your Firebase Project. +You find the credentials in the Firebase Project under Project-Overview -> Project Settings. + +```javascript +const firebaseConfig = { + apiKey: "apiKey", + authDomain: "authDomain", + projectId: "projectId", + storageBucket: "storageBucket", + messagingSenderId: "messagingSenderId", + appId: "appId", + measurementId: "measurementId" +}; +``` + +### install the necessary npm packages in the testing_zone folder + +To run JsPsych experiments you will need at least the JsPsych package itself. Change the directory to the testing_zone folder and run: + +```shell +npm install jspsych +``` + +In the example experiment, we also use the html-keyboard-response plugin. Install it via: + +```shell +npm install @jspsych/plugin-html-keyboard-response +``` + +Als, we use the rdk plugin. Install via: + +```shell +npm install @jspsych-contrib/plugin-rdk +``` + +A list of JsPsych packages can be found here: [JsPsych GitHub](https://github.com/jspsych/jsPsych) + +### Write your own code + +Write your own code in the src/design/main.js folder. + +### Test your experiment + +you can test the experiment locally via + +```shell +npm start +``` + +#### Attention: Testing without database + +When running the experiment locally, by default the condition and id in your main function is set to 0. If you populated +your database already (for example with an autora-firebase-runner), you can use the conditions in your database by +setting the REACT_APP_devNoDb to False in the .env file. + +### Build and deploy the experiment + +#### Build + +To build and deploy the experiment run + +```shell +npm run build +``` + +This will create a new build folder in the test_subject_environment directory. + +#### Deploy + +To deploy the experiment, run + +```shell +firebase deploy +``` + +This will deploy the experiment to firebase, and you will get a link where participants can access it. \ No newline at end of file