Beispiel #1
0
def fsolve(func, x0, args=(),
           fprime=None, full_output=0, col_deriv=0,
           xtol=1.49012e-08, maxfev=0, band=None,
           epsfcn=0.0, factor=100, diag=None):
    '''
    Wrapped scipy.optimize.fsolve to handle units

    Presumably func and x0 have units associated with them.
    We simply wrap the function, 
    '''

    def wrappedfunc(x, *args):
        x = Unit(x, x0.exponents, x0.label)
        return float(func(x))
    
    if full_output:
        x, infodict, ier, mesg = _fsolve(wrappedfunc, float(x0), args,
                                         fprime, full_output, col_deriv,
                                         xtol, maxfev, band,
                                         epsfcn, factor, diag)
        x = Unit(x, x0.exponents, x0.label)
        return x, infodict, ier, mesg
    else:
        x, = _fsolve(wrappedfunc, float(x0), args,
                    fprime, full_output, col_deriv,
                    xtol, maxfev, band,
                    epsfcn, factor, diag)
        
        x = Unit(x, x0.exponents, x0.label)
        return x
Beispiel #2
0
def fsolve(func,
           t0,
           args=(),
           fprime=None,
           full_output=0,
           col_deriv=0,
           xtol=1.49012e-08,
           maxfev=0,
           band=None,
           epsfcn=0.0,
           factor=100,
           diag=None):
    """wrapped fsolve command to work with units.

    We get the units on the function argument, then wrap the function so we can
    add units to the argument and return floats. Finally we call the original
    fsolve from scipy.

    """

    try:
        # units on initial guess, normalized
        tU = [t / float(t) for t in t0]
    except TypeError:
        tU = t0 / float(t0)

    def wrapped_func(t, *args):
        't will be unitless, so we add unit to it. t * tU has units.'
        try:
            T = [x1 * x2 for x1, x2 in zip(t, tU)]
        except TypeError:
            T = t * tU

        try:
            return [float(x) for x in func(T, *args)]
        except TypeError:
            return float(func(T))

    sol = _fsolve(wrapped_func, t0, args, fprime, full_output, col_deriv, xtol,
                  maxfev, band, epsfcn, factor, diag)

    if full_output:
        x, infodict, ier, mesg = sol
        try:
            x = [x1 * x2 for x1, x2 in zip(x, tU)]
        except TypeError:
            x = x * tU
        return x, infodict, ier, mesg
    else:
        try:
            x = [x1 * x2 for x1, x2 in zip(sol, tU)]
        except TypeError:
            x = sol * tU
        return x
Beispiel #3
0
def wave_number(f, h, rho=1025, g=9.80665):
    """
    Calculates wave number
    
    To compute wave number from angular frequency (w), convert w to f before 
    using this function (f = w/2*pi)
    
    Parameters
    -----------
    f: numpy array
        Frequency [Hz]
    h: float
        Water depth [m]
    rho: float (optional)
        Water density [kg/m^3]
    g: float (optional)
        Gravitational acceleration [m/s^2]
    
    Returns
    -------
    k: pandas DataFrame
        Wave number [1/m] indexed by frequency [Hz]
    """
    try:
        f = np.array(f)
    except:
        pass
    assert isinstance(f, np.ndarray), 'f must be of type np.ndarray'
    assert isinstance(h, (int, float)), 'h must be of type int or float'
    assert isinstance(rho, (int, float)), 'rho must be of type int or float'
    assert isinstance(g, (int, float)), 'g must be of type int or float'

    w = 2 * np.pi * f  # angular frequency
    xi = w / np.sqrt(g / h)  # note: =h*wa/sqrt(h*g/h)
    yi = xi * xi / np.power(1.0 - np.exp(-np.power(xi, 2.4908)), 0.4015)
    k0 = yi / h  # Initial guess without current-wave interaction

    # Eq 11 in IEC 62600-101 using initial guess from Guo (2002)
    def func(kk):
        val = np.power(w, 2) - g * kk * np.tanh(kk * h)
        return val

    mask = np.abs(func(k0)) > 1e-9
    if mask.sum() > 0:
        k0_mask = k0[mask]
        w = w[mask]

        k, info, ier, mesg = _fsolve(func, k0_mask, full_output=True)
        assert ier == 1, 'Wave number not found. ' + mesg
        k0[mask] = k

    k = pd.DataFrame(k0, index=f, columns=['k'])

    return k
Beispiel #4
0
def fsolve(func, t0, args=(),
           fprime=None, full_output=0, col_deriv=0,
           xtol=1.49012e-08, maxfev=0, band=None,
           epsfcn=0.0, factor=100, diag=None):
    """wrapped fsolve command to work with units.

    We get the units on the function argument, then wrap the function so we can
    add units to the argument and return floats. Finally we call the original
    fsolve from scipy.

    """

    try:
        # units on initial guess, normalized
        tU = [t / float(t) for t in t0]
    except TypeError:
        tU = t0 / float(t0)

    def wrapped_func(t, *args):
        't will be unitless, so we add unit to it. t * tU has units.'
        try:
            T = [x1 * x2 for x1, x2 in zip(t, tU)]
        except TypeError:
            T = t * tU

        try:
            return [float(x) for x in func(T, *args)]
        except TypeError:
            return float(func(T))

    sol = _fsolve(wrapped_func, t0, args,
                  fprime, full_output, col_deriv,
                  xtol, maxfev, band,
                  epsfcn, factor, diag)

    if full_output:
        x, infodict, ier, mesg = sol
        try:
            x = [x1 * x2 for x1, x2 in zip(x, tU)]
        except TypeError:
            x = x * tU
        return x, infodict, ier, mesg
    else:
        try:
            x = [x1 * x2 for x1, x2 in zip(sol, tU)]
        except TypeError:
            x = sol * tU
        return x
Beispiel #5
0
def nsolve(objective, x0, *args, **kwargs):
    """A Wrapped version of scipy.optimize.fsolve.

    objective: a callable function f(x) = 0
    x0: the initial guess for the solution.

    This version warns you if the call did not finish cleanly and prints the message.

    Returns:
       If there is only one result it returns a float, otherwise it returns an array.
    """
    if 'full_output' not in kwargs:
        kwargs['full_output'] = 1

    ans, info, flag, msg = _fsolve(objective, x0, *args, **kwargs)

    if flag != 1:
        raise Exception('nsolve did not finish cleanly: {}'.format(msg))

    if len(ans) == 1:
        return float(ans)
    else:
        return ans
Beispiel #6
0
def nsolve(objective, x0, *args, **kwargs):
    """A Wrapped version of scipy.optimize.fsolve.

    objective: a callable function f(x) = 0
    x0: the initial guess for the solution.

    This version warns you if the call did not finish cleanly and prints the message.

    Returns:
       If there is only one result it returns a float, otherwise it returns an array.
    """
    if 'full_output' not in kwargs:
        kwargs['full_output'] = 1

    ans, info, flag, msg = _fsolve(objective, x0, *args, **kwargs)

    if flag != 1:
        raise Exception('nsolve did not finish cleanly: {}'.format(msg))

    if len(ans) == 1:
        return float(ans)
    else:
        return ans
Beispiel #7
0
def kappa_from_fofrh_and_sizedist(f_of_RH, dist, wavelength, RH, verbose = False, f_of_RH_collumn = None):
    """
    Calculates kappa from f of RH and a size distribution.
    Parameters
    ----------
    f_of_RH: TimeSeries
    dist: SizeDist
    wavelength: float
    RH: float
        Relative humidity at which the f_of_RH is taken.
    column: string
        when f_of_RH has more than one collumn name the one to be used
    verbose: bool

    Returns
    -------
    TimeSeries
    """

    def minimize_this(gf, sr, f_rh_soll, ext, wavelength, verbose = False):
        gf = float(gf)
        sr_g = sr.apply_growth(gf, how='shift_bins')
        sr_g_opt = sr_g.calculate_optical_properties(wavelength)
        ext_g = sr_g_opt.extinction_coeff_sum_along_d.data.values[0][0]
        f_RH = ext_g / ext
        out = f_RH - f_rh_soll

        if verbose:
            print('test growhtfactor: %s'%gf)
            print('f of RH soll/is: %s/%s'%(f_rh_soll,f_RH))
            print('diviation from soll: %s'%out)
            print('---------')
        return out

    # make sure f_of_RH has only one collumn
    if f_of_RH.data.shape[1] > 1:
        if not f_of_RH_collumn:
            txt = 'f_of_RH has multiple collumns (%s). Please name the one you want to use by setting the f_of_RH_collumn argument.'%(f_of_RH.data.columns)
            raise ValueError(txt)
        else:
            f_of_RH = f_of_RH._del_all_columns_but(f_of_RH_collumn)

    n_values = dist.data.shape[0]
    gf_calc = _np.zeros(n_values)
    kappa_calc = _np.zeros(n_values)
    f_of_RH_aligned = f_of_RH.align_to(dist)
    for e in range(n_values):
        frhsoll = f_of_RH_aligned.data.values[e][0]
        if _np.isnan(frhsoll):
            kappa_calc[e] = _np.nan
            gf_calc[e]  = _np.nan
            continue

        if type(dist.index_of_refraction).__name__ == 'float':
            ior = dist.index_of_refraction
        else:
            ior = dist.index_of_refraction.iloc[e][0]
        if _np.isnan(ior):
            kappa_calc[e] = _np.nan
            gf_calc[e]  = _np.nan
            continue

        sr = dist.copy()
        sr.data = sr.data.iloc[[e],:]
        sr.index_of_refraction = ior
        sr_opt = sr.calculate_optical_properties(wavelength)
        ext = sr_opt.extinction_coeff_sum_along_d.data.values[0][0]


        if ext == 0:
            kappa_calc[e] = _np.nan
            gf_calc[e]  = _np.nan
            continue

        if verbose:
            print('goal for f_rh: %s'%frhsoll)
            print('=======')

        gf_out = _fsolve(minimize_this, 1, args = (sr, frhsoll, ext, wavelength, verbose), factor=0.5, xtol = 0.005)
        gf_calc[e] = gf_out

        if verbose:
            print('resulting gf: %s'%gf_out)
            print('=======\n')

        kappa_calc[e] = kappa_simple(gf_out, RH, inverse=True)
    ts_kappa = _timeseries.TimeSeries(_pd.DataFrame(kappa_calc, index = f_of_RH_aligned.data.index, columns= ['kappa']))
    ts_kappa._data_period = f_of_RH_aligned._data_period
    ts_kappa._y_label = '$\kappa$'

    ts_gf = _timeseries.TimeSeries(_pd.DataFrame(gf_calc, index = f_of_RH_aligned.data.index, columns= ['growth factor']))
    ts_kappa._data_period = f_of_RH_aligned._data_period
    ts_gf._y_label = 'growth factor$'
    return ts_kappa, ts_gf
Beispiel #8
0
def kappa_from_fofrh_and_sizedist(f_of_RH,
                                  dist,
                                  wavelength,
                                  RH,
                                  verbose=False):
    """
    Calculates kappa from f of RH and a size distribution.
    Parameters
    ----------
    f_of_RH: TimeSeries
    dist: SizeDist
    wavelength: float
    RH: float
        Relative humidity at which the f_of_RH is taken.
    verbose: bool

    Returns
    -------
    TimeSeries
    """
    def minimize_this(gf, sr, f_rh_soll, ext, wavelength, verbose=False):
        gf = float(gf)
        sr_g = sr.apply_growth(gf, how='shift_bins')
        sr_g_opt = sr_g.calculate_optical_properties(wavelength)
        ext_g = sr_g_opt.extinction_coeff_sum_along_d.data.values[0][0]
        f_RH = ext_g / ext
        out = f_RH - f_rh_soll

        if verbose:
            print('test growhtfactor: %s' % gf)
            print('f of RH soll/is: %s/%s' % (f_rh_soll, f_RH))
            print('diviation from soll: %s' % out)
            print('---------')
        return out

    n_values = dist.data.shape[0]
    gf_calc = _np.zeros(n_values)
    kappa_calc = _np.zeros(n_values)
    f_of_RH_aligned = f_of_RH.align_to(dist)
    for e in range(n_values):
        frhsoll = f_of_RH_aligned.data.values[e][0]
        if _np.isnan(frhsoll):
            kappa_calc[e] = _np.nan
            gf_calc[e] = _np.nan
            continue

        ior = dist.index_of_refraction.iloc[e][0]
        if _np.isnan(ior):
            kappa_calc[e] = _np.nan
            gf_calc[e] = _np.nan
            continue

        sr = dist.copy()
        sr.data = sr.data.iloc[[e], :]
        sr.index_of_refraction = ior
        sr_opt = sr.calculate_optical_properties(wavelength)
        ext = sr_opt.extinction_coeff_sum_along_d.data.values[0][0]

        if ext == 0:
            kappa_calc[e] = _np.nan
            gf_calc[e] = _np.nan
            continue

        if verbose:
            print('goal for f_rh: %s' % frhsoll)
            print('=======')

        gf_out = _fsolve(minimize_this,
                         1,
                         args=(sr, frhsoll, ext, wavelength, verbose),
                         factor=0.5,
                         xtol=0.005)
        gf_calc[e] = gf_out

        if verbose:
            print('resulting gf: %s' % gf_out)
            print('=======\n')

        kappa_calc[e] = kappa_simple(gf_out, RH, inverse=True)
    ts_kappa = _timeseries.TimeSeries(
        _pd.DataFrame(kappa_calc,
                      index=f_of_RH_aligned.data.index,
                      columns=['kappa']))
    ts_kappa._data_period = f_of_RH_aligned._data_period
    ts_kappa._y_label = '$\kappa$'

    ts_gf = _timeseries.TimeSeries(
        _pd.DataFrame(gf_calc,
                      index=f_of_RH_aligned.data.index,
                      columns=['growth factor']))
    ts_kappa._data_period = f_of_RH_aligned._data_period
    ts_gf._y_label = 'growth factor$'
    return ts_kappa, ts_gf
Beispiel #9
0
def partition_with_curve(x, numpart, curve, method='brentq', return_exp=False, excluded=[]):
    """
    Partition `x` in `numparts` parts following bpf

    x : float     --> the value to partition
    numpart : int --> the number of partitions
    curve : bpf   --> the curve to follow.
                      It is not important over which interval x it is defined.
                      The y coord defines the width of the partition (see example)
    return_exp : bool --> | False -> the return value is the list of the partitions
                          | True  -> the return value is a tuple containing the list
                                     of the partitions and the exponent of the
                                     weighting function

    Returns: the list of the partitions

    Example
    =======

    # Partition the value 45 into 7 partitions following the given curve
    >>> import bpf4 as bpf
    >>> curve = bpf.halfcos2(0, 11, 1, 0.5, exp=0.5)
    >>> distr = partition_with_curve(45, 7, curve)
    >>> distr
    array([ 11.        ,  10.98316635,  10.4796218 ,   7.89530421,
             3.37336152,   0.76854613,   0.5       ])
    >>> abs(sum(distr) - 45) < 0.001
    True
    """
    x0, x1 = curve.bounds()
    n = x

    def func(r):
        return sum((bpf.expon(x0, x0, x1, x1, exp=r)|curve).map(numpart)) - n
    try:
        if method == 'brentq':
            r = _brentq(func, x0, x1)
            curve = bpf.expon(x0, x0, x1, x1, exp=r)|curve
            parts = curve.map(numpart)
        elif method == 'fsolve':
            xs = np.linspace(x0, x1, 100)
            rs = [round(float(_fsolve(func, x)), 10) for x in xs]
            rs = set(r for r in rs if x0 <= r <= x1 and r not in excluded)
            parts = []
            for r in rs:
                curve = bpf.expon(x0, x0, x1, x1, exp=r)|curve
                parts0 = curve.map(numpart)
                parts.extend(parts0)
    except ValueError:
        minvalue = curve(bpf.minimum(curve))
        maxvalue = curve(bpf.maximum(curve))
        if n/numpart < minvalue:
            s = """
        no solution can be found for the given parameters. x is too small
        for the possible values given in the bpf, for this amount of partitions
        try either giving a bigger x, lowering the number of partitions or
        allowing smaller possible values in the bpf
                """
        elif n/numpart > maxvalue:
            s = """
            no solution can be found for the given parameters. x is too big
        for the possible values given in the bpf. try either giving a
        smaller x, increasing the number of partitions or allowing bigger
        possible values in the bpf
                """
        else:
            s = """???"""
        ERROR['partition_with_curve.func'] = func
        raise ValueError(s)
    if abs(sum(parts) - n)/n > 0.001:
        print("Error exceeds threshold: ", parts, sum(parts))
    if return_exp:
        return parts, r
    return parts
Beispiel #10
0
    def run(self,
            field                = 5.0,             # V/cm
            temperature          = 295.15,          # K
            runtime              = None,            # seconds
            exposure             = 0.5,             # [0-1]
            plot                 = True,
            res                  = 200,              # px/cm
            cursor_ovr           = {'hover': False},
            back_col             = 0.3,
            band_col             = 1,
            well_col             = 0.05,
            noise                = 0.015,
            detectlim            = 0.04,
            interpol             = 'linear',   # 'cubic','nearest'
            dset_name            = 'vertical', # 'horizontal'
            replNANs             = True):      # replace NANs by 'nearest' interpolation

        max_mob = 0
        
        for i,lane in enumerate(self.samples):            
            for j,frag in enumerate(lane):
                mob      = size_to_mobility(len(frag), 
                                            field, 
                                            self.gel_concentration) # cm/s
                frag.mobility = mob
                self.samples[i][j] = frag
                max_mob = max((max_mob, mob))

        # vWBR eq. parameters muL, muS, gamma

        mu = _np.zeros(100)
        for i, Li in enumerate(_np.linspace(100, 50000, 100)):
            mu[i] = size_to_mobility(Li, 
                                     field, 
                                     self.gel_concentration)

        muS0 = 3.5E-4  # cm^2/(V.sec)  ############################################
        muL0 = 1.0E-4  # cm^2/(V.sec)  ############################################
        gamma0 = 8000  # bp            ############################################
        
        vWBR = lambda L, muS, muL, gamma: (1/muS+(1/muL-1/muS)*(1-_np.exp(-L/gamma)))**-1
        
        def residuals(pars, L, mu):
            return mu - vWBR(L, *pars)
        
        pars, cov, infodict, mesg, ier = _leastsq(residuals, 
                                                  [muS0, muL0, gamma0],
                                                 args=(_np.linspace(100, 50000, 100), mu),
                                                 full_output=True)
        muS, muL, gamma = pars

        time = runtime or (0.9 * self.gel_length)/max_mob  # sec

        # Free solution mobility estimate
        
        space = _np.logspace(_np.log10(100), _np.log10(3000), 10)        
        DNAspace_for_mu0     = _np.array([round(val, 0) for val in space])
        
        Tvals = _np.unique(dset['T']) # (g/(100 mL))*100

        # Mobility dependence on size (mu(L)) for each agarose percentage (Ti)
        ln_mu_LxT = []
        for Lj in DNAspace_for_mu0:
            ln_mu_T = []
            for Ti in Tvals:
                mu = size_to_mobility(Lj, field, Ti)
                ln_mu_T.append(_np.log(mu))
            ln_mu_LxT.append(ln_mu_T)
        ln_mu_LxT = _np.array(ln_mu_LxT)
        # Linear regression for each DNA size
        lregr_stats = []
        exclude = []
        for l in range(len(DNAspace_for_mu0)):
            not_nan = _np.logical_not(_np.isnan(ln_mu_LxT[l]))
            if sum(not_nan) > 1:
                # (enough points for linear regression)
                gradient, intercept, r_value, p_value, std_err =\
                          _stats.linregress(Tvals[not_nan], ln_mu_LxT[l][not_nan])
                lregr_stats.append((gradient, intercept, r_value, p_value,
                                    std_err))
                exclude.append(False)
            else:
                exclude.append(True)
        exclude = _np.array(exclude)
        ln_mu_LxT = ln_mu_LxT[~exclude]
        if len(lregr_stats) > 0:
            # Free solution mobility determination
            ln_mu0 = _np.mean([row[1] for row in lregr_stats])  # mean of intercepts
            mu0 = _np.exp(ln_mu0)  # cm^2/(V.seg)
        else:
            mu0 = None
       

        self.freesol_mob = mu0

        eta = 2.414E-5*10**(247.8*(temperature-140))  # (Pa.s)=(kg/(m.s)) accurate to within 2.5% from 0 °C to 370 °C
    
        pore_size = lambda gamma, muL, mu0, lp, b: (gamma*muL*lp*b/mu0)**(1/2)

        pore_size_fit = lambda C: 143*C**(-0.59)
        
        a = pore_size(gamma, muL, mu0, lp, b)
        a_fit = pore_size_fit(self.gel_concentration)  # ##################################
        a_fit = a_fit.to('m')              # ##################################
        self.poresize = a
        self.poresize_fit = a_fit
        reduced_field = lambda eta, a, mu0, E, kB, T: eta*a**2*mu0*E/(kB*T)
        epsilon = reduced_field(eta, a, mu0, field*100, kB, temperature)
        # Diffusion coefficient of a blob
        Dblob = lambda kB, T, eta, a: kB*T/(eta*a)
        Db = Dblob(kB, temperature, eta, a)
        
        # Diffusion regime frontiers (in number of occupied pores)

        def diff_Zimm_Rouse(Nbp, args):
            kB, T, qeff, eta, mu0, a, b, l, lp = args
            Nbp = Nbp[0]
            L = Nbp * b
            Rg = radius_gyration(L, lp)
            D0 = free_solution(kB, T, eta, Rg)
            DRouse = Ogston_Rouse(Nbp, kB, T, a, eta, b, l)
            g = Zimm_g(Nbp, DRouse, qeff, mu0, kB, T)
            g = g.to_base_units()
            DZimm = Ogston_Zimm(D0, g)
            return DZimm - DRouse
        
        Zimm_Rouse = lambda x0, args: (
            Nbp_to_N(_fsolve(diff_Zimm_Rouse, x0, args)[0],args[5], args[6], args[7]))
        
        equil_accel = lambda epsilon: epsilon**(-2/3)
        accel_plateau = lambda epsilon: epsilon**(-1)
        
        
        N_lim1 = accel_plateau(epsilon)     # #################################
        N_lim2 = equil_accel(epsilon)       # # ***   Major problem    ***   ##
        N_lim3 = Zimm_Rouse(2000,           # #################################
                            [kB, temperature, qeff, eta, mu0, a, b, l, lp])

        N_to_Nbp = lambda N, a, b, l: N*(l/b)*(a/l)**2      # number of base pairs (bp)
        self.accel_to_plateau = N_to_Nbp(N_lim1, a, b, l)
        self.equil_to_accel = N_to_Nbp(N_lim2, a, b, l)
        self.Zimm_to_Rouse = N_to_Nbp(N_lim3, a, b, l)
        
        for i,lane in enumerate(self.samples):            
            for j,frag in enumerate(lane):
                Nbp = len(frag)
                N = Nbp_to_N(Nbp, a, b, l)
                if N < N_lim3:  # Ogston-Zimm                    
                    L = Nbp * b   # (m)
                    Rg = radius_gyration(L, lp)  # (m)
                    D0 = free_solution(kB, temperature, eta, Rg)  # (m^2/s)
                    DRouse = Ogston_Rouse(Nbp, kB, temperature,
                                          a, eta, b, l)  # (m^2/s)
                    g = Zimm_g(Nbp, DRouse, qeff, mu0, kB, temperature)  # base
                    D = Ogston_Zimm(D0, g)                               # unit
                elif N < N_lim2: # Rouse/Reptation-equilibrium                    
                    D = Db/N**2
                elif N > N_lim1: # Reptation-plateau (reptation with orientation)                    
                    D = Db*epsilon**(3/2)
                else:            # Accelerated-reptation
                    D = Db*epsilon*N**(-1/2)
                frag.band_width = _np.sqrt(2*D*time) + self.welly  
                self.samples[i][j]=frag

        # Total bandwidths
        time0 = self.welly/(mu0*field)
        
        bandwidths0 = [mobs*time0[l]*field for l, mobs in
                       enumerate(mobilities)]

        # Max intensities
        raw_Is = []
        maxI = Q_(-_np.inf, 'ng/cm')
        minI = Q_(_np.inf, 'ng/cm')
        for i,lane in enumerate(self.samples):
            lane_I = []
            for j,frag in enumerate(lane):
                frag_Qty = quantities[i][j]
                frag_Wth = bandwidths[i][j]  # w=FWHM or w=FWTM ???
                if False:
                    FWHM = Gauss_FWHM(frag_Wth)
                else:
                    FWHM = frag_Wth
                std_dev = Gauss_dev(FWHM)
                auc = frag_Qty  # area under curve proportional to DNA quantity
                Gauss_hgt = lambda auc, dev: auc/(dev*_np.sqrt(2*_np.pi))
                frag_I = Gauss_hgt(auc, std_dev)  # peak height
                if frag_I > maxI:
                    maxI = frag_I
                if frag_I < minI:
                    minI = frag_I
                lane_I.append(frag_I)
            raw_Is.append(lane_I)

        # max intensity normalization
        satI = maxI+exposure*(minI-maxI)
        intensities = [[(1-back_col)/satI*fragI for fragI in lane]
                       for lane in raw_Is]
        self.intensities = intensities

        # Plot gel
        if plot:
            # Title
            mins, secs = divmod(time.to('s').magnitude, 60)  # time is in secs
            hours, mins = divmod(mins, 60)
            hours = Q_(hours, 'hr')
            mins = Q_(mins, 'min')
            secs = Q_(secs, 's')
            title = ('E = %.2f V/cm\n'
                     'C = %.1f %%\n'
                     'T = %.2f K\n'
                     't = %d h %02d m\n'
                     'expo = %.1f' % (field,
                                      self.gel_concentration,
                                      temperature,
                                      hours, mins,
                                      exposure))

            gel_width = len(samples) * (self.wellx + self.wellsep) + self.wellsep  # cm

            pxl_x = int( gel_width * res)
            pxl_y = int( gel_len   * res)
            lane_centers = [(l+1)*self.wellsep + sum(wellx[:l]) + 0.5*wellx[l]
                            for l in range(nlanes)]
            rgb_arr = _np.zeros(shape=(pxl_y, pxl_x, 3), dtype=_np.float32)
            bandlengths = wellx
            bands_pxlXYmid = []
            # Paint the bands
            for i in range(nlanes):
                distXmid = lane_centers[i]
                pxlXmid = int(round((distXmid * res).magnitude))
                bandlength = bandlengths[i]
                from_x = int(round(((distXmid - bandlength/2.0) * res).magnitude))
                to_x = int(round(((distXmid + bandlength/2.0) * res).magnitude))
                bands_pxlXYmid.append([])
                for j in range(len(lanes[i])):
                    distYmid = distances[i][j]
                    pxlYmid = int(round((distYmid * res).magnitude))
                    bands_pxlXYmid[i].append((pxlXmid, pxlYmid))
                    bandwidth = bandwidths[i][j]  # w=FWHM or w=FWTM ???
                    if False:
                        FWHM = Gauss_FWHM(bandwidth)
                    else:
                        FWHM = bandwidth
                    std_dev = Gauss_dev(FWHM)
                    maxI = intensities[i][j]
                    midI = Gaussian(distYmid, maxI, distYmid, std_dev)
                    if pxlYmid < len(rgb_arr):  # band within gel frontiers
                        rgb_arr[pxlYmid, from_x:to_x] += midI
                    bckwdYstop = False if pxlYmid > 0 else True
                    forwdYstop = False if pxlYmid < len(rgb_arr)-1 else True
                    pxlYbck = pxlYmid-1
                    pxlYfor = pxlYmid+1
                    while not bckwdYstop or not forwdYstop:
                        if not bckwdYstop:
                            distYbck = Q_(pxlYbck, 'px')/res
                            bckYI = Gaussian(distYbck, maxI, distYmid, std_dev)
                            if pxlYbck < len(rgb_arr):
                                rgb_arr[pxlYbck, from_x:to_x] += bckYI
                            pxlYbck -= 1
                            if bckYI <= 1E-5 or pxlYbck == -1:
                                bckwdYstop = True
                        if not forwdYstop:
                            distYfor = Q_(pxlYfor, 'px')/res
                            forYI = Gaussian(distYfor, maxI, distYmid, std_dev)
                            rgb_arr[pxlYfor, from_x:to_x] += forYI
                            pxlYfor += 1
                            if forYI <= 1E-5 or pxlYfor == pxl_y:
                                forwdYstop = True
                                
            # Background color
            if noise is None or noise <= 0:
                rgb_arr += back_col
            else:
                bckg = _np.random.normal(back_col, noise,
                                        (len(rgb_arr), len(rgb_arr[0])))
                rgb_arr += bckg[:, :, _np.newaxis]
            # Saturation
            rgb_arr[rgb_arr > 1] = 1
            rgb_arr[rgb_arr < 0] = 0
            # bands_arr = _np.ma.masked_where(rgb_arr == back_col, rgb_arr)  ###########
            bands_arr = rgb_arr

            # Plot
            
            fig = _plt.figure()
            
            ax1 = fig.add_subplot(111, facecolor=str(back_col))
            ax1.xaxis.tick_top()
            ax1.yaxis.set_ticks_position('left')
            ax1.spines['left'].set_position(('outward', 8))
            ax1.spines['left'].set_bounds(0, gel_len)
            ax1.spines['right'].set_visible(False)
            ax1.spines['bottom'].set_visible(False)
            ax1.spines['top'].set_visible(False)
            ax1.spines['right'].set_color(str(back_col))
            ax1.spines['bottom'].set_color(str(back_col))
            ax1.xaxis.set_label_position('top')
            
            
            #_plt.xticks(lane_centers, names)
            majorLocator = _FixedLocator(list(range(int(gel_len+1))))
            minorLocator = _FixedLocator([j/10.0 for k in
                                         range(0, int(gel_len+1)*10, 10)
                                         for j in range(1+k, 10+k, 1)])
            ax1.yaxis.set_major_locator(majorLocator)
            ax1.yaxis.set_minor_locator(minorLocator)
            ax1.tick_params(axis='x', which='both', top='off')
            
            
            # Gel image
            bands_plt = ax1.imshow(bands_arr, extent=[0, gel_width, gel_len, 0],
                                   interpolation='none')
            # Draw wells
            
            for i in range(nlanes):
                ctr = lane_centers[i]
                wx = wellx[i]
                wy = welly[i]
                ax1.fill_between(x=[ctr-wx/2, ctr+wx/2], y1=[0, 0],
                                 y2=[-wy, -wy], color=str(well_col))
            # Invisible rectangles overlapping the bands for datacursor to detect
            bands = []
            for i in range(nlanes):
                bandlength = bandlengths[i]
                center = lane_centers[i]
                x = center - bandlength/2.0
                for j in range(len(lanes[i])):
                    dna_frag = lanes[i][j]
                    bandwidth = bandwidths[i][j]
                    dist = distances[i][j].magnitude
                    y = dist - bandwidth/2.0
                    pxlX, pxlY = bands_pxlXYmid[i][j]
                    band_midI = bands_arr[pxlY, pxlX][0]
                    alpha = 0 if abs(band_midI - back_col) >= detectlim else 0.4
                    band = _plt.Rectangle((x, y), bandlength, bandwidth,
                                         fc='none', ec='w', ls=':', alpha=alpha,
                                         label='{} bp'.format(len(dna_frag)))
                    _plt.gca().add_patch(band)
                    bands.append(band)
            _plt.ylim(gel_len, -max(welly))
            xlim = sum(wellx) + (nlanes+1)*self.wellsep
            _plt.xlim(0, xlim)
            _plt.ylabel('Distance (cm)')
            _plt.xlabel('Lanes')
            bbox_args = dict(boxstyle='round,pad=0.6', fc='none')
            an1 = _plt.annotate(title, xy=(0, 0),
                               xytext=(xlim+0.4, (gel_len+max(welly))/2.0),
                               va="center", bbox=bbox_args)
            an1.draggable()
            _plt.gca().set_aspect('equal', adjustable='box')
            cursor_args = dict(display='multiple',
                               draggable=True,
                               hover=False,
                               bbox=dict(fc='white'),
                               arrowprops=dict(arrowstyle='simple',
                                               fc='white', alpha=0.5),
                               xytext=(15, -15),
                               formatter='{label}'.format)
            if cursor_ovr:
                for key in cursor_ovr:
                    cursor_args[key] = cursor_ovr[key]
            if cursor_args['hover'] == True:
                cursor_args['display'] = 'single'
            #_datacursor(bands, **cursor_args)
            return _plt
        return None