                       Changes in version 0.99.0                        

The version number drops from 1.1.0.9000 to 0.99.0 because Bioconductor
requires new submissions to use 0.99.z. The changes below are relative
to PhiSpace 1.1.0, the release archived on Zenodo.

Renamed functions

  - getClass() is now getLabels(). The old name masked
    methods::getClass() when PhiSpace was attached. Replace getClass(
    with getLabels( in existing scripts; the arguments and results are
    unchanged.

Changes that can alter results

  - Functions with a seed argument (findNiches(), clusterPhiSpace(),
    rankFeatures(), spatialSampler(), subsample(), pseudoBulk() and
    tunePhiSpace()) now set the seed with withr::local_seed() and
    restore the session's random-number state on exit. Their own results
    are unchanged, but random numbers drawn after the call differ from
    before, when the call left the generator in the state given by its
    seed.
  - getPC() now computes totVar, props and accuProps from the centred
    (and, if requested, scaled) data that it decomposes. Previous
    versions divided by the total variance of the raw data, so the
    proportions were too small whenever column means were not zero.
    Scores and loadings are unchanged. The corrected proportions also
    change the variance_explained value that findNiches() stores in its
    metadata.
  - clusterPhiSpace() now computes its principal components with a full
    singular value decomposition instead of irlba. irlba could return
    inaccurate trailing components, and the default ncomp (up to 30
    components) could include them in the clustering. Clusters can
    therefore differ from previous versions, mainly for score matrices
    with few columns.
  - PLS fits (mvr(method = "PLS"), and therefore PhiSpace(),
    PhiSpaceR_1ref() and tunePhiSpace()) now find each weight vector
    with an exact eigendecomposition (eigen()) instead of
    irlba::partial_eigen(). irlba stopped at a tolerance of about 1e-5
    and used a random start, so coefficients differed from
    pls::kernelpls.fit by up to about 2e-5 (relative) and varied with
    the random seed. They now agree with pls::kernelpls.fit to
    about 1e-14 and do not depend on the seed. Scores change by a
    similarly small amount.
  - With a "rank" assay, PhiSpace(), PhiSpaceR_1ref() and tunePhiSpace()
    rank the selected features again after feature selection. This
    second ranking now ranks the genes within each cell, as RankTransf()
    does. Previous versions ranked each gene across cells, so a cell's
    ranked values depended on the other cells in the same reference or
    query. Scores computed from a "rank" assay change.
  - mvr(method = "PCA") now uses a full singular value decomposition
    when ncomp is at least half of the smaller dimension of X, where
    irlba can be inaccurate.
  - getPC() (and therefore findNiches() and computeUMAP()) also uses a
    full singular value decomposition when ncomp is at least half of the
    smaller dimension of X. irlba warned "You're computing too large a
    percentage of total singular values" in these cases, for example in
    the CosMx and CITE-seq vignettes. In tests, the results agreed with
    irlba to about 1e-14, apart from the signs of components. The CosMx
    niches and the CITE-seq result checks are unchanged.
  - getPC() also uses the full singular value decomposition when the
    smaller dimension of X is at most 100, as for PhiSpace score
    matrices with one column per cell type. On a 2,702 x 7 score matrix
    with 3 components, irlba did not converge, and its principal
    component scores changed with the session's random state by up
    to 0.17 (scores of about 1 in size). The niches of findNiches()
    therefore changed between runs, although it takes a seed: it sets
    the seed after the PCA. Where irlba converged, results are unchanged
    apart from rounding and the signs of components.

New features

  - Two package vignettes run on small Bioconductor datasets:
    vignette("PhiSpace") annotates single-cell RNA-seq of mouse blood
    stem and progenitor cells (scRNAseq) with bulk ImmGen samples
    (celldex), and vignette("PhiSpaceST") annotates a 10x Visium section
    of the mouse brain (STexampleData) with single-cell RNA-seq of the
    cortex and hippocampus, and finds niches. The eight larger case
    studies are website articles in vignettes/articles/ and are not part
    of the package.
  - tunePhiSpace() runs in non-interactive sessions. New arguments ncomp
    (when tune_ncomp = TRUE) and nfeat give the values to use after the
    cross-validation. If they are not given, an interactive session
    plots the loss and asks, as before. A non-interactive session
    chooses the ncomp with the lowest loss and needs nfeat when
    tune_nfeat = TRUE. Previous versions always asked with readline(),
    which fails in a non-interactive session, and ignored ncomp when
    tune_ncomp = TRUE. The loss plots are now drawn only in interactive
    sessions.
  - New logNormTransf() divides counts by size factors (by default the
    library sizes, centred to mean 1) and takes log2(x + 1). Its values
    are identical to those of scuttle::logNormCounts() with default
    arguments, which Bioconductor 3.24 deprecates. scranTransf() now
    uses it; its values are unchanged, but a sparse or DelayedArray
    count matrix now gives a dgCMatrix assay.
  - New trainPhiSpace() fits a PhiSpace model on a reference and returns
    it as a PhiSpaceModel object, which can be saved and applied later
    to queries with predict(), without the reference cells. The model
    records the training assay, the PhiSpace version and, optionally,
    the reference name, species and gene ID type. PhiSpaceR_1ref() now
    uses the same code and also returns its model (model); its scores
    are unchanged.
  - A PhiSpaceModel also stores the cross-products X'X and X'Y of the
    centred (and scaled) reference over its genes (keepStats = TRUE, not
    for the "rank" assay). When a query lacks some model genes, for
    example a targeted spatial panel, predict() refits the model exactly
    on the genes that the query has, without the reference cells; the
    result equals a model trained with selectedFeat set to those genes
    (to about 1e-14 in the tests). Models without these statistics keep
    the coefficients of the shared genes and warn that the scores are
    approximate. X'X takes 8 bytes per pair of genes: 72 MB for 3,000
    genes, 3.2 GB for 20,000 genes.
  - A model trained with nfeat selects its genes from a fit on all
    reference genes, whereas PhiSpace() selects them among the genes
    shared with the query. Such a model therefore stores the statistics
    of the top nfeatPool genes per phenotype (default 2,000). If a query
    lacks some model genes, predict() selects nfeat genes per phenotype
    again among these candidate genes, from a refit on the candidates
    that the query has, and refits the model on them. predict(nfeat = )
    selects another number of genes. To select genes among those of a
    known query platform, train with genes.
  - predict() and PhiSpace() check that a query looks preprocessed like
    the model's reference. The model records a summary of the reference
    values over its genes (valueProfile), and a warning is given if the
    query's non-zero values are integers and the reference's are not (or
    the reverse), if only one of them has negative values, if their 99th
    percentiles differ by more than a factor of 10, or if only one of
    the model and the query assay is "rank". On the PerturbSeq and CosMx
    data, raw counts and centred and scaled values were detected; the
    five vignette queries gave no warning. trainPhiSpace() gains a
    preprocessing description, printed with the model. When a query
    shares no gene with a model, the error says whether one side uses
    Ensembl IDs or whether the genes match when case is ignored (another
    species).
  - A PhiSpaceModel trained with phenotypes records the number of
    reference cells in each label (classSizes). print() shows, for each
    phenotype, the number of labels and the smallest and largest label
    with their cell counts. Labels with few cells give less reliable
    scores.
  - PhiSpace() accepts a PhiSpaceModel as the reference, or a list that
    mixes models and reference objects. A model is restricted once to
    the genes shared by all queries, as above; updateRef = TRUE is not
    available for a model.
  - New vignette "Pretrain PhiSpace models: train once, annotate later".
    It trains models on the four whole-transcriptome lung references of
    the CosMx vignette with nfeat = 500, applies them to the 960-gene
    CosMx panel, and compares them with the per-query fit: median
    correlation per cell type 0.998 to 0.9995, the same top cell type
    for 96% to 97% of cells per model, and niches with an adjusted Rand
    index of 0.97. Models trained on the panel genes give the same
    scores as the per-query fit.

Memory and speed

  - PhiSpace() scores a query with all PhiSpaceModel references in one
    pass: the query matrix is multiplied once by the coefficients of all
    models, without selecting the model genes from the query or
    transposing it. predict() uses the same computation for one model.
    Models on the "rank" assay, and queries that are neither a sparse
    Matrix nor a base matrix, are scored one model at a time, as before.
    Scores agree with the previous computation to about 1e-15. With the
    four CosMx lung models (98,002 cells, 960 genes), PhiSpace()
    took 1.1 s instead of 3.6 s. With eight synthetic models (300,000
    cells, 20,000 genes, 40 cell types per model), it took 48 s instead
    of 69 s, with the same peak R memory (9.4 GB). With one model and a
    whole-transcriptome query (PerturbSeq, 23,384 genes), the time was
    unchanged and peak R memory fell from 940 MB to 366 MB.

  - mvr() and phenotype() no longer convert a sparse predictor matrix to
    a dense one. Centring and scaling are applied implicitly, inside the
    matrix products. On a simulated 20,000 x 8,000 sparse matrix (10%
    non-zero, 20 responses), peak R memory (including the 185 MB input)
    fell from 3,574 MB to 906 MB for the PLS fit and from 4,646 MB
    to 988 MB for prediction; run times fell from 9.3 s to 0.9 s and
    from 4.0 s to 0.4 s. In fresh vignette builds, peak memory fell
    from 17-24 GB to 7.2 GB for BridgeAnnotation and from 64.9 GB
    to 16.1 GB for CITE-seq.

  - mvr() gains a keepComps argument that selects the numbers of
    components whose coefficients are returned. The default, 1:ncomp,
    returns all of them as before. PhiSpace(), PhiSpaceR_1ref(),
    tunePhiSpace() and rankFeatures() keep only the final coefficients.
    As a result, the atlas_re$reg_re$coefficients array returned by
    PhiSpaceR_1ref() now has one slice, named "<ncomp> comps"; select it
    by that name, not by position. phenotype() accepts both forms, so
    objects saved by earlier versions still work.

  - getPC(scale = TRUE) computes the column standard deviations of a
    sparse matrix without converting it to a dense one. On a
    simulated 50,000 x 2,000 sparse matrix (5% non-zero, 57 MB), the
    memory used above the R session baseline fell from 2,302 MB to 261
    MB, and the run time from 8.8 s to 6.2 s. Results agree to
    about 1e-14.

  - RankTransf() no longer converts a sparse count matrix with no
    negative values to a dense one. The ranks are unchanged.

  - PhiSpaceR_1ref() (and therefore PhiSpace()) no longer subsets the
    reference and query objects to their shared genes, which copied
    every assay. It extracts only the assay it uses and keeps the
    selected genes before it transposes the matrix. Results are
    unchanged. On simulated data (3.3 GB of input: a 10,000-cell
    reference and a 40,000-cell query, each with three assays), peak R
    memory fell from 7.4 GB to 3.7 GB.

  - spatialSmoother() smooths an assay with one sparse cell-by-cell
    weight matrix instead of a loop over cells, and pseudoBulk()
    aggregates with a sparse indicator matrix instead of a loop over
    pseudo-bulks. scoreCells() computes correlation scores for blocks of
    cells at once and no longer holds the class's whole query matrix as
    a dense matrix. Results agree with the previous versions to
    about 2e-15. On simulated data (5,000 genes, 30,000 cells),
    smoothing an assay took 8 s instead of 332 s, pseudoBulk() 0.7 s
    instead of 22 s, and Spearman correlation scoring (25,000 query
    cells, 20 classes) 5 s instead of 26 s.

  - clusterPhiSpace() no longer stores a cells x cells distance matrix
    to compute silhouette widths (select_k_method = "silhouette"). The
    distances from blocks of cells to all cells are computed and summed
    by cluster, so memory grows linearly with the number of cells;
    previously the matrix was rebuilt for each k and
    cluster::silhouette() copied it. The widths agree with
    cluster::silhouette() to about 1e-16. With 30,000 cells and five
    values of k, peak R memory fell from 10.9 GB to 0.8 GB; the run time
    rose from 45 s to 81 s. cluster moved from Imports to Suggests.

  - Two optional limits for large inputs. Both are off by default:
    
      - clusterPhiSpace() gains silhouette_max_cells. If set, silhouette
        widths are computed on a random subsample of that many cells;
        k-means still clusters every cell.
      - rankFeatures(method = "DWD") accepts dwd_params$max_cells. If
        set, a fit with a non-linear kernel stops before it builds a
        cells x cells kernel matrix for more cells than this.

Bug fixes

  - rankFeatures() with method = "PLSDA" or method = "PLS" returned
    scores = NULL, because it did not ask the PLS fit for the component
    scores. It now returns them as an observations x ncomp matrix.
    Importance scores and feature rankings are unchanged.

  - rankFeatures(method = "DWD") with the default linear kernel failed
    with "non-conformable arguments" whenever there were at least as
    many cells as genes. kerndwd then returns one coefficient per gene,
    which rankFeatures() treated as one weight per cell. These
    coefficients are now the importance scores, and the DWD scores are
    computed from them. Fits with fewer cells than genes, or with a
    non-linear kernel, are unchanged.

  - pseudoBulk() drew the pseudo-bulks of a cluster with only one cell
    from the wrong cells: for the cell at position i, sample() drew from
    cells 1 to i. It now draws from that cell only. Clusters with two or
    more cells give the same pseudo-bulks as before, for the same seed.

  - print(), summary() and plot() for clusterPhiSpace() results now read
    the variance proportions that getPC() returns. Previously print()
    reported 0% variance explained, summary() and plot(type =
    "variance") failed, and the PCA plot axis labels showed no
    percentages.

  - scranTransf() replaced non-positive size factors with an arbitrary
    positive size factor instead of the smallest one: it took a position
    in the sorted size factors and read that position in the unsorted
    ones. It now uses the smallest positive size factor. Data with only
    positive size factors give the same result as before.

Progress messages

  - findNiches(), zeroFeatQC(), spatialSampler() and spatialSmoother()
    now report progress with message() instead of cat(), so
    suppressMessages() and the knitr chunk option message = FALSE
    silence them. findNiches(), zeroFeatQC() and spatialSampler() gain a
    verbose argument (default TRUE); spatialSmoother() already had one.
  - findNiches() reports niche sizes on one line instead of printing a
    table.

Other changes

  - The package moved to its own repository,
    github.com/mao-lab-org/PhiSpace, with the package at the root.
    Install it with BiocManager::install("mao-lab-org/PhiSpace"). The
    website moved to https://mao-lab-org.github.io/PhiSpace/; the old
    address redirects there. jiadongm/PhiSpace keeps the papers'
    analysis code, and its pkg/ folder is no longer updated.
  - The package now cites the published papers: Mao et al. (2025),
    Genome Biology 26, 323 (doi:10.1186/s13059-025-03755-8), and Mao et
    al. (2026), Cell Reports Methods 6(8), 101483
    (doi:10.1016/j.crmeth.2026.101483). citation("PhiSpace") returns
    both. The README files, DESCRIPTION, the PhiSpace() and
    PhiSpaceR_1ref() help pages and the vignettes no longer link to the
    bioRxiv preprints.
  - mvr(method = "PCA") no longer hides irlba's warnings. With the full
    singular value decomposition used for large ncomp, the only warning
    left is that irlba did not converge.
  - The License field changed from AGPL (>= 3) to AGPL-3.
  - Raw scores from phenotype() (for example PhiSpaceScore and YrefHat)
    no longer carry a "scaled:center" attribute.
  - scuttle moved from Imports to Suggests: the package code no longer
    calls it, and the examples and tests use scuttle::mockSCE().
  - pls is now a suggested package; the unit tests compare mvr() with
    pls::kernelpls.fit.
  - The PhiSpaceR_1ref() help page now documents every value that the
    scoreCells fallback changes.

scoreCells()

  - scoreCells() now selects signature genes by mean expression by
    default (signature_method = "mean"). The previous default,
    "findMarkers", calls scran::findMarkers(), which Bioconductor 3.24
    deprecates. Results change for multi-class references unless
    signature_method = "findMarkers" is given; this includes the
    PhiSpace() fallback when fallback_min_classes is above 2.
  - scoreCells() now calculates each class's correlation score using
    that class's effective signature genes. Previous versions used every
    gene shared by the reference and query.
  - scoreCells() gains a signature_genes argument for supplying a
    character vector to a one-class reference or an exactly named list
    to a multi-class reference.
  - Runs now record whether signatures were generated or supplied and
    which effective genes were used for correlation scoring.
