Get asymmetries for 2e random-frequency scan. 
            Based on bnmr_2e.cpp by rmlm (Oct 4, 2017).

def _get_2e_asym(self):
        """
            Get asymmetries for 2e random-frequency scan. 
            Based on bnmr_2e.cpp by rmlm (Oct 4, 2017).
        """
        
        # get needed PPG parameters for splitting 1D histos into 2D histos
        try:        
            # get frequency vector
            freq = np.arange(self.ppg['freq_start'].mean,\
                             self.ppg['freq_stop'].mean+\
                                    self.ppg['freq_incr'].mean,\
                             self.ppg['freq_incr'].mean)
                             
            # number of dwelltimes per frequency bin 
            ndwell = 2*int(self.ppg['ndwell_per_f'].mean)-1
            
            # number of RF on delays for the start bin. 
            start_bin = int(self.ppg['rf_on_delay'].mean)
            
            # get bin centers in ms
            time = self.ppg['rf_on_ms'].mean*(np.arange(ndwell)+0.5-ndwell/2.)
            
            # get the time and index of the middle time 
            mid_time_i = int(np.floor(ndwell/2.))
            mid_time = time[mid_time_i]
        
            # beam off time after pulse in ms
            beam_off = int(self.ppg['beam_off_ms'].mean)
        
        except KeyError:
            raise RuntimeError("Not all dictionary variables read out to "+\
                               "proper locations")
            
        # setup output
        out = bdict()
        out['freq'] = freq
        out['time'] = time
            
        # get data
        data = np.array(self._get_area_data()) # [[fp], [bfm], [bp], [bm]]
        
        # discared initial bad bins, and beam-off trailing bins
        data = data[:,start_bin:len(freq)*ndwell+start_bin]
        
        # split data by frequency
        nsplit = len(data[0])/ndwell
        
        fp = np.array(np.split(data[0],nsplit))
        fm = np.array(np.split(data[1],nsplit))
        bp = np.array(np.split(data[2],nsplit))
        bm = np.array(np.split(data[3],nsplit))
        
        # get raw asymmetries 
        asym_p_2cntr = (bp-fp)/(bp+fp)      # two counter
        asym_m_2cntr = (bm-fm)/(bm+fm)      # two counter
        r = np.sqrt(bp*fm/(bm*fp))
        asym_4cntr = (r-1)/(r+1)            # four counter
        
        # get raw asymmetry errors
        asym_p_2cntr_err = 2*np.sqrt(bp*fp)/((bp+fp)**1.5)
        asym_m_2cntr_err = 2*np.sqrt(bm*fm)/((bm+fm)**1.5)
        asym_4cntr_err = r*np.sqrt(1./bp+1./bm+1./fp+1./fm)/((r+1)**2)

        # save to output
        out['raw_p'] = np.array([asym_p_2cntr,asym_p_2cntr_err])
        out['raw_n'] = np.array([asym_m_2cntr,asym_m_2cntr_err])
        out['raw_c'] = np.array([asym_4cntr,asym_4cntr_err])

        # wrap asymmetry arrays into one for calculations [p,m,4]
        # indexing is now [pm4][freq][time bin]
        asym     = np.array([asym_p_2cntr,    asym_m_2cntr,    asym_4cntr])
        asym_err = np.array([asym_p_2cntr_err,asym_m_2cntr_err,asym_4cntr_err])

        # compute differenced asymmetries via slopes from weighted least squares 
        # minimization.
        if ndwell >= 5:
            
            # calculate needed components element-wise
            w = asym_err**-2
            x = time
            y = asym
            
            wx = w*x
            wy = w*y
            wxy = w*x*y
            wxx = w*x*x
        
            # sum over values i < mid_time_i within each asymmetry and frequency 
            # Indexing: [pm4][freq]
            w_pre   = np.sum(w  [:,:,:mid_time_i],2)    
            wx_pre  = np.sum(wx [:,:,:mid_time_i],2)
            wy_pre  = np.sum(wy [:,:,:mid_time_i],2)
            wxy_pre = np.sum(wxy[:,:,:mid_time_i],2)
            wxx_pre = np.sum(wxx[:,:,:mid_time_i],2)
            
            # sum over values i > mid_time_i
            w_pst   = np.sum(w  [:,:,-mid_time_i:],2)
            wx_pst  = np.sum(wx [:,:,-mid_time_i:],2)
            wy_pst  = np.sum(wy [:,:,-mid_time_i:],2)
            wxy_pst = np.sum(wxy[:,:,-mid_time_i:],2)
            wxx_pst = np.sum(wxx[:,:,-mid_time_i:],2)
            
            # calculate slopes and intercepts
            delta_pre = w_pre*wxx_pre - wx_pre**2
            delta_pst = w_pst*wxx_pst - wx_pst**2
            
            sl_pre = (w_pre*wxy_pre - wx_pre*wy_pre)/delta_pre
            sl_pst = (w_pst*wxy_pst - wx_pst*wy_pst)/delta_pst
            dsl_pre = np.sqrt(w_pre/delta_pre)
            dsl_pst = np.sqrt(w_pst/delta_pst)
            
            intr_pre = (wy_pre*wxx_pre - wx_pre*wxy_pre)/delta_pre
            intr_pst = (wy_pst*wxx_pst - wx_pst*wxy_pst)/delta_pst
            dintr_pre = np.sqrt(wxx_pre/delta_pre)
            dintr_pst = np.sqrt(wxx_pst/delta_pst)
            
            # extrapolate to middle time bin
            asym_slopes = intr_pst-intr_pre+(sl_pst-sl_pre)*mid_time
            asym_slopes_err = np.sqrt(dintr_pre**2 + dintr_pst**2 + \
                                      (dsl_pre**2 + dsl_pst**2) * mid_time**2)
            
            # save to output        
            out['sl_p'] = np.array([asym_slopes[0],asym_slopes_err[0]])
            out['sl_n'] = np.array([asym_slopes[1],asym_slopes_err[1]])
            out['sl_c'] = np.array([asym_slopes[2],asym_slopes_err[2]])
        
        # calculate asymmetry using differences
        asym_diff = asym[:,:,mid_time_i+1] - asym[:,:,mid_time_i-1]
        asym_diff_err = np.sqrt(asym_err[:,:,mid_time_i+1]**2+\
                                asym_err[:,:,mid_time_i-1]**2)
        
        # save to output
        out['dif_p'] = np.array([asym_diff[0],asym_diff_err[0]])
        out['dif_n'] = np.array([asym_diff[1],asym_diff_err[1]])
        out['dif_c'] = np.array([asym_diff[2],asym_diff_err[2]])   
            
        return out