Exemple #1
0
    def __init__(self, session_params, ax_interactive=None):
        # incorporate kwargs
        self.params = session_params
        self.__dict__.update(self.params)
        self.verify_params()

        # sync
        self.sync_flag = multiprocessing.Value('b', False)
        self.sync_to_save = multiprocessing.Queue()

        # saver
        self.saver = Saver(self.subj, self.name, self, sync_flag=self.sync_flag)

        # hardware
        self.cam = PSEye(sync_flag=self.sync_flag, **self.cam_params)
        self.ar = AnalogReader(saver_obj_buffer=self.saver.buf, sync_flag=self.sync_flag, **self.ar_params)
        # communication
        self.ni = NI845x(i2c_on=self.imaging)

        # interactivity
        self.ax_interactive = ax_interactive
        
        # runtime variables
        self.notes = {}
        self.mask_idx = -1 #for reselecting mask
        self.session_on = 0
        self.on = False
        self.session_complete = False
        self.session_kill = False
        self.trial_flag = False
        self.trial_on = 0
        self.trial_off = 0
        self.trial_idx = -1
        self.stim_cycle_idx = 0
        self.paused = False
        self.deliver_override = False
        self.roi_pts = None
        self.eyelid_buffer = np.zeros(self.eyelid_buffer_size)-1
        self.eyelid_buffer_ts = np.zeros(self.eyelid_buffer_size)-1
        self.past_flag = False
        
        # sync
        self.sync_flag.value = True #trigger all processes to get time
        self.sync_val = now() #get this process's time
        procs = dict(saver=self.saver, cam=self.cam.pseye, ar=self.ar)
        sync_vals = {o:procs[o].sync_val.value for o in procs} #collect all process times
        sync_vals['session'] = self.sync_val
        self.sync_to_save.put(sync_vals)
        
        # more runtime, anything that must occur after sync
        _,self.im = self.cam.get()
Exemple #2
0
class Session(object):

    def __init__(self, session_params, ax_interactive=None):
        # incorporate kwargs
        self.params = session_params
        self.__dict__.update(self.params)
        self.verify_params()

        # sync
        self.sync_flag = multiprocessing.Value('b', False)
        self.sync_to_save = multiprocessing.Queue()

        # saver
        self.saver = Saver(self.subj, self.name, self, sync_flag=self.sync_flag)

        # hardware
        self.cam = PSEye(sync_flag=self.sync_flag, **self.cam_params)
        self.ar = AnalogReader(saver_obj_buffer=self.saver.buf, sync_flag=self.sync_flag, **self.ar_params)
        # communication
        self.ni = NI845x(i2c_on=self.imaging)

        # interactivity
        self.ax_interactive = ax_interactive
        
        # runtime variables
        self.notes = {}
        self.mask_idx = -1 #for reselecting mask
        self.session_on = 0
        self.on = False
        self.session_complete = False
        self.session_kill = False
        self.trial_flag = False
        self.trial_on = 0
        self.trial_off = 0
        self.trial_idx = -1
        self.stim_cycle_idx = 0
        self.paused = False
        self.deliver_override = False
        self.roi_pts = None
        self.eyelid_buffer = np.zeros(self.eyelid_buffer_size)-1
        self.eyelid_buffer_ts = np.zeros(self.eyelid_buffer_size)-1
        self.past_flag = False
        
        # sync
        self.sync_flag.value = True #trigger all processes to get time
        self.sync_val = now() #get this process's time
        procs = dict(saver=self.saver, cam=self.cam.pseye, ar=self.ar)
        sync_vals = {o:procs[o].sync_val.value for o in procs} #collect all process times
        sync_vals['session'] = self.sync_val
        self.sync_to_save.put(sync_vals)
        
        # more runtime, anything that must occur after sync
        _,self.im = self.cam.get()
        

    @property
    def session_runtime(self):
        if self.session_on != 0:
            return now()-self.session_on
        else:
            return -1
    @property
    def trial_runtime(self):
        if self.trial_on != False:
            return now()-self.trial_on
        else:
            return -1
    def name_as_str(self):
        return self.name.strftime('%Y%m%d%H%M%S')

    def verify_params(self):
        if self.name is None:
            self.name = pd.datetime.now()
        self.cam_params.update(dict(save_name=pjoin(self.subj.subj_dir, self.name_as_str()+'_cams.h5')))

    def pause(self, val):
        self.paused = val
        if self.imaging:
            if val == True:
                self.stop_acq()
            elif val == False:
                self.start_acq()

    def update_licked(self):
        l = self.ar.licked

    def start_acq(self):
        if self.imaging:
            self.ni.write_dio(LINE_SI_ON, 1)
            self.ni.write_dio(LINE_SI_ON, 0)
    def stop_acq(self):
        if self.imaging:
            self.ni.write_dio(LINE_SI_OFF, 1)
            self.ni.write_dio(LINE_SI_OFF, 0)

    def wait(self, dur, t0=None):
        if t0 is None:
            t0 = now()
        while now()-t0 < dur:
            pass

    def next_stim_type(self, inc=True):
        st = self.cycle[self.stim_cycle_idx]
        if inc:
            self.stim_cycle_idx += 1
            if self.stim_cycle_idx == len(self.cycle):
                self.stim_cycle_idx = 0
        return st
        
    @property
    def current_stim_state(self):
        return STIM_TYPES[self.cycle[self.stim_cycle_idx]]
        
    def deliver_trial(self):
        while self.on:
            if self.trial_flag:

                # prepare trial
                self.trial_idx += 1
                self.trial_on = now()
                self.cam.set_flush(False)
                kind = self.next_stim_type()
           
                # deilver trial
                self.wait(self.intro)
                cs_time,us_time = self.send_stim(kind)
                
                # replay
                self.wait(self.display_lag)
                self.past_flag = [cs_time[1], us_time[1]]
                
                # finish trial
                self.wait(self.trial_duration, t0=self.trial_on)
                self.trial_off = now()

                # save trial info
                self.cam.set_flush(True)
                
                trial_dict = dict(\
                start   = self.trial_on,\
                end     = self.trial_off,\
                cs_ts0  = cs_time[0],\
                cs_ts1  = cs_time[1],\
                us_ts0  = us_time[0],\
                us_ts1  = us_time[1],\
                kind    = kind,\
                idx     = self.trial_idx,\
                )
                self.saver.write('trials',trial_dict)
                
                self.trial_flag = False
                self.trial_on = False
    
    def dummy_puff(self):
        self.ni.write_dio(LINE_US, 1)
        self.wait(self.us_dur)
        self.ni.write_dio(LINE_US, 0)
    def dummy_light(self, state):
        self.ni.write_dio(LINE_CS, state)
    def send_stim(self, kind):
        if kind == CS:
            t = (now(), now2())
            self.ni.write_i2c('CS_ON')
            self.ni.write_dio(LINE_CS, 1)
            self.wait(self.cs_dur)
            self.ni.write_i2c('CS_OFF')
            self.ni.write_dio(LINE_CS, 0)
            stim_time = [t,(-1,-1)]

        elif kind == US:
            self.wait(self.cs_dur) # for trial continuity
            t = (now(), now2())
            self.ni.write_i2c('US_ON')
            self.ni.write_dio(LINE_US, 1)
            self.wait(self.us_dur)
            self.ni.write_i2c('US_OFF')
            self.ni.write_dio(LINE_US, 0)
            stim_time = [(-1,-1),t]

        elif kind == CSUS:
            t_cs = (now(), now2())
            self.ni.write_i2c('CS_ON')
            self.ni.write_dio(LINE_CS, 1)
            self.wait(self.csus_gap)
            t_us = (now(), now2())
            self.ni.write_i2c('US_ON')
            self.ni.write_dio(LINE_US, 1)
            self.wait(self.us_dur) # assumes US ends before CS does
            self.ni.write_i2c('US_OFF')
            self.ni.write_dio(LINE_US, 0)
            self.wait(self.cs_dur, t0=t_cs[0])
            self.ni.write_i2c('CS_OFF')
            self.ni.write_dio(LINE_CS, 0)
            stim_time = [t_cs,t_us]

        return stim_time

    def acquire_mask(self):
        x,y = self.cam.resolution[0]
        if self.roi_pts is None:
            self.roi_pts = [[0,0],[x,0],[x,y],[0,y]]
            logging.warning('No ROI found, using default')
        self.mask_idx += 1
        pts_eye = np.array(self.roi_pts, dtype=np.int32)
        mask_eye = np.zeros([y,x], dtype=np.int32)
        cv2.fillConvexPoly(mask_eye, pts_eye, (1,1,1), lineType=cv2.LINE_AA)
        self.mask = mask_eye
        self.mask_flat = self.mask.reshape((1,-1))
        self.saver.write('mask{}'.format(self.mask_idx), self.mask)
        logging.info('New mask set.')
        
    def run(self):
        try:
            self.acquire_mask()
            self.session_on = now()
            self.on = True
            self.ar.begin_saving()
            self.cam.begin_saving()
            self.cam.set_flush(True)
            self.start_acq()
        
            # main loop
            threading.Thread(target=self.deliver_trial).start()
            threading.Thread(target=self.update_eyelid).start()
            while True:

                if self.trial_on or self.paused:
                    continue

                if self.session_kill:
                    break
                
                moving = self.determine_motion()
                eyelid = self.determine_eyelid()
                
                if self.deliver_override or ((now()-self.trial_off>self.min_iti) and (not moving) and (eyelid)):
                    self.trial_flag = True
                    self.deliver_override = False

            self.end()

        except:
            logging.error('Session has encountered an error!')
            raise
    def determine_eyelid(self):
        return np.mean(self.eyelid_buffer[-self.eyelid_window:]) < self.eyelid_thresh
    def update_eyelid(self):
        while self.on:
            imts,im = self.cam.get()
            if im is None:
                continue
            self.im = im
            roi_data = self.extract(self.im)
            self.eyelid_buffer = np.roll(self.eyelid_buffer, -1)
            self.eyelid_buffer_ts = np.roll(self.eyelid_buffer_ts, -1)
            self.eyelid_buffer[-1] = roi_data
            self.eyelid_buffer_ts[-1] = imts
    def extract(self, fr):
        if fr is None:
            return 0
        flat = fr.reshape((1,-1)).T
        dp = (self.mask_flat.dot(flat)).T
        return np.squeeze(dp/self.mask_flat.sum(axis=-1))
    def determine_motion(self):
        return self.ar.moving
   
    def end(self):
        self.on = False
        self.stop_acq()
        to_end = [self.ar, self.cam]
        if self.imaging:
            to_end.append(self.ni)
        for te in to_end:
            te.end()
            time.sleep(0.100)
        self.saver.end(notes=self.notes)
        self.session_on = False
            
    def get_code(self):
        py_files = [pjoin(d,f) for d,_,fs in os.walk(os.getcwd()) for f in fs if f.endswith('.py') and not f.startswith('__')]
        code = {}
        for pf in py_files:
            with open(pf, 'r') as f:
                code[pf] = f.read()
        return json.dumps(code)
Exemple #3
0
    def __init__(self, subj, session_params, cam_params=default_cam_params):
        self.params = self.DEFAULT_PARAMS
        self.cam_params = cam_params

        self.subj = subj
        self.name = time.strftime("%Y%m%d_%H%M%S")
        self.saver = Saver(self.subj, self.name)

        # kwargs
        self.params.update(session_params)
        # checks on kwargs
        assert self.params["min_isi"] > self.params["stim_duration"]  # otherwise same-side stims can overlap
        if self.params["lick_rule_side"]:
            assert self.params[
                "lick_rule_any"
            ]  # if must lick correct side in lick phase, then must lick at all in lick phase
        # extensions of kwargs
        if not isinstance(self.params["lam"], list):
            self.params["lam"] = [self.params["lam"]]
        if not isinstance(self.params["n_trials"], list):
            self.params["n_trials"] = [self.params["n_trials"]]
        assert len(self.params["lam"]) == len(self.params["n_trials"])
        self.params["subj_name"] = self.subj.name
        # self.params['phase_durations'][self.PHASE_STIM] = self.params['stim_phase_intro_duration'] + self.params['stim_phase_duration']
        self.cam_params.update(dict(save_name=pjoin(self.subj.subj_dir, self.name)))
        if self.params["hints_on"] is True:
            self.params["hints_on"] = 1e6
        # add them in
        self.__dict__.update(self.params)

        # hardware
        syncobj = multiprocessing.Value("d", 0)
        threading.Thread(target=update_sync, args=(syncobj,)).start()
        self.cam = PSEye(clock_sync_obj=syncobj, **self.cam_params)
        self.cam.start()
        self.lr = AnalogReader(
            saver=self.saver, lick_thresh=5.0, ports=["ai0", "ai1", "ai5", "ai6"], runtime_ports=[0, 1]
        )
        sd = self.stim_duration
        if self.stim_duration_override:
            sd = self.stim_duration_override
        self.stimulator = Valve(saver=self.saver, ports=["port0/line0", "port0/line1"], name="stimulator", duration=sd)
        self.spout = Valve(
            saver=self.saver, ports=["port0/line2", "port0/line3"], name="spout", duration=self.reward_duration
        )
        self.light = Light(saver=self.saver, port="ao0")
        self.speaker = Speaker(saver=self.saver)

        # trials init
        self.trials = self.generate_trials()

        # save session info
        self.saver.save_session(self)

        # runtime variables
        self.session_on = 0
        self.session_complete = False
        self.session_kill = False
        self.trial_on = 0
        self.trial_idx = -1
        self.valid_trial_idx = 0
        self.saving_trial_idx = 0
        self.session_runtime = -1
        self.trial_runtime = -1
        self.trial_outcomes = []
        self.trial_corrects = []  # for use in use_trials==False
        self.rewards_given = 0
        self.paused = 0
        self.holding = False
        self.percentages = [0.0, 0.0]  # L/R
        self.side_ns = [0, 0]  # L/R
        self.bias_correction_percentages = [0.0, 0.0]  # L/R
        self.perc_valid = 0.0
        self.iter_write_begin = now()
Exemple #4
0
class Session(object):

    L, R, X = 0, 1, 2
    COR, INCOR, EARLY, BOTH, NULL, KILLED, SKIPPED = 0, 1, 2, 3, 4, 5, 6
    PHASE_INTRO, PHASE_STIM, PHASE_DELAY, PHASE_LICK, PHASE_REWARD, PHASE_ITI, PHASE_END = [0, 1, 2, 3, 4, 5, 6]
    DEFAULT_PARAMS = dict(
        n_trials=[100],  # if a list, corresponds to list of lams
        lam=[
            0.5
        ],  # poisson lambda for one side, as fraction of n_total_stims. ex 0.9 means that on any given trial, the expected number of stims on one side will be 0.9*n_total_stims and the expected number of stims on the other side will be 0.1*n_total_stims. If a list, will be sequential trials with those params, numbers of trials specified by n_trials parameter. If any item in this list is a list, it means multiple shuffled trials equally distributed across those lams.
        # n_total_stims               = 10.,
        rate_sum=5.0,
        max_rate_frac=2.0,
        # max_n_stims                 = 20, #on one side
        stim_duration=0.050,
        stim_duration_override=False,
        stim_phase_duration=[5.0, 1.0],  # mean, std
        enforce_stim_phase_duration=True,
        stim_phase_intro_duration=0.200,
        stim_phase_end_duration=0.050,
        min_isi=0.100,
        reward_duration=[0.010, 0.010],
        penalty_iti_frac=1.0,  # fraction of ITI to add to normal ITI when trial was incorrect
        distribution_mode="poisson",
        phase_durations={
            PHASE_INTRO: 1.0,
            PHASE_STIM: None,
            PHASE_DELAY: 1.0,
            PHASE_LICK: 4.0,
            PHASE_REWARD: 4.0,
            PHASE_ITI: 3.0,
            PHASE_END: 0.0,
        },  # PHASE_END must always have 0.0 duration
        iter_resolution=0.030,
        hold_rule=True,
        puffs_on=True,
        rewards_on=True,
        hints_on=False,  # False, or n first trials to give hints, or True for all trials
        hint_interval=0.500,
        bias_correction=True,
        bias_correction_window=6,
        max_bias_correction=0.1,
        lick_rule_phase=True,  # must not lick before the lick phase
        lick_rule_side=True,  # must not lick incorrect side during the lick phase
        lick_rule_any=True,  # must lick some port in lick phase to get reward
        multiple_decisions=False,  # can make multiple attempts at correct side
        use_trials=True,  # if false, trials are ignored. used for training
        trial_vid_freq=2,  # movie for every n trials
        extra_bigdiff_trials=False,
        condition=-1,
    )

    def __init__(self, subj, session_params, cam_params=default_cam_params):
        self.params = self.DEFAULT_PARAMS
        self.cam_params = cam_params

        self.subj = subj
        self.name = time.strftime("%Y%m%d_%H%M%S")
        self.saver = Saver(self.subj, self.name)

        # kwargs
        self.params.update(session_params)
        # checks on kwargs
        assert self.params["min_isi"] > self.params["stim_duration"]  # otherwise same-side stims can overlap
        if self.params["lick_rule_side"]:
            assert self.params[
                "lick_rule_any"
            ]  # if must lick correct side in lick phase, then must lick at all in lick phase
        # extensions of kwargs
        if not isinstance(self.params["lam"], list):
            self.params["lam"] = [self.params["lam"]]
        if not isinstance(self.params["n_trials"], list):
            self.params["n_trials"] = [self.params["n_trials"]]
        assert len(self.params["lam"]) == len(self.params["n_trials"])
        self.params["subj_name"] = self.subj.name
        # self.params['phase_durations'][self.PHASE_STIM] = self.params['stim_phase_intro_duration'] + self.params['stim_phase_duration']
        self.cam_params.update(dict(save_name=pjoin(self.subj.subj_dir, self.name)))
        if self.params["hints_on"] is True:
            self.params["hints_on"] = 1e6
        # add them in
        self.__dict__.update(self.params)

        # hardware
        syncobj = multiprocessing.Value("d", 0)
        threading.Thread(target=update_sync, args=(syncobj,)).start()
        self.cam = PSEye(clock_sync_obj=syncobj, **self.cam_params)
        self.cam.start()
        self.lr = AnalogReader(
            saver=self.saver, lick_thresh=5.0, ports=["ai0", "ai1", "ai5", "ai6"], runtime_ports=[0, 1]
        )
        sd = self.stim_duration
        if self.stim_duration_override:
            sd = self.stim_duration_override
        self.stimulator = Valve(saver=self.saver, ports=["port0/line0", "port0/line1"], name="stimulator", duration=sd)
        self.spout = Valve(
            saver=self.saver, ports=["port0/line2", "port0/line3"], name="spout", duration=self.reward_duration
        )
        self.light = Light(saver=self.saver, port="ao0")
        self.speaker = Speaker(saver=self.saver)

        # trials init
        self.trials = self.generate_trials()

        # save session info
        self.saver.save_session(self)

        # runtime variables
        self.session_on = 0
        self.session_complete = False
        self.session_kill = False
        self.trial_on = 0
        self.trial_idx = -1
        self.valid_trial_idx = 0
        self.saving_trial_idx = 0
        self.session_runtime = -1
        self.trial_runtime = -1
        self.trial_outcomes = []
        self.trial_corrects = []  # for use in use_trials==False
        self.rewards_given = 0
        self.paused = 0
        self.holding = False
        self.percentages = [0.0, 0.0]  # L/R
        self.side_ns = [0, 0]  # L/R
        self.bias_correction_percentages = [0.0, 0.0]  # L/R
        self.perc_valid = 0.0
        self.iter_write_begin = now()

    def generate_trials(self):
        timess = []
        lamss = []
        durs = []
        for lam, n_trials in zip(self.lam, self.n_trials):
            for _ in xrange(n_trials):
                if isinstance(lam, list):
                    lami = np.random.choice(lam)
                else:
                    lami = lam
                lams = [lami, 1.0 - lami]
                lamss.append(lams)
                dur = np.random.normal(*self.stim_phase_duration)
                durs.append(dur)
                tt = self.generate_stim_times(
                    lams=lams, dur=dur, intro_dur=self.stim_phase_intro_duration, min_isi=self.min_isi
                )
                if self.extra_bigdiff_trials and 1.0 in lams and np.random.random() < 0.7:
                    enforce_n = np.random.choice([11, 12, 13, 14, 15])
                    while max([len(i) for i in tt]) < enforce_n:
                        tt = self.generate_stim_times(
                            lams=lams, dur=dur, intro_dur=self.stim_phase_intro_duration, min_isi=self.min_isi
                        )
                timess.append(tt)

        trials = np.zeros(
            (len(timess)), dtype=[("times", vlen_array_dtype), ("correct", int), ("lams", float, 2), ("dur", float)]
        )
        lr_idxer = [0, 1]
        for tidx, times, lams, d in zip(range(len(timess)), timess, lamss, durs):
            np.random.shuffle(lr_idxer)
            times = [times[lr_idxer[0]], times[lr_idxer[1]]]  # left or right equally likely to correspond to lam[0]
            lams = [lams[lr_idxer[0]], lams[lr_idxer[1]]]  # maintain alignment with lams
            lenL, lenR = [len(times[i]) for i in [self.L, self.R]]
            if lenL > lenR:
                correct = self.L
            elif lenR > lenL:
                correct = self.R
            elif lenR == lenL:
                raise Exception("Indeterminate trial included. This should not be possible!")

            trials[tidx] = (times, correct, lams, d)

        return np.array(trials)

    def generate_stim_times(self, lams, dur, intro_dur, min_isi, time_resolution=0.001):
        n_stim = [0, 0]
        while n_stim[0] == n_stim[1] or np.max(n_stim) / dur > self.rate_sum * self.max_rate_frac:
            if self.distribution_mode == "poisson":
                n_stim = np.random.poisson(self.rate_sum * dur * np.asarray(lams))
            elif self.distribution_mode == "abs":
                n_stim = np.round(self.n_total_stims * np.asarray(lams))

        t = np.arange(intro_dur, intro_dur + dur + time_resolution, time_resolution)

        def make(sz):
            times = np.append(0, np.sort(np.random.choice(t, size=sz, replace=False)))
            while len(times) > 1 and np.diff(times).min() < min_isi:
                times = np.append(0, np.sort(np.random.choice(t, size=sz, replace=False)))
            return times

        stim_times = map(make, n_stim)

        return np.array(stim_times)

    def get_cum_performance(self):
        cum = np.asarray(self.trial_outcomes)
        markers = cum.copy()
        if self.lick_rule_phase:
            ignore = [self.SKIPPED, self.EARLY, self.NULL, self.KILLED]
        else:
            ignore = [self.SKIPPED, self.NULL, self.KILLED]
        valid = np.array([c not in ignore for c in cum]).astype(bool)
        cum = cum == self.COR
        cum = [
            np.mean([c for c, v in zip(cum[:i], valid[:i]) if v]) if np.any(valid[:i]) else 0.0
            for i in xrange(1, len(cum) + 1)
        ]  # cumulative
        return cum, markers, np.asarray(self.trial_corrects)

    def stimulate(self, trial):
        sides = [self.L, self.R]
        np.random.shuffle(sides)
        for side in sides:
            tr = trial["times"][side]
            si = self.stim_idx[side]

            if si >= len(tr):
                return

            dt_phase = now() - self.phase_start
            if dt_phase >= tr[si]:
                self.stimulator.go(side)
                self.stim_idx[side] += 1

        if np.all(np.asarray(self.stim_idx) == np.asarray(map(len, trial["times"]))):
            self.stim_complete = True

    def to_phase(self, ph):
        self.phase_times[self.current_phase][1] = now()
        self.phase_times[ph][0] = now()

        self.current_phase = ph
        self.phase_start = now()
        self.hinted = False
        if ph == self.PHASE_END:
            # sanity check. should have been rewarded only if solely licked on correct side
            if (
                self.lick_rule_side
                and self.lick_rule_phase
                and (not self.licked_early)
                and (not self.multiple_decisions)
                and self.use_trials
            ):
                assert bool(self.rewarded) == (
                    any(self.lick_phase_licks[self.trial["correct"]])
                    and (not any(self.lick_phase_licks[-self.trial["correct"] + 1]))
                )

            if not self.rewarded:
                if self.use_trials:
                    self.trial_corrects.append(self.trial["correct"])
                else:
                    self.trial_corrects.append(self.X)

            # determine trial outcome
            if not self.use_trials:
                if any(self.lick_phase_licks):
                    outcome = self.COR
                else:
                    outcome = self.INCOR
            elif self.use_trials:
                if self.rewarded and self.lick_rule_side and not self.multiple_decisions:
                    outcome = self.COR
                elif self.rewarded and ((not self.lick_rule_side) or self.multiple_decisions):
                    lpl_min = np.array([min(i) if len(i) else -1 for i in self.lickph_andon_licks])
                    if np.all(lpl_min == -1):
                        outcome = self.NULL
                    else:
                        lpl_min[lpl_min == -1] = now()
                        if np.argmin(lpl_min) == self.trial["correct"]:
                            outcome = self.COR
                        else:
                            outcome = self.INCOR
                elif self.trial_kill:
                    outcome = self.KILLED
                elif self.licked_early:
                    outcome = self.EARLY
                # this BOTH logic no longer works bc i include reward phase licks in lickphaselicks. both will never show up, though it still can *rarely* be a cause for trial failure
                # elif (any(self.lick_phase_licks[self.L]) and any(self.lick_phase_licks[self.R])):
                #    outcome = self.BOTH
                elif any(self.lick_phase_licks[-self.trial["correct"] + 1]):
                    outcome = self.INCOR
                elif not any(self.lick_phase_licks):
                    outcome = self.NULL
            self.trial_outcomes.append(outcome)

    def update_licked(self):
        l = self.lr.licked()
        tst = now()
        for idx, li in enumerate(l):
            self.licks[idx] += [
                tst
            ] * li  # represent not the number of licks but the number of times the LR class queried daq and found a positive licking signal
            if self.current_phase in [self.PHASE_LICK, self.PHASE_REWARD]:
                self.lickph_andon_licks[idx] += [tst] * li
            if self.current_phase == self.PHASE_LICK:
                self.lick_phase_licks[idx] += [tst] * li

        if self.hold_rule:
            if (not self.holding) and np.any(self.lr.holding):
                self.holding = True
                self.paused += 1
            elif self.holding and not np.any(self.lr.holding):
                self.paused = max(self.paused - 1, 0)
                self.holding = False
            if self.holding:
                self.speaker.pop()

    def run_phase(self, ph):
        ph_dur = self.phase_durations[ph]  # intended phase duration
        if ph == self.PHASE_STIM:
            ph_dur = (
                self.stim_phase_intro_duration + self.trial["dur"] + self.stim_phase_end_duration
            )  # before dur was introduced: np.max(np.concatenate(self.trial['times']))+self.stim_phase_end_duration
        dt_phase = now() - self.phase_start
        self.session_runtime = now() - self.session_on
        self.trial_runtime = now() - self.trial_on
        self.update_licked()

        # special cases
        if ph == self.PHASE_ITI and not self.rewarded:
            ph_dur *= 1 + self.penalty_iti_frac

        if now() - self.iter_write_begin >= self.iter_resolution:
            iter_info = dict(
                trial=self.trial_idx,
                phase=ph,
                licks=np.array([len(i) for i in self.licks]),
                licked_early=self.licked_early,
                dt_phase=dt_phase,
                paused=self.paused,
            )
            self.saver.write("iterations", iter_info)
            self.iter_write_begin = now()

        if self.paused and self.current_phase in [self.PHASE_INTRO, self.PHASE_STIM, self.PHASE_DELAY, self.PHASE_LICK]:
            self.trial_kill = True
            return

        if self.trial_kill and not self.current_phase == self.PHASE_ITI:
            self.to_phase(self.PHASE_ITI)
            return

        # Intro
        if ph == self.PHASE_INTRO:
            self.light.off()
            if not self.intro_signaled:
                self.speaker.intro()
                self.intro_signaled = True

            # comment out to give intro time with licks allowed? doesnt work, cause lick variable accumulates over trial
            if any(self.licks) and self.lick_rule_phase:
                self.licked_early = min(self.licks[0] + self.licks[1])
                self.to_phase(self.PHASE_ITI)
                return

            if dt_phase >= ph_dur:
                self.to_phase(self.PHASE_STIM)
                return

        # Stim
        elif ph == self.PHASE_STIM:
            if any(self.licks) and self.lick_rule_phase:
                self.licked_early = min(self.licks[0] + self.licks[1])
                self.to_phase(self.PHASE_ITI)
                return

            if dt_phase >= ph_dur:
                self.to_phase(self.PHASE_DELAY)
                return

            if self.puffs_on:
                self.stimulate(self.trial)

            if (not self.enforce_stim_phase_duration) and self.stim_complete:
                self.to_phase(self.PHASE_DELAY)
                return

        # Delay
        elif ph == self.PHASE_DELAY:
            if any(self.licks) and self.lick_rule_phase:
                self.licked_early = min(self.licks[0] + self.licks[1])
                self.to_phase(self.PHASE_ITI)
                return

            if dt_phase >= ph_dur:
                self.to_phase(self.PHASE_LICK)
                return

        # Lick
        elif ph == self.PHASE_LICK:
            self.light.on()

            if (
                self.hints_on
                and self.hints_on > self.trial_idx
                and (not self.hinted or (now() - self.hinted) > self.hint_interval)
                and self.use_trials
            ):
                self.stimulator.go(self.trial["correct"])
                self.hinted = now()

            if not self.laser_signaled:
                self.speaker.laser()
                self.laser_signaled = True

            if not self.lick_rule_any:
                self.to_phase(self.PHASE_REWARD)
                return

            if any(self.lick_phase_licks) and not self.multiple_decisions:
                self.to_phase(self.PHASE_REWARD)
                return
            elif (
                any(self.lick_phase_licks)
                and self.multiple_decisions
                and any(self.lick_phase_licks[self.trial["correct"]])
            ):
                self.to_phase(self.PHASE_REWARD)
                return

            # if time is up, to reward phase
            if dt_phase >= ph_dur:
                self.to_phase(self.PHASE_REWARD)
                return

        # Reward
        elif ph == self.PHASE_REWARD:
            self.light.on()  # probably redundant

            if (
                self.lick_rule_side
                and any(self.lick_phase_licks[-self.trial["correct"] + 1])
                and not self.rewarded
                and not self.multiple_decisions
            ):
                self.speaker.wrong()
                self.to_phase(self.PHASE_ITI)
                return

            # sanity check. cannot reach here if any incorrect licks, ensure that:
            if self.lick_rule_side and not self.multiple_decisions:
                assert not any(self.lick_phase_licks[-self.trial["correct"] + 1])

            # if no licks at all, go straight to ITI
            if self.lick_rule_any and not any(self.lick_phase_licks):
                self.to_phase(self.PHASE_ITI)
                return

            # if allowed multiple choices but only licked wrong side
            if self.multiple_decisions and not any(self.lick_phase_licks[self.trial["correct"]]):
                self.to_phase(self.PHASE_ITI)
                return

            # sanity check. can only reach here if licked correct side only
            if self.lick_rule_any and self.lick_rule_side:
                assert any(self.lick_phase_licks[self.trial["correct"]])

            # from this point on, it is assumed that rewarding should occur.
            if self.use_trials:
                rside = self.trial["correct"]
            else:
                rside = np.argmin([min(i) if len(i) else now() for i in self.lick_phase_licks])
            if not self.corside_added:
                self.trial_corrects.append(rside)
                self.corside_added = True

            if (
                self.hints_on
                and self.hints_on > self.trial_idx
                and (not self.hinted or (now() - self.hinted) > self.hint_interval)
            ):
                self.stimulator.go(rside)
                self.hinted = now()

            if self.rewards_on and not self.rewarded:
                self.spout.go(side=rside)
                self.rewarded = now()
                self.rewards_given += 1

            if dt_phase >= ph_dur:
                self.to_phase(self.PHASE_ITI)
        # ITI
        elif ph == self.PHASE_ITI:
            self.light.off()

            if self.licked_early and self.lick_rule_phase and not self.error_signaled:
                self.speaker.error()
                self.error_signaled = True

            if dt_phase >= ph_dur:
                self.to_phase(self.PHASE_END)
                return

    def determine_skip(self):
        to = np.asarray(self.trial_outcomes)
        if len(to) == 0:
            return False
        corincor_idxs = np.where(np.logical_or(to == self.COR, to == self.INCOR))[0]
        all_val = np.sum([i in [self.COR, self.INCOR, self.EARLY, self.NULL, self.KILLED] for i in to])
        if all_val != 0:
            self.perc_valid = float(len(corincor_idxs)) / all_val
        corincor_trials = self.trials[corincor_idxs]
        trcor = corincor_trials["correct"]
        subcor = to[corincor_idxs] == self.COR

        if np.sum(trcor == self.L) > 0:
            perc_l = np.mean(subcor[trcor == self.L])
            self.percentages[self.L] = perc_l
            self.side_ns[self.L] = np.sum(trcor == self.L)
        if np.sum(trcor == self.R) > 0:
            perc_r = np.mean(subcor[trcor == self.R])
            self.percentages[self.R] = perc_r
            self.side_ns[self.R] = np.sum(trcor == self.R)

        if (
            np.sum(trcor == self.L) < self.bias_correction_window
            or np.sum(trcor == self.R) < self.bias_correction_window
        ):
            return False

        perc_l = np.mean(subcor[trcor == self.L][-self.bias_correction_window :])
        perc_r = np.mean(subcor[trcor == self.R][-self.bias_correction_window :])
        self.bias_correction_percentages = [perc_l, perc_r]

        if perc_l == perc_r:
            return False

        this_cor = self.trials[self.trial_idx]["correct"]
        if self.bias_correction_percentages[this_cor] < self.bias_correction_percentages[-1 + this_cor]:
            return False

        if min(self.bias_correction_percentages) == 0:
            pthresh = self.max_bias_correction
        else:
            pthresh = float(min(self.bias_correction_percentages)) / max(self.bias_correction_percentages)

        return np.random.random() > pthresh

    def next_trial(self):
        # init the trial
        self.skip_trial = False
        self.trial_idx += 1
        if self.trial_idx > 1 and self.trial_outcomes[-1] in [self.COR, self.INCOR]:
            self.valid_trial_idx += 1
        if self.trial_idx >= len(self.trials):
            self.session_complete = True
            return

        self.skip_trial = self.determine_skip()

        self.trial_on = now()

        self.trial = self.trials[self.trial_idx]
        self.current_phase = self.PHASE_INTRO
        self.stim_idx = [0, 0]  # index of next stim, for [L,R]
        self.trial_start = now()
        self.phase_start = self.trial_start
        self.phase_times = [[-1, -1] for _ in self.phase_durations]

        _ = self.lr.licked()  # to clear any residual signal
        self.licks = [[], []]
        self.lick_phase_licks = [[], []]
        self.lickph_andon_licks = [[], []]
        self.licked_early = False
        self.rewarded = False
        self.error_signaled = False
        self.laser_signaled = False
        self.intro_signaled = False
        self.stim_complete = False
        self.trial_kill = False
        self.hinted = False
        self.corside_added = False

        if self.skip_trial:
            self.cam.SAVING.value = 0
        else:
            if self.saving_trial_idx % self.trial_vid_freq == 0:
                self.cam.SAVING.value = 1
            else:
                self.cam.SAVING.value = 0
            self.saving_trial_idx += 1

        # logging.info('Starting trial %i.' %self.trial_idx)

        # run the trial loop
        if self.skip_trial:
            self.trial_outcomes.append(self.SKIPPED)
            self.trial_corrects.append(self.trial["correct"])
        else:
            while self.current_phase != self.PHASE_END:
                self.run_phase(self.current_phase)

        # save trial info
        trial_info = dict(
            idx=self.trial_idx,
            ns=[len(tt) for tt in self.trial["times"]],
            start_time=self.trial_start,
            licksL=self.licks[self.L],
            licksR=self.licks[self.R],
            licked_early=self.licked_early,
            phase_times=self.phase_times,
            rewarded=self.rewarded,
            outcome=self.trial_outcomes[-1],
            hints=(self.hints_on and self.hints_on > self.trial_idx),
            end_time=now(),
            condition=self.condition,
        )
        self.saver.write("trials", trial_info)

    def run(self):
        self.session_on = now()
        self.lr.begin_saving()

        while True:
            self.next_trial()
            if self.session_kill:
                logging.info("Session killed manually")
                self.paused = False
                break
            if self.session_complete:
                logging.info("Session complete")
                break

        self.session_on = False
        self.end()

    def end(self):
        to_end = [self.lr, self.stimulator, self.spout, self.light, self.cam, self.saver]
        for te in to_end:
            te.end()
            time.sleep(0.050)

    def get_code(self):
        py_files = [
            pjoin(d, f) for d, _, fs in os.walk(os.getcwd()) for f in fs if f.endswith(".py") and not f.startswith("__")
        ]
        code = {}
        for pf in py_files:
            with open(pf, "r") as f:
                code[pf] = f.read()
        return json.dumps(code)
Exemple #5
0
    def __init__(self, session_params, mp285=None, actuator=None):
        self.params = self.DEFAULT_PARAMS

        # incorporate kwargs
        self.params.update(session_params)
        self.__dict__.update(self.params)
        self.verify_params()

        # sync
        self.sync_flag = multiprocessing.Value('b', False)
        self.sync_to_save = multiprocessing.Queue()

        # saver
        self.saver = Saver(self.subj,
                           self.name,
                           self,
                           sync_flag=self.sync_flag)

        # hardware
        self.cam = PSEye(sync_flag=self.sync_flag, **self.cam_params)
        self.ar = AnalogReader(saver_obj_buffer=self.saver.buf,
                               sync_flag=self.sync_flag,
                               **self.ar_params)
        self.stimulator = Valve(saver=self.saver,
                                name='stimulator',
                                **self.stimulator_params)
        self.spout = Valve(saver=self.saver, name='spout', **self.spout_params)
        self.light = Light(saver=self.saver, **self.light_params)
        self.light.set(0)
        self.opto = Opto(saver=self.saver, **self.opto_params)
        self.speaker = Speaker(saver=self.saver)

        # mp285, actuator
        self.mp285 = mp285
        self.actuator = actuator
        self.actuator.saver = self.saver
        self.mp285_go(self.position_stim)
        if self.retract_ports:
            self.actuator.retract()
            #self.mp285_go(self.position_stim)
        else:
            self.actuator.extend()
            #self.mp285_go(self.position_lick)

        # communication
        self.sic = SICommunicator(self.imaging)

        # trials
        self.th = TrialHandler(saver=self.saver,
                               condition=self.condition,
                               **self.trial_params)

        # runtime variables
        self.stdinerr = None
        self.notes = {}
        self.session_on = 0
        self.session_complete = False
        self.session_kill = False
        self.session_runtime = -1
        self.trial_runtime = -1
        self.rewards_given = 0
        self.paused = 0
        self.holding = False
        self.current_phase = PHASE_INTRO
        self.live_figure = None

        # sync
        self.sync_flag.value = True  #trigger all processes to get time
        self.sync_val = now()  #get this process's time
        procs = dict(saver=self.saver, cam=self.cam.pseye, ar=self.ar)
        sync_vals = {o: procs[o].sync_val.value
                     for o in procs}  #collect all process times
        sync_vals['session'] = self.sync_val
        self.sync_to_save.put(sync_vals)
Exemple #6
0
class Session(object):

    DEFAULT_PARAMS = {}

    def __init__(self, session_params, mp285=None, actuator=None):
        self.params = self.DEFAULT_PARAMS

        # incorporate kwargs
        self.params.update(session_params)
        self.__dict__.update(self.params)
        self.verify_params()

        # sync
        self.sync_flag = multiprocessing.Value('b', False)
        self.sync_to_save = multiprocessing.Queue()

        # saver
        self.saver = Saver(self.subj,
                           self.name,
                           self,
                           sync_flag=self.sync_flag)

        # hardware
        self.cam = PSEye(sync_flag=self.sync_flag, **self.cam_params)
        self.ar = AnalogReader(saver_obj_buffer=self.saver.buf,
                               sync_flag=self.sync_flag,
                               **self.ar_params)
        self.stimulator = Valve(saver=self.saver,
                                name='stimulator',
                                **self.stimulator_params)
        self.spout = Valve(saver=self.saver, name='spout', **self.spout_params)
        self.light = Light(saver=self.saver, **self.light_params)
        self.light.set(0)
        self.opto = Opto(saver=self.saver, **self.opto_params)
        self.speaker = Speaker(saver=self.saver)

        # mp285, actuator
        self.mp285 = mp285
        self.actuator = actuator
        self.actuator.saver = self.saver
        self.mp285_go(self.position_stim)
        if self.retract_ports:
            self.actuator.retract()
            #self.mp285_go(self.position_stim)
        else:
            self.actuator.extend()
            #self.mp285_go(self.position_lick)

        # communication
        self.sic = SICommunicator(self.imaging)

        # trials
        self.th = TrialHandler(saver=self.saver,
                               condition=self.condition,
                               **self.trial_params)

        # runtime variables
        self.stdinerr = None
        self.notes = {}
        self.session_on = 0
        self.session_complete = False
        self.session_kill = False
        self.session_runtime = -1
        self.trial_runtime = -1
        self.rewards_given = 0
        self.paused = 0
        self.holding = False
        self.current_phase = PHASE_INTRO
        self.live_figure = None

        # sync
        self.sync_flag.value = True  #trigger all processes to get time
        self.sync_val = now()  #get this process's time
        procs = dict(saver=self.saver, cam=self.cam.pseye, ar=self.ar)
        sync_vals = {o: procs[o].sync_val.value
                     for o in procs}  #collect all process times
        sync_vals['session'] = self.sync_val
        self.sync_to_save.put(sync_vals)

    def name_as_str(self):
        return self.name.strftime('%Y%m%d%H%M%S')

    def verify_params(self):
        if self.name is None:
            self.name = pd.datetime.now()
        logging.info('Session: {}'.format(self.name))
        self.cam_params.update(
            dict(save_name=pjoin(self.subj.subj_dir, self.name_as_str())))

    def pause(self, val):
        if val is True:
            self.paused += 1
            self.light.set(0)
            #self.sic.stop_acq()
        elif val is False:
            self.paused = max(self.paused - 1, 0)
            if self.paused == 0:
                self.sic.start_acq()

    def stimulate(self):
        n = len(self.th.trt)
        t0 = now()
        while self.current_phase == PHASE_STIM and self.stim_idx < n:
            dt = now() - t0
            if dt >= self.th.trt['time'][self.stim_idx]:
                #logging.debug(dt-self.th.trt['time'][self.stim_idx])
                self.stimulator.go(self.th.trt['side'][self.stim_idx])
                self.stim_idx += 1

    def to_phase(self, ph):
        # Write last phase
        if not (self.th.idx == 0 and ph == 0):
            phase_info = dict(
                trial=self.th.idx,
                phase=self.current_phase,
                start_time=self.phase_start,
                end_time=now(),
            )
            self.saver.write('phases', phase_info)

        self.current_phase = ph

        # tell imaging
        self.sic.i2c('{}.{}'.format(self.th.idx, self.current_phase))

        # determine duration
        if self.current_phase == PHASE_STIM:
            self.current_phase_duration = self.th.phase_dur
        elif self.current_phase == PHASE_DELAY:
            self.current_phase_duration = self.th.delay_dur
        else:
            self.current_phase_duration = self.phase_durations[
                ph]  #intended phase duration

        self.phase_start = now()
        self.last_hint = now()

        # Flush flag for camera:
        if ph in [PHASE_ITI, PHASE_END]:
            self.cam.flushing.value = True
        else:
            self.cam.flushing.value = False

        # Opto LED : based on constants defined in manipulations.py
        if self.th.manip == MANIP_NONE:
            self.opto.set(0)
        elif self.th.manip == MANIP_OPTO_STIMDELAY and self.current_phase in [
                PHASE_STIM, PHASE_DELAY
        ]:
            self.opto.set(1)
        elif self.th.manip == MANIP_OPTO_LICK and self.current_phase == PHASE_LICK:
            self.opto.set(1)
        elif self.th.manip == MANIP_OPTO_REWARDITI and self.current_phase in [
                PHASE_REWARD, PHASE_ITI
        ]:
            self.opto.set(1)
        else:
            self.opto.set(0)

        # Trial ending logic
        if ph == PHASE_END:
            lpl = self.licks[self.licks['phase'] ==
                             PHASE_LICK]  #lick phase licks

            # sanity check. should have been rewarded only if solely licked on correct side
            if self.th.rule_side and self.th.rule_phase and (
                    not self.licked_early) and (
                        not self.th.rule_fault) and self.use_trials:
                assert bool(self.rewarded) == (
                    any(lpl['side'] == self.th.trial.side)
                    and not any(lpl['side'] == -self.th.trial.side + 1))

            # determine trial outcome
            if not self.use_trials:
                if any(lpl):
                    outcome = COR
                else:
                    outcome = INCOR
            elif not self.th.rule_any:
                lprl = self.licks[(self.licks['phase'] == PHASE_LICK) |
                                  (self.licks['phase'] == PHASE_REWARD)]
                if not any(lprl):
                    outcome = NULL
                elif lprl[0]['side'] == self.th.trial.side:
                    outcome = COR
                else:
                    outcome = INCOR
            elif self.use_trials:
                if self.rewarded and self.th.rule_side and not self.th.rule_fault:
                    outcome = COR
                elif self.rewarded and ((not self.th.rule_side)
                                        or self.th.rule_fault):
                    if not any(lpl):
                        outcome = NULL
                    else:
                        if lpl[0]['side'] == self.th.trial.side:
                            outcome = COR
                        else:
                            outcome = INCOR
                elif self.trial_kill:
                    outcome = KILLED
                elif self.licked_early:
                    outcome = EARLY[self.licked_early['side']]
                elif any(lpl['side'] == -self.th.trial.side + 1):
                    outcome = INCOR
                elif not any(lpl):
                    outcome = NULL
            # Save trial info
            nLnR = self.stimulator.get_nlnr()
            if config.TESTING_MODE:
                fake_outcome = np.random.choice(
                    [COR, INCOR, EARLY_L, EARLY_R, NULL, KILLED],
                    p=[0.5, 0.3, 0.15 / 2, 0.15 / 2, 0.04, 0.01])
                self.th.end_trial(fake_outcome, -0.1 * (fake_outcome == COR),
                                  nLnR)
                if fake_outcome == COR:
                    self.rewards_given += 1
            else:
                self.th.end_trial(outcome, self.rewarded, nLnR)

    def update_licked(self):
        l = self.ar.licked
        tst = now()
        for idx, li in enumerate(l):
            if li:
                try:
                    self.licks[self.lick_idx] = (self.current_phase, tst, idx)
                    self.lick_idx += 1
                except:
                    logging.error(self.licks)
                if self.lick_idx >= len(self.licks):
                    self.licks = self.licks.resize(len(self.licks) + 2000)

        if self.hold_rule:
            if (not self.holding) and np.any(self.ar.holding):
                self.holding = True
                self.pause(True)
            elif self.holding and not np.any(self.ar.holding):
                self.pause(False)
                self.holding = False
            if self.holding:
                self.speaker.pop(wait=False)

    def run_phase(self):
        ph = self.current_phase
        ph_dur = self.current_phase_duration
        dt_phase = now() - self.phase_start
        self.session_runtime = now() - self.session_on
        self.trial_runtime = now() - self.th.trial.start
        self.update_licked()

        # special cases
        if ph == PHASE_ITI and not self.rewarded:
            ph_dur *= 1 + self.penalty_iti_frac

        if self.paused and self.current_phase in [
                PHASE_INTRO, PHASE_STIM, PHASE_DELAY, PHASE_LICK
        ]:
            self.trial_kill = True
            return

        if self.trial_kill and not self.current_phase == PHASE_ITI:
            self.to_phase(PHASE_ITI)
            return

        # Intro
        if ph == PHASE_INTRO:
            self.light.set(0)
            if not self.intro_signaled:
                self.speaker.intro()
                self.intro_signaled = True
            if dt_phase >= ph_dur:
                self.to_phase(PHASE_STIM)
                return

        # Stim
        elif ph == PHASE_STIM:
            if self.th.rule_phase and any(self.licks['phase'] == PHASE_STIM):
                self.licked_early = self.licks[0]
                self.to_phase(PHASE_ITI)
                return

            if dt_phase >= ph_dur:
                self.to_phase(PHASE_DELAY)
                return

            if self.puffs_on and not self.stimulated:
                threading.Thread(target=self.stimulate).start()
                self.stimulated = True

        # Delay
        elif ph == PHASE_DELAY:
            if any(self.licks['phase'] == PHASE_DELAY) and self.th.rule_phase:
                self.licked_early = self.licks[0]
                self.to_phase(PHASE_ITI)
                return

            if dt_phase >= ph_dur:
                self.to_phase(PHASE_LICK)
                return

            if self.th.rule_hint_delay and now(
            ) - self.last_hint > self.next_hint_interval:
                self.stimulator.go(self.th.trial.side)
                self.last_hint = now()

                self.next_hint_interval = np.random.normal(*self.hint_interval)
                if self.next_hint_interval < 0:
                    self.next_hint_interval = self.hint_interval[0]

        # Lick
        elif ph == PHASE_LICK:

            #if self.retract_ports and (self.mp285 is not None) and (not self.moved_ports):
            #self.mp285_go(self.position_lick)
            #self.moved_ports = True
            if self.retract_ports and (self.actuator
                                       is not None) and (not self.moved_ports):
                self.actuator.extend()
                self.moved_ports = True

            if self.th.rule_hint_delay and now(
            ) - self.last_hint > self.next_hint_interval:  #and ((self.retract_ports and self.mp285.is_moving) or (not self.retract_ports)):
                self.stimulator.go(self.th.trial.side)
                self.last_hint = now()

                self.next_hint_interval = np.random.normal(*self.hint_interval)
                if self.next_hint_interval < 0:
                    self.next_hint_interval = self.hint_interval[0]

            if 'light' in self.go_cue:
                self.light.set(1)
            if 'sound' in self.go_cue and not self.laser_signaled:
                self.speaker.laser()
                self.laser_signaled = True

            #if self.mp285.is_moving:
            #    return # DO NOT PROCESS LICKS UNTIL MP285 REACHES DESTINATION

            if not self.th.rule_any:
                self.to_phase(PHASE_REWARD)
                return

            if any(self.licks['phase'] ==
                   PHASE_LICK) and not self.th.rule_fault:
                self.to_phase(PHASE_REWARD)
                return
            elif any(self.licks['phase'] ==
                     PHASE_LICK) and self.th.rule_fault and any(self.licks[
                         (self.licks['phase'] == PHASE_LICK)
                         & (self.licks['side'] == self.th.trial.side)]):
                self.to_phase(PHASE_REWARD)
                return

            # if time is up, to reward phase
            if dt_phase >= ph_dur:
                self.to_phase(PHASE_REWARD)
                return

        # Reward
        elif ph == PHASE_REWARD:
            if 'light' in self.go_cue:
                self.light.set(1)  #probably redundant

            if self.th.rule_side and any(self.licks[
                (self.licks['phase'] == PHASE_LICK)
                    & (self.licks['side'] == -self.th.trial.side +
                       1)]) and not self.rewarded and not self.th.rule_fault:
                # we arrived here after an incorrect lick
                if not self.wrong_signaled:
                    self.speaker.wrong()
                    self.wrong_signaled = True
                    self.do_reward = False
                #self.to_phase(PHASE_ITI) # by commenting it out, the ports sit there even though there's no reward
                #return

            # sanity check. cannot reach here if any incorrect licks, ensure that:
            if self.th.rule_side and (
                    not self.th.rule_fault) and self.do_reward == True:
                assert (not any(self.licks[
                    (self.licks['phase'] == PHASE_LICK)
                    & (self.licks['side'] == -self.th.trial.side + 1)]))

            # if no licks at all back in lick phase, go straight to ITI
            if self.th.rule_any and not any(
                    self.licks[self.licks['phase'] == PHASE_LICK]):
                self.to_phase(PHASE_ITI)
                return

            # if allowed multiple choices but only licked wrong side by the time the lick phase had ended
            if self.th.rule_any and self.th.rule_fault and not any(
                    self.licks[(self.licks['phase'] == PHASE_LICK)
                               & (self.licks['side'] == self.th.trial.side)]):
                self.to_phase(PHASE_ITI)
                return

            # sanity check. can only reach here if licked correct side only
            if self.th.rule_any and self.th.rule_side and self.do_reward:
                assert any(
                    self.licks[(self.licks['side'] == self.th.trial.side)
                               & (self.licks['phase'] == PHASE_LICK)])

            # from this point on, it is assumed that rewarding should occur if do_reward is True (otherwise would either have moved to ITI, or do_reward would now be False)

            if self.use_trials:
                rside = self.th.trial.side
            else:
                rside = (self.licks[self.licks['phase'] == PHASE_LICK].side)[0]

            if self.rewards_on and (not self.rewarded) and self.do_reward:
                self.spout.go(side=rside, scale=self.th.reward_scale)
                self.rewarded = now()
                self.rewards_given += 1

            if self.th.rule_hint_reward and now(
            ) - self.last_hint > self.next_hint_interval:
                self.stimulator.go(self.th.trial.side)
                self.last_hint = now()

                self.next_hint_interval = np.random.normal(*self.hint_interval)
                if self.next_hint_interval < 0:
                    self.next_hint_interval = self.hint_interval[0]

            if dt_phase >= ph_dur:
                self.to_phase(PHASE_ITI)
        # ITI
        elif ph == PHASE_ITI:
            #if self.retract_ports and (self.mp285 is not None) and (not self.returned_ports):
            #    self.mp285_go(self.position_stim)
            #    self.returned_ports = True
            if self.retract_ports and (self.actuator is not None) and (
                    not self.returned_ports):
                self.actuator.retract()
                self.returned_ports = True

            self.light.set(0)

            if any((self.licks['phase'] > PHASE_INTRO)
                   & (self.licks['phase'] < PHASE_LICK)
                   ) and self.th.rule_phase and not self.error_signaled:
                self.speaker.error()
                self.error_signaled = True

            if dt_phase >= ph_dur:
                if self.rewarded or (
                        self.motion_control and self.ar.moving) or (
                            not self.motion_control) or self.session_kill:
                    self.to_phase(PHASE_END)
                else:
                    pass
                return

    def next_trial(self):

        self.th.next_trial()

        # Phase reset
        self.to_phase(PHASE_INTRO)
        _ = self.ar.licked  # to clear any residual signal

        # Check for mp285 adjustment
        if self.th.do_adjust_mp285 is not False:
            adj = self.th.do_adjust_mp285
            logging.info('Adjusting MP285, moving to side {}'.format(adj))
            self.mp285.nudge(adj)

        # Trial-specific runtime vars
        self.licks = np.zeros((2000, ),
                              dtype=[('phase', int), ('ts', float),
                                     ('side', int)])
        self.lick_idx = 0

        # Event trackers
        self.stim_idx = 0
        self.do_reward = True  # until determined otherwise
        self.rewarded = False
        self.wrong_signaled = False
        self.error_signaled = False
        self.laser_signaled = False
        self.intro_signaled = False
        self.moved_ports = False
        self.returned_ports = False
        self.stimulated = False
        self.licked_early = False
        self.trial_kill = False
        self.last_hint = -1
        self.next_hint_interval = self.hint_interval[0]

        while self.current_phase != PHASE_END:
            self.run_phase()

        # Return value indicating whether another trial is appropriate
        if self.session_kill:
            self.paused = False
            return False
        else:
            return True

    def mp285_go(self, pos):
        threading.Thread(target=self.mp285.goto, args=(pos, )).start()

    def email_update(self):
        p = self.th.history_glob.iloc[-10:].fillna(0)
        ntrials = len(p)
        tim = int((now() - self.session_on) / 60.)
        s = 'Subject: {}\nTime: {} mins\nN Trials: {}\nRewards: {}\nLevel: {}\nPerformance:\n{}\n'.format(
            self.subj.name, tim, ntrials, self.rewards_given, self.th.level,
            str(p))
        email_alert(s,
                    subject='Rig{}, {}min, {}, {}'.format(
                        config.rig_id, tim, self.subj.name,
                        self.name_as_str()),
                    figure=self.live_figure)

    def run(self):
        try:
            self.session_on = now()
            self.ar.begin_saving()
            self.cam.begin_saving()

            self.sic.start_acq()

            cont = True
            last_email = now() - 960
            while cont:
                if now() - last_email > (900):
                    threading.Thread(target=self.email_update).start()
                    last_email = now()
                cont = self.next_trial()

            self.sic.stop_acq()

            self.end()
        except:
            logging.error('Session has encountered an error!')
            email_alert('Session error!',
                        subject='ERROR! Puffs Interface Alert')
            raise

    def end(self):
        self.actuator.saver = None
        to_end = [
            self.ar, self.stimulator, self.spout, self.light, self.cam,
            self.opto, self.sic
        ]
        for te in to_end:
            try:
                te.end()
            except:
                warnings.warn('Failed to end one of the processes: {}'.format(
                    str(te)))
            time.sleep(0.100)
        self.saver.end(notes=self.notes)
        self.session_on = False

    def get_code(self):
        py_files = [
            pjoin(d, f) for d, _, fs in os.walk(os.getcwd()) for f in fs
            if f.endswith('.py') and not f.startswith('__')
        ]
        code = {}
        for pf in py_files:
            with open(pf, 'r') as f:
                code[pf] = f.read()
        return json.dumps(code)