agglovar 0.0.1.dev8__tar.gz → 0.0.1.dev10__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 (51) hide show
  1. {agglovar-0.0.1.dev8/src/agglovar.egg-info → agglovar-0.0.1.dev10}/PKG-INFO +1 -1
  2. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/__init__.py +3 -1
  3. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/bed/intersect.py +13 -3
  4. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/bed/join.py +17 -11
  5. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/dev/imports.py +41 -0
  6. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/kmer/plot.py +22 -6
  7. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/merge/base.py +47 -21
  8. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/merge/cumulative.py +53 -27
  9. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/meta/descriptors.py +1 -0
  10. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/pairwise/base.py +9 -4
  11. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/pairwise/overlap/_overlap.py +55 -38
  12. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/pairwise/overlap/_stage.py +10 -0
  13. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/util/var.py +4 -0
  14. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10/src/agglovar.egg-info}/PKG-INFO +1 -1
  15. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/LICENSE +0 -0
  16. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/README.md +0 -0
  17. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/pyproject.toml +0 -0
  18. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/setup.cfg +0 -0
  19. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/align/__init__.py +0 -0
  20. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/align/op.py +0 -0
  21. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/align/score.py +0 -0
  22. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/bed/__init__.py +0 -0
  23. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/bed/col.py +0 -0
  24. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/bed/merge.py +0 -0
  25. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/dev/__init__.py +0 -0
  26. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/fa.py +0 -0
  27. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/io.py +0 -0
  28. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/kmer/__init__.py +0 -0
  29. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/kmer/util.py +0 -0
  30. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/merge/__init__.py +0 -0
  31. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/merge/config/__init__.py +0 -0
  32. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/merge/config/parser.py +0 -0
  33. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/merge/config/stage.py +0 -0
  34. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/merge/config/strategy.py +0 -0
  35. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/merge/om/__init__.py +0 -0
  36. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/merge/om/_executor_base.py +0 -0
  37. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/merge/om/_executor_nr.py +0 -0
  38. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/meta/__init__.py +0 -0
  39. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/meta/decorators.py +0 -0
  40. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/pairwise/__init__.py +0 -0
  41. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/pairwise/overlap/__init__.py +0 -0
  42. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/pairwise/overlap/_const.py +0 -0
  43. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/pairwise/weights.py +0 -0
  44. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/schema.py +0 -0
  45. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/seqmatch.py +0 -0
  46. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/util/__init__.py +0 -0
  47. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar/util/str.py +0 -0
  48. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar.egg-info/SOURCES.txt +0 -0
  49. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar.egg-info/dependency_links.txt +0 -0
  50. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/src/agglovar.egg-info/requires.txt +0 -0
  51. {agglovar-0.0.1.dev8 → agglovar-0.0.1.dev10}/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.dev8
3
+ Version: 0.0.1.dev10
4
4
  Summary: Toolkit for fast genomic variant transformations and intersects
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.12
@@ -1,6 +1,6 @@
1
1
  """Agglovar: A toolkit for fast genomic variant transformations and intersects."""
2
2
 
3
- __version__ = '0.0.1.dev8'
3
+ __version__ = '0.0.1.dev10'
4
4
 
5
5
  __all__ = [
6
6
  'align',
@@ -13,6 +13,7 @@ __all__ = [
13
13
  'meta',
14
14
  'schema',
15
15
  'seqmatch',
16
+ 'util',
16
17
  ]
17
18
 
18
19
  from . import align
@@ -25,3 +26,4 @@ from . import merge
25
26
  from . import meta
26
27
  from . import schema
27
28
  from . import seqmatch
29
+ from . import util
@@ -86,6 +86,17 @@ def as_proportion(
86
86
  if name is None or not (name := name.strip()):
87
87
  raise ValueError('Name must be a non-empty string')
88
88
 
89
+ if isinstance(df_a, pl.DataFrame):
90
+ df_a = df_a.lazy()
91
+
92
+ if isinstance(df_b, pl.DataFrame):
93
+ df_b = df_b.lazy()
94
+
95
+ col_set_a = set(df_a.collect_schema().names())
96
+
97
+ if '_index' not in col_set_a:
98
+ df_a = df_a.with_row_index('_index')
99
+
89
100
  # Collapse b, index a
90
101
  df_b_nr = (
91
102
  merge_depth(df_b, 0, col_names_b)
@@ -96,8 +107,6 @@ def as_proportion(
96
107
 
97
108
  df_a = (
98
109
  df_a
99
- .drop('_index', strict=False)
100
- .with_row_index('_index')
101
110
  .filter(
102
111
  pl.col(col_names_a.pos).is_not_null(),
103
112
  pl.col(col_names_a.end).is_not_null(),
@@ -128,11 +137,12 @@ def as_proportion(
128
137
  df_a
129
138
  .select(
130
139
  '_index',
140
+ '_index_a_bed_as_prop',
131
141
  (col_expr_a.end - col_expr_a.pos).alias('len')
132
142
  )
133
143
  .join(
134
144
  df_join.lazy(),
135
- on='_index',
145
+ on='_index_a_bed_as_prop',
136
146
  how='left',
137
147
  )
138
148
  .select(
@@ -46,6 +46,15 @@ class _JoinResources:
46
46
  if isinstance(df_b, pl.DataFrame):
47
47
  df_b = df_b.lazy()
48
48
 
49
+ cols_a = set(df_a.collect_schema().keys())
50
+ cols_b = set(df_b.collect_schema().keys())
51
+
52
+ if '_index' not in cols_a:
53
+ df_a = df_a.with_row_index('_index')
54
+
55
+ if '_index' not in cols_b:
56
+ df_b = df_b.with_row_index('_index')
57
+
49
58
  # Set column names
50
59
  ref_cols = get_coord_cols('ref')
51
60
 
@@ -65,16 +74,18 @@ class _JoinResources:
65
74
  # Prepare tables
66
75
  df_a = (
67
76
  df_a
68
- .select(*col_select_a)
69
- .drop('_index_a', strict=False)
70
- .with_row_index('_index_a')
77
+ .select(
78
+ pl.col('_index').alias('_index_a'),
79
+ *col_select_a
80
+ )
71
81
  )
72
82
 
73
83
  df_b = (
74
84
  df_b
75
- .select(*col_select_b)
76
- .drop('_index_b', strict=False)
77
- .with_row_index('_index_b')
85
+ .select(
86
+ pl.col('_index').alias('_index_b'),
87
+ *col_select_b
88
+ )
78
89
  )
79
90
 
80
91
  self.df_a = df_a
@@ -322,13 +333,10 @@ def pairwise_join_tree(
322
333
 
323
334
  for df_batch_b in (
324
335
  df_b
325
- .drop('_index_b', strict=False)
326
- .with_row_index('_index_b')
327
336
  .select(pl.col('_index_b'), *col_b.exprs())
328
337
  .collect_batches(chunk_size=chunk_size)
329
338
  ):
330
339
  for index_b, chrom_b, pos_b, end_b in df_batch_b.iter_rows():
331
- # print(f'Adding: {chrom_b} - {pos_b} - {end_b}')
332
340
  itree[chrom_b].addi(pos_b - distance, end_b + distance, index_b)
333
341
 
334
342
  # Intersect
@@ -336,8 +344,6 @@ def pairwise_join_tree(
336
344
 
337
345
  for df_chunk in (
338
346
  df_a
339
- .drop('_index_a', strict=False)
340
- .with_row_index('_index_a')
341
347
  .select(pl.col('_index_a'), *col_a.exprs())
342
348
  .collect_batches(chunk_size=chunk_size)
343
349
  ):
@@ -6,13 +6,17 @@ __all__ = [
6
6
  'find_imports',
7
7
  'get_defined_names',
8
8
  'get_module_definitions',
9
+ 'librl',
9
10
  ]
10
11
 
11
12
  import ast
13
+ from collections.abc import Iterable
12
14
  from dataclasses import dataclass, field
15
+ from graphlib import TopologicalSorter
13
16
  import importlib
14
17
  import inspect
15
18
  import re
19
+ import sys
16
20
  from types import ModuleType
17
21
  from typing import Any, Generator, Optional
18
22
 
@@ -439,3 +443,40 @@ def get_module_definitions(
439
443
  if include_dunder or not target.id.startswith('__'):
440
444
  yield target.id
441
445
 
446
+ def librl(
447
+ packages: Iterable[str] | str = 'agglovar',
448
+ ) -> None:
449
+ """Reload libraries.
450
+
451
+ Reloads all modules in the given packages and their dependencies. Attempts to resolve import
452
+ order by inspecting members of each package.
453
+
454
+ :param packages: Names of packages to reload. Must be top-level package names (name
455
+ before the first dot).
456
+ """
457
+ if isinstance(packages, str):
458
+ packages = {packages,}
459
+ else:
460
+ packages = set(packages)
461
+
462
+ deps: dict[str, set[str]] = {}
463
+
464
+ for key in sorted(sys.modules.keys()):
465
+ if key.split('.', 1)[0] not in packages:
466
+ continue
467
+
468
+ deps.setdefault(key, set())
469
+
470
+ for attr_name in dir(sys.modules[key]):
471
+ attr = getattr(sys.modules[key], attr_name)
472
+
473
+ if '__module__' in dir(attr):
474
+ module = attr.__module__
475
+
476
+ if not attr.__module__.split('.', 1)[0] in packages or module == key:
477
+ continue
478
+
479
+ deps[key].add(attr.__module__)
480
+
481
+ for key in TopologicalSorter(deps).static_order():
482
+ importlib.reload(sys.modules[key])
@@ -32,6 +32,19 @@ def dotplot(
32
32
  ) -> t_mpl_Figure:
33
33
  """Generate a dotplot for two sequences.
34
34
 
35
+ Configuration items:
36
+
37
+ * label_x, label_y: Axis labels (default "Sequence", and "Reference")
38
+ * start_x, start_y: Start position along each axis. Should be the first base position in the extracted region (Default 0).
39
+ * linewidth: Width of the lines (default 1)
40
+ * plot_width, plot_height, plot_dpi: Plot dimensions (default 7, 7, 300)
41
+ * invert_y: Invert the y axis (default False)
42
+ * color: A tuple of matplotlib colors for forward and reverse k-mer dots (default ("black", "red"))
43
+ * May be "reverse" or "rev" to reverse the order of the default colors.
44
+ * color_reverse: Reverse the order of the default colors (defaults to invert_y)
45
+ * kmer_util: A kmer_util.KmerUtil object (default None)
46
+ * k: K-mer size if kmer_util is missing (default 32)
47
+
35
48
  :params seq_x: Sequence 1 (horizontal axis).
36
49
  :params seq_y: Sequence 2 (vertical axis).
37
50
  :params config: Configuration dictionary.
@@ -56,10 +69,8 @@ def dotplot(
56
69
  # Get k-mer util.
57
70
  if 'kutil' in config:
58
71
  kutil = config['kutil']
59
- elif 'k' in config:
60
- kutil = kmer_util.KmerUtil(int(config['k']))
61
72
  else:
62
- kutil = kmer_util.KmerUtil(32)
73
+ kutil = kmer_util.KmerUtil(int(config.get('k', 32)))
63
74
 
64
75
  if anno_list is None:
65
76
  anno_list = list()
@@ -72,8 +83,10 @@ def dotplot(
72
83
  elif isinstance(color, str):
73
84
  if color.lower() in {'reverse', 'rev'}:
74
85
  color = ('red', 'black')
86
+ elif color.lower() in {'forward', 'fwd'}:
87
+ color = ('black', 'red')
75
88
  else:
76
- raise ValueError(f'Argument color is a string: Must be "reverse" or "rev": {color}')
89
+ raise ValueError(f'Argument color is a string: Must be "reverse" or "rev" or a tuple of two colors: {color}')
77
90
 
78
91
  elif isinstance(color, (list, tuple)):
79
92
  if len(color) != 2:
@@ -99,9 +112,12 @@ def dotplot(
99
112
 
100
113
  plot_props = dict() # Plot properties, such as x and y limits
101
114
 
115
+ if config.get('color_reverse', invert_y):
116
+ color = color[::-1]
117
+
102
118
  # Process annotation dict
103
- anno_list_background = list()
104
- anno_list_foreground = list()
119
+ anno_list_background = []
120
+ anno_list_foreground = []
105
121
 
106
122
  if anno_list is None:
107
123
  anno_list = list()
@@ -21,8 +21,8 @@ from ..util.str import collision_rename
21
21
  CallsetDefType: TypeAlias = (
22
22
  pl.DataFrame
23
23
  | pl.LazyFrame
24
- | tuple[
25
- pl.DataFrame | pl.LazyFrame, Optional[Any]
24
+ | Iterable[
25
+ pl.DataFrame | pl.LazyFrame | str
26
26
  ]
27
27
  )
28
28
  """Alias for acceptable types."""
@@ -38,12 +38,17 @@ class MergeBase(ABC):
38
38
  @abstractmethod
39
39
  def __call__(
40
40
  self,
41
- callsets: Iterable[CallsetDefType]
41
+ callsets: Iterable[CallsetDefType],
42
+ retain_index: bool = False,
43
+ pre_filter: Optional[Iterable[pl.Expr]] = None,
42
44
  ) -> pl.LazyFrame:
43
45
  """
44
46
  Intersect callsets.
45
47
 
46
48
  :param callsets: Callsets to intersect.
49
+ :param retain_index: If `True`, do not drop an existing "_index" column if it exists.
50
+ :param pre_filter: If set, filter each table with these expressions. Filter is applied
51
+ last (after "_index" is set).
47
52
 
48
53
  :return: A merged callset table.
49
54
  """
@@ -52,7 +57,8 @@ class MergeBase(ABC):
52
57
  @staticmethod
53
58
  def get_intersect_tuples(
54
59
  callsets: Iterable[CallsetDefType],
55
- retain_index: bool = False
60
+ retain_index: bool = False,
61
+ pre_filter: Optional[Iterable[pl.Expr]] = None,
56
62
  ) -> list[tuple[pl.LazyFrame, str, int]]:
57
63
  """
58
64
  Transform input arguments to a list of tuples with set fields.
@@ -72,6 +78,8 @@ class MergeBase(ABC):
72
78
  :param callsets: Callsets parameter. May be an iterable of DataFrames, LazyFrames, or
73
79
  tuples of (DataFrame, name).
74
80
  :param retain_index: If `True`, do not drop an existing "_index" column if it exists.
81
+ :param pre_filter: If set, filter each table with these expressions. Filter is applied
82
+ last (after "_index" is set).
75
83
 
76
84
  :return: A list of tuples where each tuple element represents one input source.
77
85
  """
@@ -84,30 +92,43 @@ class MergeBase(ABC):
84
92
 
85
93
  i = 0
86
94
 
95
+ pre_filter = list(pre_filter if pre_filter is not None else [])
96
+
87
97
  if callsets is None:
88
98
  raise ValueError('Missing callsets')
89
99
 
90
100
  for callset in callsets:
91
- if isinstance(callset, tuple):
92
- if not len(callset) == 2:
101
+ # Get callset table and name
102
+
103
+ if isinstance(callset, (pl.LazyFrame, pl.DataFrame)):
104
+ callset_table = callset
105
+ callset_name = None
106
+ else:
107
+ try:
108
+ callset = tuple(callset)
109
+ except TypeError:
93
110
  raise ValueError(
94
- f'Callset at index {i} tuple must have exactly 2 elements: {callset}'
111
+ f'Callset at index {i}: Callset must be a Polars table or an iterable '
112
+ f'of two elements (table, name): Received {type(callset)}'
95
113
  )
96
114
 
97
- callset_table = callset[0]
98
- callset_name = _get_name(callset[1], i, name_set)
115
+ if len(callset) == 1:
116
+ callset_table = callset[0]
117
+ callset_name = None
99
118
 
100
- else:
101
- callset_table = callset
102
- callset_name = _get_name(None, i, name_set)
119
+ elif len(callset) == 2:
120
+ callset_table = callset[0]
121
+ callset_name = str(callset[1])
103
122
 
104
- if isinstance(callset_table, pl.DataFrame):
105
- callset_table = callset_table.lazy()
106
- elif not isinstance(callset_table, pl.LazyFrame):
107
- raise TypeError(
108
- f'Callset at index {i} must be a DataFrame or LazyFrame, got {type(callset_table)}'
109
- )
123
+ else:
124
+ raise ValueError(f'Callset at index {i}: Input callsets must be a sequence of one or two elements: Received {len(callset)}')
110
125
 
126
+ if not isinstance(callset_table, (pl.LazyFrame, pl.DataFrame)):
127
+ raise ValueError(f'Callset at index {i}: First element Polars table: Received {type(callset_table)}')
128
+
129
+ callset_name = _get_name(callset_name, i, name_set)
130
+
131
+ # Set index if missing or forced
111
132
  if not (retain_index and '_index' in callset_table.collect_schema().names()):
112
133
  callset_table = (
113
134
  callset_table
@@ -118,22 +139,27 @@ class MergeBase(ABC):
118
139
  # Add missing IDs
119
140
  callset_table = (
120
141
  callset_table
142
+ .rename({'_index': '_mg_src_index'})
121
143
  .with_columns(
122
144
  pl.coalesce(
123
145
  pl.col(r'^id$'), # Existing ID if present
124
- pl.concat_str(pl.lit('var'), pl.col('_index')) # Fill missing/null
146
+ pl.concat_str(pl.lit('var'), pl.col('_mg_src_index')) # Fill missing/null
125
147
  ).cast(pl.String).alias('id')
126
148
  )
127
149
  .with_columns(
128
150
  (
129
151
  pl.when(pl.col('id').is_null())
130
- .then(pl.concat_str(pl.lit('var'), pl.col('_index')))
152
+ .then(pl.concat_str(pl.lit('var'), pl.col('_mg_src_index')))
131
153
  .otherwise(pl.col('id'))
132
154
  ).cast(pl.String).alias('id')
133
155
  )
156
+ .filter(
157
+ *pre_filter
158
+ )
134
159
  )
135
160
 
136
- callset_tuple_list.append((callset_table, callset_name, i))
161
+ # Finalize
162
+ callset_tuple_list.append(tuple((callset_table, callset_name, i)))
137
163
  name_set.add(callset_name)
138
164
 
139
165
  i += 1
@@ -15,11 +15,13 @@ where multiple join choices are possible.
15
15
  """
16
16
 
17
17
  __all__ = [
18
+ 'LeadStrategy',
18
19
  'MergeCumulative',
19
20
  ]
20
21
 
21
22
  from collections.abc import Iterable
22
23
  from enum import Enum
24
+ from typing import Optional
23
25
 
24
26
  import polars as pl
25
27
  import polars.selectors as cs
@@ -72,12 +74,11 @@ def _init_cumulative(
72
74
  :return: A table ready to be used as a cumulative table or appended to an existing one.
73
75
  """
74
76
  mg_cols = ('_mg_src', '_mg_src_pos', '_mg_stat')
75
- required_cols = set(required_cols) - set(mg_cols)
77
+ required_cols = (set(required_cols) - set(mg_cols)) | {'_mg_src_index'}
76
78
 
77
79
  return (
78
80
  df_next
79
81
  .select(
80
- pl.col('_index').alias('_mg_index'),
81
82
  *required_cols
82
83
  )
83
84
  .with_columns( # Source columns
@@ -85,7 +86,7 @@ def _init_cumulative(
85
86
  pl.struct(
86
87
  pl.lit(src_index).cast(pl.Int32).alias('index'),
87
88
  pl.lit(src_name).cast(pl.String).alias('id'),
88
- pl.col('_mg_index').alias('var_index'),
89
+ pl.col('_mg_src_index').alias('var_index'),
89
90
  pl.col('id').cast(pl.String).alias('var_id'),
90
91
  )
91
92
  ).alias('_mg_src'),
@@ -99,11 +100,11 @@ def _init_cumulative(
99
100
  # May be a Polars bug.
100
101
  pl.col('_mg_src_pos').list.concat(pl.col('pos').cast(pl.Int64))
101
102
  )
103
+ .drop('_mg_src_index')
102
104
  )
103
105
 
104
106
  def _get_match(
105
107
  df_cumulative: pl.LazyFrame,
106
- df_next: pl.LazyFrame,
107
108
  df_join: pl.DataFrame,
108
109
  src_index: int,
109
110
  src_name: str,
@@ -111,9 +112,10 @@ def _get_match(
111
112
  ) -> pl.LazyFrame:
112
113
  return ( # Update matching variants
113
114
  df_cumulative
115
+ .with_row_index('_index')
114
116
  .join(
115
117
  df_join.lazy().select(pl.all().name.prefix('_mg_join_')),
116
- left_on='_mg_index', right_on='_mg_join_index_a', how='inner'
118
+ left_on='_index', right_on='_mg_join_index_a', how='inner'
117
119
  )
118
120
  .with_columns(
119
121
  pl.col('_mg_src').list.concat(
@@ -132,7 +134,7 @@ def _get_match(
132
134
  })
133
135
  )
134
136
  )
135
- .drop('_mg_index', cs.starts_with('_mg_join_'))
137
+ .drop('_index', cs.starts_with('_mg_join_'))
136
138
  )
137
139
 
138
140
 
@@ -161,7 +163,10 @@ class MergeCumulative(MergeBase):
161
163
  def __call__(
162
164
  self,
163
165
  callsets: Iterable[CallsetDefType],
164
- retain_index: bool = False
166
+ retain_index: bool = False,
167
+ pre_filter: Optional[Iterable[pl.Expr]] = None,
168
+ sort: bool = True,
169
+ add_id: bool = True,
165
170
  ) -> pl.LazyFrame:
166
171
  """
167
172
  Intersect callsets.
@@ -169,10 +174,12 @@ class MergeCumulative(MergeBase):
169
174
  :param callsets: Callsets to intersect.
170
175
  :param retain_index: If `True`, do not drop an existing "_index" column in callset tables
171
176
  if they exist.
177
+ :param pre_filter: If set, filter each table with these expressions. Filter is applied
178
+ last (after "_index" is set).
172
179
 
173
180
  :return: A merged callset table.
174
181
  """
175
- callsets = self.get_intersect_tuples(callsets, retain_index)
182
+ callsets = self.get_intersect_tuples(callsets, retain_index, pre_filter)
176
183
 
177
184
  if len(callsets) == 0:
178
185
  raise ValueError('No callsets to intersect.')
@@ -195,7 +202,7 @@ class MergeCumulative(MergeBase):
195
202
  df_next, src_name, src_index = callsets[0]
196
203
 
197
204
  merge_stat_cols = { # Empty join, gets names of columns this join will return
198
- col: dtype for col, dtype in self.pairwise_join.join(df_next.head(0).drop('_index'), df_next.head(0).drop('_index')).collect_schema().items()
205
+ col: dtype for col, dtype in self.pairwise_join.join(df_next.head(0), df_next.head(0)).collect_schema().items()
199
206
  if col not in {'index_a', 'index_b', 'id_a', 'id_b'}
200
207
  }
201
208
 
@@ -216,16 +223,23 @@ class MergeCumulative(MergeBase):
216
223
  # Intersect
217
224
  df_join = (
218
225
  (
219
- self.pairwise_join.join(df_cumulative, df_next.drop('_index'))
226
+ self.pairwise_join.join(df_cumulative, df_next)
220
227
  .unique('index_a', keep='first')
221
228
  .unique('index_b', keep='first')
222
229
  .sort('index_a', 'index_b')
223
230
  )
224
231
  .join(
225
- df_next.select('_index', 'pos', 'id'),
232
+ (
233
+ df_next
234
+ .with_row_index('_index')
235
+ .select('_index', '_mg_src_index', 'pos', 'id')
236
+ ),
226
237
  left_on='index_b', right_on='_index',
227
238
  how='left'
228
239
  )
240
+ .with_columns(
241
+ pl.col('_mg_src_index').alias('index_b')
242
+ )
229
243
  .collect()
230
244
  )
231
245
 
@@ -235,36 +249,36 @@ class MergeCumulative(MergeBase):
235
249
  # 3) New: New records in this callset
236
250
 
237
251
  df_match = _get_match(
238
- df_cumulative, df_next, df_join, src_index, src_name, merge_stat_cols
252
+ df_cumulative, df_join, src_index, src_name, merge_stat_cols
239
253
  )
240
254
 
241
255
  df_nomatch = ( # Gather records that were not updated
242
256
  df_cumulative
257
+ .with_row_index('_index')
243
258
  .join(
244
259
  df_join.lazy(),
245
- left_on='_mg_index', right_on='index_a', how='anti'
260
+ left_on='_index', right_on='index_a', how='anti'
246
261
  )
247
- .drop('_mg_index')
262
+ .drop('_index')
248
263
  )
249
264
 
250
- df_new = (
265
+ df_new = ( # New records
251
266
  _init_cumulative(
252
267
  (
253
268
  df_next
254
269
  .join(
255
270
  df_join.lazy(),
256
- left_on='_index', right_on='index_b', how='anti'
271
+ left_on='_mg_src_index', right_on='index_b', how='anti'
257
272
  )
258
273
  ),
259
274
  src_index, src_name, required_cols, merge_stat_cols
260
275
  )
261
- .drop('_mg_index')
262
276
  )
263
277
 
264
278
  df_cumulative = (
265
279
  pl.concat([df_match, df_nomatch, df_new])
266
280
  .sort('chrom', 'pos', 'end')
267
- .with_row_index('_mg_index')
281
+ .with_row_index('_mg_src_index')
268
282
  .collect()
269
283
  .lazy()
270
284
  )
@@ -272,7 +286,7 @@ class MergeCumulative(MergeBase):
272
286
  # Subset to merge columns (drop columns needed for join)
273
287
  df_cumulative = (
274
288
  df_cumulative
275
- .select(cs.starts_with('_mg_').exclude('_mg_index'))
289
+ .select(cs.starts_with('_mg_').exclude('_mg_src_index'))
276
290
  .rename(lambda col: col.removeprefix('_'))
277
291
  )
278
292
 
@@ -320,31 +334,43 @@ class MergeCumulative(MergeBase):
320
334
  .filter(pl.col('mg_src_lead').struct.field('index') == src_index)
321
335
  .select(
322
336
  '_mg_index',
323
- pl.col('mg_src_lead').struct.field('var_index').alias('_mg_src_var_index'),
337
+ pl.col('mg_src_lead').struct.field('var_index').alias('_mg_src_index'),
324
338
  )
325
339
  ),
326
- left_on='_index', right_on='_mg_src_var_index', how='inner'
340
+ on='_mg_src_index', how='inner'
327
341
  )
328
- .drop('_index')
342
+ .drop('_mg_src_index')
329
343
  )
330
344
 
331
345
  for col in set(table_cols) - df_next_cols:
332
346
  df_next = df_next.with_columns(pl.lit(None).cast(all_col_dict[col]).alias(col))
333
347
 
334
- df_next = df_next.with_columns(
335
- pl.col('filter').fill_null([])
348
+ df_next = (
349
+ df_next.with_columns(
350
+ pl.col('filter').fill_null([])
351
+ )
352
+ .select('_mg_index', *table_cols)
336
353
  )
337
354
 
338
355
  lead_list.append(df_next)
339
356
 
340
- return (
357
+ df_merge = (
341
358
  pl.concat(lead_list)
342
359
  .join(
343
360
  df_cumulative.lazy(),
344
361
  on='_mg_index', how='inner'
345
362
  )
346
- .with_columns(id_version_expr())
363
+ )
364
+
365
+ if add_id:
366
+ df_merge = df_merge.with_columns(id_version_expr())
367
+
368
+ if sort:
369
+ df_merge = df_merge.sort('chrom', 'pos', 'end', 'id')
370
+
371
+
372
+ return (
373
+ df_merge
347
374
  .drop(*(['_mg_index'] + ['filter'] if drop_filter else []))
348
375
  .select(col_order)
349
- .sort('chrom', 'pos', 'end', 'id')
350
376
  )
@@ -14,6 +14,7 @@ Classes:
14
14
  __all__ = [
15
15
  'AutoInitBase',
16
16
  'CheckedBool',
17
+ 'CheckedObject',
17
18
  'CheckedString',
18
19
  'OneWayBool',
19
20
  'BoundedInt',
@@ -9,7 +9,6 @@ __all__ = [
9
9
 
10
10
  from abc import ABC, abstractmethod
11
11
  from collections.abc import Iterator, Iterable
12
- from typing import Optional
13
12
 
14
13
  import polars as pl
15
14
 
@@ -40,12 +39,15 @@ class PairwiseJoin(ABC):
40
39
  def join_iter(
41
40
  self,
42
41
  df_a: pl.DataFrame | pl.LazyFrame,
43
- df_b: pl.DataFrame | pl.LazyFrame
42
+ df_b: pl.DataFrame | pl.LazyFrame,
43
+ retain_index: bool = False,
44
44
  ) -> Iterator[pl.LazyFrame]:
45
45
  """Find all pairs of variants in two sources that meet a set of criteria.
46
46
 
47
47
  :param df_a: Source dataframe.
48
48
  :param df_b: Target dataframe.
49
+ :param retain_index: If True, do not drop an existing "_index" column in callset tables
50
+ if they exist.
49
51
 
50
52
  :yields: A LazyFrame for each chunk.
51
53
  """
@@ -54,7 +56,8 @@ class PairwiseJoin(ABC):
54
56
  def join(
55
57
  self,
56
58
  df_a: pl.DataFrame | pl.LazyFrame,
57
- df_b: pl.DataFrame | pl.LazyFrame
59
+ df_b: pl.DataFrame | pl.LazyFrame,
60
+ retain_index: bool = False,
58
61
  ) -> pl.LazyFrame:
59
62
  """Find all pairs of variants in two sources that meet a set of criteria.
60
63
 
@@ -62,11 +65,13 @@ class PairwiseJoin(ABC):
62
65
 
63
66
  :param df_a: Table A.
64
67
  :param df_b: Table B.
68
+ :param retain_index: If True, do not drop an existing "_index" column in callset tables
69
+ if they exist.
65
70
 
66
71
  :returns: A join table.
67
72
  """
68
73
  return pl.concat(
69
- self.join_iter(df_a, df_b)
74
+ self.join_iter(df_a, df_b, retain_index=retain_index)
70
75
  )
71
76
 
72
77
  @property
@@ -8,10 +8,14 @@ from collections.abc import (
8
8
  Iterable,
9
9
  Iterator,
10
10
  Mapping,
11
+ MutableMapping,
11
12
  )
12
13
  import functools
13
14
  import operator
14
- from typing import Optional
15
+ from typing import (
16
+ Any,
17
+ Optional,
18
+ )
15
19
  from warnings import warn
16
20
 
17
21
  import polars as pl
@@ -189,21 +193,26 @@ class PairwiseOverlap(PairwiseJoin):
189
193
  def weight_expr(self):
190
194
  return self.weight_strategy.expr
191
195
 
192
-
193
196
  def join_iter(
194
197
  self,
195
198
  df_a: pl.DataFrame | pl.LazyFrame,
196
199
  df_b: pl.DataFrame | pl.LazyFrame,
200
+ retain_index: bool = False,
197
201
  ) -> Iterator[pl.LazyFrame]:
198
202
  """Find all pairs of variants in two sources that meet a set of criteria.
199
203
 
200
204
  :param df_a: Source dataframe.
201
205
  :param df_b: Target dataframe.
206
+ :param retain_index: If True, do not drop an existing "_index" column in callset tables
207
+ if they exist.
202
208
 
203
209
  :yields: A LazyFrame for each chunk.
204
210
  """
205
211
 
206
- if self.is_equi_offset or self.chunk_size == 0:
212
+ # Prepare tables
213
+ df_a, df_b = self._prepare_tables(df_a, df_b, warn_on_reserved=True, retain_index=retain_index)
214
+
215
+ if self.chunk_size == 0 or (self.is_equi_offset and self.chunk_size is None):
207
216
  return self._join_iter_notchunked(df_a, df_b)
208
217
 
209
218
  return self._join_iter_chunked(df_a, df_b)
@@ -215,22 +224,16 @@ class PairwiseOverlap(PairwiseJoin):
215
224
  ) -> Iterator[pl.LazyFrame]:
216
225
  """Find all pairs of variants in two sources that meet a set of criteria.
217
226
 
218
- :param df_a: Source dataframe.
219
- :param df_b: Target dataframe.
227
+ :param df_a: Source dataframe after `_prepare_tables()`.
228
+ :param df_b: Target dataframe after `_prepare_tables()`.
220
229
 
221
230
  :yields: A LazyFrame for each chunk.
222
231
  """
223
-
224
- # print('_join_iter_chunked(): Starting')
225
-
226
232
  join_empty = True # Detects if no joins were written
227
233
 
228
234
  chunk_range = self._get_chunk_range()
229
235
  chunk_size = self.chunk_size if self.chunk_size is not None else DEFAULT_CHUNK_SIZE
230
236
 
231
- # Prepare tables
232
- df_a, df_b = self._prepare_tables(df_a, df_b, warn_on_reserved=True)
233
-
234
237
  for chrom, last_index_a in (
235
238
  df_a
236
239
  .group_by('chrom_a')
@@ -308,18 +311,13 @@ class PairwiseOverlap(PairwiseJoin):
308
311
  ) -> Iterator[pl.LazyFrame]:
309
312
  """Find all pairs of variants in two sources that meet a set of criteria.
310
313
 
311
- :param df_a: Source dataframe.
312
- :param df_b: Target dataframe.
314
+ :param df_a: Source dataframe after `_prepare_tables()`.
315
+ :param df_b: Target dataframe after `_prepare_tables()`.
313
316
 
314
317
  :yields: A LazyFrame for each chunk.
315
318
  """
316
- # print('_join_iter_notchunked(): Starting')
317
-
318
319
  join_empty = True # Detects if no joins were written
319
320
 
320
- # Prepare tables
321
- df_a, df_b = self._prepare_tables(df_a, df_b, warn_on_reserved=True)
322
-
323
321
  chrom_list = sorted(
324
322
  set(df_a.select('chrom_a').unique().collect().to_series().to_list())
325
323
  | set(df_b.select('chrom_b').unique().collect().to_series().to_list())
@@ -415,7 +413,8 @@ class PairwiseOverlap(PairwiseJoin):
415
413
  self,
416
414
  df_a: pl.DataFrame | pl.LazyFrame,
417
415
  df_b: pl.DataFrame | pl.LazyFrame,
418
- warn_on_reserved: bool = False
416
+ retain_index: bool = False,
417
+ warn_on_reserved: bool = False,
419
418
  ) -> tuple[pl.LazyFrame, pl.LazyFrame]:
420
419
  """Prepares tables for join.
421
420
 
@@ -424,6 +423,8 @@ class PairwiseOverlap(PairwiseJoin):
424
423
 
425
424
  :param df_a: Table A.
426
425
  :param df_b: Table B.
426
+ :param retain_index: If True, do not drop an existing "_index" column in callset tables
427
+ if they exist.
427
428
  :param warn_on_reserved: If True, generate a warning if reserved columns are found and
428
429
  drop them. If false, raise an error.
429
430
 
@@ -460,25 +461,37 @@ class PairwiseOverlap(PairwiseJoin):
460
461
  )
461
462
 
462
463
  # Drop reserved columns
463
- if reserved_cols := sorted(self.check_reserved_cols(columns_a)):
464
- err_str = f'Reserved columns in table "A": {", ".join(reserved_cols)}'
464
+ reserved_col_a = self.check_reserved_cols(columns_a)
465
+ reserved_col_b = self.check_reserved_cols(columns_b)
466
+
467
+ retain_index_a = '_index' in reserved_col_a and retain_index
468
+ retain_index_b = '_index' in reserved_col_b and retain_index
469
+
470
+ if retain_index:
471
+ reserved_col_a -= {'_index', }
472
+ reserved_col_b -= {'_index', }
473
+
474
+ if reserved_col_a:
475
+ reserved_col_a = sorted(reserved_col_a)
476
+ err_str = f'Reserved columns in table "A": {", ".join(reserved_col_a)}'
465
477
 
466
478
  if warn_on_reserved:
467
479
  warn(f'{err_str}: Dropping column(s)')
468
480
  else:
469
481
  raise ValueError(err_str)
470
482
 
471
- df_a = df_a.drop(reserved_cols, strict=False)
483
+ df_a = df_a.drop(reserved_col_a, strict=False)
472
484
 
473
- if reserved_cols := sorted(self.check_reserved_cols(columns_b)):
474
- err_str = f'Reserved columns in table "B": {", ".join(reserved_cols)}'
485
+ if reserved_col_b:
486
+ reserved_col_b = sorted(reserved_col_b)
487
+ err_str = f'Reserved columns in table "B": {", ".join(reserved_col_b)}'
475
488
 
476
489
  if warn_on_reserved:
477
490
  warn(f'{err_str}: Dropping column(s)')
478
491
  else:
479
492
  raise ValueError(err_str)
480
493
 
481
- df_b = df_b.drop(reserved_cols, strict=False)
494
+ df_b = df_b.drop(reserved_col_b, strict=False)
482
495
 
483
496
  # Cast columns
484
497
  try:
@@ -526,15 +539,19 @@ class PairwiseOverlap(PairwiseJoin):
526
539
  )
527
540
 
528
541
  # Set index
529
- df_a = (
530
- df_a
531
- .with_row_index('_index')
532
- )
542
+ if not retain_index_a:
543
+ df_a = (
544
+ df_a
545
+ .drop('_index', strict=False)
546
+ .with_row_index('_index')
547
+ )
533
548
 
534
- df_b = (
535
- df_b
536
- .with_row_index('_index')
537
- )
549
+ if not retain_index_b:
550
+ df_b = (
551
+ df_b
552
+ .drop('_index', strict=False)
553
+ .with_row_index('_index')
554
+ )
538
555
 
539
556
  # Set ID
540
557
  if 'id' not in columns_a:
@@ -644,7 +661,7 @@ class PairwiseOverlap(PairwiseJoin):
644
661
  def _append_join_cols(
645
662
  self,
646
663
  exprs: Iterable[pl.Expr | str] | pl.Expr | str,
647
- join_expr_map: Mapping[str, Optional[pl.Expr]],
664
+ join_expr_map: MutableMapping[str, Optional[pl.Expr]],
648
665
  no_replace: bool = False,
649
666
  ) -> None:
650
667
  """Append expressions to the list of columns included in the join table.
@@ -660,7 +677,7 @@ class PairwiseOverlap(PairwiseJoin):
660
677
 
661
678
  :param exprs: A join column or a list of join columns where each may be defined as a string
662
679
  (known column name or a field already in the join table) or a Polars expression.
663
- :param join_col_exprs: A dictionary to which the expressions will be added.
680
+ :param join_expr_map: A dictionary to which the expressions will be added.
664
681
  :param no_replace: Do not replace existing join column expressions if True.
665
682
  """
666
683
  if isinstance(exprs, (pl.Expr, str)):
@@ -937,7 +954,7 @@ class PairwiseOverlap(PairwiseJoin):
937
954
  @classmethod
938
955
  def from_definiton(
939
956
  cls,
940
- overlap_def,
957
+ overlap_def: Mapping[str, Any] | Iterable[Mapping[str, Any]],
941
958
  ):
942
959
  keys: Optional[set[str]] = None
943
960
 
@@ -959,12 +976,12 @@ class PairwiseOverlap(PairwiseJoin):
959
976
  f'of stage definitions: {type(overlap_def)}'
960
977
  )
961
978
 
962
- overlap_def = {
979
+ overlap_def: Mapping[str, Any] | Iterable[Mapping[str, Any]] = {
963
980
  'stages': stage_list,
964
981
  }
965
982
 
966
983
  # Parse arguments
967
- parsed_def = {}
984
+ parsed_def: dict[str, Any] = {}
968
985
 
969
986
  for key, value in overlap_def.items():
970
987
  if key == 'stages':
@@ -38,6 +38,10 @@ class PairwiseOverlapStage():
38
38
  :ivar match_ref: "REF" column must match between two variants.
39
39
  :ivar match_alt: "ALT" column must match between two variants.
40
40
  :ivar match_vartype: "VARTYPE" column must match between two variants.
41
+ :ivar match_filter_pass: If true, then matched variants must either pass filters ("filter"
42
+ column emtpy for both variants) or must fail filters ("filter" column non-empty for both
43
+ variants). This does not compare the contents of the "filter" column, just the presence or
44
+ absence of a filter.
41
45
  :ivar match_prop_min: Minimum matched base proportion in alignment or None to not match.
42
46
  """
43
47
  ro_min: Optional[float] = BoundedFloat(min_val=0.0, max_val=1.0)
@@ -82,6 +86,7 @@ class PairwiseOverlapStage():
82
86
  self.match_ref = match_ref
83
87
  self.match_alt = match_alt
84
88
  self.match_vartype = match_vartype
89
+ self.match_filter_pass = match_filter_pass
85
90
  self.seg_ro_min = seg_ro_min
86
91
  self.match_prop_min = match_prop_min
87
92
 
@@ -185,6 +190,11 @@ class PairwiseOverlapStage():
185
190
  pl.col('seg_ro') >= self.seg_ro_min
186
191
  )
187
192
 
193
+ if self.match_filter_pass:
194
+ join_predicates_list.append(
195
+ (pl.col('filter_a').list.len() == 0) ^ (pl.col('filter_b').list.len() == 0)
196
+ )
197
+
188
198
  if self.match_prop_min is not None and self.match_prop_min > 0.0:
189
199
  join_filters_list.append(
190
200
  pl.col('match_prop') >= self.match_prop_min
@@ -1,5 +1,9 @@
1
1
  """Variant utilites."""
2
2
 
3
+ __all__ = [
4
+ 'id_version_expr',
5
+ ]
6
+
3
7
  from typing import Any
4
8
 
5
9
  import polars as pl
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agglovar
3
- Version: 0.0.1.dev8
3
+ Version: 0.0.1.dev10
4
4
  Summary: Toolkit for fast genomic variant transformations and intersects
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.12
File without changes
File without changes
File without changes