pav3 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. pav3/__init__.py +70 -0
  2. pav3/__main__.py +6 -0
  3. pav3/align/__init__.py +17 -0
  4. pav3/align/features.py +571 -0
  5. pav3/align/lcmodel/__init__.py +30 -0
  6. pav3/align/lcmodel/_lcmodel.py +278 -0
  7. pav3/align/lcmodel/_lcmodel_logistic.py +105 -0
  8. pav3/align/lcmodel/_lcmodel_null.py +55 -0
  9. pav3/align/lcmodel/_util.py +247 -0
  10. pav3/align/lift.py +887 -0
  11. pav3/align/op.py +363 -0
  12. pav3/align/records.py +338 -0
  13. pav3/align/score.py +552 -0
  14. pav3/align/tables.py +1079 -0
  15. pav3/align/trim.py +1151 -0
  16. pav3/anno.py +82 -0
  17. pav3/call/__init__.py +18 -0
  18. pav3/call/expr.py +91 -0
  19. pav3/call/integrate.py +699 -0
  20. pav3/call/intra.py +551 -0
  21. pav3/call/util.py +70 -0
  22. pav3/cli/__init__.py +29 -0
  23. pav3/cli/_cli.py +137 -0
  24. pav3/cli/_common_opt.py +161 -0
  25. pav3/cli/_lift_cli.py +139 -0
  26. pav3/cli/_returncode.py +82 -0
  27. pav3/cli/_subcommand_batch.py +188 -0
  28. pav3/cli/_subcommand_call.py +197 -0
  29. pav3/cli/_subcommand_license.py +56 -0
  30. pav3/cli/_subcommand_lift.py +100 -0
  31. pav3/cli/_subcommand_maketable.py +106 -0
  32. pav3/cli/_subcommand_subseq.py +117 -0
  33. pav3/const.py +198 -0
  34. pav3/data/lcmodel/default/model.json +4 -0
  35. pav3/data/lcmodel/human_95_80/model.json +15 -0
  36. pav3/data/lcmodel/human_95_80/model_info.tar.gz +0 -0
  37. pav3/data/lcmodel/human_95_80/weights.npz +0 -0
  38. pav3/data/lcmodel/human_98_80/model.json +15 -0
  39. pav3/data/lcmodel/human_98_80/model_info.tar.gz +0 -0
  40. pav3/data/lcmodel/human_98_80/weights.npz +0 -0
  41. pav3/data/lcmodel/prim_90_50/model.json +15 -0
  42. pav3/data/lcmodel/prim_90_50/model_info.tar.gz +0 -0
  43. pav3/data/lcmodel/prim_90_50/weights.npz +0 -0
  44. pav3/data/lcmodel/primdiv_85_50/model.json +15 -0
  45. pav3/data/lcmodel/primdiv_85_50/model_info.tar.gz +0 -0
  46. pav3/data/lcmodel/primdiv_85_50/weights.npz +0 -0
  47. pav3/data/tracks/alignment_track_fields.tsv +27 -0
  48. pav3/data/tracks/variant_track_fields.tsv +41 -0
  49. pav3/data/workflow/Snakefile +82 -0
  50. pav3/data/workflow/profiles/pav_default/config.yaml +80 -0
  51. pav3/data/workflow/rules/align.smk +366 -0
  52. pav3/data/workflow/rules/call.smk +749 -0
  53. pav3/data/workflow/rules/data.smk +133 -0
  54. pav3/data/workflow/rules/pipeline.smk +26 -0
  55. pav3/data/workflow/rules/tracks.smk +215 -0
  56. pav3/dev/__init__.py +19 -0
  57. pav3/dev/imports.py +440 -0
  58. pav3/dev/inter.py +24 -0
  59. pav3/dev/path.py +48 -0
  60. pav3/dev/sm.py +95 -0
  61. pav3/dev/tables.py +37 -0
  62. pav3/errors.py +33 -0
  63. pav3/fig/__init__.py +13 -0
  64. pav3/fig/const.py +39 -0
  65. pav3/fig/density.py +154 -0
  66. pav3/fig/tracks.py +1116 -0
  67. pav3/fig/util.py +110 -0
  68. pav3/format.py +122 -0
  69. pav3/inv.py +1163 -0
  70. pav3/io.py +720 -0
  71. pav3/kde.py +220 -0
  72. pav3/lgsv/__init__.py +21 -0
  73. pav3/lgsv/call.py +360 -0
  74. pav3/lgsv/chain.py +244 -0
  75. pav3/lgsv/interval.py +697 -0
  76. pav3/lgsv/io.py +273 -0
  77. pav3/lgsv/region_kde.py +199 -0
  78. pav3/lgsv/resources.py +152 -0
  79. pav3/lgsv/struct.py +483 -0
  80. pav3/lgsv/variant.py +1276 -0
  81. pav3/lift/__init__.py +75 -0
  82. pav3/lift/errors.py +35 -0
  83. pav3/lift/output.py +516 -0
  84. pav3/lift/run.py +332 -0
  85. pav3/lift/source.py +422 -0
  86. pav3/lift/source_table.py +278 -0
  87. pav3/maketable.py +450 -0
  88. pav3/params.py +768 -0
  89. pav3/pipeline.py +1214 -0
  90. pav3/region.py +416 -0
  91. pav3/schema.py +301 -0
  92. pav3/seq.py +609 -0
  93. pav3/util.py +178 -0
  94. pav3/vcf.py +1850 -0
  95. pav3/workflow/__init__.py +11 -0
  96. pav3/workflow/call.py +649 -0
  97. pav3/workflow/perf.py +1017 -0
  98. pav3/workflow/resource.py +523 -0
  99. pav3-3.0.0.dist-info/METADATA +279 -0
  100. pav3-3.0.0.dist-info/RECORD +104 -0
  101. pav3-3.0.0.dist-info/WHEEL +5 -0
  102. pav3-3.0.0.dist-info/entry_points.txt +2 -0
  103. pav3-3.0.0.dist-info/licenses/LICENSE +21 -0
  104. pav3-3.0.0.dist-info/top_level.txt +1 -0
pav3/__init__.py ADDED
@@ -0,0 +1,70 @@
1
+ """PAV Python library routines."""
2
+
3
+ import importlib
4
+ import logging
5
+
6
+ __version__ = '3.0.0'
7
+
8
+ __all__ = [
9
+ 'align',
10
+ 'anno',
11
+ 'call',
12
+ 'cli',
13
+ 'const',
14
+ 'errors',
15
+ 'fig',
16
+ 'format',
17
+ 'io',
18
+ 'inv',
19
+ 'kde',
20
+ 'lgsv',
21
+ 'maketable',
22
+ 'params',
23
+ 'pipeline',
24
+ 'region',
25
+ 'schema',
26
+ 'seq',
27
+ 'util',
28
+ 'vcf',
29
+ 'workflow',
30
+ ]
31
+
32
+ __license__ = 'MIT License'
33
+ __author__ = 'Peter Audano'
34
+ __copyright__ = 'Copyright (c) 2024 Peter Audano'
35
+ __credits__ = ['Peter Audano']
36
+ __maintainer__ = 'Peter Audano'
37
+ __email__ = 'peter.audano@jax.org'
38
+ __status__ = 'Development'
39
+
40
+ __license_spdx__ = 'MIT'
41
+
42
+ __license_text__ = """MIT License
43
+
44
+ Copyright (c) 2024 Peter Audano
45
+
46
+ Permission is hereby granted, free of charge, to any person obtaining a copy
47
+ of this software and associated documentation files (the "Software"), to deal
48
+ in the Software without restriction, including without limitation the rights
49
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
50
+ copies of the Software, and to permit persons to whom the Software is
51
+ furnished to do so, subject to the following conditions:
52
+
53
+ The above copyright notice and this permission notice shall be included in all
54
+ copies or substantial portions of the Software.
55
+
56
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
57
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
58
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
59
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
60
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
61
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
62
+ SOFTWARE.
63
+ """
64
+
65
+ for name in __all__:
66
+ globals()[name] = importlib.import_module(f'.{name}', package=__name__)
67
+
68
+
69
+ # Configure logging
70
+ logging.getLogger(__name__).addHandler(logging.NullHandler())
pav3/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Support for direct pav3 execution."""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == '__main__':
6
+ main()
pav3/align/__init__.py ADDED
@@ -0,0 +1,17 @@
1
+ """Alignment handling routines."""
2
+
3
+ import importlib
4
+
5
+ __all__ = [
6
+ 'features',
7
+ 'lcmodel',
8
+ 'lift',
9
+ 'op',
10
+ 'records',
11
+ 'score',
12
+ 'tables',
13
+ 'trim',
14
+ ]
15
+
16
+ for name in __all__:
17
+ globals()[name] = importlib.import_module(f'.{name}', package=__name__)
pav3/align/features.py ADDED
@@ -0,0 +1,571 @@
1
+ """Calculate alignment features.
2
+
3
+ Alignment features are derived from alignment tables and provide summary statistics for alignment records.
4
+ """
5
+
6
+ __all__ = [
7
+ 'ALIGN_FEATURE_SCORE_PROP_CONF',
8
+ 'ALIGN_TABLE_COLUMNS',
9
+ 'FeatureGenerator',
10
+ 'feature',
11
+ ]
12
+
13
+ from dataclasses import dataclass, field
14
+ from functools import wraps
15
+ import inspect
16
+ from typing import Any, Callable, Iterable, Optional
17
+
18
+ import polars as pl
19
+
20
+ from .score import ScoreModel, get_score_model
21
+
22
+ from . import op
23
+
24
+ # Features PAV saves to alignment tables
25
+ ALIGN_TABLE_COLUMNS = (
26
+ 'score', 'score_prop', 'match_prop', 'qry_prop'
27
+ )
28
+
29
+ # Use this value if ANCHOR_PROP is in the alignment features stored in BED files.
30
+ ALIGN_FEATURE_SCORE_PROP_CONF = 0.85
31
+
32
+ _feature_registry: dict[str, Callable] = {}
33
+ _feature_schema: dict[str, type[pl.DataType]] = {}
34
+
35
+
36
+ def feature(dtype: type[pl.DataType], name: str = None) -> Callable:
37
+ """Register a feature function to FeatureGenerator (decorator).
38
+
39
+ The decorator creates a wrapper that accepts standard parameters but only passes the parameters that the decorated
40
+ function actually accepts.
41
+
42
+ :param dtype: Data type of feature.
43
+ :param name: Name of feature. If None, uses the function name.
44
+
45
+ :returns: The decorated function with registration side effects.
46
+ """
47
+
48
+ def decorator(func: Callable) -> Callable:
49
+ nonlocal name
50
+
51
+ if name is None:
52
+ name = func.__name__
53
+
54
+ # Get function signature
55
+ param_names = set(inspect.signature(func).parameters.keys()) - {'self'}
56
+
57
+ @wraps(func)
58
+ def wrapper(
59
+ self,
60
+ df: pl.DataFrame,
61
+ df_qry_fai: Optional[pl.DataFrame] = None,
62
+ temp_features: Optional[set[str]] = None,
63
+ ) -> pl.DataFrame:
64
+ """Wrap feature functions with a consistent signature.
65
+
66
+ Only passes parameters that the decorated function accepts.
67
+
68
+ :param self: The FeatureGenerator instance.
69
+ :param df: Table of alignment records.
70
+ :param df_qry_fai: Table of sequence lengths (optional).
71
+ :param temp_features: Set to track temporary features (optional).
72
+
73
+ :returns: A table with new or updated feature columns.
74
+ """
75
+ # Build kwargs with only the parameters the function accepts
76
+ kwargs: dict[str, Any] = {'df': df}
77
+
78
+ if 'df_qry_fai' in param_names and df_qry_fai is not None:
79
+ kwargs['df_qry_fai'] = df_qry_fai
80
+
81
+ if 'temp_features' in param_names and temp_features is not None:
82
+ kwargs['temp_features'] = temp_features
83
+
84
+ return (
85
+ func(self, **kwargs)
86
+ .with_columns(pl.col(name).cast(dtype))
87
+ )
88
+
89
+ # Register the wrapper in the class registry
90
+ # This will be done when the class is defined
91
+ # if not hasattr(FeatureGenerator, '_feature_registry'):
92
+ # FeatureGenerator._feature_registry = {}
93
+
94
+ _feature_registry[name] = wrapper
95
+ _feature_schema[name] = dtype
96
+
97
+ def wrapper_no_if(
98
+ self, *args, **kwargs
99
+ ):
100
+ """Wrap function and add dtype cast.
101
+
102
+ :param self: The FeatureGenerator instance.
103
+ :param args: Positional arguments.
104
+ :param kwargs: Keyword arguments.
105
+
106
+ :returns: A table with new or updated feature columns.
107
+ """
108
+ return (
109
+ func(self, *args, **kwargs)
110
+ .with_columns(pl.col(name).cast(dtype))
111
+ )
112
+
113
+ return wrapper_no_if
114
+
115
+ return decorator
116
+
117
+
118
+ @dataclass(frozen=True)
119
+ class FeatureGenerator:
120
+ """Class for generating alignment features.
121
+
122
+ :param features: Names of features to compute. If empty or None, re-compute existing features..
123
+ :param score_model: Model to use to compute alignment scores. A string specification is used to construct a score
124
+ model (the object attribute is always a ScoreModel object).
125
+ :param score_prop_conf: When determining if an alignment is anchored by a confident alignment upstream or
126
+ downstream, this is the minimum score proportion to flag an alignment as confident.
127
+ :param only_features: If True, only return alignment features, else, return the full alignment table.
128
+ :param force_all: If True, compute all features, even if they are already present.
129
+ :param force_score: If True, recompute alignment scores even if they are already present in the alignment table.
130
+ """
131
+
132
+ features: Optional[Iterable[str] | str] = field(default=ALIGN_TABLE_COLUMNS)
133
+ score_model: ScoreModel | str = field(default_factory=get_score_model)
134
+ score_prop_conf: float = field(default=ALIGN_FEATURE_SCORE_PROP_CONF)
135
+ only_features: bool = field(default=False)
136
+ force_all: bool = field(default=False)
137
+ force_score: bool = field(default=False)
138
+
139
+ # # Class-level feature registry
140
+ # _feature_registry: dict[str, Callable] = field(init=False, repr=False, default_factory=dict)
141
+ # _feature_schema: dict[str, type[pl.DataType]] = field(init=False, repr=False, default_factory=dict)
142
+
143
+ def __post_init__(self):
144
+ """Post-initialization."""
145
+ object.__setattr__(self, 'features', self.__class__.get_feature_list(self.features))
146
+
147
+ if isinstance(self.score_model, str):
148
+ object.__setattr__(self, 'score_model', get_score_model(self.score_model))
149
+
150
+ if self.score_model is None:
151
+ object.__setattr__(self, 'score_model', get_score_model())
152
+
153
+ # Validate parameter types
154
+ if not isinstance(self.score_prop_conf, (int, float)):
155
+ raise ValueError(f'score_prop_conf must be numeric: {type(self.score_prop_conf)}')
156
+
157
+ if not isinstance(self.only_features, bool):
158
+ raise ValueError(f'only_features must be boolean: {type(self.only_features)}')
159
+
160
+ if not isinstance(self.force_all, bool):
161
+ raise ValueError(f'force_all must be boolean: {type(self.force_all)}')
162
+
163
+ if not isinstance(self.force_score, bool):
164
+ raise ValueError(f'force_score must be boolean: {type(self.force_score)}')
165
+
166
+ def __call__(
167
+ self,
168
+ df: pl.DataFrame,
169
+ df_qry_fai: Optional[pl.DataFrame] = None
170
+ ) -> pl.DataFrame:
171
+ """Get a table of alignment features.
172
+
173
+ :param df: DataFrame of alignment records.
174
+ :param df_qry_fai: FAI file for query sequences. Needed if features require the query length (i.e. proportion of
175
+ a query in an alignment record).
176
+
177
+ :returns: A DataFrame of alignment features.
178
+
179
+ :raises ValueError: If the requested features could not be generated. Features might be unknown, or the
180
+ alignment table is missing required columns.
181
+ """
182
+ # Set features to compute
183
+ auto_features = False # True if features were extracted from df columns
184
+
185
+ if not self.features:
186
+ features = list(df.columns)
187
+ auto_features = True
188
+ else:
189
+ features = self.features
190
+
191
+ # Compute features
192
+ found_features = set()
193
+ temp_features = set()
194
+
195
+ for feature in features:
196
+
197
+ if (
198
+ feature in df.columns and
199
+ not self.force_all and
200
+ not (
201
+ self.force_score and feature in {'score', 'score_mm'}
202
+ )
203
+ ):
204
+ found_features.add(feature)
205
+ continue
206
+
207
+ try:
208
+ df = self.append_feature(feature, df, df_qry_fai, temp_features)
209
+ found_features.add(feature)
210
+
211
+ except Exception as e:
212
+ raise ValueError(f'Failed to generate feature "{feature}": {e}') from e
213
+
214
+ # Report errors
215
+ if not auto_features and (unknown_features := set(features) - found_features):
216
+ n = len(unknown_features)
217
+ s = ', '.join(sorted(unknown_features)[:3]) + ('...' if n > 3 else '')
218
+ raise ValueError(f'Could not generate {n:,d} unknown features: {s}')
219
+
220
+ # Only return features
221
+ if self.only_features:
222
+ df = df.select(features)
223
+
224
+ # Remove temporary features
225
+ df = df.drop(temp_features - found_features, strict=False)
226
+
227
+ # Return dataframe
228
+ return df
229
+
230
+ def append_feature(
231
+ self,
232
+ feature_name: str,
233
+ df: pl.DataFrame,
234
+ df_qry_fai: Optional[pl.DataFrame],
235
+ temp_features: Optional[set[str]],
236
+ is_temp: bool = False
237
+ ) -> pl.DataFrame:
238
+ """Append a feature to a DataFrame.
239
+
240
+ :param feature_name: Name of the feature.
241
+ :param df: Table of alignment records.
242
+ :param df_qry_fai: Table of sequence lengths.
243
+ :param temp_features: Set to track temporary features. Features marked as temporary will be added to this set.
244
+ :param is_temp: If True, add feature name to temp_features.
245
+
246
+ :returns: A DataFrame with a new or updated feature column.
247
+
248
+ :raises ValueError: If the feature is unknown.
249
+ """
250
+ if feature_name not in _feature_registry:
251
+ raise ValueError(f'Unknown feature: {feature_name}')
252
+
253
+ df = _feature_registry[feature_name](self, df, df_qry_fai, temp_features)
254
+
255
+ if is_temp:
256
+ if temp_features is None:
257
+ raise ValueError('Cannot mark feature as temp: temp_features is None')
258
+ temp_features.add(feature_name)
259
+
260
+ return df
261
+
262
+ def get_schema(self) -> dict[str, type[pl.DataType]]:
263
+ """Get a schema for features in this feature generator.
264
+
265
+ :returns: A dictionary mapping feature names to feature data types.
266
+ """
267
+ return {
268
+ feature: _feature_schema[feature]
269
+ for feature in self.features
270
+ }
271
+
272
+ @classmethod
273
+ def all_feature_schema(cls) -> dict[str, type[pl.DataType]]:
274
+ """Get a schema for all features.
275
+
276
+ :returns: A dictionary mapping feature names to feature data types.
277
+ """
278
+ return _feature_schema.copy()
279
+
280
+ @feature(pl.Float32)
281
+ def score(
282
+ self,
283
+ df: pl.DataFrame
284
+ ) -> pl.DataFrame:
285
+ """Score alignment records.
286
+
287
+ :param df: Table of alignment records.
288
+
289
+ :returns: A table with new or updated feature columns.
290
+ """
291
+ return df.with_columns(
292
+ self.score_model.score_align_table(df)
293
+ .alias('score')
294
+ )
295
+
296
+ @feature(pl.Float32)
297
+ def score_mm(
298
+ self,
299
+ df: pl.DataFrame
300
+ ) -> pl.DataFrame:
301
+ """Score aligned bases in alignment records.
302
+
303
+ Score ignores gaps by using `score_model.mismatch_model()`.
304
+
305
+ :param df: Table of alignment records.
306
+
307
+ :returns: A table with new or updated feature columns.
308
+ """
309
+ return df.with_columns(
310
+ self.score_model.mismatch_model().score_align_table(df)
311
+ .alias('score_mm')
312
+ )
313
+
314
+ @feature(pl.Float32)
315
+ def score_prop(
316
+ self,
317
+ df: pl.DataFrame,
318
+ df_qry_fai: pl.DataFrame,
319
+ temp_features: set[str]
320
+ ) -> pl.DataFrame:
321
+ """Score proportion.
322
+
323
+ Divide the alignment score ("SCORE" column) by the maximum alignment score if all query bases were aligned and
324
+ matched (i.e. score / (match_score * query_length)).
325
+
326
+ :param df: Table of alignment records.
327
+ :param df_qry_fai: Table of sequence lengths.
328
+ :param temp_features: Temporary features generated by this function are added to this set.
329
+
330
+ :returns: A table with new or updated feature columns.
331
+ """
332
+ if 'score' not in df.columns:
333
+ df = self.append_feature('score', df, df_qry_fai, temp_features, is_temp=True)
334
+
335
+ return df.with_columns(
336
+ df
337
+ .select(
338
+ (
339
+ pl.col('score')
340
+ / (
341
+ (
342
+ pl.col('qry_end') - pl.col('qry_pos')
343
+ )
344
+ .fill_null(0.0)
345
+ .map_elements(self.score_model.match, return_dtype=pl.Float64)
346
+ )
347
+ ).cast(pl.Float32)
348
+ )
349
+ .to_series()
350
+ .alias('score_prop')
351
+ )
352
+
353
+ @feature(pl.Float32)
354
+ def score_mm_prop(
355
+ self,
356
+ df: pl.DataFrame,
357
+ df_qry_fai: pl.DataFrame,
358
+ temp_features: set[str]
359
+ ) -> pl.DataFrame:
360
+ """
361
+ Score mismatch proportion.
362
+
363
+ Divide the mismatch alignment score ("score_mm" column, gap penalties ignored) by the maximum alignment score if
364
+ all non-gap query bases were aligned and matched (i.e. score / (match_score * aligned_bases)).
365
+
366
+ :param df: Table of alignment records.
367
+ :param df_qry_fai: Table of sequence lengths.
368
+ :param temp_features: Temporary features generated by this function are added to this set.
369
+
370
+ :returns: A table with new or updated feature columns.
371
+ """
372
+ if 'score_mm' not in df.columns:
373
+ df = self.append_feature('score_mm', df, df_qry_fai, temp_features, is_temp=True)
374
+
375
+ return (
376
+ df.lazy()
377
+ .with_columns(
378
+ pl.col('align_ops').struct.field("op_code").alias('_op_code'),
379
+ pl.col('align_ops').struct.field("op_len").alias('_op_len')
380
+ )
381
+ .with_columns(
382
+ (
383
+ (
384
+ pl.col('score_mm')
385
+ / (
386
+ (
387
+ pl.col("_op_len") * (
388
+ pl.col("_op_code").list.eval(pl.element().is_in([op.EQ, op.X]))
389
+ )
390
+ )
391
+ .list.sum()
392
+ .map_elements(self.score_model.match, return_dtype=pl.Float64)
393
+ )
394
+ ).cast(pl.Float32)
395
+ )
396
+ .alias('score_mm_prop')
397
+ )
398
+ .drop(['_op_code', '_op_len'])
399
+ .collect()
400
+ )
401
+
402
+ @feature(pl.Float32)
403
+ def match_prop(
404
+ self,
405
+ df: pl.DataFrame
406
+ ) -> pl.DataFrame:
407
+ """Score proportion of maximum.
408
+
409
+ Compute the proportion of matching bases over aligned bases (i.e. EQ / (X + EQ)).
410
+
411
+ The match proportion is defined as the number of matches divided by the number of aligned bases. Unlike
412
+ the match score proportion, this match proportion is not based on an alignment score model where matches and
413
+ mismatches are typically weighted differently.
414
+
415
+ :param df: Table of alignment records.
416
+
417
+ :returns: A table with new or updated feature columns.
418
+ """
419
+ return (
420
+ df.lazy()
421
+ .with_columns(
422
+ pl.col('align_ops').struct.field("op_code").alias('_op_code'),
423
+ pl.col('align_ops').struct.field("op_len").alias('_op_len')
424
+ )
425
+ .with_columns(
426
+ (
427
+ (
428
+ (
429
+ pl.col("_op_len") * (
430
+ pl.col("_op_code").list.eval(pl.element() == op.EQ)
431
+ )
432
+ )
433
+ .list.sum()
434
+ ) / (
435
+ (
436
+ pl.col("_op_len") * (
437
+ pl.col("_op_code").list.eval(pl.element().is_in([op.EQ, op.X]))
438
+ )
439
+ )
440
+ .list.sum()
441
+ )
442
+ ).alias('match_prop')
443
+ )
444
+ .drop(['_op_code', '_op_len'])
445
+ .collect()
446
+ )
447
+
448
+ @feature(pl.Float32)
449
+ def anchor_prop(
450
+ self,
451
+ df: pl.DataFrame
452
+ ) -> pl.DataFrame:
453
+ """Anchor proportion.
454
+
455
+ Determine if an alignment record is between high-confidence alignment records along the query sequence. Values
456
+ are reported as 0.0 (no confident alignment on the query sequence), 0.5 (at least confident alignment upstream
457
+ or downstream, but not both), and 1.0 (at least one confident alignment both upstream and downstream).
458
+
459
+ :param df: Table of alignment records.
460
+
461
+ :returns: A table with new or updated feature columns.
462
+ """
463
+ df_qry_min_max = (
464
+ df
465
+ .filter(
466
+ pl.col('score_prop') >= self.score_prop_conf
467
+ )
468
+ .group_by('qry_id')
469
+ .agg(
470
+ qry_min=(
471
+ pl.col('qry_order').min()
472
+ ),
473
+ qry_max=(
474
+ pl.col('qry_order').max()
475
+ )
476
+ )
477
+ )
478
+
479
+ return df.with_columns(
480
+ df.join(
481
+ df_qry_min_max,
482
+ on='qry_id',
483
+ how='left'
484
+ )
485
+ .select(
486
+ (
487
+ (
488
+ (
489
+ (pl.col('qry_order') > pl.col('qry_min')).fill_null(False)
490
+ ) + (
491
+ (pl.col('qry_order') < pl.col('qry_max')).fill_null(False)
492
+ )
493
+ ) / 2.0
494
+ )
495
+ )
496
+ .to_series()
497
+ .alias('anchor_prop')
498
+ )
499
+
500
+ @feature(pl.Float32)
501
+ def qry_prop(
502
+ self,
503
+ df: pl.DataFrame,
504
+ df_qry_fai: pl.DataFrame
505
+ ) -> pl.DataFrame:
506
+ """Get the proportion of the query sequence aligned in this record.
507
+
508
+ :param df: Table of alignment records.
509
+ :param df_qry_fai: Table of sequence lengths.
510
+
511
+ :returns: A table with new or updated feature columns.
512
+ """
513
+ return df.with_columns(
514
+ df
515
+ .join(
516
+ df_qry_fai.rename({'chrom': 'qry_id', 'len': 'qry_len'}, strict=False),
517
+ on='qry_id', how='left'
518
+ )
519
+ .select(
520
+ (pl.col('qry_end') - pl.col('qry_pos')) / pl.col('qry_len')
521
+ )
522
+ .to_series()
523
+ .alias('qry_prop')
524
+ )
525
+
526
+ @classmethod
527
+ def get_feature_list(
528
+ cls,
529
+ features: Iterable[str] | str | None
530
+ ) -> tuple[str, ...]:
531
+ """Get a list of features from a feature argument.
532
+
533
+ If features is an iterable, it is converted to a tuple. If features is a string and matches a feature keyword,
534
+ then a pre-configured feature tuple is returned. If None, then the alignment table default columns are returned.
535
+
536
+ Feature keywords:
537
+ all: All known features.
538
+ align: Default columns of a PAV alignment table.
539
+
540
+ :param features: Features to compute.
541
+
542
+ :returns: List of feature names.
543
+ """
544
+ if features is None:
545
+ return ALIGN_TABLE_COLUMNS
546
+
547
+ elif isinstance(features, str):
548
+ if features == 'all':
549
+ features = cls.all_features()
550
+ elif features == 'align':
551
+ features = ALIGN_TABLE_COLUMNS
552
+ else:
553
+ features = tuple(features.split(','))
554
+
555
+ try:
556
+ features = tuple(features)
557
+ except TypeError as e:
558
+ raise ValueError(f'Unrecognized feature type, expected string or iterable: {type(features)}') from e
559
+
560
+ if unknown_features := set(features) - set(cls.all_features()):
561
+ raise ValueError(f'Unrecognized feature(s): {", ".join(sorted(unknown_features))}')
562
+
563
+ return features
564
+
565
+ @staticmethod
566
+ def all_features() -> tuple[str, ...]:
567
+ """Get all available features.
568
+
569
+ :returns: List of feature names.
570
+ """
571
+ return tuple(_feature_registry.keys())
@@ -0,0 +1,30 @@
1
+ """Low-confidence (LC) alignment prediction.
2
+
3
+ LC models examine alignment features and make a confidence prediction. The built-in logistic model,
4
+ :class:`LCAlignModelLogistic`, uses logistic regression on a list af alignment features. The built-in null model,
5
+ :class:LCAlignModelNull`, does not predict any LC alignments.
6
+ """
7
+
8
+ # TODO: Add feature scaling
9
+
10
+ __all__ = [
11
+ 'LCAlignModel',
12
+ 'LCAlignModelLogistic',
13
+ 'LCAlignModelNull',
14
+ 'get_model',
15
+ 'locate_config_filesystem',
16
+ 'locate_config_package',
17
+ 'locate_model',
18
+ 'null_model',
19
+ ]
20
+
21
+ from ._lcmodel import LCAlignModel
22
+ from ._lcmodel_logistic import LCAlignModelLogistic
23
+ from ._lcmodel_null import LCAlignModelNull
24
+ from ._util import (
25
+ get_model,
26
+ locate_config_filesystem,
27
+ locate_config_package,
28
+ locate_model,
29
+ null_model
30
+ )