Samad14 commited on
Commit
093fbb8
·
verified ·
1 Parent(s): a62ca71

Upload folder using huggingface_hub (part 2)

Browse files
tests/test_expression_recovery.R ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Execute the shipped recovery scripts and compare against direct package results.
2
+ suppressPackageStartupMessages({library(limma); library(tximport); library(DESeq2)})
3
+ folder <- tempfile("recovery-test-"); dir.create(folder)
4
+ templates <- normalizePath("app/rnaseq/recovery_templates")
5
+ set.seed(819)
6
+ samples <- paste0("s", 1:6)
7
+ meta <- data.frame(sample = samples, condition = rep(c("control", "test"), each = 3), experimental_unit = samples)
8
+ metadata <- file.path(folder, "metadata.tsv")
9
+ write.table(meta, metadata, sep = "\t", quote = FALSE, row.names = FALSE)
10
+ expression <- matrix(exp(rnorm(1200, mean = 3, sd = 0.4)), nrow = 200, dimnames = list(paste0("g", 1:200), samples))
11
+ expression[1:20, 4:6] <- expression[1:20, 4:6] * 3
12
+ path <- file.path(folder, "expression.tsv")
13
+ write.table(data.frame(gene = rownames(expression), expression), path, sep = "\t", quote = FALSE, row.names = FALSE)
14
+ out <- file.path(folder, "limma")
15
+ status <- system2(file.path(R.home("bin"), "Rscript"), c(file.path(templates, "limma_trend.R"), path, metadata, "control", "test", "FPKM", out))
16
+ stopifnot(status == 0)
17
+ observed <- read.delim(file.path(out, "limma_trend_results.tsv"))
18
+ expected <- topTable(eBayes(lmFit(log2(expression + 1), model.matrix(~factor(meta$condition, levels = c("control", "test")))), trend = TRUE), coef = 2, number = Inf, sort.by = "none")
19
+ stopifnot(max(abs(observed$logFC - expected$logFC)) < 1e-10, max(abs(observed$adj.P.Val - expected$adj.P.Val)) < 1e-10)
20
+ stopifnot(all(file.exists(file.path(out, paste0("volcano.", c("svg", "pdf", "png"))))))
21
+
22
+ # Genuine Salmon file shape with fractional estimated counts and sample-dependent lengths.
23
+ files <- setNames(file.path(folder, paste0(samples, ".sf")), samples)
24
+ for (i in seq_along(files)) {
25
+ counts <- rnbinom(200, mu = if (i > 3) c(rep(350, 20), rep(100, 180)) else rep(100, 200), size = 12) + 0.25
26
+ length <- seq(700, 1695, by = 5)
27
+ effective <- length - 120 + i * 3
28
+ abundance <- counts / effective
29
+ q <- data.frame(Name = paste0("tx", 1:200), Length = length, EffectiveLength = effective, TPM = abundance / sum(abundance) * 1e6, NumReads = counts)
30
+ write.table(q, files[i], sep = "\t", quote = FALSE, row.names = FALSE)
31
+ }
32
+ manifest <- file.path(folder, "quantfiles.tsv"); mapping <- file.path(folder, "tx2gene.tsv")
33
+ write.table(data.frame(sample = samples, quant_file = unname(files)), manifest, sep = "\t", quote = FALSE, row.names = FALSE)
34
+ tx2gene <- data.frame(transcript = paste0("tx", 1:200), gene = paste0("g", 1:200))
35
+ write.table(tx2gene, mapping, sep = "\t", quote = FALSE, row.names = FALSE)
36
+ txout <- file.path(folder, "tximport")
37
+ status <- system2(file.path(R.home("bin"), "Rscript"), c(file.path(templates, "tximport_deseq2.R"), manifest, mapping, metadata, "control", "test", txout))
38
+ stopifnot(status == 0)
39
+ observed_dds <- readRDS(file.path(txout, "dds_with_length_offsets.rds"))
40
+ txi <- tximport(files, type = "salmon", tx2gene = tx2gene)
41
+ meta$condition <- factor(meta$condition, levels = c("control", "test")); rownames(meta) <- meta$sample
42
+ expected_dds <- DESeq(DESeqDataSetFromTximport(txi, meta, ~condition))
43
+ stopifnot(isTRUE(all.equal(normalizationFactors(observed_dds), normalizationFactors(expected_dds))), isTRUE(all.equal(counts(observed_dds), counts(expected_dds))))
44
+ observed <- read.delim(file.path(txout, "deseq2_tximport_results.tsv"))
45
+ expected <- as.data.frame(results(expected_dds, contrast = c("condition", "test", "control"), alpha = 0.05))
46
+ stopifnot(isTRUE(all.equal(observed$log2FoldChange, expected$log2FoldChange)), isTRUE(all.equal(observed$padj, expected$padj)))
47
+ source(file.path(templates, "review_design.R"))
48
+ meta$experimental_unit[2] <- meta$experimental_unit[1]
49
+ write.table(meta, metadata, sep = "\t", quote = FALSE, row.names = FALSE)
50
+ stopifnot(inherits(try(review_design(metadata, samples, "control", "test"), silent = TRUE), "try-error"))
51
+ cat("Expression recovery known-answer tests passed\n")
tests/test_geo_recovery.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import csv
3
+ import hashlib
4
+ import importlib.util
5
+ import io
6
+ import zipfile
7
+
8
+ import httpx
9
+ import pytest
10
+
11
+ from app.rnaseq.geo_counts import GeoCountsError, fetch_matrix, fetch_series
12
+ from app.rnaseq.geo_recovery import TEMPLATES, recovery_bundle
13
+ from app.routers import geo_search
14
+
15
+
16
+ SERIES = """^SERIES = GSE1
17
+ !Series_type = Expression profiling by high throughput sequencing
18
+ !Series_title = A normalized study
19
+ !Series_overall_design = Review groups
20
+ !Series_contact_email = researcher@example.org
21
+ !Series_relation = BioProject: https://www.ncbi.nlm.nih.gov/bioproject/PRJNA123
22
+ !Series_sample_id = GSM1
23
+ !Series_sample_id = GSM2
24
+ !Series_sample_id = GSM3
25
+ !Series_sample_id = GSM4
26
+ !Series_supplementary_file = ftp://ftp.ncbi.nlm.nih.gov/geo/series/GSEnnn/GSE1/suppl/expression_FPKM.tsv
27
+ """
28
+ SAMPLES = "\n".join(f"^SAMPLE = GSM{i}\n!Sample_title = sample{i}\n!Sample_supplementary_file_1 = ftp://ftp.ncbi.nlm.nih.gov/geo/samples/GSMnnn/GSM{i}/suppl/raw_counts.tsv" for i in range(1, 5))
29
+
30
+
31
+ def study():
32
+ def respond(request):
33
+ return httpx.Response(200, text=SERIES if request.url.params["targ"] == "self" else SAMPLES)
34
+ async def run():
35
+ async with httpx.AsyncClient(transport=httpx.MockTransport(respond)) as client:
36
+ return await fetch_series(client, "GSE1")
37
+ return asyncio.run(run())
38
+
39
+
40
+ def test_sample_level_recovery_preserves_sources_without_automatic_merge():
41
+ series = study()
42
+ assert len(series["files"]) == 5
43
+ assert series["files"][0]["analysis_eligible"] is False
44
+ assert series["raw_candidates"] == [f"GSM{i}/raw_counts.tsv" for i in range(1, 5)]
45
+ assert series["read_projects"] == ["PRJNA123"]
46
+ # A complete deposited matrix from a sample supplement can be validated.
47
+ matrix = "gene\tGSM1\tGSM2\tGSM3\tGSM4\ng1\t1\t2\t3\t4\ng2\t5\t6\t7\t8\n"
48
+ async def run():
49
+ async with httpx.AsyncClient(transport=httpx.MockTransport(lambda request: httpx.Response(200, text=matrix))) as client:
50
+ return await fetch_matrix(client, series, "GSM1/raw_counts.tsv")
51
+ result, source = asyncio.run(run())
52
+ assert result["columns"] == ["GSM1", "GSM2", "GSM3", "GSM4"]
53
+ assert "/geo/samples/" in source
54
+ assert series["files"][1]["name"] == "GSM1/raw_counts.tsv"
55
+
56
+
57
+ def test_bundle_includes_source_identity_templates_and_verified_checksums():
58
+ archive = zipfile.ZipFile(io.BytesIO(recovery_bundle(study())))
59
+ assert {"source_record.json", "metadata_to_review.tsv", "author_request_draft.txt", "featurecounts.sh", "featurecounts_to_matrix.py", "tximport_deseq2.R", "limma_trend.R", "review_design.R"}.issubset(archive.namelist())
60
+ for line in archive.read("checksums.sha256").decode().splitlines():
61
+ checksum, name = line.split(" ")
62
+ assert hashlib.sha256(archive.read(name)).hexdigest() == checksum
63
+ rows = list(csv.DictReader(io.StringIO(archive.read("metadata_to_review.tsv").decode()), delimiter="\t"))
64
+ assert len(rows) == 4 and all(not row["condition"] and not row["experimental_unit"] for row in rows)
65
+ assert "researcher@example.org" in archive.read("author_request_draft.txt").decode()
66
+ assert all("/" not in name and ".." not in name for name in archive.namelist())
67
+
68
+
69
+ def test_recovery_endpoint_returns_zip(monkeypatch):
70
+ snapshot = study()
71
+ async def fake(client, accession):
72
+ return snapshot
73
+ monkeypatch.setattr(geo_search, "fetch_series", fake)
74
+ response = asyncio.run(geo_search.download_geo_recovery("GSE1"))
75
+ assert response.media_type == "application/zip"
76
+ assert response.headers["content-disposition"].endswith('GSE1_expression_recovery.zip"')
77
+ assert zipfile.is_zipfile(io.BytesIO(response.body))
78
+
79
+
80
+ def test_wrong_assay_cannot_download_expression_instructions():
81
+ with pytest.raises(GeoCountsError, match="CUT"):
82
+ recovery_bundle({"workflow_issue": "CUT&Tag requires a different workflow"})
83
+
84
+
85
+ def test_featurecounts_converter_preserves_integers_and_explicit_column_mapping(tmp_path):
86
+ spec = importlib.util.spec_from_file_location("count_converter", TEMPLATES / "featurecounts_to_matrix.py")
87
+ module = importlib.util.module_from_spec(spec); spec.loader.exec_module(module)
88
+ native = tmp_path / "counts.tsv"; mapping = tmp_path / "bam_samples.tsv"; output = tmp_path / "raw.tsv"
89
+ native.write_text("# Program: featureCounts\nGeneid\tChr\tStart\tEnd\tStrand\tLength\tb.bam\ta.bam\tc.bam\td.bam\ng1\t1\t1\t10\t+\t10\t1\t2\t3\t4\ng2\t1\t20\t40\t-\t20\t5\t6\t7\t8\n")
90
+ mapping.write_text("bam\tsample\na.bam\tGSM1\nb.bam\tGSM2\nc.bam\tGSM3\nd.bam\tGSM4\n")
91
+ module.convert(native, mapping, output)
92
+ assert output.read_text().splitlines() == ["gene\tGSM2\tGSM1\tGSM3\tGSM4", "g1\t1\t2\t3\t4", "g2\t5\t6\t7\t8"]
93
+ native.write_text(native.read_text().replace("\t1\t2\t3\t4", "\t1.5\t2\t3\t4"))
94
+ with pytest.raises(ValueError, match="integer"):
95
+ module.convert(native, mapping, output)