Replies: 3 comments
-
Just read through this really quickly, and my initial response is very positive!
Whenever I code I tend to try to make the "master class" just an interface/spec, and then implement a concrete class on top which could do a lot of heavy lifting during init, but will also let people inherit the master class themselves if they want a special init behavior. Not sure what exactly you mean here by "generic types", generic types of what? Also: Do you know of/have used mypy? Would be really nice of this new class helped improve the typing situation (and would clarify what these 'generic types' are really about).
As it was my suggestion, I think so. But these two things could go hand in hand. An ExperimentSpec could be contructed from code or config, and then used to initialize and run an experiment. Might not be necessary though with your refactor, not sure.
Depends? Split if it makes sense (eases readability/leads to code reuse), but don't split for the sake of splitting :)
I think we can assume that experiments can be quite multimodal and some will always require a higher degree of customization. @JohnGriffiths is prob better suited to answer this though. |
Beta Was this translation helpful? Give feedback.
-
Thanks for kicking this off Parv. Will give my feedback on the Qs soon. First though wanted to flag something work looking at when thinking about experiment class etc - this repo designed for gtec unicorn (which is also one of our supported devices). It's not super well documented by looks like a version of some of the things we are contemplating here. |
Beta Was this translation helpful? Give feedback.
-
Some more reference points - kernel tasks sdk: https://docs.kernel.com/docs/kernel-tasks-sdk Not sure the latter will be accessible, so here is the content: This is an example of how to implement a PsychoPy-based task using our Kernel Tasks SDK. The Kernel Tasks SDK is available to all customers as software that is provided with your system. This specific example is code from the Finger-Tapping Task that is shipped as an example task with every system. """ Runs the task from the menu """
import logging
from datetime import datetime
from importlib.metadata import version as get_version
import numpy as np
import pandas as pd
from kernel.sdk.task import TaskClient
from kernel.sdk.task_registry import TaskRegistry
from kernel.tasks.tap.params import TapParams
from kernel.tasks.tap.prompts import TapPrompts
from psychopy import core, event
logger = logging.getLogger(__name__)
event.globalKeys.add(key="c", modifiers=["ctrl"], func=core.quit)
event.globalKeys.add(key="escape", func=core.quit)
def run_task(params: TapParams, prompts: TapPrompts, practice: bool):
tc = TaskClient(
multicast_ip=params.ip,
multicast_port=params.port,
debug=params.debug_mode,
enforce_registry_entry=not practice,
experiment_name="finger_tapping",
experiment_version=get_version("kernel.tasks.tap"),
)
params.send_params(tc)
# start experiment
block_stim_info = prompts.practice_block_stim_info if practice else prompts.block_stim_info
exp_type = "practice" if practice else TaskRegistry.FT02
quit_request = False
with tc.experiment(type=exp_type):
logger.info(f"starting experiment with type {exp_type}")
# baseline rest period
if not practice:
logger.info("initial rest period")
prompts.win_flip()
prompts.draw_fixation()
prompts.win_flip()
tc.rest(params.baseline_dur)
# loop through blocks
for b_type, t_types in block_stim_info:
prompts.draw_hand(b_type)
prompts.win_flip()
# start block
with tc.block(type=b_type):
# loop through trials
for idx_trial, t_type in enumerate(t_types):
# ITI
with tc.event("ITI"):
ITI_dur = np.random.normal(loc=params.ITI_dur_mean, scale=params.ITI_dur_std, size=1)[0]
keypresses = event.waitKeys(maxWait=abs(ITI_dur), keyList=params.quit_keys, clearEvents=True)
if keypresses:
quit_request = True
break
# start trial
with tc.trial(type=t_type):
# draw hand + finger cue
prompts.draw_hand(b_type)
prompts.draw_finger_cue(b_type, t_type)
prompts.win_flip()
keypresses = event.waitKeys(maxWait=params.tap_dur, keyList=params.quit_keys, clearEvents=True)
logger.info(f"tap with type {t_type}")
prompts.draw_hand(b_type) if idx_trial != len(t_types) - 1 else prompts.draw_fixation()
prompts.win_flip()
if keypresses:
quit_request = True
break
if quit_request:
break
# ease transition b/w blocks
if practice:
core.wait(1)
prompts.win_flip()
else:
# rest
logger.info("rest")
prompts.draw_fixation()
prompts.win_flip()
tc.rest(params.rest_dur)
if not practice and not quit_request:
tc.rest(params.end_baseline_dur)
if practice:
prompts.draw_end_practice(wait=True)
else:
prompts.draw_finished(ease_in=0.5)
if params.debug_mode:
df_out = pd.DataFrame(tc.debug_events)
df_out.timestamp *= 1e-9
df_out.to_csv(f'task_df_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv', index=False)
logger.info("ending experiment") You can see from this example that we have implemented a TaskClient that abstracts the process of sending event data according to the guidance that we’ve posted here: https://docs.kernel.com/docs/kernel-tasks-sdk Note that following the documented guidance is necessary for compliance with the SNIRF specification event structure and we recommend using our TaskClient to simplify getting it right when your SNIRF files are exported. As always, we're here to support any issues you encounter. |
Beta Was this translation helpful? Give feedback.
-
Hey, this is Parv Agarwal - and I'm the GSOC student working on the Experiment Class refactor such that it gets rid of the repeated code and makes it more generic using classes etc. following what's detailed in https://docs.google.com/document/d/11zC94sozuMYtfOcvQl38MuaC1HednfGrA1ymsTapKHk/edit#heading=h.ms4qewfw0h65.
I have a question on how that needs to be done however, since there are various ways it can be done. There is a lot of repeated code in each experiment, and most of it in the main process function seems to follow this algorithm ->
There are some variations in the Cueing and Go-No-Go Experiments and I assume further experiments will need additional methods to work with it. I would prefer a main function in a header class that does all the work where the trials, the stimulus and the specific mechanisms of the experiment are passed in as generic types, however this may depend on a lot of things, and could prove tricky especially since its Python.
So the questions are really this,
This is the most repeated code in each experiment class,
I'm gonna try to play around with the paradigm I have in mind, but let me know what works best in conjuction with every other defined element. Even if we do implement something like I have in mind, we would need to make sure that it plays well with all the code that's already written and calling these individual experiments so would need a lot of creative thinking. Let me know if this seems sensible or is it just simpler to just have shared data members or something.
Beta Was this translation helpful? Give feedback.
All reactions