173 lines
6.0 KiB
Python
173 lines
6.0 KiB
Python
import sys
|
|||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
|
||
|
|
ROOT = Path(__file__).resolve().parents[2]
|
||
|
|
if str(ROOT) not in sys.path:
|
||
|
|
sys.path.insert(0, str(ROOT))
|
||
|
|
|
||
|
|
|
||
|
|
def _write_matrix(path: Path, header, rows):
|
||
|
|
path.write_text(
|
||
|
|
"\n".join(
|
||
|
|
["\t".join(header)]
|
||
|
|
+ ["\t".join([str(row[0]), *[str(v) for v in row[1:]]]) for row in rows]
|
||
|
|
)
|
||
|
|
+ "\n",
|
||
|
|
encoding="utf-8",
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_featurecounts_paired_end_counts_fragments(monkeypatch, tmp_path):
|
||
|
|
from apps.cli import run_rnaseq_quant as quant
|
||
|
|
|
||
|
|
captured = {}
|
||
|
|
|
||
|
|
def fake_run(cmd, check, capture_output, text):
|
||
|
|
captured["cmd"] = list(cmd)
|
||
|
|
return object()
|
||
|
|
|
||
|
|
monkeypatch.setattr(quant.subprocess, "run", fake_run)
|
||
|
|
annotation = tmp_path / "genes.gtf"
|
||
|
|
annotation.write_text("placeholder\n", encoding="utf-8")
|
||
|
|
|
||
|
|
result = quant.run_featurecounts(
|
||
|
|
[tmp_path / "S1.bam"],
|
||
|
|
annotation,
|
||
|
|
tmp_path / "featurecounts.txt",
|
||
|
|
cpu=2,
|
||
|
|
library_strandness="non_stranded",
|
||
|
|
command_log=tmp_path / "featurecounts.command.txt",
|
||
|
|
)
|
||
|
|
|
||
|
|
assert result == annotation
|
||
|
|
assert "-p" in captured["cmd"]
|
||
|
|
assert "--countReadPairs" in captured["cmd"]
|
||
|
|
assert captured["cmd"].index("--countReadPairs") > captured["cmd"].index("-p")
|
||
|
|
|
||
|
|
|
||
|
|
def test_tx2gene_parser_supports_gff3_and_gtf_attributes(tmp_path):
|
||
|
|
from apps.cli.run_rnaseq_quant import parse_tx2gene_from_gff
|
||
|
|
|
||
|
|
annotation = tmp_path / "mixed.gff3"
|
||
|
|
annotation.write_text(
|
||
|
|
"\n".join(
|
||
|
|
[
|
||
|
|
"chr1\tsrc\tmRNA\t1\t100\t.\t+\t.\tID=tx.gff3;Parent=gene.gff3",
|
||
|
|
'chr1\tsrc\ttranscript\t200\t300\t.\t+\t.\tgene_id "gene.gtf"; transcript_id "tx.gtf";',
|
||
|
|
"chr1\tsrc\ttranscript\t400\t500\t.\t+\t.\tID=tx.name;Name=gene.name",
|
||
|
|
"chr1\tsrc\tCDS\t600\t700\t.\t+\t0\tID=cds1;Parent=tx.parent;gene_id=gene.cds",
|
||
|
|
]
|
||
|
|
)
|
||
|
|
+ "\n",
|
||
|
|
encoding="utf-8",
|
||
|
|
)
|
||
|
|
|
||
|
|
tx2gene = parse_tx2gene_from_gff(annotation)
|
||
|
|
|
||
|
|
assert tx2gene["tx.gff3"] == "gene.gff3"
|
||
|
|
assert tx2gene["tx.gtf"] == "gene.gtf"
|
||
|
|
assert tx2gene["tx.name"] == "gene.name"
|
||
|
|
assert tx2gene["tx.parent"] == "gene.cds"
|
||
|
|
|
||
|
|
|
||
|
|
def test_matrix_bridge_qc_accepts_salmon_when_samples_and_genes_match(tmp_path):
|
||
|
|
from apps.cli.run_rnaseq_quant import compute_matrix_bridge_qc
|
||
|
|
|
||
|
|
count_matrix = tmp_path / "Os.count_matrix.txt"
|
||
|
|
salmon_tpm = tmp_path / "Os.salmon.tpm_matrix.txt"
|
||
|
|
tx2gene = tmp_path / "Os.salmon.tx2gene.tsv"
|
||
|
|
index_dir = tmp_path / "salmon_index"
|
||
|
|
index_dir.mkdir()
|
||
|
|
|
||
|
|
rows = [(f"gene{i}", i, i + 1) for i in range(1, 21)]
|
||
|
|
_write_matrix(count_matrix, ["gene_id", "S1", "S2"], rows)
|
||
|
|
_write_matrix(salmon_tpm, ["gene_id", "S1", "S2"], [(f"gene{i}", 50000, 50000) for i in range(1, 21)])
|
||
|
|
tx2gene.write_text(
|
||
|
|
"transcript_id\tgene_id\n" + "\n".join(f"tx{i}\tgene{i}" for i in range(1, 21)) + "\n",
|
||
|
|
encoding="utf-8",
|
||
|
|
)
|
||
|
|
(index_dir / "ref_indexing.log").write_text(
|
||
|
|
"Removed 2 transcripts that were sequence duplicates of indexed transcripts.\n",
|
||
|
|
encoding="utf-8",
|
||
|
|
)
|
||
|
|
(index_dir / "info.json").write_text('{"keep_duplicates": false}\n', encoding="utf-8")
|
||
|
|
|
||
|
|
qc = compute_matrix_bridge_qc(
|
||
|
|
count_matrix=count_matrix,
|
||
|
|
abundance_tpm=salmon_tpm,
|
||
|
|
tx2gene=tx2gene,
|
||
|
|
engine="salmon",
|
||
|
|
salmon_index_dir=index_dir,
|
||
|
|
output_path=tmp_path / "Os.rnaseq_quant.matrix_bridge_qc.tsv",
|
||
|
|
)
|
||
|
|
|
||
|
|
assert qc["status"] == "pass"
|
||
|
|
assert qc["sample_columns_match"] is True
|
||
|
|
assert qc["gene_overlap_vs_featurecounts_pct"] == 100.0
|
||
|
|
assert qc["tpm_column_sums_within_tolerance"] is True
|
||
|
|
assert qc["duplicate_transcripts_removed"] == 2
|
||
|
|
assert qc["recommended_expression_tpm_source"] == "salmon_tximport_tpm"
|
||
|
|
|
||
|
|
|
||
|
|
def test_matrix_bridge_qc_falls_back_when_tx2gene_fallback_is_high(tmp_path):
|
||
|
|
from apps.cli.run_rnaseq_quant import compute_matrix_bridge_qc
|
||
|
|
|
||
|
|
count_matrix = tmp_path / "Os.count_matrix.txt"
|
||
|
|
salmon_tpm = tmp_path / "Os.salmon.tpm_matrix.txt"
|
||
|
|
tx2gene = tmp_path / "Os.salmon.tx2gene.tsv"
|
||
|
|
|
||
|
|
rows = [(f"gene{i}", i, i + 1) for i in range(1, 21)]
|
||
|
|
_write_matrix(count_matrix, ["gene_id", "S1", "S2"], rows)
|
||
|
|
_write_matrix(salmon_tpm, ["gene_id", "S1", "S2"], [(f"gene{i}", 50000, 50000) for i in range(1, 21)])
|
||
|
|
tx2gene.write_text(
|
||
|
|
"transcript_id\tgene_id\n" + "\n".join(f"tx{i}\tgene{i}" for i in range(1, 21)) + "\n",
|
||
|
|
encoding="utf-8",
|
||
|
|
)
|
||
|
|
|
||
|
|
qc = compute_matrix_bridge_qc(
|
||
|
|
count_matrix=count_matrix,
|
||
|
|
abundance_tpm=salmon_tpm,
|
||
|
|
tx2gene=tx2gene,
|
||
|
|
engine="salmon",
|
||
|
|
tx2gene_metrics={"tx2gene_fallback_pct": 6.0},
|
||
|
|
)
|
||
|
|
|
||
|
|
assert qc["status"] == "fallback"
|
||
|
|
assert qc["recommended_expression_tpm_source"] == "featurecounts_tpm"
|
||
|
|
assert "tx2gene_fallback_gt_5pct" in qc["failure_reasons"]
|
||
|
|
|
||
|
|
|
||
|
|
def test_expression_resolver_prefers_salmon_tpm_when_bridge_qc_passes(tmp_path):
|
||
|
|
from apps.cli.run_expression import resolve_rnaseq_expression_matrices
|
||
|
|
|
||
|
|
final_dir = tmp_path / "final"
|
||
|
|
final_dir.mkdir()
|
||
|
|
for name in (
|
||
|
|
"Os.fpkm_matrix.txt",
|
||
|
|
"Os.tpm_matrix.txt",
|
||
|
|
"Os.salmon.tpm_matrix.txt",
|
||
|
|
"Os.rnaseq_sample_qc.vst_matrix.tsv",
|
||
|
|
):
|
||
|
|
_write_matrix(final_dir / name, ["gene_id", "S1", "S2"], [("gene1", 1, 2)])
|
||
|
|
(final_dir / "Os.rnaseq_quant.matrix_bridge_qc.tsv").write_text(
|
||
|
|
"metric\tvalue\n"
|
||
|
|
"status\tpass\n"
|
||
|
|
"recommended_expression_tpm_source\tsalmon_tximport_tpm\n"
|
||
|
|
"gene_overlap_vs_featurecounts_pct\t99.5\n"
|
||
|
|
"sample_columns_match\ttrue\n",
|
||
|
|
encoding="utf-8",
|
||
|
|
)
|
||
|
|
|
||
|
|
resolved = resolve_rnaseq_expression_matrices(
|
||
|
|
final_dir=final_dir,
|
||
|
|
prefix="Os",
|
||
|
|
fpkm_file=final_dir / "Os.fpkm_matrix.txt",
|
||
|
|
tpm_file=final_dir / "Os.tpm_matrix.txt",
|
||
|
|
vst_file=final_dir / "Os.rnaseq_sample_qc.vst_matrix.tsv",
|
||
|
|
)
|
||
|
|
|
||
|
|
assert resolved["tpm_file"] == final_dir / "Os.salmon.tpm_matrix.txt"
|
||
|
|
assert resolved["tpm_source"] == "salmon_tximport_tpm"
|
||
|
|
assert "bridge_qc_passed" in resolved["tpm_source_reason"]
|