def __init__(self): """initialization of the experiment In this method experiment initialization takes place. Start date ist set, default values are set, user defined init_experiment method is called and the list of runs is generated from given parameters and adapt settings. """ self.cls = type(self).__name__ self._create_time = datetime.datetime.now().strftime('%Y-%m-%dT%H.%M.%S') self.subject_name = '' self._sl = SaveLoad(self) self.runs = [] self.parameters = {} self.variable = {} self.adapt_settings = [] self.signals = {} self.pre_signal = [] self.reference_signal = [] self.between_signal = [] self.test_signal = [] self.post_signal = [] self._save_names = {} self.allow_debug = True self.discard_unfinished_runs = True self.sample_rate = 48000 self.calib = 0 self.feedback = True self.debug = True self.visual_indicator = True self.init_experiment(self) self.time_to_signal(self) self._sl.unify_signals(self) self._generate_runs()
class Experiment(): """ Base class of all earyx experiments This class provides the base methods and data structures for all experiments built with earyx but it is never directly instanciated because some methods are just "Interfaces" Attributes ---------- subject_name : str name or abbrevation of test subject runs : list becomes list of :class:`Run`. This list is auto generated while experiment creation parameters : dict becomes dict of dict containing name, values, unit and description. For simple parameter handling there is the method :func:`add_parameter`. There must be at least one parameter in each experiment. variable : dict needs to contain name (string), value, unit (string). For simple variable handling there is the mdthod :func:`set_variable`. Each experiment has exactly one variable. adapt_settings : list list of adapt settings used in the experiment. For simple adapt settings handling there is the method :func:`add_adapt_setting`. There must be at least one adapt setting in each experiment. pre_signal : numpy array, float or tuple (optional) Signal part played before each trial. There are some options for simple signal generation. For more information see documentation section: `Zero signals` reference_signal : numpy array, list of numpy arrays Signal part(s) not containing the test sequence. In case of N-AFC experiment this can also an list of N-1 signals, which are randomly spread of the N-1 reference intervals *not* containing the test sequence. between_signal : numpy array, float or tuple (optional) Signal played between each signal part. Most of the time this variable is used for silence between the signal parts. For that rason it can be initialized as float or tuple. For more information, see documentation section: `Zero signals`. If not specified, singnal parts are simply joined together test_signal : numpy array Signal part containing the test sequence. post_signal : numpy array, float or tuple (optional) if specified, this signal part is played at the end of trial. allow_debug : boolean (optional) With this variable the experiment creator can specify whether the test subject is allowed to enable plotting or not. default: True discard_unfinished_runs : boolean (optional) Usually unfinished runs ar not saved when experiment is interrupted. With this flag set to **True** the test subjuct is able to continue unfinished runs later. Default: False sample_rate : int (optional) sampling frequency for all signals of the whole experiment. Default: 48000 calib : int or float (optional) This variable is a place holder for a calibration value. It can be used as factor or summand to fit the signal loudness to the test system settings. Without explicit usage in your signal generation this value has no impact on the signals. Default: 0. task : str Description of experiment and description of what the test subjects have to do. This message is displayed befor the experimetn starts. """ def __init__(self): """initialization of the experiment In this method experiment initialization takes place. Start date ist set, default values are set, user defined init_experiment method is called and the list of runs is generated from given parameters and adapt settings. """ self.cls = type(self).__name__ self._create_time = datetime.datetime.now().strftime('%Y-%m-%dT%H.%M.%S') self.subject_name = '' self._sl = SaveLoad(self) self.runs = [] self.parameters = {} self.variable = {} self.adapt_settings = [] self.signals = {} self.pre_signal = [] self.reference_signal = [] self.between_signal = [] self.test_signal = [] self.post_signal = [] self._save_names = {} self.allow_debug = True self.discard_unfinished_runs = True self.sample_rate = 48000 self.calib = 0 self.feedback = True self.debug = True self.visual_indicator = True self.init_experiment(self) self.time_to_signal(self) self._sl.unify_signals(self) self._generate_runs() def init_experiment(self, experiment): """method to init experiment wide settings. This method is called only once at the beginning of experiment. All settings with experiment wide validity should be defined here. """ raise NotImplemented def _generate_runs(self): outer_param_list = [] for name, parameter in self.parameters.items(): inner_param_list = [{name:value} for value in parameter["values"]] outer_param_list.append(inner_param_list) combinations = list(product(self.adapt_settings,*outer_param_list)) for combi in combinations: params = {} for param in combi[1:]: params.update(param) adapt_settings = combi[0] AdaptClass = getattr(earyx.adapt, "Adapt%s" % adapt_settings["type"]) RunClass = type('Run'+adapt_settings["type"],(Run,AdaptClass,),{}) run = RunClass(params, self.variable["start_val"], adapt_settings, self.reference_signal, self.pre_signal, self.between_signal, self.post_signal, self.sample_rate, self.calib) self.init_run(run) self.time_to_signal(run) self.runs.append(run) self._sl.unify_signals(run) def add_parameter(self,name,values,unit,description=""): """Adds a new parameter to the experiment. This method offers a simple way to add a new parameter to the experiment. Usually this method is used in :func:`init_experiment`. To add more than one parameter this method can be called multiple times. Parameters ---------- name : str name of the parameter. It **must not** contain any white space!! values : list list of all values of an parameter. If the parameter only contains one value it must be a list with one element unit : str unit of the parameter description : str (optional) short description Examples -------- this.add_parameter(name='noise_level',values=[-60, -40, -20], unit='dB') .. note:: The name must not contain any white space because in runs and trials the run specific value of this parameter can be referenced by its name. """ values = list(values) new_param = {"values": values, "unit": unit, "desc": description} self.parameters[name] = new_param def set_variable(self, name, start_val, unit, description=""): """Stes the variable of the experiment. This method offers a simple way to set the variable of the experiment. Usually this method is called in :func:`int_experiment`. Because every experiment has only one variable it can be called only once. Parameters ---------- name : str name of the variable. start_val : int start value of the variable unit : str unit of the parameter description : str (optional) short description Examples -------- this.set_variable(name='frequency', start_val=1000, unit="Hz") """ var = {"name": name, "unit": unit, "start_val": start_val, "desc": description} self.variable = var def add_adapt_setting(self, adapt_method="1up2down", max_reversals=6, start_step=5, min_step=1, **kwargs): """Add adapt method/setting. This method adds a adaption method and its start values to the experiment. Parameters ---------- adapt_method : str a number of adapt methods is pre-implemented: 1up2down 2up1down 1up3down WUD (weighted up down) max_reversals : int number of reversals the trial is running start_step : int start step how the variable is changing min_step : int min step, if reached trial goes on for max_reversals kwargs : dict (optional) dict of keywords and arguments may used for special adapt method WUD needs key: pc_convergence value: float 0..1 """ new_adapt = {"type": adapt_method, "max_reversals": max_reversals, "start_step": start_step, "minstep": min_step} new_adapt.update(kwargs) self.adapt_settings.append(new_adapt) def build_signal(self): """ specific build is made by method of the different experiment classes. """ raise NotImplemented def check_answer(self): """ specific answer check is made by method of the different experiment classes. """ raise NotImplemented def set_answer(self, run, trial, answer): """Depending on the given answer a new step is calculated by an adapt method. Parameters ---------- run : :class:`Run` trial : :class:`Trial` answer : int or str """ trial.answer = answer trial.is_correct = trial.answer == trial.correct_answer run.trials.append(trial) def adapt(self, run): """ apply selected adapt rule to variable""" try: step = run.adapt(run.trials) except expt.RunFinishedException: run.finished = time.strftime('%d-%b-%Y__%H:%M:%S') if self.discard_unfinished_runs: for tr in run.trials: self._sl.unify_signals(tr) self._sl.update_struct() raise run.variable += step if not self.discard_unfinished_runs: self._sl.unify_signals(run.trials[-1]) self._sl.update_struct() if abs(step) == run.minstep and not run.start_measurement_idx: run.start_measurement_idx = len(run.trials) if not self.discard_unfinished_runs: self._sl.update_struct() raise expt.RunStartMeasurement def load(self): """ load experiment After experiment initialization a saved experiment can be loaded. This method is only a wrapper for the load method of the _sl attribute, which contains all load and save logic. """ self._sl.load() def finalize(self, save): """ finalize experiment This method should be called befor experiment exit. """ if save: self._sl.update_struct() path = self._sl.pack() self._sl.clear_temp() return path self._sl.clear_temp() def generate_trial(self, run): """ generates trial This method generates a trial useing the parameter set with its specific values given by the current run. Parameters ---------- run : :class:`Run` Reference on the Run, the next Trial is needed for. Returns ------- trial : :class:`Trial` new Trial object with all parameters and variable values are set correctly """ trial = Trial(run.variable,run._parameters,run.reference_signal, run.pre_signal, run.between_signal, run.post_signal, run.sample_rate, run.calib, None) return trial def time_to_signal(self, obj): """ convert given time to zero signal This function checks if for a given object a signal is specified as float or tuple and converts it into a zero signal of given length. Parameters ---------- obj : :class:`Run``, :class:`Trial` or subtype of :class:`Experiment` The Experiment, Run or Trial to check signals """ signals = ['reference_signal','test_signal', 'pre_signal', 'post_signal', 'between_signal'] for signal in signals: sig = getattr(obj,signal) if isinstance(sig,tuple): if len(sig) == 1: setattr(obj,signal,np.zeros(sig[0]*obj.sample_rate)) else: setattr(obj,signal, np.zeros((sig[0]*obj.sample_rate, sig[1]))) elif isinstance(sig, int) or isinstance(sig, float): setattr(obj,signal, np.zeros(sig*obj.sample_rate)) def next_trial(self, run): """bulid next trial Parameters ---------- run : :class:`Run` Returns ------- trial : :class:`Trial` signal : list of numpy arrays """ trial = self.generate_trial(run) trial.correct_answer = self.correct_answer() self.init_trial(trial) self.time_to_signal(trial) signal = self.build_signal(trial) return trial, signal def skip_run(self, run): """ sets skipped to True """ run.skipped = True