diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index d8ec70e8..8f88eeee 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -65,5 +65,17 @@ You can do this with `R -e 'reticulate::py_discover_config(required_module = "ma +Output of `Rmagic::check_pymagic_version()`: + +
+ +``` +If you are running MAGIC in R, please run `Rmagic::check_pymagic_version()` and paste the results here. + +You can do this with `R -e 'Rmagic::check_pymagic_version()'` +``` + +
+ **Additional context** Add any other context about the problem here. diff --git a/.travis.yml b/.travis.yml index 1e35624a..cfba9b64 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,7 @@ addons: - deadsnakes packages: - libhdf5-dev - - python3.6 + - python3.6-dev cache: - packages @@ -40,6 +40,7 @@ before_install: install: - cd python; pip install --user -q . - cd ../Rmagic; R -e 'install.packages("devtools", repos="http://cloud.r-project.org")' + - R -e 'install.packages("BiocManager", repos="http://cloud.r-project.org"); BiocManager::install("multtest")' - R -e 'devtools::install_deps(dep = T, upgrade="always")' - cd .. @@ -48,7 +49,7 @@ script: - cd Rmagic; R CMD build . - R CMD check *tar.gz - cd ../python; pip install --user -q .[test] - - if [ "$TRAVIS_PYTHON_VERSION" != "3.5" ]; then black . --check --diff; fi + - if [ "$TRAVIS_PYTHON_VERSION" != "3.5" ]; then black . --check --diff -t py35; fi - python setup.py test - pip install --user -q .[doc] - cd doc; make html; cd .. diff --git a/Rmagic/DESCRIPTION b/Rmagic/DESCRIPTION index 09b49346..8ca343f3 100644 --- a/Rmagic/DESCRIPTION +++ b/Rmagic/DESCRIPTION @@ -1,7 +1,7 @@ Package: Rmagic Type: Package Title: MAGIC - Markov Affinity-Based Graph Imputation of Cells -Version: 2.0.3 +Version: 2.0.3.999 Authors@R: c(person(given = "David", family = "van Dijk", email = "davidvandijk@gmail.com", role = c("aut")), person(given = 'Scott', family = 'Gigante', email = 'scott.gigante@yale.edu', role = 'cre', comment = c(ORCID = '0000-0002-4544-2764'))) @@ -22,5 +22,5 @@ Suggests: phateR License: GPL-2 | file LICENSE LazyData: true -RoxygenNote: 6.1.1 +RoxygenNote: 7.0.2 Encoding: UTF-8 diff --git a/Rmagic/NAMESPACE b/Rmagic/NAMESPACE index 8f9c6419..380b5dac 100644 --- a/Rmagic/NAMESPACE +++ b/Rmagic/NAMESPACE @@ -8,9 +8,11 @@ S3method(magic,default) S3method(magic,seurat) S3method(print,magic) S3method(summary,magic) +export(check_pymagic_version) export(install.magic) export(library.size.normalize) export(magic) export(pymagic_is_available) import(Matrix) importFrom(ggplot2,ggplot) +importFrom(utils,packageVersion) diff --git a/Rmagic/R/magic.R b/Rmagic/R/magic.R index b2fc8fdb..e5ff0d90 100644 --- a/Rmagic/R/magic.R +++ b/Rmagic/R/magic.R @@ -21,6 +21,11 @@ #' sets the level of diffusion. If 'auto', t is selected according to the #' Procrustes disparity of the diffused data.' #' @param npca number of PCA components that should be used; default: 100. +#' @param solver str, optional, default: 'exact' +#' Which solver to use. "exact" uses the implementation described +#' in van Dijk et al. (2018). "approximate" uses a faster implementation +#' that performs imputation in the PCA space and then projects back to the +#' gene space. Note, the "approximate" solver may return negative values. #' @param init magic object, optional #' object to use for initialization. Avoids recomputing #' intermediate steps if parameters are the same. @@ -113,6 +118,7 @@ magic.default <- function( decay = 1, t = 3, npca = 100, + solver = 'exact', init = NULL, t.max = 20, knn.dist.method = 'euclidean', @@ -134,32 +140,16 @@ magic.default <- function( message("Argument alpha is deprecated. Using decay instead.") decay <- alpha } - knn <- as.integer(x = knn) - t.max <- as.integer(x = t.max) - n.jobs <- as.integer(x = n.jobs) - if (is.numeric(x = npca)) { - npca <- as.integer(x = npca) - } else if (!is.null(x = npca) && is.na(x = npca)) { - npca <- NULL - } - if (is.numeric(x = decay)) { - decay <- as.double(x = decay) - } else if (!is.null(x = decay) && is.na(x = decay)) { - decay <- NULL - } - if (is.numeric(x = t)) { - t <- as.integer(x = t) - } else if (is.null(x = t) || is.na(x = t)) { - t <- 'auto' - } - if (is.numeric(x = seed)) { - seed <- as.integer(x = seed) - } else if (!is.null(x = seed) && is.na(x = seed)) { - seed <- NULL - } - if (is.numeric(x = verbose)) { - verbose <- as.integer(x = verbose) - } + # validate parameters + knn <- check.int(x = knn) + t.max <- check.int(x = t.max) + n.jobs <- check.int(x = n.jobs) + npca <- check.int.or.null(npca) + knn.max <- check.int.or.null(knn.max) + seed <- check.int.or.null(seed) + verbose <- check.int.or.null(verbose) + decay <- check.double.or.null(decay) + t <- check.int.or.string(t, 'auto') if (!methods::is(object = data, "Matrix")) { data <- as.matrix(x = data) } @@ -190,6 +180,7 @@ magic.default <- function( "decay" = decay, "t" = t, "npca" = npca, + "solver" = solver, "knn.dist.method" = knn.dist.method ) # use pre-initialized values if given @@ -205,10 +196,12 @@ magic.default <- function( decay = decay, t = t, n_pca = npca, + solver = solver, knn_dist = knn.dist.method, n_jobs = n.jobs, random_state = seed, - verbose = verbose + verbose = verbose, + ... ) } } @@ -219,10 +212,12 @@ magic.default <- function( decay = decay, t = t, n_pca = npca, + solver = solver, knn_dist = knn.dist.method, n_jobs = n.jobs, random_state = seed, - verbose = verbose + verbose = verbose, + ... ) } result <- operator$fit_transform( @@ -254,6 +249,7 @@ magic.seurat <- function( decay = 1, t = 3, npca = 100, + solver = "exact", init = NULL, t.max = 20, knn.dist.method = 'euclidean', @@ -271,12 +267,14 @@ magic.seurat <- function( decay = decay, t = t, npca = npca, + solver = solver, init = init, t.max = t.max, knn.dist.method = knn.dist.method, verbose = verbose, n.jobs = n.jobs, - seed = seed + seed = seed, + ... ) data@data <- t(x = as.matrix(x = results$result)) return(data) @@ -290,6 +288,7 @@ magic.seurat <- function( decay = decay, t = t, npca = npca, + solver = solver, init = init, t.max = t.max, knn.dist.method = knn.dist.method, @@ -316,6 +315,7 @@ magic.Seurat <- function( decay = 1, t = 3, npca = 100, + solver = 'exact', init = NULL, t.max = 20, knn.dist.method = 'euclidean', @@ -336,17 +336,19 @@ magic.Seurat <- function( decay = decay, t = t, npca = npca, + solver = solver, init = init, t.max = t.max, knn.dist.method = knn.dist.method, verbose = verbose, n.jobs = n.jobs, - seed = seed + seed = seed, + ... ) assay_name <- paste0('MAGIC_', assay) data[[assay_name]] <- Seurat::CreateAssayObject(data = t(x = as.matrix(x = results$result))) print(paste0("Added MAGIC output to ", assay_name, ". To use it, pass assay='", assay_name, - "' to downstream methods or set seurat_object@active.assay <- '", assay_name, "'.")) + "' to downstream methods or set DefaultAssay(seurat_object) <- '", assay_name, "'.")) Seurat::Tool(object = data) <- results[c('operator', 'params')] return(data) } else { diff --git a/Rmagic/R/utils.R b/Rmagic/R/utils.R index 18afa937..3b58de65 100644 --- a/Rmagic/R/utils.R +++ b/Rmagic/R/utils.R @@ -9,6 +9,10 @@ null_equal <- function(x, y) { } } +#' Check that the current MAGIC version in Python is up to date. +#' +#' @importFrom utils packageVersion +#' @export check_pymagic_version <- function() { pyversion <- strsplit(pymagic$`__version__`, '\\.')[[1]] rversion <- strsplit(as.character(packageVersion("Rmagic")), '\\.')[[1]] @@ -17,12 +21,12 @@ check_pymagic_version <- function() { if (as.integer(pyversion[1]) < major_version) { warning(paste0("Python MAGIC version ", pymagic$`__version__`, " is out of date (recommended: ", major_version, ".", minor_version, "). Please update with pip ", - "(e.g. pip install --upgrade magic-impute) or Rmagic::install.magic().")) + "(e.g. ", reticulate::py_config()$python, " -m pip install --upgrade magic-impute) or Rmagic::install.magic().")) return(FALSE) } else if (as.integer(pyversion[2]) < minor_version) { warning(paste0("Python MAGIC version ", pymagic$`__version__`, " is out of date (recommended: ", major_version, ".", minor_version, "). Consider updating with pip ", - "(e.g. pip install --upgrade magic-impute) or Rmagic::install.magic().")) + "(e.g. ", reticulate::py_config()$python, " -m pip install --upgrade magic-impute) or Rmagic::install.magic().")) return(FALSE) } return(TRUE) @@ -112,7 +116,7 @@ install.magic <- function(envname = "r-reticulate", method = "auto", error = function(e) { stop(paste0( "Cannot locate MAGIC Python package, please install through pip ", - "(e.g. pip install magic-impute) and then restart R." + "(e.g. ", reticulate::py_config()$python, " -m pip install magic-impute) and then restart R." )) } ) @@ -124,3 +128,38 @@ pymagic <- NULL py_config <- reticulate::py_discover_config(required_module = "magic") load_pymagic() } + +###### +# Parameter validation +###### + +check.int <- function(x) { + as.integer(x) +} + +check.int.or.null <- function(x) { + if (is.numeric(x = x)) { + x <- as.integer(x = x) + } else if (!is.null(x = x) && is.na(x = x)) { + x <- NULL + } + x +} + +check.double.or.null <- function(x) { + if (is.numeric(x = x)) { + x <- as.integer(x = x) + } else if (!is.null(x = x) && is.na(x = x)) { + x <- NULL + } + x +} + +check.int.or.string <- function(x, str) { + if (is.numeric(x = x)) { + x <- as.integer(x = x) + } else if (is.null(x = x) || is.na(x = x)) { + x <- str + } + x +} diff --git a/Rmagic/man/check_pymagic_version.Rd b/Rmagic/man/check_pymagic_version.Rd new file mode 100644 index 00000000..0a029d51 --- /dev/null +++ b/Rmagic/man/check_pymagic_version.Rd @@ -0,0 +1,11 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils.R +\name{check_pymagic_version} +\alias{check_pymagic_version} +\title{Check that the current MAGIC version in Python is up to date.} +\usage{ +check_pymagic_version() +} +\description{ +Check that the current MAGIC version in Python is up to date. +} diff --git a/Rmagic/man/install.magic.Rd b/Rmagic/man/install.magic.Rd index e25bd0da..a3e8ddf5 100644 --- a/Rmagic/man/install.magic.Rd +++ b/Rmagic/man/install.magic.Rd @@ -4,8 +4,13 @@ \alias{install.magic} \title{Install MAGIC Python Package} \usage{ -install.magic(envname = "r-reticulate", method = "auto", - conda = "auto", pip = TRUE, ...) +install.magic( + envname = "r-reticulate", + method = "auto", + conda = "auto", + pip = TRUE, + ... +) } \arguments{ \item{envname}{Name of environment to install packages into} diff --git a/Rmagic/man/magic.Rd b/Rmagic/man/magic.Rd index e3652800..5468e57e 100644 --- a/Rmagic/man/magic.Rd +++ b/Rmagic/man/magic.Rd @@ -9,20 +9,62 @@ \usage{ magic(data, ...) -\method{magic}{default}(data, genes = NULL, knn = 5, knn.max = NULL, - decay = 1, t = 3, npca = 100, init = NULL, t.max = 20, - knn.dist.method = "euclidean", verbose = 1, n.jobs = 1, - seed = NULL, k = NULL, alpha = NULL, ...) - -\method{magic}{seurat}(data, genes = NULL, knn = 5, knn.max = NULL, - decay = 1, t = 3, npca = 100, init = NULL, t.max = 20, - knn.dist.method = "euclidean", verbose = 1, n.jobs = 1, - seed = NULL, ...) - -\method{magic}{Seurat}(data, assay = NULL, genes = NULL, knn = 5, - knn.max = NULL, decay = 1, t = 3, npca = 100, init = NULL, - t.max = 20, knn.dist.method = "euclidean", verbose = 1, - n.jobs = 1, seed = NULL, ...) +\method{magic}{default}( + data, + genes = NULL, + knn = 5, + knn.max = NULL, + decay = 1, + t = 3, + npca = 100, + solver = "exact", + init = NULL, + t.max = 20, + knn.dist.method = "euclidean", + verbose = 1, + n.jobs = 1, + seed = NULL, + k = NULL, + alpha = NULL, + ... +) + +\method{magic}{seurat}( + data, + genes = NULL, + knn = 5, + knn.max = NULL, + decay = 1, + t = 3, + npca = 100, + solver = "exact", + init = NULL, + t.max = 20, + knn.dist.method = "euclidean", + verbose = 1, + n.jobs = 1, + seed = NULL, + ... +) + +\method{magic}{Seurat}( + data, + assay = NULL, + genes = NULL, + knn = 5, + knn.max = NULL, + decay = 1, + t = 3, + npca = 100, + solver = "exact", + init = NULL, + t.max = 20, + knn.dist.method = "euclidean", + verbose = 1, + n.jobs = 1, + seed = NULL, + ... +) } \arguments{ \item{data}{input data matrix or Seurat object} @@ -50,6 +92,12 @@ Procrustes disparity of the diffused data.'} \item{npca}{number of PCA components that should be used; default: 100.} +\item{solver}{str, optional, default: 'exact' +Which solver to use. "exact" uses the implementation described +in van Dijk et al. (2018). "approximate" uses a faster implementation +that performs imputation in the PCA space and then projects back to the +gene space. Note, the "approximate" solver may return negative values.} + \item{init}{magic object, optional object to use for initialization. Avoids recomputing intermediate steps if parameters are the same.} diff --git a/autoblack.sh b/autoblack.sh index cfbaf2b4..981a7b4f 100644 --- a/autoblack.sh +++ b/autoblack.sh @@ -6,7 +6,7 @@ set -e files=\$(git diff --staged --name-only --diff-filter=d -- "*.py") for file in \$files; do - black -q \$file + black -q -t py35 \$file git add \$file done EOF diff --git a/python/magic/io.py b/python/magic/io.py index dd23dd2d..aadbf6df 100644 --- a/python/magic/io.py +++ b/python/magic/io.py @@ -16,7 +16,7 @@ def load_csv( **kwargs ): """magic.io is deprecated. Please use scprep.io instead. - Read more at http://scprep.readthedocs.io/ + Read more at http://scprep.readthedocs.io/ """ raise RuntimeError( "magic.io is deprecated. Please use scprep.io instead. " @@ -35,7 +35,7 @@ def load_tsv( **kwargs ): """magic.io is deprecated. Please use scprep.io instead. - Read more at http://scprep.readthedocs.io/ + Read more at http://scprep.readthedocs.io/ """ raise RuntimeError( "magic.io is deprecated. Please use scprep.io instead. " @@ -60,7 +60,7 @@ def load_fcs( ], ): """magic.io is deprecated. Please use scprep.io instead. - Read more at http://scprep.readthedocs.io/ + Read more at http://scprep.readthedocs.io/ """ raise RuntimeError( "magic.io is deprecated. Please use scprep.io instead. " @@ -71,7 +71,7 @@ def load_fcs( def load_mtx(mtx_file, cell_axis="row", gene_names=None, cell_names=None, sparse=None): """magic.io is deprecated. Please use scprep.io instead. - Read more at http://scprep.readthedocs.io/ + Read more at http://scprep.readthedocs.io/ """ raise RuntimeError( "magic.io is deprecated. Please use scprep.io instead. " @@ -82,7 +82,7 @@ def load_mtx(mtx_file, cell_axis="row", gene_names=None, cell_names=None, sparse def load_10X(data_dir, sparse=True, gene_labels="symbol", allow_duplicates=None): """magic.io is deprecated. Please use scprep.io instead. - Read more at http://scprep.readthedocs.io/ + Read more at http://scprep.readthedocs.io/ """ raise RuntimeError( "magic.io is deprecated. Please use scprep.io instead. " @@ -93,7 +93,7 @@ def load_10X(data_dir, sparse=True, gene_labels="symbol", allow_duplicates=None) def load_10X_zip(filename, sparse=True, gene_labels="symbol", allow_duplicates=None): """magic.io is deprecated. Please use scprep.io instead. - Read more at http://scprep.readthedocs.io/ + Read more at http://scprep.readthedocs.io/ """ raise RuntimeError( "magic.io is deprecated. Please use scprep.io instead. " @@ -106,7 +106,7 @@ def load_10X_HDF5( filename, genome=None, sparse=True, gene_labels="symbol", allow_duplicates=None ): """magic.io is deprecated. Please use scprep.io instead. - Read more at http://scprep.readthedocs.io/ + Read more at http://scprep.readthedocs.io/ """ raise RuntimeError( "magic.io is deprecated. Please use scprep.io instead. " diff --git a/python/magic/magic.py b/python/magic/magic.py index 9dde3f1c..65c02bcb 100644 --- a/python/magic/magic.py +++ b/python/magic/magic.py @@ -200,8 +200,7 @@ def knn_max(self, value): @property def diff_op(self): - """The diffusion operator calculated from the data - """ + """The diffusion operator calculated from the data""" if self.graph is not None: return self.graph.diff_op else: @@ -360,7 +359,7 @@ def set_params(self, **params): reset_kernel = True del params["knn"] if "knn_max" in params and params["knn_max"] != self.knn_max: - self.knn = params["knn_max"] + self.knn_max = params["knn_max"] reset_kernel = True del params["knn_max"] if "decay" in params and params["decay"] != self.decay: @@ -460,10 +459,14 @@ def fit(self, X, graph=None): graph = None else: self.knn = graph.knn - self.knn_max = graph.knn_max self.alpha = graph.decay self.n_pca = graph.n_pca self.knn_dist = graph.distance + try: + self.knn_max = graph.knn_max + except AttributeError: + # not all graphs have knn_max + self.knn_max = None self.X = X @@ -497,7 +500,7 @@ def fit(self, X, graph=None): def _parse_genes(self, X, genes): if ( genes is None - and isinstance(X, (pd.SparseDataFrame, sparse.spmatrix)) + and (sparse.issparse(X) or scprep.utils.is_sparse_dataframe(X)) and np.prod(X.shape) > 5000 * 20000 ): warnings.warn( @@ -863,7 +866,10 @@ def _impute( ax.plot(x, error_vec) if t_opt is not None: ax.plot( - t_opt, error_vec[t_opt - 1], "ro", markersize=10, + t_opt, + error_vec[t_opt - 1], + "ro", + markersize=10, ) ax.plot(x, np.full(len(error_vec), threshold), "k--") ax.set_xlabel("t") @@ -919,6 +925,6 @@ def knnDREMI( n_mesh=n_mesh, n_jobs=n_jobs, plot=plot, - **kwargs + **kwargs, ) return dremi diff --git a/python/magic/plot.py b/python/magic/plot.py index 6af1ea11..136f778c 100644 --- a/python/magic/plot.py +++ b/python/magic/plot.py @@ -127,10 +127,7 @@ def animate_magic( show = False data_magic = scprep.select.select_cols(data, idx=genes) - if isinstance(data_magic, pd.SparseDataFrame): - data_magic = data_magic.to_dense() - elif sparse.issparse(data_magic): - data_magic = data_magic.toarray() + data_magic = scprep.utils.toarray(data_magic) c = data_magic[gene_color] if gene_color is not None else None sc = ax.scatter(data_magic[gene_x], data_magic[gene_y], c=c, cmap=cmap) ax.set_title("t = 0") diff --git a/python/magic/preprocessing.py b/python/magic/preprocessing.py index 6ce983c1..a16e87b0 100644 --- a/python/magic/preprocessing.py +++ b/python/magic/preprocessing.py @@ -8,7 +8,7 @@ def library_size_normalize(data, verbose=False): """magic.preprocessing is deprecated. Please use scprep.normalize instead. - Read more at http://scprep.readthedocs.io/ + Read more at http://scprep.readthedocs.io/ """ raise RuntimeError( "magic.preprocessing is deprecated. Please use scprep.normalize instead. " diff --git a/python/magic/utils.py b/python/magic/utils.py index 84d3f870..69a74a39 100644 --- a/python/magic/utils.py +++ b/python/magic/utils.py @@ -132,11 +132,11 @@ def matrix_is_equivalent(X, Y): def convert_to_same_format(data, target_data, columns=None, prevent_sparse=False): # create new data object - if isinstance(target_data, pd.SparseDataFrame): + if scprep.utils.is_sparse_dataframe(target_data): if prevent_sparse: data = pd.DataFrame(data) else: - data = pd.SparseDataFrame(data) + data = scprep.utils.SparseDataFrame(data) pandas = True elif isinstance(target_data, pd.DataFrame): data = pd.DataFrame(data) diff --git a/python/magic/version.py b/python/magic/version.py index 5fa9130a..f6bb6f4d 100644 --- a/python/magic/version.py +++ b/python/magic/version.py @@ -1 +1 @@ -__version__ = "2.0.3" +__version__ = "2.0.4" diff --git a/python/setup.py b/python/setup.py index 413fffee..9535e4a8 100644 --- a/python/setup.py +++ b/python/setup.py @@ -42,7 +42,9 @@ description="MAGIC", author="", author_email="", - packages=["magic",], + packages=[ + "magic", + ], license="GNU General Public License Version 2", install_requires=install_requires, extras_require={"test": test_requires, "doc": doc_requires},