mhcflow 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mhcflow/__init__.py +5 -0
- mhcflow/__main__.py +5 -0
- mhcflow/_version.py +21 -0
- mhcflow/cli.py +75 -0
- mhcflow/dumper.py +83 -0
- mhcflow/finalizer.py +100 -0
- mhcflow/fisher.py +287 -0
- mhcflow/helper.py +185 -0
- mhcflow/logger.py +55 -0
- mhcflow/mhcflow.py +71 -0
- mhcflow/realigner.py +135 -0
- mhcflow/runnable.py +218 -0
- mhcflow/tag_builder.py +87 -0
- mhcflow-0.2.0.dist-info/METADATA +342 -0
- mhcflow-0.2.0.dist-info/RECORD +18 -0
- mhcflow-0.2.0.dist-info/WHEEL +4 -0
- mhcflow-0.2.0.dist-info/entry_points.txt +2 -0
- mhcflow-0.2.0.dist-info/licenses/LICENSE +693 -0
mhcflow/__init__.py
ADDED
mhcflow/__main__.py
ADDED
mhcflow/_version.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
|
|
5
|
+
|
|
6
|
+
TYPE_CHECKING = False
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from typing import Tuple
|
|
9
|
+
from typing import Union
|
|
10
|
+
|
|
11
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
12
|
+
else:
|
|
13
|
+
VERSION_TUPLE = object
|
|
14
|
+
|
|
15
|
+
version: str
|
|
16
|
+
__version__: str
|
|
17
|
+
__version_tuple__: VERSION_TUPLE
|
|
18
|
+
version_tuple: VERSION_TUPLE
|
|
19
|
+
|
|
20
|
+
__version__ = version = '0.2.0'
|
|
21
|
+
__version_tuple__ = version_tuple = (0, 2, 0)
|
mhcflow/cli.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
from tinyscibio import parse_path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def parse_cmd() -> argparse.ArgumentParser:
|
|
7
|
+
parser = argparse.ArgumentParser()
|
|
8
|
+
parser.add_argument(
|
|
9
|
+
"--bam",
|
|
10
|
+
metavar="FILE",
|
|
11
|
+
type=parse_path,
|
|
12
|
+
required=True,
|
|
13
|
+
help="specify path to BAM file",
|
|
14
|
+
)
|
|
15
|
+
parser.add_argument(
|
|
16
|
+
"--ref",
|
|
17
|
+
metavar="FILE",
|
|
18
|
+
type=parse_path,
|
|
19
|
+
required=True,
|
|
20
|
+
help="specify path to HLA reference in Fasta format.",
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--tag",
|
|
24
|
+
metavar="FILE",
|
|
25
|
+
type=parse_path,
|
|
26
|
+
required=True,
|
|
27
|
+
help="specify path to HLA tag sequence file.",
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--bed",
|
|
31
|
+
metavar="FILE",
|
|
32
|
+
type=parse_path,
|
|
33
|
+
required=True,
|
|
34
|
+
help="specify path to HLA region in BED format.",
|
|
35
|
+
)
|
|
36
|
+
parser.add_argument(
|
|
37
|
+
"--freq",
|
|
38
|
+
metavar="FILE",
|
|
39
|
+
type=parse_path,
|
|
40
|
+
required=True,
|
|
41
|
+
help="specify path to HLA frequency file.",
|
|
42
|
+
)
|
|
43
|
+
parser.add_argument(
|
|
44
|
+
"--outdir",
|
|
45
|
+
metavar="DIR",
|
|
46
|
+
type=parse_path,
|
|
47
|
+
required=True,
|
|
48
|
+
help="specify path to output folder.",
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"--min-ecnt",
|
|
52
|
+
metavar="INT",
|
|
53
|
+
type=int,
|
|
54
|
+
default=999,
|
|
55
|
+
help="specify minimum # of mm events (999).",
|
|
56
|
+
)
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--nproc",
|
|
59
|
+
metavar="INT",
|
|
60
|
+
type=int,
|
|
61
|
+
default=8,
|
|
62
|
+
help="specify # processes to use (8).",
|
|
63
|
+
)
|
|
64
|
+
parser.add_argument(
|
|
65
|
+
"--overwrite", action="store_true", help="specify to overwrite scores."
|
|
66
|
+
)
|
|
67
|
+
parser.add_argument(
|
|
68
|
+
"--no-clean",
|
|
69
|
+
action="store_true",
|
|
70
|
+
help="specify to NOT clean intermediate files.",
|
|
71
|
+
)
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
"--debug", action="store_true", help="specify to enter debug mode."
|
|
74
|
+
)
|
|
75
|
+
return parser
|
mhcflow/dumper.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import multiprocessing as mp
|
|
2
|
+
from functools import partial
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import polars as pl
|
|
6
|
+
from tinyscibio import BAMetadata, _PathLike, make_dir, parse_path
|
|
7
|
+
|
|
8
|
+
from .helper import (
|
|
9
|
+
FileManifest,
|
|
10
|
+
_check_rg_exists,
|
|
11
|
+
_check_single_rg,
|
|
12
|
+
_get_sm,
|
|
13
|
+
_verify_prev_run,
|
|
14
|
+
)
|
|
15
|
+
from .logger import logger
|
|
16
|
+
from .runnable import _samtools_fastq
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _bam2fq_from_idx(
|
|
20
|
+
idx_fspath: _PathLike,
|
|
21
|
+
bametadata: BAMetadata,
|
|
22
|
+
outdir: _PathLike,
|
|
23
|
+
nproc: int = 1,
|
|
24
|
+
overwrite: bool = False,
|
|
25
|
+
) -> FileManifest:
|
|
26
|
+
logger.info(f"Start to dump reads to fastq given ids: {idx_fspath}")
|
|
27
|
+
# set up output dir
|
|
28
|
+
outdir = parse_path(outdir)
|
|
29
|
+
make_dir(outdir, parents=True, exist_ok=True)
|
|
30
|
+
|
|
31
|
+
# get sm field from rg
|
|
32
|
+
_check_rg_exists(bametadata)
|
|
33
|
+
_check_single_rg(bametadata)
|
|
34
|
+
rg = bametadata.read_groups[0]
|
|
35
|
+
sm = _get_sm(rg)
|
|
36
|
+
|
|
37
|
+
fm_json = outdir / f"{sm}.dumper.file_manifest.json"
|
|
38
|
+
if fm_json.exists():
|
|
39
|
+
logger.info(
|
|
40
|
+
f"Detected file manifest from previous dumper run: {str(fm_json)}"
|
|
41
|
+
)
|
|
42
|
+
fm = FileManifest._from_json(fm_json)
|
|
43
|
+
if _verify_prev_run(fm, overwrite):
|
|
44
|
+
return fm
|
|
45
|
+
|
|
46
|
+
fm = FileManifest()
|
|
47
|
+
logdir = outdir / "log"
|
|
48
|
+
make_dir(logdir, parents=True, exist_ok=True)
|
|
49
|
+
dumper_done = logdir / f"{sm}.dumper.done"
|
|
50
|
+
idx_df = pl.read_csv(idx_fspath, separator="\t")
|
|
51
|
+
if "qnames" not in idx_df.columns:
|
|
52
|
+
raise pl.exceptions.ColumnNotFoundError()
|
|
53
|
+
|
|
54
|
+
# split read ids into batches and output each batch to file
|
|
55
|
+
# each idx file will be input to samtools view -N
|
|
56
|
+
qnames = idx_df["qnames"].unique().to_numpy()
|
|
57
|
+
qnames_batches = np.array_split(qnames, nproc)
|
|
58
|
+
idxs = []
|
|
59
|
+
for i in range(len(qnames_batches)):
|
|
60
|
+
qname_batch_fspath = outdir / f"{sm}.fisher.idxs.{i}.txt"
|
|
61
|
+
pl.DataFrame({"qnames": qnames_batches[i]}).write_csv(
|
|
62
|
+
qname_batch_fspath, include_header=False
|
|
63
|
+
)
|
|
64
|
+
idxs.append(qname_batch_fspath)
|
|
65
|
+
# run samtools fastq to extract read and dump to fq
|
|
66
|
+
r1s, r2s = [], []
|
|
67
|
+
dones, logs = [], []
|
|
68
|
+
with mp.Pool(processes=nproc) as pool:
|
|
69
|
+
for res in pool.imap_unordered(
|
|
70
|
+
partial(_samtools_fastq, bam_fspath=bametadata.fspath), idxs
|
|
71
|
+
):
|
|
72
|
+
r1, r2, done, log = res
|
|
73
|
+
r1s.append(r1)
|
|
74
|
+
r2s.append(r2)
|
|
75
|
+
dones.append(done)
|
|
76
|
+
logs.append(log)
|
|
77
|
+
dumper_done.touch()
|
|
78
|
+
fm._register_inputs(idx=idx_fspath, bam=bametadata.fspath)
|
|
79
|
+
fm._register_outputs(r1s=r1s, r2s=r2s, idxs=idxs)
|
|
80
|
+
fm._register_aux(done=dumper_done)
|
|
81
|
+
fm._register_intermediate_aux(dones=dones, logs=logs)
|
|
82
|
+
fm._to_json(fm_json)
|
|
83
|
+
return fm
|
mhcflow/finalizer.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import polars as pl
|
|
4
|
+
from pyfaidx import Faidx
|
|
5
|
+
from tinyscibio import BAMetadata, _PathLike, make_dir, parse_path
|
|
6
|
+
|
|
7
|
+
from .helper import (
|
|
8
|
+
FileManifest,
|
|
9
|
+
_check_rg_exists,
|
|
10
|
+
_check_single_rg,
|
|
11
|
+
_get_sm,
|
|
12
|
+
_verify_prev_run,
|
|
13
|
+
)
|
|
14
|
+
from .logger import logger
|
|
15
|
+
from .realigner import _run_realigner
|
|
16
|
+
from .runnable import _novoindex
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _dump_seq(allele: str, fa: Faidx, out: Path) -> None:
|
|
20
|
+
with open(out, "a") as f:
|
|
21
|
+
sequence = fa.fetch(allele, 1, fa.index[allele].rlen)
|
|
22
|
+
f.write(f">{allele}\n{str(sequence)}\n")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _run_finalizer(
|
|
26
|
+
bam_fspath: _PathLike,
|
|
27
|
+
ref: _PathLike,
|
|
28
|
+
fisher_fm_json: _PathLike,
|
|
29
|
+
typer_res_fspath: _PathLike,
|
|
30
|
+
outdir: _PathLike,
|
|
31
|
+
nproc: int = 1,
|
|
32
|
+
overwrite: bool = False,
|
|
33
|
+
) -> FileManifest:
|
|
34
|
+
logger.info("Finalize sample HLA reference and realignemnt.")
|
|
35
|
+
bametadata = BAMetadata(str(bam_fspath))
|
|
36
|
+
_check_rg_exists(bametadata)
|
|
37
|
+
_check_single_rg(bametadata)
|
|
38
|
+
rg = bametadata.read_groups[0]
|
|
39
|
+
sm = _get_sm(rg)
|
|
40
|
+
|
|
41
|
+
outdir = parse_path(outdir)
|
|
42
|
+
make_dir(outdir, parents=True, exist_ok=True)
|
|
43
|
+
|
|
44
|
+
fm_json = outdir / f"{sm}.finalizer.file_manifest.json"
|
|
45
|
+
if fm_json.exists():
|
|
46
|
+
logger.info(
|
|
47
|
+
f"Detected file manifest from previous run: {str(fm_json)}"
|
|
48
|
+
)
|
|
49
|
+
finalizer_fm = FileManifest._from_json(fm_json)
|
|
50
|
+
if _verify_prev_run(finalizer_fm, overwrite):
|
|
51
|
+
return finalizer_fm
|
|
52
|
+
|
|
53
|
+
finalizer_fm = FileManifest()
|
|
54
|
+
logdir = outdir / "log"
|
|
55
|
+
make_dir(logdir, parents=True, exist_ok=True)
|
|
56
|
+
finalizer_done = logdir / f"{sm}.finalizer.done"
|
|
57
|
+
|
|
58
|
+
ref = parse_path(ref)
|
|
59
|
+
fai = ref.parent / parse_path(f"{ref.name}.fai")
|
|
60
|
+
if not fai.exists():
|
|
61
|
+
raise FileNotFoundError
|
|
62
|
+
|
|
63
|
+
typer_res = pl.read_csv(typer_res_fspath, separator="\t")
|
|
64
|
+
# 3 locus * 2 alleles = 6
|
|
65
|
+
if typer_res.shape[0] != 6:
|
|
66
|
+
raise ValueError
|
|
67
|
+
|
|
68
|
+
fa_out = outdir / f"{sm}.hla.fasta"
|
|
69
|
+
fa = Faidx(ref)
|
|
70
|
+
# do not forget to take unique for homozygous genotype of HLA gene.
|
|
71
|
+
# I dont want duplicated sequences in the fasta.
|
|
72
|
+
_ = list(
|
|
73
|
+
map(
|
|
74
|
+
lambda x: _dump_seq(x, fa, fa_out),
|
|
75
|
+
typer_res["allele"].unique().to_list(),
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
nix, index_log, index_done = _novoindex(fa_out)
|
|
79
|
+
|
|
80
|
+
realigner_fm = _run_realigner(
|
|
81
|
+
bam_fspath, nix, fisher_fm_json, outdir, nproc, overwrite
|
|
82
|
+
)
|
|
83
|
+
finalizer_done.touch()
|
|
84
|
+
|
|
85
|
+
# record file manifest for finalizer
|
|
86
|
+
finalizer_fm._register_inputs(hlaref=str(ref), typer_res=typer_res_fspath)
|
|
87
|
+
finalizer_fm._register_aux(done=finalizer_done, myself=fm_json)
|
|
88
|
+
finalizer_fm._register_outputs(
|
|
89
|
+
sample_hlaref=str(fa_out),
|
|
90
|
+
**realigner_fm.outputs,
|
|
91
|
+
)
|
|
92
|
+
finalizer_fm._register_intermediate(**realigner_fm.intermediates)
|
|
93
|
+
finalizer_fm._register_intermediate_aux(
|
|
94
|
+
index_log=index_log,
|
|
95
|
+
index_done=index_done,
|
|
96
|
+
**realigner_fm.intermediate_aux,
|
|
97
|
+
)
|
|
98
|
+
finalizer_fm._to_json(fm_json)
|
|
99
|
+
|
|
100
|
+
return finalizer_fm
|
mhcflow/fisher.py
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import multiprocessing as mp
|
|
2
|
+
import time
|
|
3
|
+
from functools import partial
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import polars as pl
|
|
7
|
+
import pysam
|
|
8
|
+
from tinyscibio import (
|
|
9
|
+
BAMetadata,
|
|
10
|
+
_PathLike,
|
|
11
|
+
bed_to_df,
|
|
12
|
+
make_dir,
|
|
13
|
+
parse_path,
|
|
14
|
+
walk_bam,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from .dumper import _bam2fq_from_idx
|
|
18
|
+
from .helper import (
|
|
19
|
+
FileManifest,
|
|
20
|
+
_check_rg_exists,
|
|
21
|
+
_check_single_rg,
|
|
22
|
+
_get_sm,
|
|
23
|
+
_verify_prev_run,
|
|
24
|
+
)
|
|
25
|
+
from .logger import logger
|
|
26
|
+
from .tag_builder import PREBUILD_TYPE, build
|
|
27
|
+
|
|
28
|
+
CHR6 = ["6", "chr6", "NC00006", "CM000668"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _fish_unplaced(
|
|
32
|
+
bam_fspath: _PathLike, prebuilt_tag: PREBUILD_TYPE, out: _PathLike
|
|
33
|
+
) -> tuple[pl.DataFrame, _PathLike]:
|
|
34
|
+
logger.info("Fish unplaced sequence with tag pattern")
|
|
35
|
+
out = parse_path(out)
|
|
36
|
+
logdir = out.parent / "log"
|
|
37
|
+
make_dir(logdir, parents=True, exist_ok=True)
|
|
38
|
+
done = logdir / f"{out.stem}.done"
|
|
39
|
+
if done.exists():
|
|
40
|
+
logger.info(
|
|
41
|
+
"Found result of fished sequences with tag pattern "
|
|
42
|
+
f"from previous run: {done}. Skip."
|
|
43
|
+
)
|
|
44
|
+
df = pl.read_csv(out, separator="\t")
|
|
45
|
+
return (df, done)
|
|
46
|
+
qnames = []
|
|
47
|
+
start_t = time.time()
|
|
48
|
+
with pysam.AlignmentFile(str(bam_fspath), "rb") as bamf:
|
|
49
|
+
for aln in bamf.fetch(until_eof=True):
|
|
50
|
+
if not aln.is_unmapped:
|
|
51
|
+
continue
|
|
52
|
+
assert aln.query_sequence is not None
|
|
53
|
+
match = list(prebuilt_tag.iter(aln.query_sequence))
|
|
54
|
+
if match:
|
|
55
|
+
qnames += [aln.query_name]
|
|
56
|
+
logger.info(
|
|
57
|
+
f"Fish unplaced sequence with tag pattern: {time.time() - start_t} sec"
|
|
58
|
+
)
|
|
59
|
+
logger.info(f"Fished {len(qnames)} unplaced sequence with tag pattern")
|
|
60
|
+
merged_qnames = (
|
|
61
|
+
pl.DataFrame({"qnames": qnames}) if qnames else pl.DataFrame()
|
|
62
|
+
)
|
|
63
|
+
merged_qnames.write_csv(out, separator="\t")
|
|
64
|
+
done.touch()
|
|
65
|
+
return merged_qnames, done
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _fish_one_hla(region: str, bam_fspath: str) -> pl.DataFrame:
|
|
69
|
+
return walk_bam(bam_fspath, region, exclude=0, return_qname=True)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _fish_multi_hla(
|
|
73
|
+
bed_fsapth: _PathLike,
|
|
74
|
+
bam_fspath: _PathLike,
|
|
75
|
+
out: _PathLike,
|
|
76
|
+
nproc: int = 4,
|
|
77
|
+
) -> tuple[pl.DataFrame, _PathLike]:
|
|
78
|
+
logger.info(f"Fish sequence mapped to regions defined in {bed_fsapth}.")
|
|
79
|
+
out = parse_path(out)
|
|
80
|
+
logdir = out.parent / "log"
|
|
81
|
+
make_dir(logdir, parents=True, exist_ok=True)
|
|
82
|
+
done = logdir / f"{out.stem}.done"
|
|
83
|
+
if done.exists():
|
|
84
|
+
logger.info(
|
|
85
|
+
"Found result of fished sequences with tag pattern "
|
|
86
|
+
f"from previous run: {done}. Skip."
|
|
87
|
+
)
|
|
88
|
+
df = pl.read_csv(out, separator="\t")
|
|
89
|
+
return (df, done)
|
|
90
|
+
df = bed_to_df(bed_fsapth)
|
|
91
|
+
regions = [f"{row[0]}:{row[1]}-{row[2]}" for row in df.rows()]
|
|
92
|
+
nproc = min(len(regions), nproc) # nproc set to minimum of these 2 values
|
|
93
|
+
qnames: list[pl.DataFrame] = []
|
|
94
|
+
start_t = time.time()
|
|
95
|
+
with mp.get_context("spawn").Pool(processes=nproc) as pool:
|
|
96
|
+
for res in pool.imap_unordered(
|
|
97
|
+
partial(_fish_one_hla, bam_fspath=str(bam_fspath)),
|
|
98
|
+
regions,
|
|
99
|
+
):
|
|
100
|
+
if res is not None:
|
|
101
|
+
qnames += [res.select("qnames")]
|
|
102
|
+
logger.info(
|
|
103
|
+
"Fished sequence mapped to regions defined in BED file: "
|
|
104
|
+
f"{time.time() - start_t} sec."
|
|
105
|
+
)
|
|
106
|
+
logger.info(
|
|
107
|
+
f"Fished {len(qnames)} mapped to HLA regions defined in BED file"
|
|
108
|
+
)
|
|
109
|
+
merged_qnames = pl.concat(qnames) if qnames else pl.DataFrame()
|
|
110
|
+
merged_qnames.write_csv(out, separator="\t")
|
|
111
|
+
done.touch()
|
|
112
|
+
return (merged_qnames, done)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _fish_one_region(
|
|
116
|
+
region: tuple[str, int, int],
|
|
117
|
+
bam_fspath: _PathLike,
|
|
118
|
+
prebuilt_tag: PREBUILD_TYPE,
|
|
119
|
+
) -> pl.DataFrame | None:
|
|
120
|
+
qnames = []
|
|
121
|
+
sn, start, end = region
|
|
122
|
+
with pysam.AlignmentFile(str(bam_fspath), "rb") as bamf:
|
|
123
|
+
for aln in bamf.fetch(contig=sn, start=start, stop=end):
|
|
124
|
+
assert aln.query_sequence is not None
|
|
125
|
+
match = list(prebuilt_tag.iter(aln.query_sequence))
|
|
126
|
+
if match:
|
|
127
|
+
qnames += [aln.query_name]
|
|
128
|
+
return pl.DataFrame({"qnames": qnames}) if qnames else None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _fish_multi_regions(
|
|
132
|
+
split_regions: list[tuple[str, int, int]],
|
|
133
|
+
bam_fspath: _PathLike,
|
|
134
|
+
prebuilt_tag: PREBUILD_TYPE,
|
|
135
|
+
out: _PathLike,
|
|
136
|
+
nproc: int = 4,
|
|
137
|
+
) -> tuple[pl.DataFrame, _PathLike]:
|
|
138
|
+
logger.info("Fish sequence with tag pattern.")
|
|
139
|
+
out = parse_path(out)
|
|
140
|
+
logdir = out.parent / "log"
|
|
141
|
+
make_dir(logdir, parents=True, exist_ok=True)
|
|
142
|
+
done = logdir / f"{out.stem}.done"
|
|
143
|
+
if done.exists():
|
|
144
|
+
logger.info(
|
|
145
|
+
"Found result of fished sequences with tag pattern "
|
|
146
|
+
f"from previous run: {done}. Skip."
|
|
147
|
+
)
|
|
148
|
+
df = pl.read_csv(out, separator="\t")
|
|
149
|
+
return (df, done)
|
|
150
|
+
start_t = time.time()
|
|
151
|
+
qnames: list[pl.DataFrame] = []
|
|
152
|
+
with mp.Pool(processes=nproc) as pool:
|
|
153
|
+
for res in pool.imap_unordered(
|
|
154
|
+
partial(
|
|
155
|
+
_fish_one_region,
|
|
156
|
+
bam_fspath=bam_fspath,
|
|
157
|
+
prebuilt_tag=prebuilt_tag,
|
|
158
|
+
),
|
|
159
|
+
split_regions,
|
|
160
|
+
):
|
|
161
|
+
if res is not None:
|
|
162
|
+
qnames += [res]
|
|
163
|
+
logger.info(f"Fish sequence with tag pattern: {time.time() - start_t} sec")
|
|
164
|
+
logger.info(
|
|
165
|
+
f"Fished {sum([df.shape[0] for df in qnames])} readss with tag pattern."
|
|
166
|
+
)
|
|
167
|
+
# merged_qnames will be empty dataframe if there were no df returned
|
|
168
|
+
merged_qnames = pl.concat(qnames) if qnames else pl.DataFrame()
|
|
169
|
+
# empty file will be generated when merged_qnames is empty df
|
|
170
|
+
merged_qnames.write_csv(out, separator="\t")
|
|
171
|
+
done.touch()
|
|
172
|
+
return (merged_qnames, done)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _split_regions(
|
|
176
|
+
regions: dict[str, list[int]],
|
|
177
|
+
by: str,
|
|
178
|
+
n_splits: int = 4,
|
|
179
|
+
split_size: int = 500_000,
|
|
180
|
+
) -> list[tuple[str, int, int]]:
|
|
181
|
+
if by not in ["len", "num"]:
|
|
182
|
+
raise ValueError(f"Unsupported split by method {by=}")
|
|
183
|
+
logger.info("Split regions into smaller intevals.")
|
|
184
|
+
splits = []
|
|
185
|
+
for sn, region in regions.items():
|
|
186
|
+
start, end = region
|
|
187
|
+
n_split = (
|
|
188
|
+
((end - start + 1) // split_size) + 1 if by == "len" else n_splits
|
|
189
|
+
)
|
|
190
|
+
s = np.array_split(np.arange(end), n_split)
|
|
191
|
+
splits += [(sn, int(split[0]), int(split[-1])) for split in s]
|
|
192
|
+
|
|
193
|
+
logger.info(f"Split regions into {len(splits)} intevals.")
|
|
194
|
+
return splits
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _run_fisher(
|
|
198
|
+
bam_fspath: _PathLike,
|
|
199
|
+
tag_fspath: _PathLike,
|
|
200
|
+
bed_fspath: _PathLike,
|
|
201
|
+
outdir: _PathLike,
|
|
202
|
+
prebuild_method: str = "ahocorasick",
|
|
203
|
+
nproc: int = 4,
|
|
204
|
+
overwrite: bool = False,
|
|
205
|
+
) -> FileManifest:
|
|
206
|
+
logger.info("Start to fish HLA-relevant reads.")
|
|
207
|
+
outdir = parse_path(outdir)
|
|
208
|
+
make_dir(outdir, parents=True, exist_ok=True)
|
|
209
|
+
|
|
210
|
+
bametadata = BAMetadata(str(bam_fspath))
|
|
211
|
+
_check_rg_exists(bametadata)
|
|
212
|
+
_check_single_rg(bametadata)
|
|
213
|
+
rg = bametadata.read_groups[0]
|
|
214
|
+
sm = _get_sm(rg)
|
|
215
|
+
|
|
216
|
+
fm_json = outdir / f"{sm}.fisher.file_manifest.json"
|
|
217
|
+
if fm_json.exists():
|
|
218
|
+
logger.info(
|
|
219
|
+
f"Detected file manifest from previous fisher run: {str(fm_json)}"
|
|
220
|
+
)
|
|
221
|
+
fisher_fm = FileManifest._from_json(fm_json)
|
|
222
|
+
if _verify_prev_run(fisher_fm, overwrite):
|
|
223
|
+
return fisher_fm
|
|
224
|
+
|
|
225
|
+
fisher_fm = FileManifest()
|
|
226
|
+
logdir = outdir / "log"
|
|
227
|
+
make_dir(logdir, parents=True, exist_ok=True)
|
|
228
|
+
fisher_done = logdir / f"{sm}.fisher.done"
|
|
229
|
+
|
|
230
|
+
prebuilt_tag = build(tag_fspath, method=prebuild_method)
|
|
231
|
+
|
|
232
|
+
bam_fspath = bametadata.fspath
|
|
233
|
+
hla_qname_out = outdir / f"{sm}.fisher.hla_bed.idx"
|
|
234
|
+
hla_bed_qnames, hla_bed_done = _fish_multi_hla(
|
|
235
|
+
bed_fspath, bam_fspath, hla_qname_out
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
regions = {
|
|
239
|
+
sn: [1, ln] for sn, ln in bametadata.seqmap().items() if sn in CHR6
|
|
240
|
+
}
|
|
241
|
+
splits = _split_regions(regions, n_splits=nproc, by="num")
|
|
242
|
+
chr6_qname_out = outdir / f"{sm}.fisher.chr6.idx"
|
|
243
|
+
chr6_qnames, chr6_done = _fish_multi_regions(
|
|
244
|
+
splits, bam_fspath, prebuilt_tag, chr6_qname_out, nproc
|
|
245
|
+
)
|
|
246
|
+
unplaced_qname_out = outdir / f"{sm}.fisher.unplaced.idx"
|
|
247
|
+
unplaced_qnames, unplaced_done = _fish_unplaced(
|
|
248
|
+
bam_fspath, prebuilt_tag, unplaced_qname_out
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
merged_qnames = pl.concat(
|
|
252
|
+
[hla_bed_qnames, chr6_qnames, unplaced_qnames]
|
|
253
|
+
).unique()
|
|
254
|
+
fisher_idx_out = outdir / f"{sm}.fisher.idx.final.tsv"
|
|
255
|
+
merged_qnames.write_csv(fisher_idx_out, separator="\t")
|
|
256
|
+
|
|
257
|
+
dumper_fm = _bam2fq_from_idx(fisher_idx_out, bametadata, outdir, nproc)
|
|
258
|
+
|
|
259
|
+
logger.info(f"Fished {merged_qnames.shape[0]} in total.")
|
|
260
|
+
fisher_done.touch()
|
|
261
|
+
|
|
262
|
+
logger.info("Register all relevant files to manifest.")
|
|
263
|
+
# register all relevant files to manifest
|
|
264
|
+
fisher_fm._register_inputs(
|
|
265
|
+
bam=bametadata.fspath, tag=tag_fspath, bed=bed_fspath
|
|
266
|
+
)
|
|
267
|
+
fisher_fm._register_aux(done=fisher_done, myself=fm_json)
|
|
268
|
+
fisher_fm._register_outputs(
|
|
269
|
+
**dumper_fm.outputs,
|
|
270
|
+
hla_fished_idx=hla_qname_out,
|
|
271
|
+
chr6_fished_idx=chr6_qname_out,
|
|
272
|
+
unplaced_fished_idx=unplaced_qname_out,
|
|
273
|
+
fished_all_idx=fisher_idx_out,
|
|
274
|
+
)
|
|
275
|
+
fisher_fm._register_intermediate(**dumper_fm.outputs)
|
|
276
|
+
fisher_fm._register_intermediate_aux(
|
|
277
|
+
**dumper_fm.intermediate_aux,
|
|
278
|
+
**dumper_fm.aux,
|
|
279
|
+
hla_bed_done=hla_bed_done,
|
|
280
|
+
chr6_done=chr6_done,
|
|
281
|
+
unplaced_done=unplaced_done,
|
|
282
|
+
)
|
|
283
|
+
# dump manifest to disk in json format.
|
|
284
|
+
logger.info(f"Dump manifest to {fm_json}")
|
|
285
|
+
fisher_fm._to_json(json_out=fm_json)
|
|
286
|
+
|
|
287
|
+
return fisher_fm
|