예제 #1
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)
예제 #2
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)