//  [python]
def low_coverage(molecules, reads, is_invalid, plot=False, ax=None, filter_on=True):
    """
    Fits a two-component gaussian mixture model to the data. If a component is found
    to fit a low-coverage fraction of the data, this fraction is set as invalid. Not
    all datasets contain this fraction.

    For best results, should be run after filter.low_count()

    :param molecules: scipy.stats.coo_matrix, molecule count matrix
    :param reads: scipy.stats.coo_matrix, read count matrix
    :param is_invalid:  np.ndarray(dtype=bool), declares valid and invalid cells
    :param bool plot: if True, plot a summary of the filter
    :param ax: Must be passed if plot is True. Indicates the axis on which to plot the
      summary.
    :param filter_on: indicate whether low coverage filter is on
    :return: is_invalid, np.ndarray(dtype=bool), updated valid and invalid cells
    """
    ms = np.ravel(molecules.tocsr()[~is_invalid, :].sum(axis=1))
    rs = np.ravel(reads.tocsr()[~is_invalid, :].sum(axis=1))

    if ms.shape[0] < 10 or rs.shape[0] < 10:
        log.notify(
            'Low coverage filter passed-through; too few cells to calculate '
            'mixture model.')
        return is_invalid

    # get read / cell ratio, filter out low coverage cells
    ratio = rs / ms

    # fit two GMMs on one and two modes
    col_ratio = ratio[:, np.newaxis]
    gmm1 = mixture.GaussianMixture(n_components=1)
    gmm2 = mixture.GaussianMixture(n_components=2)
    gmm1.fit(col_ratio)
    gmm2.fit(col_ratio)

    if filter_on:
    # check if adding a second component is necessary; if not, filter is pass-through
        filter_on = gmm2.bic(col_ratio) / gmm1.bic(col_ratio) < 0.95

    if filter_on:
        res = gmm2.predict(col_ratio)

        # Molecule sum means
        means = [np.mean(ms[res == 0]), np.mean(ms[res == 1])]
        failing = np.where(res == np.argmin(means))[0]

        # set smaller mean as invalid
        is_invalid = is_invalid.copy()
        is_invalid[np.where(~is_invalid)[0][failing]] = True
    else:
        res, means = None, None

    if plot and ax:
        logms = np.log10(ms)
        try:
            seqc.plot.scatter.continuous(logms, ratio, colorbar=False, ax=ax, s=3)
        except LinAlgError:
            warnings.warn('SEQC: Insufficient number of cells to calculate density for '
                          'coverage plot')
            ax.scatter(logms, ratio, s=3)
        ax.set_xlabel('log10(molecules)')
        ax.set_ylabel('reads / molecule')
        if filter_on:
            ax.set_title('Coverage: {:.2}%'.format(np.sum(failing) / len(failing) * 100))
        else:
            ax.set_title('Coverage')
        xmin, xmax = np.min(logms), np.max(logms)
        ymax = np.max(ratio)
        ax.set_xlim((xmin, xmax))
        ax.set_ylim((0, ymax))
        seqc.plot.xtick_vertical(ax=ax)

        # plot 1d conditional densities of two-component model
        # todo figure out how to do this!!

        # plot the discarded cells in red, like other filters
        if filter_on:
            ax.scatter(
                logms[res == np.argmin(means)], ratio[res == np.argmin(means)],
                s=4, c='indianred')

    return is_invalid