Spaces:
Paused
Paused
File size: 7,716 Bytes
ff34739 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | """Acquire the known-good control: subsample the 10x pbmc_1k_v3 FASTQs to ~40k read pairs,
and download the 3M-february-2018 cell-barcode whitelist (with a computed md5).
Deterministic subsample: the same seeded ``seqkit sample`` mask is applied to R1 and R2 (they
share record order), so pairs stay locked; ``seqkit head`` trims to an exact count.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import subprocess
import tarfile
from pathlib import Path
from seqcolyte.io.external import require_tool, tool_version
_REPO = Path(__file__).resolve().parents[1]
TARBALL_URL = "https://cf.10xgenomics.com/samples/cell-exp/3.0.0/pbmc_1k_v3/pbmc_1k_v3_fastqs.tar"
TARBALL_SIZE = 5_549_312_000
WHITELIST_URL = "https://raw.githubusercontent.com/f0t1h/3M-february-2018/master/3M-february-2018.txt.gz"
WHITELIST_SIZE = 18_350_152
WHITELIST_COUNT = 6_794_880
def _run(cmd: list[str], **kw) -> subprocess.CompletedProcess:
return subprocess.run(cmd, check=True, **kw)
def _md5(path: Path) -> str:
h = hashlib.md5()
with open(path, "rb") as fh:
for chunk in iter(lambda: fh.read(1 << 20), b""):
h.update(chunk)
return h.hexdigest()
def ensure_tarball(tarball: Path, *, url: str = TARBALL_URL, expected_size: int = TARBALL_SIZE) -> None:
if not tarball.exists():
print(f"[get_data] downloading {url} -> {tarball}")
tarball.parent.mkdir(parents=True, exist_ok=True)
_run(["wget", "-c", "-q", "-O", str(tarball), url])
size = tarball.stat().st_size
if size != expected_size:
raise RuntimeError(f"tarball size {size} != expected {expected_size}; re-download")
print(f"[get_data] tarball OK ({size} bytes)")
def _concat_members(tarball: Path, members: list[str], out: Path) -> None:
with open(out, "wb") as fh:
for m in members:
_run(["tar", "-xOf", str(tarball), m], stdout=fh)
def extract_reads(tarball: Path, workdir: Path) -> tuple[Path, Path]:
"""Concatenate the per-lane R1s and R2s (in lane order) into two gzipped FASTQs."""
with tarfile.open(tarball) as tf:
names = tf.getnames()
r1 = sorted(n for n in names if "_R1_" in n)
r2 = sorted(n for n in names if "_R2_" in n)
if not r1 or not r2:
raise RuntimeError(f"no R1/R2 members found in {tarball}")
full_r1, full_r2 = workdir / "full_R1.fastq.gz", workdir / "full_R2.fastq.gz"
print(f"[get_data] concatenating {len(r1)} R1 + {len(r2)} R2 lane files")
_concat_members(tarball, r1, full_r1)
_concat_members(tarball, r2, full_r2)
return full_r1, full_r2
def _num_seqs(seqkit: str, path: Path) -> int:
out = _run([seqkit, "stats", "-T", str(path)], capture_output=True, text=True).stdout
header, row = out.splitlines()[0].split("\t"), out.splitlines()[1].split("\t")
return int(row[header.index("num_seqs")].replace(",", ""))
def _subsample_one(seqkit: str, src: Path, dst: Path, prop: float, n: int, seed: int) -> None:
# Two steps (no pipe): order-preserving Bernoulli sample with a fixed seed, then trim to
# exactly n. Same seed + same record order on R1 and R2 keeps pairs locked. A pipe would
# SIGPIPE `seqkit sample` when `seqkit head` closes early, so materialize a temp file.
tmp = dst.with_suffix(dst.suffix + ".sampled.tmp.gz")
try:
_run([seqkit, "sample", "-s", str(seed), "-p", f"{prop:.8f}", str(src), "-o", str(tmp)])
_run([seqkit, "head", "-n", str(n), str(tmp), "-o", str(dst)])
finally:
tmp.unlink(missing_ok=True)
def subsample(full_r1: Path, full_r2: Path, out_r1: Path, out_r2: Path, *, n: int, seed: int) -> int:
seqkit = require_tool("seqkit")
total = _num_seqs(seqkit, full_r1)
prop = min(1.0, (n * 2.0) / total) if total else 1.0
print(f"[get_data] {total} pairs total; sampling p={prop:.6f} then head -n {n} (seed={seed})")
out_r1.parent.mkdir(parents=True, exist_ok=True)
_subsample_one(seqkit, full_r1, out_r1, prop, n, seed)
_subsample_one(seqkit, full_r2, out_r2, prop, n, seed)
kept = _num_seqs(seqkit, out_r1)
kept2 = _num_seqs(seqkit, out_r2)
if kept != kept2:
raise RuntimeError(f"R1/R2 subsample counts differ: {kept} != {kept2}")
print(f"[get_data] wrote {kept} pairs -> {out_r1}, {out_r2}")
return kept
def cmd_data(args: argparse.Namespace) -> int:
tarball = Path(args.tarball)
ensure_tarball(tarball, url=args.url, expected_size=args.expected_size)
workdir = Path(args.workdir)
workdir.mkdir(parents=True, exist_ok=True)
full_r1, full_r2 = extract_reads(tarball, workdir)
try:
subsample(full_r1, full_r2, Path(args.out_r1), Path(args.out_r2), n=args.n, seed=args.seed)
finally:
for f in (full_r1, full_r2):
f.unlink(missing_ok=True)
if not args.keep_tarball:
tarball.unlink(missing_ok=True)
print(f"[get_data] removed {tarball}")
return 0
def cmd_whitelist(args: argparse.Namespace) -> int:
out = Path(args.out)
out.parent.mkdir(parents=True, exist_ok=True)
if not out.exists():
print(f"[get_data] downloading whitelist -> {out}")
_run(["wget", "-c", "-q", "-O", str(out), args.url])
size = out.stat().st_size
if size != WHITELIST_SIZE:
raise RuntimeError(f"whitelist size {size} != expected {WHITELIST_SIZE}")
md5 = _md5(out)
provenance = {
"name": "3M-february-2018",
"path": str(out.relative_to(_REPO)) if out.is_absolute() else str(out),
"source_url": args.url,
"size_bytes_gz": size,
"count": WHITELIST_COUNT,
"md5": md5,
"md5_provenance": "computed_local_no_official_checksum",
"retrieved_date": args.date,
"tool_versions": {"seqkit": tool_version("seqkit")},
}
Path(args.provenance_out).write_text(json.dumps(provenance, indent=2) + "\n")
print(f"[get_data] whitelist OK ({size} bytes, md5={md5}) — provenance -> {args.provenance_out}")
print("[get_data] note: no official 10x checksum exists; md5 recorded in the sidecar, not the committed spec")
return 0
def main(argv: list[str] | None = None) -> int:
ap = argparse.ArgumentParser(prog="get_data", description="Acquire the pbmc_1k_v3 control + whitelist")
sub = ap.add_subparsers(dest="cmd", required=True)
d = sub.add_parser("data", help="download + subsample pbmc_1k_v3 to ~N pairs")
d.add_argument("--tarball", default=str(_REPO / "data/raw/pbmc_1k_v3_fastqs.tar"))
d.add_argument("--url", default=TARBALL_URL)
d.add_argument("--expected-size", type=int, default=TARBALL_SIZE, dest="expected_size")
d.add_argument("--workdir", default=str(_REPO / "data/raw"))
d.add_argument("--out-r1", default=str(_REPO / "data/raw/pbmc_1k_v3_sub_R1.fastq.gz"), dest="out_r1")
d.add_argument("--out-r2", default=str(_REPO / "data/raw/pbmc_1k_v3_sub_R2.fastq.gz"), dest="out_r2")
d.add_argument("--n", type=int, default=40000)
d.add_argument("--seed", type=int, default=100)
d.add_argument("--keep-tarball", action="store_true", dest="keep_tarball")
d.set_defaults(func=cmd_data)
w = sub.add_parser("whitelist", help="download 3M-february-2018 whitelist + compute md5")
w.add_argument("--url", default=WHITELIST_URL)
w.add_argument("--out", default=str(_REPO / "whitelists/3M-february-2018.txt.gz"))
w.add_argument("--provenance-out", default=str(_REPO / "whitelists/3M-february-2018.provenance.json"),
dest="provenance_out")
w.add_argument("--date", default="2026-07-07")
w.set_defaults(func=cmd_whitelist)
args = ap.parse_args(argv)
return args.func(args)
if __name__ == "__main__":
raise SystemExit(main())
|