Repository navigation
BulkSeq Studio v0.31.0
Scientific output changes. The three alternative differential-expression engines (edgeR quasi-likelihood, limma-voom, microarray limma) gain result columns and a covariate screen they did not have, three sanity checks now report findings on designs and routes they previously passed over in silence, the covariate screen stops testing the sample sheet's descriptive identifier columns, and the wording of checks 21 and 23 and of the count-model diagnostic placeholders changed. Re-run any result you intend to publish, and see the items marked scientific below.
Minor release: engine parity and honest reporting. Everything an alternative engine can produce, it now produces; the report, run summary and pre-run checks state what the run actually did instead of describing the DESeq2 route by default. The DESeq2 route's numbers are unchanged, verified byte for byte on the bundled pasilla benchmark and on two Fusarium graminearum studies (evidence below). The workflow version is bumped to 0.31.0, so an existing project re-copies the bundled workflow on its next run; that copy is how the new shared workflow/scripts/de_common.R, which every engine script now sources at start-up, reaches an existing project.
Changed
- edgeR-QLF and limma-voom write the
ncbi_geneidannotation column (scientific).run_edger.Randrun_voom.Rnow take the NCBI GeneID from the annotation'sdb_xreffield through the same sharedannotate_from_gtfhelperrun_deseq2.Ruses, at the same position in the results table. KEGG enrichment reads that column when an organism's KEGG gene list is keyed by bare NCBI GeneIDs and no OrgDb is available — S. pombe is the catalogued case — so KEGG over-representation and GSEA on those two engines are no longer empty by construction for such an organism. The wiring is verified; the realized row counts are not, because no S. pombe run was performed on an alternative engine for this release and the Ensembl Drosophila annotation the pasilla benchmark uses carries nodb_xrefGeneID (the column is written and empty on all 7,481 rows there). The microarray limma route has no annotation file and does not write the column. - The alternative engines carry the effect-size companion column
padj_lfc_ge_threshold(scientific). edgeR computes it withglmTreat(..., null = "interval")and limma-voom and the microarray route withtreat()on the contrast fit, each at the same threshold rule DESeq2 uses (the configured log2 fold-change threshold when positive, otherwise 1.0, because the test needs a positive value; a non-positive value reaching the test itself now stops the run with a named message instead of being tested silently). The column is a companion only: it never enters the up/down split, which a run-level recomputation on all four engines confirms. The method that produced it is recorded per engine as aFold-change threshold test: <method> (H0 |log2FC| <= <L>)line inresults/reports/sessionInfo.txt, in the run summary and indeseq2_objects.rds. Measured: on pasilla, 893 genes at padj < 0.05 against 59 by the companion test on edgeR and 938 against 37 on limma-voom; on the microarray benchmark, 493 against 17. - The covariate-structure screen runs on every locally fitted engine (scientific). edgeR-QLF, limma-voom and microarray limma now write
results/deseq2/pca_coordinates.csv—prcompon the 500 highest-variance complete rows of that engine's own log matrix,plotPCA's convention, reproducingDESeq2::plotPCAto a relative 4.8e-14 — and check 23 is gated only against the imported-results route, which fits no model. The DESeq2 path still usesplotPCA(vsd)unchanged. Because each engine screens its own coordinates, the screen is engine-relative and advisory: on the bundled pasilla benchmark thereplicatecolumn reportsREVIEW_REQUIREDunder edgeR and limma-voom (adjusted R² 0.92, F(1,2) = 36.76, p = 0.0261, against the chance ceiling of 0.85 at n = 4, k = 2) while DESeq2 staysPASSon the same samples (adjusted R² 0.54, p = 0.168). Both readings are correct — that subset really is replicate-paired, and the variance-stabilising transform reranks the top-variance rows — but two of the four engines now raise a review finding on the flagship benchmark. - Check 23's decision rule is sample-size-aware (scientific). The fixed adjusted-R² floor of 0.5 is replaced by the value only 5% of random label assignments reach for that sample and group count, derived analytically from the same F distribution as the p-value (0.85 at n = 4, 0.57 at n = 6, 0.42 at n = 8, 0.26 at n = 12, 0.17 at n = 18, for two groups). Because that ceiling is derived from the same F distribution as the p-value, exceeding it is algebraically the same event as p < 0.05, and the two conditions agreed on every one of 40,000 random sample-size, group-count and F-statistic draws; the practical effect is therefore that the independent effect-size requirement is removed rather than rescaled, leaving the nominal 5% F-test, and the conjunction survives only as a guard for a principal component with no variance, where the p-value degenerates. The fixed floor was the stricter of the two rules from eight samples upward in two groups, so a column explaining 35% of a principal component across eighteen samples (p = 0.0097) was reported as within chance and is now review-required, as is a column explaining 45% across twelve (p = 0.0169). Neither release was silent about such a column: 0.30.1 named it in a
PASSline with its statistics, and 0.31.0 returns a different verdict on the same column rather than mentioning one it previously ignored. Measured per-axis false-positive rate under permuted labels: 0.0484 (484 of 10,000). The screen scores each column on whichever of PC1 and PC2 fits it better and tests that maximum, so the screen as a whole is not calibrated at 5% (measured 0.095 at n = 18, k = 2); that multiplicity is unchanged from 0.30.1 and the message states that the screen is advisory. - The covariate screen no longer tests the sheet's descriptive identifier columns (scientific). Check 23 screens every sample-sheet column outside the design formula, skipping an exclusion list and any column that has one level or as many levels as there are samples. Two free-text labels sat outside that list:
sample_title, which the accession importer writes from the archive's own record, andtitle, which the series importer writes from the repository entry. A project imported either way could therefore have its sample titles tested against the principal components and reported for review. That happens when the titles partition the samples the same way the contrast does, which is common because titles are usually written to name the group. The interface's design helper already refused to offer that column as a covariate, so the two disagreed. The set of descriptive columns is now declared once and both sides read it, with a test that parses each source and fails when they diverge. A project whose sheet carries either label can therefore lose a check 23 finding it previously reported; no other check changes, and a genuine covariate such asbatch,library_preporsequencing_runis screened exactly as before. - A DESeq2 design carrying an interaction or nesting operator is reported for review (scientific). Check 08 now adds a
REVIEW_REQUIREDmessage naming the coefficient that was actually tested and stating that it is the contrast at the reference level of the other design factors, not the interaction effect, and advising an explicit contrast or coefficient. The fit is not refused and additive designs are untouched — pasilla's~ conditioncheck 08 is byte-identical. The three alternative engines continue to refuse such a formula outright, as they have since 0.29.1. - Check 10 reports when the KEGG identifier form cannot be used (scientific). It escalates to
REVIEW_REQUIREDwhen the key form staysunknownfor an organism whose gene ids are mostly non-numeric, and when a GeneID-keyed organism is reached from a route that carries no usablencbi_geneidvalues, naming the engine or input route and the number of genes that could not be bridged. - limma-voom reports a moderated standard error (scientific).
lfcSEwasNAon that route and is now derived as the microarray route derives it, through one shared helper (stdev.unscaled × sqrt(s2.post), with the same finite-and-positive validation): finite and positive on all 7,481 pasilla rows, where 0.30.1 wroteNAon all of them. The genes-of-interest forest plot draws its error bars from that column, so its intervals change from zero-width to real ones on limma-voom runs. edgeR keepsNAwith the reason recorded: the quasi-likelihood fit reports no per-gene log-fold-change standard error. - Produced text changed in three places. The count-model diagnostic placeholders (dispersion, Cook's distance, library size) name the engine that ran, so an edgeR run's figures read
edgeR-QLF logCPM backendinstead of thelimma-voom logCPM backendthey wrongly carried; check 21 and check 23 messages lead with the finding and the action, with sample, column and study identifiers moved to a trailing clause; and check 00 now carries aWARNINGwhen more than one contrast is configured, naming the contrast analysed and those preserved but not run. - The KEGG identifier form comes from the organism catalogue, with the live probe as the fallback. All 45 catalogued KEGG organism codes were resolved live on 2026-09-13 and now carry a measured
kegg_key_form(20 keyed by NCBI GeneID, 25 by locus tag), forwarded to the enrichment step as a rule parameter, so a catalogued organism needs no network round trip. A code outside the catalogue is still probed live, now through a four-step fallback chain (link/<org>/pathway,list/<org>,conv/<org>/ncbi-geneid,info/<org>) and classified by a majority rule over the whole key list instead of by requiring unanimity across the first fifty keys; a total outage degrades tounknowninstead of aborting. TheKEGG key form observed:evidence line now also names the source, the endpoint that answered and the measured numeric-key fraction. An opt-in network test (BULKSEQ_KEGG_LIVE=1) resolves every catalogued code against the two endpointsdownload_KEGGuses and re-derives the form independently, so a re-keyed organism is caught rather than assumed. - The environment drops six R packages no workflow script loads.
r-ggtext,r-ggraph,r-tidygraph,bioconductor-complexheatmap,bioconductor-enhancedvolcanoandbioconductor-biomartare removed from the full specification and the lock, which a new reverse dependency gate found by scanning everylibrary/require/::reference inworkflow/scripts/*.Rand comparing it with the declared set; the gate was watched failing and naming exactly those six before they were removed. A linux-64 dry solve goes from 703 to 697 packages and re-adds none of them, a clean environment built from the trimmed lock loads all 49 probed namespaces, and a full pasilla run on it reproduced the pinned counts body hash and a byte-identical DESeq2 table with byte-identical protein-network figures. An existing installation is unaffected until it is rebuilt. The protein-network comment that described aggraphfigure was corrected to name what the script actually does, andfastpandsortmernagained pin-rationale comments next to ther-baseone. - Application dependency pins. PySide6 6.11.1 → 6.11.2, pandas 3.0.3 → 3.0.5 (3.0.4 is yanked on PyPI, recorded reason "Reported segfaults with datetime-related functionality"), pydantic 2.13.4 → 2.13.5, PyInstaller 6.21.0 → 6.22.2; numpy stays at 2.4.6, inside the range pandas 3.0.5 declares for this interpreter. Pillow is removed from the build requirements: no tracked source imports it, the application's only openpyxl consumer reads cells through
pandas.read_excel, and both a workbook with an embedded image and a plain one read correctly with Pillow uninstalled. The rebuilt Windows bundle carries noPILand its frozen self-test passes. - The graphical interface reports the run it is describing. The Outputs picker no longer offers
unchanged_genes.csvon either alternative engine, gated on a declared alternative-engine set rather than on a single hardcoded engine name (edgeR previously listed a table the workflow never produces on that route), and the engine tooltip is written from a declared mapping stating what each engine's effect-size test is and which artefacts it produces. The Pre-run checks page shows each check under a plain-language name with its recorded findings underneath instead of a bare identifier and status, re-reads the phase checks when a run finishes, and the completion message counts the checks that reported a warning or a review-required finding. The window's status bar carries a permanent version label with an accessible name, and the inset status bar now measures its message area against the leftmost laid-out child instead of a fixed reserve, so the message cannot be painted under it.
The artefact-by-engine matrix below is the state this release declares. tests/test_engine_parity.py derives the same table from the engine scripts and rule blocks and fails on any disagreement in either direction, including against this table.
| Artefact | DESeq2 | edgeR-QLF | limma-voom | limma | imported |
|---|---|---|---|---|---|
ncbi_geneid |
yes | yes | yes | no | no |
pca_coordinates |
yes | yes | yes | yes | no |
check_23_covariate_structure |
yes | yes | yes | yes | no |
padj_lfc_ge_threshold |
yes | yes | yes | yes | no |
lfc_companion_threshold_rule |
yes | yes | yes | yes | no |
companion_column_outside_the_up_down_split |
yes | yes | yes | yes | yes |
lfc_threshold_test |
greaterAbs | glmTreat | treat | treat | — |
unchanged_genes |
yes | no | no | no | no |
check_13_equivalence |
yes | no | no | no | no |
lfcSE_populated |
yes | no | yes | yes | yes |
stat_column |
yes | yes | yes | yes | yes |
check_14_wilcoxon |
yes | yes | yes | yes | no |
interaction_handling |
review required | refuses | refuses | refuses | not applicable |
sources_de_common_before_library |
yes | yes | yes | yes | no |
Two cells need a word of qualification that a table cannot carry. lfcSE_populated on the imported route means the column is preserved when the uploaded table supplies one: the importer accepts lfcSE, lfcse or se and leaves the column empty when the table has none, because an imported result is only ever as complete as the file it came from. check_23_covariate_structure means the screen runs, not that it always assesses something: a project whose sample sheet carries no column outside the design formula gets a PASS that says so explicitly, which is the pre-existing behaviour on every route.
Added
- An optional
library_namecolumn on the sample sheet. The sample table carries a human-readable library name directly aftersample_id, editable by hand and filled from the archive's ownlibrary_namefield when a study is imported by accession. It is optional, may be blank and may repeat across rows:sample_idremains the only identifier the workflow keys on, for file names, counts-matrix columns and every sample match, and nothing derives a path or a column name from the library name. The pipeline uses it where a human-readable name helps and a duplicate cannot do harm: the run summary, the study-design export and the report show it beside the sample identifier rather than instead of it, and the per-sample figure labels use it, appending the sample identifier only for the rows whose name repeats, so no plot carries two identical point labels. A project whose sheet has no such column is unaffected, and its sheet is not rewritten to add an empty one, which matters because the sheet is an input to most of the workflow and a changed timestamp would force a resumed run to rebuild. The column is descriptive, so the covariate screen does not test it. - The documentation site is rebuilt and expanded. The published handbook at https://tunabirgun.github.io/bulkseq-studio/ is replaced: six pages become nineteen, grouped as Start here, Project and data, Analysis setup, Validate and run, Explore results and Reference, so the manual's four middle groups carry the same names as the application's four-stage navigator and a reader looking at a stage in the window can find the matching chapter. New pages cover the five input routes, the sample sheet and reference, the experimental design, the pre-run checks, enrichment, meta-analysis, the microarray route, imported results, compute resources and a sixteen-term glossary whose terms are clickable wherever they appear in the text; a walkthrough page puts the decisions in order for each of the five starting points. Every page states the version it documents, and the version notices name the output changes from 0.26.6 onward so a reader can tell which release produced a given result. The site is generated rather than hand-edited:
docs_src/content.mjsholds the content andnode docs_src/build.mjswritesdocs/, which a continuous-integration step rebuilds and compares, so a hand edit underdocs/fails the build. The 0.30.1 site's benchmark figures are not carried over, and its ten interface screenshots are replaced by two retaken at 0.31.0. Those two are now derived rather than hand-supplied:scripts/capture_docs_screenshots.pyrenders them from the real interface against a synthetic project, at the size the site embeds, and a test holds the script and the site to each other so an image the handbook embeds cannot lack a recipe and a recipe cannot produce an image nothing uses. The README is cut to a front page and now defers to the handbook rather than restating it: the four per-release "what changed" sections and the accumulated output-change notices are gone, since the changelog and the site's version notices already carry them, and the long safeguard prose is reduced to five lines that link into the chapters that explain them. It gains a badge row whose release badge resolves the published version from the release list, and whose Python, Snakemake and licence badges are held topyproject.tomland the environment lock by a test, so a bump that updates one and forgets the other fails. - The results report is redesigned. The self-contained report gains a section index, readable typography, viewport-bounded definitions, and a figure viewer with Fit, Actual size, zoom percentage and keyboard controls. Sorting and zoom affect presentation only; analysis values and embedded source figures are preserved, and the explanations distinguish adjusted p-values and diagnostic patterns from probabilities of biological truth.
- A run-provenance table in the report. The report's versions section now carries App version, Workflow version, Workflow digest, Workflow copied, Project created with (only when it differs from the executed stamps), Installed environment spec, Environment lock md5 and Workflow commit, using the run summary's own wording when a value was not recorded; the muted lock-hash paragraph it replaces is gone. A non-uniform strandedness record renders as
mixedwith the per-sample list on both surfaces, from one shared renderer, so the report can no longer describe a mixed run as uniform. - Mapping evidence reaches the run summary.
run_summary.txt,run_summary.jsonandtools_references.txtnow carry the KEGG retrieval date and tool versions, the observed KEGG key form and the GO readable-symbol conversion line, from one shared list of evidence prefixes that a drift test holds againstrun_enrichment.R. - Single-contrast disclosure on every non-interface surface. A project configuring more than one contrast now gets a parse-time note on stderr, a
WARNINGin check 00, an explicitContrast analysedandConfigured but not analysedpair inrun_summary.txtandstudy_design.txt, and a card in the report. A single-contrast project is unchanged on all four surfaces. - Two Fusarium graminearum benchmarks.
fg_spores_mycelium_paired(GSE55477 / PRJNA239711 / SRP039087, six paired 90 bp samples, unstranded, spores versus mycelium; dataset publication Zhao et al. 2014, BMC Genomics 15:191, doi:10.1186/1471-2164-15-191) andfg_heat_shock_paired(GSE78885 / PRJNA314297 / SRP071140, six paired 151 bp samples, reverse-stranded, 37 °C versus 25 °C; Bui et al. 2016, Scientific Reports 6:28154, doi:10.1038/srep28154) join the benchmark catalogue with every run accession, URL, mate checksum, read and base count generated from the ENA filereport and re-verified against a fresh query (120 values across 12 runs, no differences). Both scaffold from the interface and the command line onto the F. graminearum PH-1 reference preset, and example project scaffolds ship underexamples/benchmarks/. - A declared artefact-by-engine matrix as a test.
tests/test_engine_parity.pyreads the engine scripts, the differential-expression rule blocks and the check gates and compares them with the declared table above; it also parses the table out of this changelog entry, so a published matrix that drifts from the implemented one fails too. Adding an artefact to one engine without declaring it, and declaring a cell the sources do not support, both fail. Ten negative controls cover those directions, including a companion column wired into the significance call, a diverging threshold-fallback rule and a mutated changelog cell. - One capability probe for the tests that need bash or Rscript. Six divergent probes are replaced by
tests/_runtime.py, which resolves a runtime only once it has proved it can load the packages that test needs; the skip lists before and after are identical on Windows and under WSL. Two defects it exposed are fixed: an argument with no space was relayed into a shell as a syntax error, and an unreadable candidate path raised instead of being skipped. A separate controlled re-count test flips one sample's strandedness on a completed project, asserts the counts and the DESeq2 table move and check 21 names the sample, and asserts the uniform table returns both hashes exactly.
Fixed
- Results report: long run-provenance values (for example the "not recorded (workflow copied before 0.30.1)" disclosure) wrap inside the Software and provenance table instead of being clipped at the column edge.
- Results report: runtime phase labels are no longer truncated with an ellipsis on narrow screens.
- Results report: the enlarged-figure viewer keeps the caption's question, method note and how-to-read text as separate sentences instead of running them together.
bulkseq runreacts to Ctrl-C during a quiet phase. Output is drained on a reader thread and the consumer polls it, so an interrupt is seen immediately instead of waiting for the next line of Snakemake output; it still stops the whole process tree, and the queue is drained to exhaustion after the process exits so a masked failure marker printed at the end is still detected. Measured: interrupt to process-tree termination in 0.36 s during a two-second silent phase, against no response within 20 s before.- Check Environment names the phase it is running — the WSL distribution, the bioinformatics tools, the R and Bioconductor load test — instead of one static line, and the allowance for the R load test is derived from the number of packages probed (128 s for the current 49) rather than fixed at 120 s.
- The GUI benchmark harness moved out of
installer_output/.run_gui_full.pyandrun_gui_preflight.pyare a normal test package attests/gui_benchmark/, so nothing tracked in git remains underinstaller_output/and the release upload steps no longer need the exclusion lines 0.30.1 added; a test asserts both that the upload paths carry no exclusion and thatgit ls-files installer_outputis empty. Harness run evidence is written to the ignoredbuild/gui-benchmark-runs/. - The sample sheet no longer changes in every byte when it is saved on a different platform. The sheet was serialised with the host's own line ending, so one written by the interface on Windows and saved again on Linux differed everywhere, even with nothing edited. The saver only skips writing when the bytes match, so that difference moved the file's timestamp, and the sheet is an input to most of the workflow: a resumed run rebuilt from alignment instead of continuing. The terminator is now read back from the sheet on disk and reused, so an existing project is stable on either platform and no one pays a one-off rewrite, and a newly scaffolded project is written with line feeds so its sheet does not depend on where it was created. The tests pin the behaviour to the file rather than the host, so a return to the old behaviour fails on one platform or the other.
- The Outputs picker lists the count matrix on the mixed input route. A project combining local FASTQ with downloaded accessions aligns reads and the workflow writes
results/counts/counts.txt, but the picker offered that file only on the plain FASTQ and accession routes, so the table existed and could not be opened. The three copies of the alignment-route list in the interface are replaced by one named constant, and the picker's own comment, which described the file as meaningless for a count-matrix run when the workflow does write it there, is corrected: the count-matrix route is still excluded, now deliberately and on the stated ground that the file is the user's own uploaded matrix copied to the canonical path. A test derives the routes that produce the file from the workflow guard itself and asserts the picker offers exactly those minus that one documented exclusion, so neither half can drift unnoticed. - The R regression for the KEGG GeneID bridge runs again. It read a function from
run_deseq2.Rthat now lives inde_common.R, and nothing executed the file; it is now driven by pytest through the shared runtime probe. That probe also finds the pipeline's own Rscript on Linux, so eight R regressions that silently skipped under WSL now execute there.
Continuous integration and release
- Every pytest job runs with
-rsand publishes the skipped tests and their reasons as a table in the run summary, so a test that stops running is visible instead of silent. - The test matrix gains macOS (interface, configuration and validation layer only; the pipeline does not run there). The release script gates on the Tests workflow, so macOS is now a release-blocking platform. Its first run earned its place by failing three ways that no local run could reproduce: a test executed the pipeline's read-length rule on a host whose
zcatdoes not read gzip, and now probes that capability instead of assuming it; a negative control read the previous release's renderer out of git history, which a shallow checkout does not have, so the job now fetches full history and the control skips with a stated reason where it cannot run; and two test modules treated the presence ofwsl.exeas a working runtime, when Windows ships it with no distribution installed, so both now use the shared probe that requires a distribution to have actually run a command. Because that same mistake escaped twice, it is now a gate rather than a habit: a test parses every test module and fails on ashutil.which("wsl")availability check or a subprocess call whose command begins withwsl, naming the file, so a ninth ad hoc probe cannot reach a runner before it is caught. - Both packaging jobs verify the installed dependency set before building:
pip check, then a comparison of the frozen environment against the exact pins inconstraints.txtandrequirements-build.txt, failing on a mismatch and on finding no pins at all. scripts/release.ps1downloads both artifact sets from the gated build run instead of trusting a local build, and reports the age and conclusion of the last Environment workflow run as a warning without gating on it. It now publishes this version's own changelog entry as the release body, read fromCHANGELOG.mdbetween this version's heading and the next, so the page people download from states what changed and carries the scientific-output notice instead of pointing at a file they would have to go and find. A missing or empty entry stops the release rather than publishing a bare page, and re-publishing an existing release refreshes the notes as well as the assets.- The Environment workflow gains an osx-arm64 dry-solve job: the specifications filtered to what Apple Silicon can install must solve, and the unfiltered ones must still fail naming each unavailable package, so a package becoming available is noticed.
- Structural gates were added for the micromamba bootstrap pins in the setup script (one version, two distinct digests, a URL that interpolates the version) and for the build requirements' pin form, each watched failing under mutation.
Evidence
- The DESeq2 route is unchanged on the bundled pasilla benchmark: every differential-expression table, both significant-gene lists, the equivalence table, the normalized counts, the PCA coordinates and all 54 figures are byte-identical to the pinned pasilla baseline the earlier releases were measured against (
deseq2_results.csvsha2562b040052…, 7,532 rows, 467 genes at padj < 0.05; counts body sha256f68d2786…). The artefacts that differ are check 23's reworded message, which reportsPASSbefore and after; the report and summary files, which carry the new provenance and evidence lines; anddeseq2_objects.rds, which now records the threshold-test method. - Fusarium graminearum SRP039087, run from FASTQ under a v0.29.1 workflow copy and under the 0.31.0 candidate, produced identical counts (body sha256
0e6cf4f5…) and an identical differential-expression table on every shared column: 9,028 rows, 5,734 genes at padj < 0.05, of which 2,723 up- and 2,478 down-regulated at |log2FC| > 1; all six samples inferred unstranded. The reverse-stranded SRP071140 pair is likewise identical: counts body sha2565eeca15e…, 8,280 rows, 5,836 genes at padj < 0.05, 1,996 up and 1,255 down at |log2FC| > 1, all six samples inferred reverse-stranded. The candidate tables additionally carryncbi_geneidandpadj_lfc_ge_threshold. "Counts body" above names a stated normalisation rather than the raw file, because the count matrix carries a header comment that records the per-sample strandedness the run inferred, which differs between releases by design: it istail -n +3 results/counts/counts.txt | cut -f1,7- | sha256sum, which drops the comment line, the column header and the coordinate columns and hashes the gene identifiers with the per-sample counts. A digest published without its recipe is not reproducible, and an earlier release of this project retired one for exactly that reason. - Nothing in this release was executed on macOS. Every R, Snakemake and packaging measurement comes from Windows and from WSL2 Ubuntu 24.04; the macOS test job and the osx-arm64 solve job have not yet run on a runner.
Every asset below is listed in SHA256SUMS.txt; verify a download against it before use.