agglovar 0.0.1.dev14__tar.gz → 0.0.1.dev16__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.dev14/src/agglovar.egg-info → agglovar-0.0.1.dev16}/PKG-INFO +1 -1
  2. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/pyproject.toml +1 -0
  3. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/__init__.py +1 -1
  4. agglovar-0.0.1.dev16/src/agglovar/expr/variant.py +95 -0
  5. agglovar-0.0.1.dev16/src/agglovar/merge/base.py +297 -0
  6. agglovar-0.0.1.dev16/src/agglovar/merge/cumulative.py +484 -0
  7. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/seqmatch.py +1 -1
  8. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/util/lazy.py +44 -15
  9. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16/src/agglovar.egg-info}/PKG-INFO +1 -1
  10. agglovar-0.0.1.dev14/src/agglovar/expr/variant.py +0 -61
  11. agglovar-0.0.1.dev14/src/agglovar/merge/base.py +0 -198
  12. agglovar-0.0.1.dev14/src/agglovar/merge/cumulative.py +0 -398
  13. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/LICENSE +0 -0
  14. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/README.md +0 -0
  15. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/setup.cfg +0 -0
  16. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/align/__init__.py +0 -0
  17. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/align/op.py +0 -0
  18. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/align/score.py +0 -0
  19. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/bed/__init__.py +0 -0
  20. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/bed/col.py +0 -0
  21. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/bed/intersect.py +0 -0
  22. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/bed/join.py +0 -0
  23. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/bed/merge.py +0 -0
  24. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/dev/__init__.py +0 -0
  25. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/dev/imports.py +0 -0
  26. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/expr/__init__.py +0 -0
  27. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/fa.py +0 -0
  28. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/io.py +0 -0
  29. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/kmer/__init__.py +0 -0
  30. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/kmer/plot.py +0 -0
  31. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/kmer/util.py +0 -0
  32. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/merge/__init__.py +0 -0
  33. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/merge/config/__init__.py +0 -0
  34. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/merge/config/parser.py +0 -0
  35. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/merge/config/stage.py +0 -0
  36. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/merge/config/strategy.py +0 -0
  37. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/merge/om/__init__.py +0 -0
  38. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/merge/om/_executor_base.py +0 -0
  39. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/merge/om/_executor_nr.py +0 -0
  40. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/meta/__init__.py +0 -0
  41. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/meta/decorators.py +0 -0
  42. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/meta/descriptors.py +0 -0
  43. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/pairwise/__init__.py +0 -0
  44. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/pairwise/base.py +0 -0
  45. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/pairwise/overlap/__init__.py +0 -0
  46. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/pairwise/overlap/_const.py +0 -0
  47. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/pairwise/overlap/_overlap.py +0 -0
  48. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/pairwise/overlap/_stage.py +0 -0
  49. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/pairwise/weights.py +0 -0
  50. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/schema.py +0 -0
  51. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/util/__init__.py +0 -0
  52. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/util/str.py +0 -0
  53. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/util/var.py +0 -0
  54. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/vcf/__init__.py +0 -0
  55. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/vcf/_vcf_const.py +0 -0
  56. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/vcf/_vcf_header.py +0 -0
  57. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/vcf/_vcf_read.py +0 -0
  58. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar/vcf/_vcf_write.py +0 -0
  59. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar.egg-info/SOURCES.txt +0 -0
  60. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar.egg-info/dependency_links.txt +0 -0
  61. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/src/agglovar.egg-info/requires.txt +0 -0
  62. {agglovar-0.0.1.dev14 → agglovar-0.0.1.dev16}/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.dev14
3
+ Version: 0.0.1.dev16
4
4
  Summary: Toolkit for fast genomic variant transformations and intersects
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.12
@@ -61,6 +61,7 @@ dev = [
61
61
  "flake8-pyproject>=1.2.3",
62
62
  "flake8-rst-docstrings>=0.3.1",
63
63
  "ipython>=9.4.0",
64
+ "psutil>=7.2.2",
64
65
  "pyarrow>=22.0.0",
65
66
  "pylint>=3.3.7",
66
67
  "pytest>=8.4.1",
@@ -1,6 +1,6 @@
1
1
  """Agglovar: A toolkit for fast genomic variant transformations and intersects."""
2
2
 
3
- __version__ = '0.0.1.dev14'
3
+ __version__ = '0.0.1.dev16'
4
4
 
5
5
  __all__ = [
6
6
  'align',
@@ -0,0 +1,95 @@
1
+ """Polars expressions used by variant calling routines."""
2
+
3
+ __all__ = [
4
+ 'id_snv_expr',
5
+ 'id_nonsnv_expr',
6
+ 'sort_cols',
7
+ 'id_expr_noalias',
8
+ 'id_expr',
9
+ 'lead_src_expr',
10
+ ]
11
+
12
+ from typing import Optional
13
+
14
+ import polars as pl
15
+
16
+ id_snv_expr = (
17
+ pl.concat_str(
18
+ pl.col('chrom'),
19
+ pl.lit('-'),
20
+ pl.col('pos') + 1,
21
+ pl.lit('-SNV-'),
22
+ pl.col('ref').str.to_uppercase(),
23
+ pl.col('alt').str.to_uppercase(),
24
+ )
25
+ )
26
+ """Expression for generating SNV IDs."""
27
+
28
+
29
+ id_nonsnv_expr = (
30
+ pl.concat_str(
31
+ pl.col('chrom'),
32
+ pl.lit('-'),
33
+ (pl.col('pos') + 1).cast(pl.String),
34
+ pl.lit('-'),
35
+ pl.col('vartype').str.to_uppercase(),
36
+ pl.lit('-'),
37
+ pl.col('varlen').cast(pl.String)
38
+ )
39
+ )
40
+ """Expression for generating non-SNV IDs."""
41
+
42
+
43
+ sort_cols = (
44
+ pl.col('chrom'),
45
+ pl.col('pos'),
46
+ pl.col('end'),
47
+ pl.col('filter').list.len(),
48
+ pl.coalesce(
49
+ pl.col('^id$'),
50
+ pl.lit(None),
51
+ )
52
+ )
53
+ """Sort columns for variant tables."""
54
+
55
+
56
+ id_expr_noalias = (
57
+ pl.when(pl.col('vartype').str.to_uppercase() == 'SNV')
58
+ .then(id_snv_expr)
59
+ .otherwise(id_nonsnv_expr)
60
+ )
61
+ """Expression for generating variant IDs."""
62
+
63
+ id_expr = id_expr_noalias.alias('id')
64
+ """Expression for generating variant IDs including alias "id" for the result (ideal for `with_column()`)."""
65
+
66
+
67
+ def lead_src_expr(
68
+ mg_src_col: str = 'mg_src',
69
+ mg_src_lead_col: str = 'mg_src_lead',
70
+ alias: Optional[str] = 'lead_src',
71
+ ) -> pl.Expr:
72
+ """Extract the lead source entry from a merged callset.
73
+
74
+ A merge produces an ``mg_src`` list column with one struct per contributing source and an
75
+ ``mg_src_lead`` column holding the integer position within that list of the lead
76
+ (representative) source. This routine gathers that lead entry back out as a struct, so an API
77
+ user can read the lead source directly instead of manually indexing the list with
78
+ ``pl.col('mg_src').list.get(pl.col('mg_src_lead'))``.
79
+
80
+ Agglovar does not use this internally; the merge stores only the index to avoid duplicating the
81
+ entry. It is provided as a convenience for downstream API users.
82
+
83
+ :param mg_src_col: Name of the ``mg_src`` list column.
84
+ :param mg_src_lead_col: Name of the ``mg_src_lead`` index column.
85
+ :param alias: Output column name for the resulting struct, or `None` to leave it unaliased.
86
+
87
+ :returns: An expression yielding the lead source struct, carrying the same fields as an
88
+ ``mg_src`` entry ("src_index", "src_name", "src_meta", "var_index", "var_id").
89
+ """
90
+ expr = pl.col(mg_src_col).list.get(pl.col(mg_src_lead_col))
91
+
92
+ if alias is not None:
93
+ expr = expr.alias(alias)
94
+
95
+ return expr
@@ -0,0 +1,297 @@
1
+ """Base class for callset intersects.
2
+
3
+
4
+ """
5
+
6
+ __all__ = [
7
+ 'CallsetDef',
8
+ 'MergeBase',
9
+ 'CallsetDefInputType',
10
+ ]
11
+
12
+ from abc import ABC, abstractmethod
13
+ from collections.abc import Container, Iterable
14
+ import json
15
+ from pathlib import Path
16
+ from typing import (
17
+ Any,
18
+ NamedTuple,
19
+ Optional,
20
+ TypeAlias,
21
+ )
22
+
23
+ import polars as pl
24
+
25
+ from ..meta.decorators import lockable
26
+ from ..util.str import collision_rename
27
+
28
+
29
+ class CallsetDef(NamedTuple):
30
+ """Callset definition.
31
+
32
+ :ivar table: The callset table to be merged.
33
+ :ivar name: The name of the callset within a collection of callsets.
34
+ :ivar metadata: The metadata of the callset or None if no metadata is provided.
35
+ """
36
+ table: pl.LazyFrame
37
+ name: str
38
+ metadata: Optional[str]
39
+
40
+ def __str__(self) -> str:
41
+ return self.name
42
+
43
+ def __repr__(self) -> str:
44
+ return f'CallsetDef(table={self.table!r}, name={self.name}, metadata={self.metadata})'
45
+
46
+ CallsetDefInputType: TypeAlias = (
47
+ pl.DataFrame
48
+ | pl.LazyFrame
49
+ | tuple[
50
+ pl.DataFrame | pl.LazyFrame,
51
+ ]
52
+ | tuple [
53
+ pl.DataFrame | pl.LazyFrame,
54
+ Optional[str],
55
+ ]
56
+ | tuple [
57
+ pl.DataFrame | pl.LazyFrame,
58
+ Optional[str],
59
+ Optional[str | dict],
60
+ ]
61
+ | CallsetDef
62
+ )
63
+ """Alias for acceptable input types."""
64
+
65
+
66
+ def _get_definition(
67
+ callset_def: CallsetDefInputType,
68
+ callset_index: int,
69
+ name_set: set[str],
70
+ retain_index: bool,
71
+ pre_filter: Iterable[pl.Expr],
72
+ ) -> CallsetDef:
73
+ """Get a callset definition from an input type.
74
+
75
+ :param callset_def: The callset definition to be processed.
76
+ :param callset_index: The index of the callset.
77
+ :param name_set: A set of names that have already been used.
78
+ :param retain_index: If `True`, do not drop an existing "_index" column if it exists.
79
+ :param pre_filter: If set, filter each table with these expressions. Filter is applied
80
+ last (after "_index" is set).
81
+
82
+ :return: A callset definition.
83
+ """
84
+
85
+ # Get callset table and name
86
+ callset_name = None
87
+ callset_meta = None
88
+
89
+ if isinstance(callset_def, CallsetDef):
90
+ callset_table = callset_def.table
91
+ callset_name = callset_def.name
92
+ callset_meta = callset_def.metadata
93
+
94
+ elif isinstance(callset_def, pl.LazyFrame):
95
+ callset_table = callset_def
96
+
97
+ elif isinstance(callset_def, pl.DataFrame):
98
+ callset_table = callset_def.lazy()
99
+
100
+ elif isinstance(callset_def, tuple):
101
+ if len(callset_def) < 1:
102
+ raise ValueError(
103
+ f'Callset must be a Polars table or an iterable '
104
+ f'of callset definition elements (table, name, metadata): Received an empty iterable'
105
+ )
106
+
107
+ if isinstance(callset_def[0], pl.LazyFrame):
108
+ callset_table = callset_def[0]
109
+ elif isinstance(callset_def[0], pl.DataFrame):
110
+ callset_table = callset_def[0].lazy()
111
+ else:
112
+ raise ValueError(
113
+ f'First element of callset definition must be a Polars table: '
114
+ f'Received {type(callset_def[0])}'
115
+ )
116
+
117
+ callset_name = str(callset_def[1]) if len(callset_def) > 1 else None
118
+
119
+ callset_meta = callset_def[2] if len(callset_def) > 2 else None
120
+
121
+ if callset_meta is not None:
122
+ if isinstance(callset_meta, dict):
123
+ callset_meta = json.dumps(callset_meta)
124
+ else:
125
+ callset_meta = str(callset_meta)
126
+
127
+ if len(callset_def) > 3:
128
+ raise ValueError(
129
+ f'Input callsets must be a sequence of up to three items (table, name, metadata): '
130
+ f'Received {len(callset_def)} items'
131
+ )
132
+
133
+ else:
134
+ raise ValueError(
135
+ f'Callset must be a Polars table or an iterable '
136
+ f'of callset definition elements (table, name, metadata): Received {type(callset_def)}'
137
+ )
138
+
139
+ # Unique callset name
140
+ if callset_name is None:
141
+ callset_name = f'source_{callset_index + 1}'
142
+
143
+ callset_name = _get_name(callset_name, name_set)
144
+
145
+ # Set index if missing or forced
146
+ if not (retain_index and '_index' in callset_table.collect_schema().names()):
147
+ callset_table = (
148
+ callset_table
149
+ .drop('_index', strict=False)
150
+ .with_row_index('_index')
151
+ )
152
+
153
+ # Add missing IDs
154
+ callset_table = (
155
+ callset_table
156
+ .rename({'_index': '_mg_src_index'})
157
+ .with_columns(
158
+ pl.coalesce(
159
+ pl.col(r'^id$'), # Existing ID if present
160
+ pl.lit(None)
161
+ )
162
+ .cast(pl.String)
163
+ .fill_null(
164
+ pl.concat_str(pl.lit('var'),
165
+ pl.col('_mg_src_index')),
166
+ )
167
+ .alias('id')
168
+ )
169
+ .filter(
170
+ *pre_filter
171
+ )
172
+ )
173
+
174
+ name_set.add(callset_name)
175
+
176
+ return CallsetDef(
177
+ table=callset_table,
178
+ name=callset_name,
179
+ metadata=callset_meta,
180
+ )
181
+
182
+
183
+
184
+ @lockable
185
+ class MergeBase(ABC):
186
+ """Base class for callset intersects."""
187
+
188
+ @abstractmethod
189
+ def __call__(
190
+ self,
191
+ callsets: Iterable[CallsetDefInputType],
192
+ retain_index: bool = False,
193
+ pre_filter: Optional[Iterable[pl.Expr]] = None,
194
+ temp_dir: bool | str | Path = False,
195
+ ) -> pl.LazyFrame:
196
+ """
197
+ Intersect callsets.
198
+
199
+ :param callsets: Callsets to intersect.
200
+ :param retain_index: If `True`, do not drop an existing "_index" column if it exists.
201
+ :param pre_filter: If set, filter each table with these expressions. Filter is applied
202
+ last (after "_index" is set).
203
+ :param temp_dir: How the underlying pairwise intersect materialises prepared tables before
204
+ its chunked loop. ``False`` (default) keeps tables in memory; ``True`` writes them to
205
+ the system temp directory as parquet; a ``str``/``Path`` writes them to that directory.
206
+ Temp files are always removed on exit.
207
+
208
+ :return: A merged callset table.
209
+ """
210
+ ...
211
+
212
+ @staticmethod
213
+ def get_intersect_tuples(
214
+ callsets: Iterable[CallsetDefInputType],
215
+ retain_index: bool = False,
216
+ pre_filter: Optional[Iterable[pl.Expr] | pl.Expr] = None,
217
+ ) -> list[CallsetDef]:
218
+ """
219
+ Transform input callset definitions into a normalized list of :class:`CallsetDef`.
220
+
221
+ Each returned :class:`CallsetDef` (a named tuple) has three fields:
222
+
223
+ 1. ``table``: A lazy frame.
224
+ 2. ``name``: A name for the source.
225
+ 3. ``metadata``: A metadata string for this source, or ``None`` if none was given.
226
+
227
+ The source index is not stored on the definition; it is implicitly the position of the
228
+ element in the returned list (0 for the first source, incrementing by 1).
229
+
230
+ If a source name is given, the given name is used. If it is not, then a default name is
231
+ generated from the source index (e.g. "source_1" for the first source). Names are
232
+ de-duplicated against names already seen.
233
+
234
+ Lazy frames are transformed to add a source index ("_mg_src_index" column) and to ensure a
235
+ variant ID is present ("id" column), filling missing IDs from the index.
236
+
237
+ :param callsets: Callsets parameter. May be an iterable of DataFrames, LazyFrames,
238
+ ``(table, name)`` tuples, ``(table, name, metadata)`` tuples, or :class:`CallsetDef`
239
+ objects.
240
+ :param retain_index: If `True`, do not drop an existing "_index" column if it exists.
241
+ :param pre_filter: If set, filter each table with these expressions. Filter is applied
242
+ last (after "_index" is set).
243
+
244
+ :return: A list of :class:`CallsetDef`, one per input source, in input order.
245
+ """
246
+ name_set = set()
247
+ callset_table: pl.LazyFrame
248
+
249
+ callset_tuple_list: list[CallsetDef] = []
250
+
251
+ if isinstance(pre_filter, pl.Expr):
252
+ pre_filter = [pre_filter]
253
+ else:
254
+ pre_filter = list(pre_filter if pre_filter is not None else [])
255
+
256
+ if callsets is None:
257
+ raise ValueError('Missing callsets')
258
+
259
+ for callset_def in callsets:
260
+ try:
261
+ callset = _get_definition(
262
+ callset_def=callset_def,
263
+ callset_index=len(callset_tuple_list),
264
+ name_set=name_set,
265
+ retain_index=retain_index,
266
+ pre_filter=pre_filter,
267
+ )
268
+ except ValueError as e:
269
+ raise ValueError(
270
+ f'Failed getting callset at index {len(callset_tuple_list)}: {e}'
271
+ ) from e
272
+
273
+ # Append
274
+ callset_tuple_list.append(callset)
275
+
276
+ return callset_tuple_list
277
+
278
+
279
+ def _get_name(
280
+ name: Optional[Any],
281
+ *args: Container[str]
282
+ ) -> str:
283
+ """Get a name for a source with a default set for None.
284
+
285
+ Gets a name for the variant call input source and de-duplicates names.
286
+
287
+ :param name: Input source name or None to choose a default name.
288
+ :param args: Containers with other names to avoid collisions with.
289
+
290
+ :returns: A name for this input source.
291
+ """
292
+ if name is None:
293
+ name = f'source.1'
294
+
295
+ name = collision_rename(str(name), '.', *args)
296
+
297
+ return str(name)