pypi piaso-tools 1.2.8
PIASO v1.2.8

2 hours ago

piaso-tools 1.2.8

pip install -U piaso-tools

This release fixes what a review of the whole package found in October 2026: three plots that showed wrong data, functions that gave another answer on a cytome than on an AnnData, and failures that passed without a word. A default call on an AnnData gives the same result as in 1.2.7 in most functions; every change in output is listed below, with what to do about it.

What to redo after upgrading:

  • Redraw vertical plotLigandReceptorLollipop plots (every row but the middle one named another interaction), plotLigandReceptorInteraction heatmaps drawn with one colormap (the ligand and receptor rows were swapped) and plot_dendrogram on a cytome (it clustered the groups on cell index and fragment counts).
  • Recompute ATAC FRiP from piaso.pp.calculateCellMetrics: on a matrix that counts insertions, two per fragment (the default), it was twice the fraction of fragments in peaks (up to 2.0), so halve a threshold chosen on 1.2.7's values.
  • Name or score a cytome again if you used kamon (predictCellTypeByMarker) or calculateScoreParallel on one with the default arguments: a store's raw counts were scored where an AnnData's INFOG layer is.
  • Batch-aware runGDR on an AnnData (batch_key=... with the default layer='infog') found each batch's markers on the raw counts; it now uses INFOG, as a cytome and a single batch do, so X_gdr changes.
  • runGDR with batch_key and groupby, on an AnnData or a cytome: a batch that lacks one of the groups no longer adds a dimension of unrelated genes for it, so X_gdr has one column fewer per such batch-group pair.
  • predictCellTypeByGDR labels: with COSG 1.2.0 or earlier, each reference type and each query cluster added a dimension of unrelated genes for the batch that lacks it, about half of the joint embedding. Labels transferred with it may change; re-run if affected.
  • projectGDR novelty for a reference built with batch_key reported a shift for every query, even for cells of the reference's own distribution.
  • Scrublet scores all move a little, and a cell that cannot be scored now gets a missing call, not "not a doublet".
  • Motif scans: a motif whose best word cannot reach the scan's p-value is no longer scanned; it was called at its best word's score, and numpy and Rust disagreed there.

Naming cells from marker genes is piaso.tl.kamon

The marker-based naming method has a name, KAMON, and its function is piaso.tl.kamon. piaso.tl.predictCellTypeByMarker is the same function under its earlier name and keeps working unchanged, so existing scripts and tutorials need no edit.

The API reference now documents each function once and lists its other names beside it (for example align_spatial_coordinates beside alignSpatialCoordinates), instead of repeating the whole entry.

Naming and scoring a cytome score what an AnnData scores

  • On a cytome, kamon and calculateScoreParallel score score_layer ('infog' by default, normalized on the fly from the store's INFOG parameters), as on an AnnData. They scored the store's raw counts (cytome_layer='counts') whatever score_layer said. cytome_layer now defaults to None and still decides when given, and modality defaults to 'RNA'. A store without INFOG parameters raises an error that names the call computing them (piaso.tl.infog(ds, modality='RNA')). To reproduce a 1.2.7 call on a cytome, pass cytome_layer='counts'.
  • On a cytome, kamon writes the columns an AnnData gets in .obs to the cells table: the call, its score or -log10 p-value, and with smoothing the call before smoothing and the confidence.
  • Ties between p-values go to the higher score (use_score=False). With 100 control sets the -log10 p-value tops out at log10(101), so cell types that share markers tied and the order of the marker table decided: on a 10,003-nucleus human cortex dataset with its own markers, the p-value route went from 0.698 to 0.976 accuracy, the score route's. n_ctrl_set is a parameter (100 by default), and score_method='scanpy' with use_score=False raises at once instead of failing later.
  • predictCellTypeByGDR joins the reference and the query as two batches of one runGDR. With COSG 1.2.0 or earlier, each reference type and each query cluster added a dimension of unrelated genes for the batch that lacks it, about half of the joint embedding, which Harmony then aligned and the label transfer used. Labels transferred with it may change; re-run if affected. This needs COSG 1.2.1, which scores only the groups that have cells.
  • predictCellTypeByGDR with default arguments works: a missing log1p layer is built from integer counts in .X, never on your object, and a query and a reference that share barcodes no longer fail.
  • smoothCellTypePrediction on a graph pools the cell itself and its k - 1 strongest neighbors, as documented (it left the cell out); a missing label casts no vote instead of failing.
  • On an AnnData, the default call (use_score=True) names cells as in 1.2.7.

score() and INFOG

  • cell_mask on an AnnData: the results land on your object, NaN outside the mask. They were written to a copy and lost. Integer cell_mask indices select those cells, as documented (they were read as a boolean mask).
  • Inputs that failed: a dense layer scores (it raised), and a matrix of more than 2^31 non-zero values, about a million RNA cells, is scored in row blocks (the compiled kernel's row pointers wrapped).
  • Gene sets: a set with no gene in the data is named in one warning per call (it was dropped without a word, so its cell type could never be called), and a single set with no gene raises. A gene name that an AnnData holds twice is scored over both columns, as on a cytome, with the same warning. Gene weights of another length than their set raise instead of being cut short.
  • INFOG refuses counts that are NaN or infinite (one such value made every value NaN), and the in-memory and streaming paths order genes of equal variance the same way.
  • ensure_infog_params(ds, modality) computes that modality's parameters only; it computed RNA's whatever was asked, and rewrote the store's highly_variable column on every call.

filter_cells: missing values count the same on a cytome and on an AnnData

  • A condition about a label keeps the cells that have none, on both inputs (pandas' rule). On a cytome, cell_type != 'Doublet' dropped every unlabelled cell, and with inplace=True they were deleted for good, while an AnnData kept them. != and not in are true for a missing value, every other comparison false, and not / ~ negate that. This also holds on pandas' nullable columns, where AnnData queries failed with a TypeError.
  • In a dict mask, None means "the value is missing" on both inputs (it matched nothing on a cytome).
  • Integer indices outside the cells raise (-1 selected the last cell), and a query whose value is a number ("flag + 1") is refused.
  • layer= chooses the matrix the QC thresholds read (adata.layers[layer], or {modality}_{layer} on a cytome).

Scrublet

  • Every score moves a little: each cell's neighbor graph now leaves the cell itself out, as scrublet does (each simulated doublet counted itself as a doublet neighbor). Against scrublet 0.2.3 on 4,000 nuclei, 99.7 % of scores are now identical, against 54.4 % before.
  • A cell that cannot be scored gets a missing value: a score of NaN and a missing doublet call, where it got 0 and "not a doublet" (a library of fewer than 50 cells, or with too few variable genes). On an AnnData the call is then a nullable boolean, so adata[~adata.obs['is_doublet']] raises until the missing calls are handled; piaso.pp.filter_cells(adata, mask="~is_doublet") keeps those cells.
  • library_key naming an empty column (no cell had a library) scored nothing and called every cell not a doublet; a key that is not a column pooled every cell. Both raise now.
  • layer= names the raw counts; a matrix whose values are not integers is refused instead of being scored as counts.

runSVD: the same embedding on any number of threads

  • The dense steps run on row blocks of a fixed height, so an embedding no longer depends on the number of cores (OpenBLAS split them by core count, which moved the last digits and could tip a kNN tie). Embeddings agree with 1.2.7's to 1e-11, relative, and take about as long.
  • Each component's sign follows one rule: the cell coordinate of largest magnitude is positive. A stored embedding's components may come out negated against 1.2.7's once; distances, neighbors, Leiden and UMAP do not depend on signs.
  • Fixed on a cytome:
    • without measurement, the number of features is the modality's own (an ATAC-only store with use_highly_variable=False was refused as having no feature selected);
    • with tfidf_params and a feature selection but no col_mask, the TF-IDF took the IDF of the first features instead of the selected ones;
    • with cell_mask, the TF-IDF computed in Python took the sequencing depths of the first cells instead of the masked ones;
    • cached TF-IDF statistics are checked against their matrix and recomputed when the matrix changed (after the matrix was written again they gave stale depths, an all-NaN IDF and "Eigenvalues did not converge").
  • TF-IDF statistics that are NaN, infinite or negative are refused, and a fallback from the compiled engine to Python warns.

runGDR and projectGDR

  • Batch-aware runGDR on an AnnData found each batch's markers on the raw counts when layer='infog' (the default), while a single batch and a cytome use the INFOG layer; the markers and X_gdr change, and an AnnData and the same data in a cytome now agree.
  • runGDR with batch_key and groupby: a batch that lacks one of the groups no longer adds a marker set of unrelated genes for it (one noise dimension of X_gdr per missing batch-group pair). This needs COSG 1.2.1, which scores only the groups that have cells.
  • A marker set none of whose genes could be mapped shifted every block after it (AnnData) or failed (cytome); the blocks now stay aligned. Repeated obs_names no longer fail.
  • On a cytome: a batch-aware run holds one batch's scores at a time; save_reference=False works (NameError); a de novo run (groupby=None) clusters on INFOG's 5,000 most variable genes, as on an AnnData (n_highly_variable_genes and use_highly_variable were ignored), keeps its labels as gdr_local and leaves no temporary embedding or graph.
  • On an AnnData, a de novo run no longer overwrites your INFOG layer and highly variable genes, and leaves no temporary graph.
  • projectGDR's novelty for a reference built with batch_key is calibrated on the reference's own cells projected as a query is; it was calibrated in another space, so held-out cells of the reference's own distribution came out novel. For a single-batch reference nothing changes.
  • projectGDR's control genes: frozen control genes are matched to the query by gene name (a query with the same genes in another order drew other controls while reporting them frozen), and a cytome reference freezes them too, so a projection no longer depends on the size of the query. A read-only reference works on its second use.

stitchSpace on a cytome integrates

On a cytome, stitchSpace did no integration and reported success: it looked for an embedding name PIASO never writes, ran COSG on a placeholder matrix, and so pruned every link between batches. It now reads the store's embedding (modality=), runs COSG per batch on the store's expression, and writes the integrated embedding to the store; the result equals the same data's AnnData result. filter_bbknn_computation and filter_bbknn_metric reach BBKNN (annoy was always used), and a failure raises instead of printing a message and returning as success does.

Clustering and UMAP

  • leiden and neighbors take cell_mask as indices, as documented (they read indices as a boolean mask), and refuse repeated or out-of-range ones.
  • leiden_local puts each group's labels back by position (cells sharing a barcode took the label of the last of them) and takes svd_drop_first_component for each group's embedding.
  • leiden and umap warn when they fall back to igraph or umap-learn because the compiled extension lacks an entry point.
  • The compiled leiden_csr defaults to 10 iterations, as tl.leiden does (it was 2 when called directly); tl.leiden's output does not change.

The motif scanner and the motif-to-TF tables

  • A motif whose best word cannot reach the p-value is not scanned, and one warning names such motifs with their best word's p-value. pvalue_to_threshold returns +inf for them; it returned the best word's score, where numpy and Rust disagreed (one motif matched 0 % of regions on Rust and 28 % on numpy). At p = 1e-4 this is about a hundred of JASPAR 2024's 879 vertebrate motifs, depending on the background; CIS-BP has few.
  • Short motifs' thresholds are exact: the score distribution lost its top when rounded, so some thresholds let through up to 24 times the target rate (now under 2.6 times on 800 random motifs).
  • numpy and Rust now score windows the same way and agree to the bit, and a minus-strand hit is reported at the forward start of its window.
  • build_tf_motif_map gives a JASPAR heterodimer (FOS::JUN) to each partner, as cisTarget's tables do; as one name it matched no gene, so 76 of JASPAR 2024's 879 vertebrate motifs were lost. load_cistarget_motifs(max_per_tf=k) keeps each TF's k motifs with the best similarity q-values (it kept the first k in the table's order).

Specificity matrices and SCALAR

  • specificity_matrix on a cytome: a cached matrix is used only while the labels, the cells and the matrices it came from are unchanged. A relabelled or subset store returned the old matrix, and cytorete's inferRegulon reads this cache by default. The cache holds no pickle; an older entry is recomputed once.
  • specificity_matrix on an AnnData: cosg_layer names the matrix COSG scores (every layer gave .X's result), and uns['cosg'] is left alone.
  • runSCALAR: call_breadth divides by the groups where a pair was tested (by every group, so a pair the detection floor kept out of most cell types looked specific); a misspelt layer raises (it fell back to .X); on a cytome, layer=None reads the matrix that was the AnnData's .X (else the counts), so a store and its AnnData give the same p-values; a pair listed twice is tested once, with a warning; verbose=False silences the messages and progress bars.

The PIASOmarkerDB client

  • Requests are retried with a growing pause on a failed connection or a busy server (retries=, 3 by default), and an answer that is not JSON raises an error naming the status and the start of the answer.
  • cache_dir is an opt-in response cache, kept for cache_ttl seconds (a day by default). It defaulted to ~/.piaso/markers, created by every client although nothing was cached, so a read-only home failed every call.
  • Large requests are read in pages within the server's limit (a larger page came back short and was read as the last); a cell type whose name holds a comma is matched by its exact name; offset= without limit= works.
  • as_dict=True lists a gene once per cell type (it repeated it once per study or tissue, and score() counted it each time) and warns when one cell type name gathers several contexts. loadCustomMarkers drops repeated genes too, getRecommendedStudy returns the studies' current names, and analyzeGenes with several gene sets asks for all their genes in batches of 200 instead of once per set.

calculateCellMetrics: FRiP in one unit

ATAC FRiP divided the insertions in peaks (two per fragment) by the fragments, so a cell with every fragment in peaks got 2.0. It is now the insertions in peaks over two cut sites per fragment, capped at 1. A matrix that counts each fragment once is divided by the fragments (counting='overlaps'); a matrix that records no counting, such as one converted from AnnData, is taken as insertions, with a warning; a presence matrix gets no FRiP. A failure in this step raises instead of leaving the column out silently.

Downloads in piaso.data

  • Every download goes to a temporary file, is checked (length, size or checksum) and only then renamed, so an interrupted download no longer counts as complete; connections time out after 60 s, and network errors are retried three times.
  • The newest dataset registry wins: a registry cached by an older PIASO hid the datasets added since.
  • fetch_dataset(force=True) removes the old store's -wal and -shm files, which SQLite would replay onto the new one, and a downloaded file takes your usual file permissions.

Plotting: three plots that showed wrong data, and one rule for colours and figures

Redraw these. Three plots drew wrong data under their defaults:

  • plotLigandReceptorLollipop, vertical layout (the default): every row but the middle one was labelled with another interaction, since SCALAR was added. The rows were reversed for display and their labels reversed a second time; the label colours went with the labels, so text and colour agreed with each other but not with the row. Every vertical lollipop drawn with PIASO up to this release is affected, including the old SCALAR tutorial's (vertical_layout=True); the horizontal layout was right.
  • plotLigandReceptorInteraction, one colormap (the default), horizontal layout: the ligand specificities were drawn in the bottom row, under labels that read "Ligand" above "Receptor". The two-colormap and vertical layouts were right. The labels now sit at each row's centre.
  • plot_dendrogram on a cytome: use_rep='auto' found no embedding (it looked for AnnData's X_ names only) and clustered the groups on the cells table's numeric columns, cell index and fragment counts, without a warning. It now uses the store's embedding (RNA_pca, ATAC_svd, ...); a store with neither an embedding nor highly variable genes raises, naming what to pass.

Colours follow the category, by name, in every plot. One resolver now gives each category its colour over the whole column, from the stored colours (adata.uns, a cytome's set_categories, or the colours an AnnData carried into a store) or PIASO's palette in the column's order. Colours used to be matched to the categories a plot happened to draw, so these changed colour, and now keep it:

  • embedding(cell_mask=...), which drew a category in the colour of one the mask left out;
  • a cytome converted from AnnData, whose categorical order was ignored (now its categories follow the AnnData's order and colours);
  • sankey(color_by='right') (offset by the number of left categories) and left_order='auto';
  • stacked_barplot(sort_groups=True); a violin with an empty group; the dendrogram's leaves; the cell-level heatmap's sidebar; plotGroupMetrics.

A store's stored order and colours now reach the violin, the stacked bar, the Sankey, the dendrogram and the heatmap, and labels without a stored order sort naturally (0, 1, 2, ... 10) in the stacked bar, as in the embedding. A qualitative palette named by string ('tab10', 'Set2') gives its colours in order instead of an even sample across them, and a stored colour list shorter than its categories is ignored with a warning instead of wrapped.

Missing values are drawn and named. A missing label (NaN, None, or the text 'nan' a cytome stores for one) is drawn first, in na_color, and listed as "NA", instead of becoming a 'nan' category (embedding, split_embedding, scatter, which dropped those points). Continuous NaN values are drawn in na_color; they were transparent.

Figures. show defaults to showing unless ax= is given; a function never closes a figure passed in through ax= (six of them did, so a later plt.savefig wrote a blank page); a figure that is not shown is returned, and one a function made is closed only once it is saved, so show=False followed by plt.savefig() keeps working. Layout acts on the figure drawn, not the current one, and leaves a figure with constrained layout alone. PDFs and EPS files saved with save= embed TrueType fonts, as journals ask. In a notebook, a show=False call left as a cell's last line is now displayed twice (once as the returned value); assign it (fig = ...) or end the line with ;.

Other changes in the plots' output:

  • dotplot: expression_cutoff is applied (it was ignored, and a fraction was always of values above 0), and mean_only_expressed averages the values above it; the dot-size key draws each fraction at its plotted size (it was capped, so the 80 % and 100 % dots were alike and smaller than a 100 % data dot).
  • scatter: a cytome's text column (leiden) and a boolean column are categories, as in the embedding (the first was drawn on a colorbar). With on=, a text column's missing values are drawn as "NA" (they were a category named "None"), and on= takes the store's feature tables (genes, peaks, tiles) and samples, read through cytome's accessors; an internal table is no longer accepted.
  • split_embedding: a boolean colour is categorical, as in embedding.
  • plotLigandReceptorLollipop: the circle-size key draws the plotted sizes (its dots were 1.13x wider); score_range_min and score_range_max each clip both ends of the stems (one alone clipped one end); circle_size_scale never had an effect and now warns. plotLigandReceptorInteraction: with shared_legend=True and heatmap_vmax the colorbar is labelled (its ticks were at -inf), and with preserve_input_order=True the shared scale covers the rows drawn.
  • heatmap(cell_level=True) works (it raised NameError); with standard_scale the colorbar reads "z-score".
  • plotConfusionMatrix: the reference colour bar is on top, beside its labels.
  • New options: sort_order= ('size' draws rare categories on top) and vcenter= in the embedding plots; a basis matching both RNA_umap and ATAC_umap warns, and modality= chooses.
  • import piaso no longer reseeds numpy's global random stream; the shuffled palettes are unchanged.

PIASOmarkerDB's marker regions in PIASO: getMarkerRegions

  • New: piaso.tl.getMarkerRegions queries PIASOmarkerDB's marker regions, the cell type-specific 500 bp tiles of single-nucleus chromatin atlases, up to 1,000 per cell type ranked by COSG. as_dict=True returns {cell_type: [tile IDs]} named as a PIASO store names its tiles ('chr1:3000500-3001000'), the form predictCellTypeByMarker takes, so nuclei can be named from their tile counts. top_n keeps each cell type's best regions, and tile_size maps them onto another grid.
  • The genome build is checked: each region study is on one build (hg38 for human, mm10 for mouse). Pass genome= with your data's build, and a study on the other build raises an error instead of matching nothing; regions are not lifted over between builds or species.
  • getMarkerRegions(list_studies=True) lists the region studies; the client class PIASOmarkerDB has getMarkerRegions and listRegionStudies.

Installation and packaging

  • PIASO needs Python 3.10 or later (requires-python = ">=3.10"). Pre-compiled wheels are built for Linux on x86_64, CPython 3.10 to 3.12; elsewhere pip builds PIASO from source, which needs Rust and a C compiler. The README promised macOS and Windows wheels, which were never built.
  • Requires cytome 0.3.9 and COSG 1.2.1; pip install -U piaso-tools installs them. COSG 1.2.1 scores only the groups that have cells (the runGDR and predictCellTypeByGDR fixes above), works under pandas 3 and reads a cytome's column-major matrices. matplotlib's floor is 3.6 (on 3.5 a colormap you named was silently ignored).
  • The source distribution ships Cargo.lock and is built with it, so a build from source uses the Rust crates the tests ran with, and every wheel is installed and tested before it is published.
  • The tests carry markers (slow, data, network), so pytest -m "not slow" is a fast subset.

Also in this release

  • Cell Ranger feature names made unique never repeat a name (['A', 'A', 'A-1'] gave two 'A-1'); a boolean mask with missing entries leaves them out; a mistyped cytome path raises FileNotFoundError instead of leaving an empty file behind.

Don't miss a new piaso-tools release

NewReleases is sending notifications on new releases.