agglovar 0.0.1.dev16__tar.gz → 0.0.1.dev18__tar.gz
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.
- {agglovar-0.0.1.dev16/src/agglovar.egg-info → agglovar-0.0.1.dev18}/PKG-INFO +2 -2
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/pyproject.toml +15 -6
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/__init__.py +7 -1
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/align/op.py +5 -1
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/align/score.py +14 -3
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/bed/col.py +12 -2
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/bed/intersect.py +33 -4
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/bed/join.py +85 -12
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/bed/merge.py +21 -4
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/dev/imports.py +12 -2
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/expr/variant.py +41 -1
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/fa.py +7 -1
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/io.py +11 -2
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/kmer/plot.py +5 -1
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/kmer/util.py +9 -1
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/merge/base.py +26 -17
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/merge/config/parser.py +4 -1
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/merge/config/stage.py +10 -1
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/merge/config/strategy.py +10 -1
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/merge/cumulative.py +238 -44
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/meta/decorators.py +3 -0
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/meta/descriptors.py +13 -4
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/pairwise/base.py +39 -3
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/pairwise/overlap/__init__.py +36 -0
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/pairwise/overlap/_const.py +11 -0
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/pairwise/overlap/_overlap.py +520 -79
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/pairwise/overlap/_stage.py +16 -1
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/pairwise/weights.py +14 -2
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/seqmatch.py +4 -2
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/util/lazy.py +3 -0
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/util/str.py +8 -1
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/util/var.py +4 -0
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/vcf/_vcf_const.py +3 -0
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/vcf/_vcf_header.py +21 -4
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/vcf/_vcf_read.py +69 -25
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/vcf/_vcf_write.py +3 -0
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18/src/agglovar.egg-info}/PKG-INFO +2 -2
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar.egg-info/requires.txt +1 -1
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/LICENSE +0 -0
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/README.md +0 -0
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/setup.cfg +0 -0
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/align/__init__.py +0 -0
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/bed/__init__.py +0 -0
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/dev/__init__.py +0 -0
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/expr/__init__.py +0 -0
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/kmer/__init__.py +0 -0
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/merge/__init__.py +0 -0
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/merge/config/__init__.py +0 -0
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/merge/om/__init__.py +0 -0
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/merge/om/_executor_base.py +0 -0
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/merge/om/_executor_nr.py +0 -0
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/meta/__init__.py +0 -0
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/pairwise/__init__.py +0 -0
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/schema.py +0 -0
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/util/__init__.py +0 -0
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar/vcf/__init__.py +0 -0
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar.egg-info/SOURCES.txt +0 -0
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar.egg-info/dependency_links.txt +0 -0
- {agglovar-0.0.1.dev16 → agglovar-0.0.1.dev18}/src/agglovar.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agglovar
|
|
3
|
-
Version: 0.0.1.
|
|
3
|
+
Version: 0.0.1.dev18
|
|
4
4
|
Summary: Toolkit for fast genomic variant transformations and intersects
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Requires-Python: >=3.12
|
|
@@ -10,7 +10,7 @@ Requires-Dist: biopython>=1.85
|
|
|
10
10
|
Requires-Dist: edlib>=1.3.9.post1
|
|
11
11
|
Requires-Dist: numpy>=2.3.0
|
|
12
12
|
Requires-Dist: ply>=3.11
|
|
13
|
-
Requires-Dist: polars[numpy,pandas,pyarrow]
|
|
13
|
+
Requires-Dist: polars[numpy,pandas,pyarrow]==1.42.1
|
|
14
14
|
Requires-Dist: pysam>=0.23.3
|
|
15
15
|
Provides-Extra: plot
|
|
16
16
|
Requires-Dist: matplotlib>=3.10.3; extra == "plot"
|
|
@@ -15,7 +15,9 @@ dependencies = [
|
|
|
15
15
|
"edlib>=1.3.9.post1",
|
|
16
16
|
"numpy>=2.3.0",
|
|
17
17
|
"ply>=3.11",
|
|
18
|
-
|
|
18
|
+
# 1.41.2: merge_sorted() crashes on List(Struct) columns before this (used by the cumulative
|
|
19
|
+
# merge to carry the complex-variant "seg" column).
|
|
20
|
+
"polars[numpy,pandas,pyarrow]==1.42.1",
|
|
19
21
|
"pysam>=0.23.3",
|
|
20
22
|
]
|
|
21
23
|
|
|
@@ -52,11 +54,10 @@ maintainers = [
|
|
|
52
54
|
|
|
53
55
|
[dependency-groups]
|
|
54
56
|
dev = [
|
|
55
|
-
"arrow[parquet]>=1.4.0",
|
|
56
57
|
"flake8>=7.3.0",
|
|
57
58
|
"flake8-bugbear>=24.12.12",
|
|
58
59
|
"flake8-docstrings>=1.7.0",
|
|
59
|
-
"flake8-dunder-all>=0.
|
|
60
|
+
"flake8-dunder-all>=0.6.0",
|
|
60
61
|
"flake8-mutable>=1.2.0",
|
|
61
62
|
"flake8-pyproject>=1.2.3",
|
|
62
63
|
"flake8-rst-docstrings>=0.3.1",
|
|
@@ -107,9 +108,17 @@ max-line-length = 120
|
|
|
107
108
|
[tool.flake8]
|
|
108
109
|
max-line-length = 120
|
|
109
110
|
docstring-convention = "pep257"
|
|
111
|
+
# DALL100/DALL101 (flake8-dunder-all >= 0.6.0) require a top-level PEP 562 __dir__ in every module.
|
|
112
|
+
# This package has no module-level __getattr__ to justify one, and a __dir__ returning only __all__
|
|
113
|
+
# would break agglovar.dev.imports.librl, which walks dir(module) to build its reload graph.
|
|
114
|
+
#
|
|
115
|
+
# These cannot go in extend-ignore: flake8 validates those codes against '^[A-Z]{1,3}[0-9]{0,3}$'
|
|
116
|
+
# and the 4-letter DALL prefix fails. per-file-ignores skips that check, but selects the single
|
|
117
|
+
# longest matching pattern rather than unioning them, so the codes must be repeated in every entry.
|
|
110
118
|
per-file-ignores = [
|
|
111
|
-
"
|
|
112
|
-
"src/agglovar/
|
|
113
|
-
"src/agglovar/
|
|
119
|
+
"*: DALL100, DALL101",
|
|
120
|
+
"src/agglovar/merge/config/parser.py: DALL100, DALL101, D200, D205, D212, D300, D400, D401, D403, D415",
|
|
121
|
+
"src/agglovar/vcf/__init__.py: DALL100, DALL101, DALL002, F401, F403",
|
|
122
|
+
"src/agglovar/pairwise/overlap/__init__.py: DALL100, DALL101, DALL002, F401, F403",
|
|
114
123
|
]
|
|
115
124
|
rst-roles = ["class", "meth", "const", "ref", "func", "mod", "attr", "data"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Agglovar: A toolkit for fast genomic variant transformations and intersects."""
|
|
2
2
|
|
|
3
|
-
__version__ = '0.0.1.
|
|
3
|
+
__version__ = '0.0.1.dev18'
|
|
4
4
|
|
|
5
5
|
__all__ = [
|
|
6
6
|
'align',
|
|
@@ -29,3 +29,9 @@ from . import schema
|
|
|
29
29
|
from . import seqmatch
|
|
30
30
|
from . import vcf
|
|
31
31
|
from . import util
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Configure logging
|
|
35
|
+
|
|
36
|
+
import logging
|
|
37
|
+
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
@@ -42,10 +42,14 @@ __all__ = [
|
|
|
42
42
|
]
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
import
|
|
45
|
+
import logging
|
|
46
46
|
from types import MappingProxyType
|
|
47
47
|
from typing import Mapping
|
|
48
48
|
|
|
49
|
+
import numpy as np
|
|
50
|
+
|
|
51
|
+
logger = logging.getLogger(__name__)
|
|
52
|
+
|
|
49
53
|
INT_STR_SET: frozenset[str] = frozenset({'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', })
|
|
50
54
|
"""Set of valid integer character strings representing operation codes."""
|
|
51
55
|
|
|
@@ -12,13 +12,24 @@ __all__ = [
|
|
|
12
12
|
'get_affine_by_params',
|
|
13
13
|
]
|
|
14
14
|
|
|
15
|
-
from abc import
|
|
16
|
-
|
|
15
|
+
from abc import (
|
|
16
|
+
ABC,
|
|
17
|
+
abstractmethod,
|
|
18
|
+
)
|
|
19
|
+
import logging
|
|
17
20
|
import re
|
|
18
|
-
from typing import
|
|
21
|
+
from typing import (
|
|
22
|
+
Any,
|
|
23
|
+
Iterable,
|
|
24
|
+
Self,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
import numpy as np
|
|
19
28
|
|
|
20
29
|
from . import op
|
|
21
30
|
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
22
33
|
AFFINE_SCORE_MATCH = 2.0
|
|
23
34
|
"""Default match score."""
|
|
24
35
|
|
|
@@ -7,13 +7,23 @@ __all__ = [
|
|
|
7
7
|
'standardize',
|
|
8
8
|
]
|
|
9
9
|
|
|
10
|
-
from collections.abc import
|
|
10
|
+
from collections.abc import (
|
|
11
|
+
Iterable,
|
|
12
|
+
Container,
|
|
13
|
+
)
|
|
11
14
|
from dataclasses import dataclass
|
|
15
|
+
import logging
|
|
12
16
|
import re
|
|
13
|
-
from typing import
|
|
17
|
+
from typing import (
|
|
18
|
+
Optional,
|
|
19
|
+
Self,
|
|
20
|
+
Any,
|
|
21
|
+
)
|
|
14
22
|
|
|
15
23
|
import polars as pl
|
|
16
24
|
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
17
27
|
|
|
18
28
|
@dataclass(frozen=True)
|
|
19
29
|
class CoordColExpr(Iterable[pl.Expr]):
|
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
"""Table intersects."""
|
|
2
2
|
|
|
3
|
+
import logging
|
|
3
4
|
from pathlib import Path
|
|
4
|
-
from typing import
|
|
5
|
+
from typing import (
|
|
6
|
+
Iterable,
|
|
7
|
+
Optional,
|
|
8
|
+
)
|
|
5
9
|
|
|
6
10
|
import polars as pl
|
|
7
11
|
|
|
8
|
-
from .
|
|
12
|
+
from .col import (
|
|
13
|
+
CoordCol,
|
|
14
|
+
get_coord_cols,
|
|
15
|
+
)
|
|
16
|
+
from .join import (
|
|
17
|
+
CHUNK_SIZE,
|
|
18
|
+
pairwise_join,
|
|
19
|
+
pairwise_join_iter,
|
|
20
|
+
)
|
|
9
21
|
from .merge import merge_depth
|
|
10
|
-
from .col import CoordCol, get_coord_cols
|
|
11
22
|
|
|
12
23
|
__all__ = [
|
|
13
24
|
'as_bool',
|
|
14
25
|
'as_proportion',
|
|
15
26
|
]
|
|
16
27
|
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
17
30
|
|
|
18
31
|
def as_bool(
|
|
19
32
|
df_a: pl.LazyFrame | pl.DataFrame,
|
|
@@ -24,6 +37,7 @@ def as_bool(
|
|
|
24
37
|
col_names_a: Optional[CoordCol | Iterable[str] | str] = None,
|
|
25
38
|
col_names_b: Optional[CoordCol | Iterable[str] | str] = None,
|
|
26
39
|
temp_dir: bool | str | Path = False,
|
|
40
|
+
chunk_size: int = CHUNK_SIZE,
|
|
27
41
|
) -> pl.LazyFrame:
|
|
28
42
|
"""Add a boolean column to df_a indicating whether each record intersects with df_b.
|
|
29
43
|
|
|
@@ -36,6 +50,8 @@ def as_bool(
|
|
|
36
50
|
:param col_names_b: Columns in b (chromosome or query ID, pos, end).
|
|
37
51
|
:param temp_dir: How to materialise the prepared tables before iterating. See
|
|
38
52
|
:func:`agglovar.bed.join.pairwise_join`.
|
|
53
|
+
:param chunk_size: Chunk A by this size per chromosome. See
|
|
54
|
+
:func:`agglovar.bed.join.pairwise_join`.
|
|
39
55
|
|
|
40
56
|
:return: A LazyFrame with two columns: ``_index`` and ``name``.
|
|
41
57
|
"""
|
|
@@ -53,12 +69,15 @@ def as_bool(
|
|
|
53
69
|
|
|
54
70
|
hit_val = not negate
|
|
55
71
|
|
|
72
|
+
logger.debug('as_bool: name=%r distance=%d negate=%s' % (name, distance, negate))
|
|
73
|
+
|
|
56
74
|
join_list = []
|
|
57
75
|
|
|
58
76
|
for df_join in pairwise_join_iter(
|
|
59
77
|
df_a=df_a,
|
|
60
78
|
df_b=df_b,
|
|
61
79
|
distance=distance,
|
|
80
|
+
chunk_size=chunk_size,
|
|
62
81
|
col_names_a=col_names_a,
|
|
63
82
|
col_names_b=col_names_b,
|
|
64
83
|
temp_dir=temp_dir,
|
|
@@ -70,6 +89,8 @@ def as_bool(
|
|
|
70
89
|
.lazy()
|
|
71
90
|
)
|
|
72
91
|
|
|
92
|
+
logger.debug('as_bool: collected %d non-empty join chunk(s)' % len(join_list))
|
|
93
|
+
|
|
73
94
|
if '_index' in df_a.collect_schema().names():
|
|
74
95
|
df_a_indexed = df_a
|
|
75
96
|
else:
|
|
@@ -107,6 +128,7 @@ def as_proportion(
|
|
|
107
128
|
col_names_a: Optional[CoordCol | Iterable[str] | str] = None,
|
|
108
129
|
col_names_b: Optional[CoordCol | Iterable[str] | str] = None,
|
|
109
130
|
temp_dir: bool | str | Path = False,
|
|
131
|
+
chunk_size: int = CHUNK_SIZE,
|
|
110
132
|
) -> pl.LazyFrame:
|
|
111
133
|
"""Compute the proportion of each interval in ``df_a`` covered by intervals in ``df_b``.
|
|
112
134
|
|
|
@@ -114,12 +136,14 @@ def as_proportion(
|
|
|
114
136
|
proportion. Zero-length intervals (``pos == end``) produce ``NaN`` (0 / 0).
|
|
115
137
|
|
|
116
138
|
:param df_a: Table a.
|
|
117
|
-
:param df_b: Table b
|
|
139
|
+
:param df_b: Table b.`
|
|
118
140
|
:param name: Name of the column to add.
|
|
119
141
|
:param col_names_a: Columns in a (chromosome or query ID, pos, end).
|
|
120
142
|
:param col_names_b: Columns in b (chromosome or query ID, pos, end).
|
|
121
143
|
:param temp_dir: How to materialise the prepared tables before iterating. See
|
|
122
144
|
:func:`agglovar.bed.join.pairwise_join`.
|
|
145
|
+
:param chunk_size: Chunk A by this size per chromosome. See
|
|
146
|
+
:func:`agglovar.bed.join.pairwise_join`.
|
|
123
147
|
|
|
124
148
|
:return: A LazyFrame with two columns: ``_index`` and ``name``.
|
|
125
149
|
"""
|
|
@@ -131,6 +155,8 @@ def as_proportion(
|
|
|
131
155
|
|
|
132
156
|
col_expr_a = col_names_a.exprs()
|
|
133
157
|
|
|
158
|
+
logger.debug('as_proportion: name=%r' % name)
|
|
159
|
+
|
|
134
160
|
if isinstance(df_a, pl.DataFrame):
|
|
135
161
|
df_a = df_a.lazy()
|
|
136
162
|
|
|
@@ -153,9 +179,12 @@ def as_proportion(
|
|
|
153
179
|
.collect()
|
|
154
180
|
)
|
|
155
181
|
|
|
182
|
+
logger.debug('as_proportion: %d non-redundant B region(s) after merge_depth' % df_b_nr.height)
|
|
183
|
+
|
|
156
184
|
df_join = pairwise_join(
|
|
157
185
|
df_a=df_a_clean,
|
|
158
186
|
df_b=df_b_nr,
|
|
187
|
+
chunk_size=chunk_size,
|
|
159
188
|
col_names_a=col_names_a,
|
|
160
189
|
col_names_b=col_names_b,
|
|
161
190
|
temp_dir=temp_dir,
|
|
@@ -5,16 +5,34 @@ __all__ = [
|
|
|
5
5
|
'pairwise_join_iter',
|
|
6
6
|
]
|
|
7
7
|
|
|
8
|
+
import logging
|
|
8
9
|
from pathlib import Path
|
|
9
|
-
|
|
10
|
+
import time
|
|
11
|
+
from typing import (
|
|
12
|
+
Iterable,
|
|
13
|
+
Iterator,
|
|
14
|
+
Optional,
|
|
15
|
+
)
|
|
10
16
|
|
|
11
17
|
import polars as pl
|
|
12
18
|
|
|
13
|
-
from .col import CoordCol, get_coord_cols
|
|
14
19
|
from ..util.lazy import materialize_pair
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
from .col import (
|
|
22
|
+
CoordCol,
|
|
23
|
+
get_coord_cols,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
CHUNK_SIZE: int = 25_000
|
|
29
|
+
"""
|
|
30
|
+
Default number of df_a records per chunk. Table A is processed one chromosome at a time and split
|
|
31
|
+
into chunks of this size or less; each chunk drives one ``join_where`` (IEJoin) against the B
|
|
32
|
+
records overlapping the chunk's position span. Larger chunks amortize the fixed per-chunk overhead
|
|
33
|
+
(query setup and the per-chunk B pre-filter) over more rows, but widen each chunk's span so more B
|
|
34
|
+
records enter each join. Tune based on available RAM.
|
|
35
|
+
"""
|
|
18
36
|
|
|
19
37
|
|
|
20
38
|
class _JoinResources:
|
|
@@ -183,6 +201,11 @@ def pairwise_join_iter(
|
|
|
183
201
|
col_names_b=col_names_b,
|
|
184
202
|
)
|
|
185
203
|
|
|
204
|
+
logger.debug(
|
|
205
|
+
'Starting bed join: distance=%d chunk_size=%d temp_dir=%r cols_a=%s cols_b=%s'
|
|
206
|
+
% (distance, chunk_size, temp_dir, tuple(resources.col_a), tuple(resources.col_b))
|
|
207
|
+
)
|
|
208
|
+
|
|
186
209
|
with materialize_pair(
|
|
187
210
|
resources.df_a, resources.df_b, temp_dir, prefix='bed_join_prep_',
|
|
188
211
|
) as (df_a_mat, df_b_mat):
|
|
@@ -262,6 +285,10 @@ def _join_chunks(
|
|
|
262
285
|
col_a = join_resources.col_a
|
|
263
286
|
col_b = join_resources.col_b
|
|
264
287
|
|
|
288
|
+
# Per-chunk timing and the df_b survivor count require extra collects, so gate them on DEBUG:
|
|
289
|
+
# an INFO-level run pays nothing and never materializes the diagnostics.
|
|
290
|
+
debug = logger.isEnabledFor(logging.DEBUG)
|
|
291
|
+
|
|
265
292
|
# Restrict the chrom loop to chroms present in both tables. Chroms unique to A
|
|
266
293
|
# would otherwise drive a wasted A chunk loop with no possible matches.
|
|
267
294
|
for chrom, last_index_a in (
|
|
@@ -275,11 +302,30 @@ def _join_chunks(
|
|
|
275
302
|
)
|
|
276
303
|
.sort(col_a.chrom)
|
|
277
304
|
).collect().rows():
|
|
305
|
+
# Collect this chromosome's A and B once into memory. The chunk loop below re-filters
|
|
306
|
+
# df_a_chrom (per index range) and df_b_chrom (per prefilter bounds) many times; leaving
|
|
307
|
+
# them lazy re-runs the chrom filter over the full materialized df_a/df_b on every chunk,
|
|
308
|
+
# which dominates runtime (fixed per-chunk cost independent of the join size). Collecting
|
|
309
|
+
# per chrom makes the per-chunk filters operate on the small per-chrom frame instead.
|
|
310
|
+
# Mirrors agglovar.pairwise.overlap._overlap._join_iter_chunked.
|
|
278
311
|
df_a_chrom = (
|
|
279
312
|
df_a.filter(pl.col(col_a.chrom) == chrom)
|
|
280
313
|
.with_row_index('_index_chrom_a')
|
|
314
|
+
.collect()
|
|
315
|
+
.lazy()
|
|
316
|
+
)
|
|
317
|
+
df_b_chrom = (
|
|
318
|
+
df_b.filter(pl.col(col_b.chrom) == chrom)
|
|
319
|
+
.collect()
|
|
320
|
+
.lazy()
|
|
281
321
|
)
|
|
282
|
-
|
|
322
|
+
|
|
323
|
+
if debug:
|
|
324
|
+
chrom_t0 = time.perf_counter()
|
|
325
|
+
chrom_chunks = 0
|
|
326
|
+
chrom_pairs = 0
|
|
327
|
+
chrom_join_s = 0.0
|
|
328
|
+
chrom_b_max = 0
|
|
283
329
|
|
|
284
330
|
start_index_a = 0
|
|
285
331
|
while start_index_a < last_index_a:
|
|
@@ -312,12 +358,39 @@ def _join_chunks(
|
|
|
312
358
|
pl.col(col_b.end) + distance >= pos_min,
|
|
313
359
|
)
|
|
314
360
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
col_a,
|
|
320
|
-
col_b,
|
|
321
|
-
).collect().lazy()
|
|
361
|
+
# The join itself is computed identically regardless of log level; only the diagnostics
|
|
362
|
+
# below are gated on DEBUG, so raising the log level cannot change the result.
|
|
363
|
+
if debug:
|
|
364
|
+
t0 = time.perf_counter()
|
|
322
365
|
|
|
366
|
+
df_join = _build_join_pair(df_a_chunk, df_b_chunk, distance, col_a, col_b).collect()
|
|
367
|
+
|
|
368
|
+
if debug:
|
|
369
|
+
dt = time.perf_counter() - t0
|
|
370
|
+
# n_b is the count of B records surviving the range prefilter for this chunk. When
|
|
371
|
+
# A is position-local it stays small; when it approaches the chrom's B total the
|
|
372
|
+
# prefilter is not pruning. pos_min-end_max is the chunk's position span.
|
|
373
|
+
n_b = df_b_chunk.select(pl.len()).collect().item()
|
|
374
|
+
|
|
375
|
+
logger.debug(
|
|
376
|
+
'Join chunk: chrom %s [%d:%d] pos=%s-%s n_b=%d pairs=%d in %.3fs'
|
|
377
|
+
% (chrom, start_index_a, end_index_a, pos_min, end_max,
|
|
378
|
+
n_b, df_join.height, dt)
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
chrom_chunks += 1
|
|
382
|
+
chrom_pairs += df_join.height
|
|
383
|
+
chrom_join_s += dt
|
|
384
|
+
if n_b > chrom_b_max:
|
|
385
|
+
chrom_b_max = n_b
|
|
386
|
+
|
|
387
|
+
yield df_join.lazy()
|
|
323
388
|
start_index_a = end_index_a
|
|
389
|
+
|
|
390
|
+
if debug:
|
|
391
|
+
logger.debug(
|
|
392
|
+
'Join chrom done: chrom %s n_a=%d chunks=%d n_b_max=%d pairs=%d '
|
|
393
|
+
'join=%.3fs wall=%.3fs'
|
|
394
|
+
% (chrom, last_index_a, chrom_chunks, chrom_b_max, chrom_pairs,
|
|
395
|
+
chrom_join_s, time.perf_counter() - chrom_t0)
|
|
396
|
+
)
|
|
@@ -4,11 +4,21 @@ __all__ = [
|
|
|
4
4
|
'merge_depth',
|
|
5
5
|
]
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
import logging
|
|
8
|
+
from typing import (
|
|
9
|
+
Optional,
|
|
10
|
+
Iterable,
|
|
11
|
+
)
|
|
8
12
|
|
|
9
13
|
import polars as pl
|
|
10
14
|
|
|
11
|
-
from .col import
|
|
15
|
+
from .col import (
|
|
16
|
+
CoordCol,
|
|
17
|
+
get_coord_cols,
|
|
18
|
+
make_unique_col,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
12
22
|
|
|
13
23
|
|
|
14
24
|
def merge_depth(
|
|
@@ -16,7 +26,7 @@ def merge_depth(
|
|
|
16
26
|
distance: int = 0,
|
|
17
27
|
col_names: Optional[CoordCol | Iterable[str] | str] = None,
|
|
18
28
|
col_max_depth: Optional[str] = 'max_depth'
|
|
19
|
-
):
|
|
29
|
+
) -> pl.LazyFrame:
|
|
20
30
|
"""Merge overlapping regions in a table.
|
|
21
31
|
|
|
22
32
|
:param df: Table of coordinates.
|
|
@@ -38,6 +48,11 @@ def merge_depth(
|
|
|
38
48
|
if isinstance(df, pl.DataFrame):
|
|
39
49
|
df = df.lazy()
|
|
40
50
|
|
|
51
|
+
logger.debug(
|
|
52
|
+
'merge_depth: distance=%d cols=%s max_depth=%r'
|
|
53
|
+
% (distance, tuple(col_names), col_max_depth)
|
|
54
|
+
)
|
|
55
|
+
|
|
41
56
|
return (
|
|
42
57
|
melt_depth(
|
|
43
58
|
df=df.with_columns(pl.col(col_names.end) + distance),
|
|
@@ -65,7 +80,7 @@ def melt_depth(
|
|
|
65
80
|
col_names: CoordCol,
|
|
66
81
|
col_loc: str = '_loc',
|
|
67
82
|
col_depth: str = '_depth',
|
|
68
|
-
):
|
|
83
|
+
) -> pl.LazyFrame:
|
|
69
84
|
"""Get a table of depths by locations of changes.
|
|
70
85
|
|
|
71
86
|
The table will have one row for each location of change with end positions having two entries (one for the
|
|
@@ -148,6 +163,8 @@ def melt_depth(
|
|
|
148
163
|
|
|
149
164
|
assert col_loc != col_depth, 'Column names "%s" and "%s" are the same' % (col_loc, col_depth)
|
|
150
165
|
|
|
166
|
+
logger.debug('melt_depth: cols=%s loc=%s depth=%s' % (tuple(col_names), col_loc, col_depth))
|
|
167
|
+
|
|
151
168
|
return (
|
|
152
169
|
pl.concat(
|
|
153
170
|
[
|
|
@@ -11,14 +11,24 @@ __all__ = [
|
|
|
11
11
|
|
|
12
12
|
import ast
|
|
13
13
|
from collections.abc import Iterable
|
|
14
|
-
from dataclasses import
|
|
14
|
+
from dataclasses import (
|
|
15
|
+
dataclass,
|
|
16
|
+
field,
|
|
17
|
+
)
|
|
15
18
|
from graphlib import TopologicalSorter
|
|
16
19
|
import importlib
|
|
17
20
|
import inspect
|
|
21
|
+
import logging
|
|
18
22
|
import re
|
|
19
23
|
import sys
|
|
20
24
|
from types import ModuleType
|
|
21
|
-
from typing import
|
|
25
|
+
from typing import (
|
|
26
|
+
Any,
|
|
27
|
+
Generator,
|
|
28
|
+
Optional,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
22
32
|
|
|
23
33
|
|
|
24
34
|
@dataclass(frozen=True)
|
|
@@ -6,13 +6,19 @@ __all__ = [
|
|
|
6
6
|
'sort_cols',
|
|
7
7
|
'id_expr_noalias',
|
|
8
8
|
'id_expr',
|
|
9
|
+
'id_expr_for_table',
|
|
9
10
|
'lead_src_expr',
|
|
10
11
|
]
|
|
11
12
|
|
|
13
|
+
import logging
|
|
12
14
|
from typing import Optional
|
|
13
15
|
|
|
14
16
|
import polars as pl
|
|
15
17
|
|
|
18
|
+
import agglovar.schema as _agg_schema
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
16
22
|
id_snv_expr = (
|
|
17
23
|
pl.concat_str(
|
|
18
24
|
pl.col('chrom'),
|
|
@@ -61,7 +67,41 @@ id_expr_noalias = (
|
|
|
61
67
|
"""Expression for generating variant IDs."""
|
|
62
68
|
|
|
63
69
|
id_expr = id_expr_noalias.alias('id')
|
|
64
|
-
"""Expression for generating variant IDs including alias "id" for the result (ideal for `with_column()`).
|
|
70
|
+
"""Expression for generating variant IDs including alias "id" for the result (ideal for `with_column()`).
|
|
71
|
+
|
|
72
|
+
Requires both the SNV columns (``ref``, ``alt``) and the non-SNV column (``varlen``) to be present:
|
|
73
|
+
Polars resolves both branches of the expression regardless of which one a row selects. Use
|
|
74
|
+
:func:`id_expr_for_table` for single-type tables, which carry only the columns listed in
|
|
75
|
+
:data:`agglovar.schema.STANDARD_FIELDS` for their type.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def id_expr_for_table(table: str, alias: Optional[str] = 'id') -> pl.Expr:
|
|
80
|
+
"""Get a variant ID expression for a single variant-type table.
|
|
81
|
+
|
|
82
|
+
Single-type tables carry only the columns listed for their type in
|
|
83
|
+
:data:`agglovar.schema.STANDARD_FIELDS`, so :data:`id_expr` cannot be applied to them (see its
|
|
84
|
+
documentation). This routine returns the one branch that matches the table's columns.
|
|
85
|
+
|
|
86
|
+
:param table: Table name from :data:`agglovar.schema.STANDARD_FIELDS` (e.g. "snv", "insdel").
|
|
87
|
+
:param alias: Output column name for the resulting IDs, or `None` to leave it unaliased.
|
|
88
|
+
|
|
89
|
+
:returns: An expression yielding variant IDs for *table*.
|
|
90
|
+
|
|
91
|
+
:raises ValueError: If *table* is not a known variant-type table.
|
|
92
|
+
"""
|
|
93
|
+
fields = _agg_schema.STANDARD_FIELDS.get(table)
|
|
94
|
+
|
|
95
|
+
if fields is None:
|
|
96
|
+
raise ValueError(
|
|
97
|
+
f'Unknown variant-type table: {table!r}: '
|
|
98
|
+
f'expected one of {", ".join(sorted(_agg_schema.STANDARD_FIELDS))}'
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Tables without "varlen" (SNV) are identified by their changed bases, all others by length
|
|
102
|
+
expr = id_nonsnv_expr if 'varlen' in fields else id_snv_expr
|
|
103
|
+
|
|
104
|
+
return expr.alias(alias) if alias is not None else expr
|
|
65
105
|
|
|
66
106
|
|
|
67
107
|
def lead_src_expr(
|
|
@@ -6,14 +6,20 @@ __all__ = [
|
|
|
6
6
|
]
|
|
7
7
|
|
|
8
8
|
import hashlib
|
|
9
|
+
import logging
|
|
9
10
|
from pathlib import Path
|
|
10
|
-
from typing import
|
|
11
|
+
from typing import (
|
|
12
|
+
Iterable,
|
|
13
|
+
Optional,
|
|
14
|
+
)
|
|
11
15
|
|
|
12
16
|
import Bio.SeqIO
|
|
13
17
|
import polars as pl
|
|
14
18
|
|
|
15
19
|
from . import io
|
|
16
20
|
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
17
23
|
|
|
18
24
|
def read_fai(
|
|
19
25
|
fai_file_name: str | Path,
|
|
@@ -4,10 +4,19 @@ __all__ = [
|
|
|
4
4
|
'PlainOrGzReader',
|
|
5
5
|
]
|
|
6
6
|
|
|
7
|
-
from dataclasses import
|
|
7
|
+
from dataclasses import (
|
|
8
|
+
dataclass,
|
|
9
|
+
field,
|
|
10
|
+
)
|
|
8
11
|
import gzip
|
|
12
|
+
import logging
|
|
9
13
|
from pathlib import Path
|
|
10
|
-
from typing import
|
|
14
|
+
from typing import (
|
|
15
|
+
Any,
|
|
16
|
+
Optional,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
11
20
|
|
|
12
21
|
|
|
13
22
|
@dataclass
|
|
@@ -5,9 +5,11 @@ __all__ = [
|
|
|
5
5
|
]
|
|
6
6
|
|
|
7
7
|
import collections
|
|
8
|
-
import
|
|
8
|
+
import logging
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
11
13
|
from . import util as kmer_util
|
|
12
14
|
|
|
13
15
|
# Get type hints for optional matplotlib objects
|
|
@@ -22,6 +24,8 @@ try:
|
|
|
22
24
|
except ImportError:
|
|
23
25
|
pass
|
|
24
26
|
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
25
29
|
|
|
26
30
|
def dotplot(
|
|
27
31
|
seq_x: str,
|
|
@@ -12,12 +12,20 @@ __all__ = [
|
|
|
12
12
|
'stream_index',
|
|
13
13
|
]
|
|
14
14
|
|
|
15
|
+
import logging
|
|
15
16
|
import math
|
|
16
17
|
from types import MappingProxyType
|
|
17
|
-
from typing import
|
|
18
|
+
from typing import (
|
|
19
|
+
Self,
|
|
20
|
+
Optional,
|
|
21
|
+
Iterator,
|
|
22
|
+
Mapping,
|
|
23
|
+
)
|
|
18
24
|
|
|
19
25
|
import numpy as np
|
|
20
26
|
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
21
29
|
# Integer to base
|
|
22
30
|
INT_TO_BASE: list[str] = ['A', 'C', 'G', 'T']
|
|
23
31
|
"""Maps 2-bit integer to base string."""
|