agglovar 0.0.1.dev12__tar.gz → 0.0.1.dev14__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.
Files changed (62) hide show
  1. {agglovar-0.0.1.dev12/src/agglovar.egg-info → agglovar-0.0.1.dev14}/PKG-INFO +2 -2
  2. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/pyproject.toml +5 -3
  3. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/__init__.py +4 -2
  4. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/align/score.py +2 -2
  5. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/bed/col.py +17 -7
  6. agglovar-0.0.1.dev14/src/agglovar/bed/intersect.py +186 -0
  7. agglovar-0.0.1.dev14/src/agglovar/bed/join.py +323 -0
  8. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/dev/imports.py +7 -5
  9. agglovar-0.0.1.dev14/src/agglovar/expr/__init__.py +7 -0
  10. agglovar-0.0.1.dev14/src/agglovar/expr/variant.py +61 -0
  11. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/kmer/plot.py +5 -2
  12. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/merge/base.py +23 -19
  13. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/merge/config/parser.py +1 -0
  14. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/merge/config/stage.py +1 -0
  15. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/merge/cumulative.py +40 -22
  16. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/meta/decorators.py +8 -10
  17. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/meta/descriptors.py +48 -11
  18. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/pairwise/base.py +11 -3
  19. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/pairwise/overlap/_const.py +3 -2
  20. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/pairwise/overlap/_overlap.py +140 -115
  21. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/pairwise/overlap/_stage.py +34 -40
  22. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/pairwise/weights.py +12 -2
  23. agglovar-0.0.1.dev14/src/agglovar/schema.py +38 -0
  24. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/util/__init__.py +2 -0
  25. agglovar-0.0.1.dev14/src/agglovar/util/lazy.py +75 -0
  26. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/util/str.py +8 -2
  27. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/util/var.py +1 -3
  28. agglovar-0.0.1.dev14/src/agglovar/vcf/__init__.py +18 -0
  29. agglovar-0.0.1.dev14/src/agglovar/vcf/_vcf_const.py +75 -0
  30. agglovar-0.0.1.dev14/src/agglovar/vcf/_vcf_header.py +920 -0
  31. agglovar-0.0.1.dev14/src/agglovar/vcf/_vcf_read.py +667 -0
  32. agglovar-0.0.1.dev14/src/agglovar/vcf/_vcf_write.py +52 -0
  33. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14/src/agglovar.egg-info}/PKG-INFO +2 -2
  34. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar.egg-info/SOURCES.txt +9 -1
  35. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar.egg-info/requires.txt +1 -1
  36. agglovar-0.0.1.dev12/src/agglovar/bed/intersect.py +0 -193
  37. agglovar-0.0.1.dev12/src/agglovar/bed/join.py +0 -376
  38. agglovar-0.0.1.dev12/src/agglovar/schema.py +0 -30
  39. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/LICENSE +0 -0
  40. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/README.md +0 -0
  41. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/setup.cfg +0 -0
  42. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/align/__init__.py +0 -0
  43. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/align/op.py +0 -0
  44. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/bed/__init__.py +0 -0
  45. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/bed/merge.py +0 -0
  46. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/dev/__init__.py +0 -0
  47. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/fa.py +0 -0
  48. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/io.py +0 -0
  49. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/kmer/__init__.py +0 -0
  50. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/kmer/util.py +0 -0
  51. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/merge/__init__.py +0 -0
  52. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/merge/config/__init__.py +0 -0
  53. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/merge/config/strategy.py +0 -0
  54. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/merge/om/__init__.py +0 -0
  55. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/merge/om/_executor_base.py +0 -0
  56. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/merge/om/_executor_nr.py +0 -0
  57. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/meta/__init__.py +0 -0
  58. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/pairwise/__init__.py +0 -0
  59. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/pairwise/overlap/__init__.py +0 -0
  60. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar/seqmatch.py +0 -0
  61. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/src/agglovar.egg-info/dependency_links.txt +0 -0
  62. {agglovar-0.0.1.dev12 → agglovar-0.0.1.dev14}/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.dev12
3
+ Version: 0.0.1.dev14
4
4
  Summary: Toolkit for fast genomic variant transformations and intersects
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.12
@@ -8,10 +8,10 @@ Description-Content-Type: text/markdown
8
8
  License-File: LICENSE
9
9
  Requires-Dist: biopython>=1.85
10
10
  Requires-Dist: edlib>=1.3.9.post1
11
- Requires-Dist: intervaltree>=3.1.0
12
11
  Requires-Dist: numpy>=2.3.0
13
12
  Requires-Dist: ply>=3.11
14
13
  Requires-Dist: polars[numpy,pandas,pyarrow]>=1.35.2
14
+ Requires-Dist: pysam>=0.23.3
15
15
  Provides-Extra: plot
16
16
  Requires-Dist: matplotlib>=3.10.3; extra == "plot"
17
17
  Provides-Extra: all
@@ -13,10 +13,10 @@ requires-python = ">=3.12"
13
13
  dependencies = [
14
14
  "biopython>=1.85",
15
15
  "edlib>=1.3.9.post1",
16
- "intervaltree>=3.1.0",
17
16
  "numpy>=2.3.0",
18
17
  "ply>=3.11",
19
18
  "polars[numpy,pandas,pyarrow]>=1.35.2",
19
+ "pysam>=0.23.3",
20
20
  ]
21
21
 
22
22
  [agglovar.urls]
@@ -107,6 +107,8 @@ max-line-length = 120
107
107
  max-line-length = 120
108
108
  docstring-convention = "pep257"
109
109
  per-file-ignores = [
110
- "src/agglovar/join/config/parser.py: D200, D205, D212, D300, D403, D415"
110
+ "src/agglovar/merge/config/parser.py: D200, D205, D212, D300, D400, D401, D403, D415",
111
+ "src/agglovar/vcf/__init__.py: DALL002, F401, F403",
112
+ "src/agglovar/pairwise/overlap/__init__.py: DALL002, F401, F403",
111
113
  ]
112
- rst-roles = ["class", "meth", "const", "ref", "func", "mod"]
114
+ 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.dev12'
3
+ __version__ = '0.0.1.dev14'
4
4
 
5
5
  __all__ = [
6
6
  'align',
@@ -13,6 +13,7 @@ __all__ = [
13
13
  'meta',
14
14
  'schema',
15
15
  'seqmatch',
16
+ 'vcf',
16
17
  'util',
17
18
  ]
18
19
 
@@ -26,4 +27,5 @@ from . import merge
26
27
  from . import meta
27
28
  from . import schema
28
29
  from . import seqmatch
29
- from . import util
30
+ from . import vcf
31
+ from . import util
@@ -128,7 +128,7 @@ class ScoreModel(ABC):
128
128
  self,
129
129
  op_arr: np.ndarray,
130
130
  ) -> float:
131
- """A vectorized implementation of summing scores for affine models.
131
+ """Sum scores for affine models using a vectorized implementation.
132
132
 
133
133
  :param op_arr: Array of alignment operations (op_code: first column, op_len: second column).
134
134
 
@@ -285,7 +285,7 @@ class AffineScoreModel(ScoreModel):
285
285
  self,
286
286
  op_arr: np.ndarray,
287
287
  ) -> float:
288
- """A vectorized implementation of summing scores for affine models.
288
+ """Sum scores for affine models using a A vectorized implementation.
289
289
 
290
290
  :param op_arr: Array of alignment operations (op_code: first column, op_len: second column).
291
291
 
@@ -14,6 +14,7 @@ from typing import Optional, Self, Any
14
14
 
15
15
  import polars as pl
16
16
 
17
+
17
18
  @dataclass(frozen=True)
18
19
  class CoordColExpr(Iterable[pl.Expr]):
19
20
  """Represents coordinate column expressions.
@@ -22,6 +23,7 @@ class CoordColExpr(Iterable[pl.Expr]):
22
23
  :ivar pos: Position column expression.
23
24
  :ivar end: End column expression.
24
25
  """
26
+
25
27
  chrom: pl.Expr
26
28
  pos: pl.Expr
27
29
  end: pl.Expr
@@ -49,11 +51,14 @@ class CoordColExpr(Iterable[pl.Expr]):
49
51
  return self.end.meta.output_name()
50
52
 
51
53
  def __iter__(self):
54
+ """Iterate over chrom, pos, and end expressions."""
52
55
  return iter((self.chrom, self.pos, self.end))
53
56
 
54
57
  def __repr__(self):
58
+ """Return a string representation."""
55
59
  return f"CoordColExpr({self.chrom!r}, {self.pos!r}, {self.end!r})"
56
60
 
61
+
57
62
  @dataclass(frozen=True, order=True)
58
63
  class CoordCol(Iterable[str], Container[str]):
59
64
  """Represents coordinate columns to use.
@@ -62,6 +67,7 @@ class CoordCol(Iterable[str], Container[str]):
62
67
  :ivar pos: Position column name.
63
68
  :ivar end: End column name.
64
69
  """
70
+
65
71
  chrom: str
66
72
  pos: str
67
73
  end: str
@@ -89,25 +95,30 @@ class CoordCol(Iterable[str], Container[str]):
89
95
  return CoordColExpr(
90
96
  *(
91
97
  pl.col(col).alias(alias + suffix)
92
- for col, alias in zip(self, alias)
98
+ for col, alias in zip(self, alias)
93
99
  )
94
100
  )
95
101
 
96
102
  def __iter__(self):
103
+ """Iterate over chrom, pos, and end column names."""
97
104
  return iter((self.chrom, self.pos, self.end))
98
105
 
99
106
  def __contains__(self, o: Optional[Any]):
107
+ """Test whether ``o`` is one of the coordinate column names."""
100
108
  return o in (self.chrom, self.pos, self.end)
101
109
 
102
110
  def __repr__(self):
111
+ """Return a string representation."""
103
112
  return f"CoordCol({self.chrom!r}, {self.pos!r}, {self.end!r})"
104
113
 
114
+
105
115
  COL_CHROM: CoordCol = CoordCol('chrom', 'pos', 'end')
106
116
  """Standard chromosome columns."""
107
117
 
108
118
  COL_QRY: CoordCol = CoordCol('qry_id', 'qry_pos', 'qry_end')
109
119
  """Standard query columns."""
110
120
 
121
+
111
122
  def get_coord_cols(
112
123
  col_names: Optional[CoordCol | str | Iterable[str]] = None,
113
124
  ) -> CoordCol:
@@ -123,7 +134,6 @@ def get_coord_cols(
123
134
 
124
135
  :return: An object with column names.
125
136
  """
126
-
127
137
  if isinstance(col_names, CoordCol):
128
138
  return col_names
129
139
 
@@ -137,14 +147,16 @@ def get_coord_cols(
137
147
  else:
138
148
  col_names_tuple = tuple((
139
149
  col.strip() if col is not None else None
140
- for col in col_names
150
+ for col in col_names
141
151
  ))
142
152
 
143
153
  if len(col_names_tuple) != 3:
144
- raise ValueError(f'Invalid cols: Expected 3 elements, found {len(col_names_tuple)}: {col_names:r}')
154
+ raise ValueError(
155
+ f'Invalid cols: Expected 3 elements, found {len(col_names_tuple)}: {col_names!r}'
156
+ )
145
157
 
146
158
  if any(col is None or col == '' for col in col_names_tuple):
147
- raise ValueError(f'Columns is missing values: {col_names:r}')
159
+ raise ValueError(f'Columns is missing values: {col_names!r}')
148
160
 
149
161
  return CoordCol(*col_names_tuple)
150
162
 
@@ -181,7 +193,6 @@ def make_unique_col(
181
193
 
182
194
  :return: Unique column name.
183
195
  """
184
-
185
196
  new_col = col
186
197
  i = 0
187
198
 
@@ -189,5 +200,4 @@ def make_unique_col(
189
200
  i += 1
190
201
  new_col = f'{col}_{i}'
191
202
 
192
-
193
203
  return new_col
@@ -0,0 +1,186 @@
1
+ """Table intersects."""
2
+
3
+ from pathlib import Path
4
+ from typing import Iterable, Optional
5
+
6
+ import polars as pl
7
+
8
+ from .join import pairwise_join, pairwise_join_iter
9
+ from .merge import merge_depth
10
+ from .col import CoordCol, get_coord_cols
11
+
12
+ __all__ = [
13
+ 'as_bool',
14
+ 'as_proportion',
15
+ ]
16
+
17
+
18
+ def as_bool(
19
+ df_a: pl.LazyFrame | pl.DataFrame,
20
+ df_b: pl.LazyFrame | pl.DataFrame,
21
+ name: str,
22
+ distance: int = 0,
23
+ negate: bool = False,
24
+ col_names_a: Optional[CoordCol | Iterable[str] | str] = None,
25
+ col_names_b: Optional[CoordCol | Iterable[str] | str] = None,
26
+ temp_dir: bool | str | Path = False,
27
+ ) -> pl.LazyFrame:
28
+ """Add a boolean column to df_a indicating whether each record intersects with df_b.
29
+
30
+ :param df_a: Table a.
31
+ :param df_b: Table b.
32
+ :param name: Name of the column to add.
33
+ :param distance: Maximum distance between two records. May be negative to require overlap.
34
+ :param negate: If True, negate the boolean column to annotate misses instead of hits.
35
+ :param col_names_a: Columns in a (chromosome or query ID, pos, end).
36
+ :param col_names_b: Columns in b (chromosome or query ID, pos, end).
37
+ :param temp_dir: How to materialise the prepared tables before iterating. See
38
+ :func:`agglovar.bed.join.pairwise_join`.
39
+
40
+ :return: A LazyFrame with two columns: ``_index`` and ``name``.
41
+ """
42
+ if name is None or not (name := name.strip()):
43
+ raise ValueError('Name must be a non-empty string')
44
+
45
+ col_names_a = get_coord_cols(col_names_a)
46
+ col_names_b = get_coord_cols(col_names_b)
47
+
48
+ if isinstance(df_a, pl.DataFrame):
49
+ df_a = df_a.lazy()
50
+
51
+ if isinstance(df_b, pl.DataFrame):
52
+ df_b = df_b.lazy()
53
+
54
+ hit_val = not negate
55
+
56
+ join_list = []
57
+
58
+ for df_join in pairwise_join_iter(
59
+ df_a=df_a,
60
+ df_b=df_b,
61
+ distance=distance,
62
+ col_names_a=col_names_a,
63
+ col_names_b=col_names_b,
64
+ temp_dir=temp_dir,
65
+ ):
66
+ join_list.append(
67
+ df_join
68
+ .select(pl.col('index_a').alias('_index'))
69
+ .collect()
70
+ .lazy()
71
+ )
72
+
73
+ if '_index' in df_a.collect_schema().names():
74
+ df_a_indexed = df_a
75
+ else:
76
+ df_a_indexed = df_a.with_row_index('_index').with_columns(pl.col('_index').cast(pl.UInt64))
77
+
78
+ if not join_list:
79
+ return (
80
+ df_a_indexed
81
+ .select(
82
+ '_index',
83
+ pl.lit(not hit_val).alias(name),
84
+ )
85
+ )
86
+
87
+ hits = pl.concat(join_list).unique('_index')
88
+
89
+ return (
90
+ df_a_indexed
91
+ .select('_index')
92
+ .join(
93
+ hits.with_columns(pl.lit(hit_val).alias(name)),
94
+ on='_index', how='left',
95
+ )
96
+ .select(
97
+ '_index',
98
+ pl.col(name).fill_null(not hit_val),
99
+ )
100
+ )
101
+
102
+
103
+ def as_proportion(
104
+ df_a: pl.LazyFrame | pl.DataFrame,
105
+ df_b: pl.LazyFrame | pl.DataFrame,
106
+ name: str,
107
+ col_names_a: Optional[CoordCol | Iterable[str] | str] = None,
108
+ col_names_b: Optional[CoordCol | Iterable[str] | str] = None,
109
+ temp_dir: bool | str | Path = False,
110
+ ) -> pl.LazyFrame:
111
+ """Compute the proportion of each interval in ``df_a`` covered by intervals in ``df_b``.
112
+
113
+ Rows in ``df_a`` with null ``pos`` or ``end`` are preserved in the output with a null
114
+ proportion. Zero-length intervals (``pos == end``) produce ``NaN`` (0 / 0).
115
+
116
+ :param df_a: Table a.
117
+ :param df_b: Table b.
118
+ :param name: Name of the column to add.
119
+ :param col_names_a: Columns in a (chromosome or query ID, pos, end).
120
+ :param col_names_b: Columns in b (chromosome or query ID, pos, end).
121
+ :param temp_dir: How to materialise the prepared tables before iterating. See
122
+ :func:`agglovar.bed.join.pairwise_join`.
123
+
124
+ :return: A LazyFrame with two columns: ``_index`` and ``name``.
125
+ """
126
+ if name is None or not (name := name.strip()):
127
+ raise ValueError('Name must be a non-empty string')
128
+
129
+ col_names_a = get_coord_cols(col_names_a)
130
+ col_names_b = get_coord_cols(col_names_b)
131
+
132
+ col_expr_a = col_names_a.exprs()
133
+
134
+ if isinstance(df_a, pl.DataFrame):
135
+ df_a = df_a.lazy()
136
+
137
+ if isinstance(df_b, pl.DataFrame):
138
+ df_b = df_b.lazy()
139
+
140
+ if '_index' not in df_a.collect_schema().names():
141
+ df_a = df_a.with_row_index('_index').with_columns(pl.col('_index').cast(pl.UInt64))
142
+
143
+ # Records with null pos/end are kept and assigned a null proportion at the end.
144
+ df_a_clean = df_a.filter(
145
+ pl.col(col_names_a.pos).is_not_null(),
146
+ pl.col(col_names_a.end).is_not_null(),
147
+ )
148
+
149
+ # Collapse overlapping b intervals so coverage is not double-counted.
150
+ df_b_nr = (
151
+ merge_depth(df_b, 0, col_names_b)
152
+ .select(*col_names_b.exprs())
153
+ .collect()
154
+ )
155
+
156
+ df_join = pairwise_join(
157
+ df_a=df_a_clean,
158
+ df_b=df_b_nr,
159
+ col_names_a=col_names_a,
160
+ col_names_b=col_names_b,
161
+ temp_dir=temp_dir,
162
+ )
163
+
164
+ df_overlap = (
165
+ df_join
166
+ .filter(pl.col('end') > pl.col('pos'))
167
+ .select(
168
+ pl.col('index_a').alias('_index'),
169
+ (pl.col('end') - pl.col('pos')).alias('_overlap'),
170
+ )
171
+ .group_by('_index')
172
+ .agg(pl.col('_overlap').sum())
173
+ )
174
+
175
+ return (
176
+ df_a
177
+ .select(
178
+ '_index',
179
+ (col_expr_a.end - col_expr_a.pos).alias('_len'),
180
+ )
181
+ .join(df_overlap.lazy(), on='_index', how='left')
182
+ .select(
183
+ '_index',
184
+ (pl.col('_overlap').fill_null(0.0) / pl.col('_len')).alias(name),
185
+ )
186
+ )