pav3 3.0.0.dev18__tar.gz → 3.0.0.dev20__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 (99) hide show
  1. {pav3-3.0.0.dev18/src/pav3.egg-info → pav3-3.0.0.dev20}/PKG-INFO +4 -1
  2. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/README.md +3 -0
  3. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/__init__.py +1 -1
  4. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/align/features.py +1 -1
  5. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/align/score.py +145 -66
  6. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/data/workflow/rules/align.smk +1 -2
  7. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/data/workflow/rules/call.smk +17 -294
  8. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/fig/tracks.py +2 -1
  9. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/lgsv/call.py +31 -12
  10. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/lgsv/interval.py +181 -22
  11. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/lgsv/variant.py +166 -132
  12. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/params.py +0 -8
  13. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/vcf.py +150 -77
  14. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/workflow/__pycache__/__init__.cpython-312.pyc +0 -0
  15. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/workflow/__pycache__/call.cpython-312.pyc +0 -0
  16. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/workflow/__pycache__/resource.cpython-312.pyc +0 -0
  17. pav3-3.0.0.dev20/src/pav3/workflow/call.py +476 -0
  18. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20/src/pav3.egg-info}/PKG-INFO +4 -1
  19. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3.egg-info/SOURCES.txt +2 -1
  20. pav3-3.0.0.dev20/tests/test_score_dtype.py +24 -0
  21. pav3-3.0.0.dev18/src/pav3/workflow/call.py +0 -184
  22. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/LICENSE +0 -0
  23. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/pyproject.toml +0 -0
  24. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/setup.cfg +0 -0
  25. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/__main__.py +0 -0
  26. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/align/__init__.py +0 -0
  27. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/align/lcmodel/__init__.py +0 -0
  28. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/align/lcmodel/_lcmodel.py +0 -0
  29. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/align/lcmodel/_lcmodel_logistic.py +0 -0
  30. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/align/lcmodel/_lcmodel_null.py +0 -0
  31. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/align/lcmodel/_util.py +0 -0
  32. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/align/lift.py +0 -0
  33. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/align/op.py +0 -0
  34. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/align/records.py +0 -0
  35. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/align/tables.py +0 -0
  36. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/align/trim.py +0 -0
  37. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/anno.py +0 -0
  38. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/call/__init__.py +0 -0
  39. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/call/expr.py +0 -0
  40. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/call/integrate.py +0 -0
  41. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/call/intra.py +0 -0
  42. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/call/util.py +0 -0
  43. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/cli/__init__.py +0 -0
  44. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/cli/_cli.py +0 -0
  45. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/cli/_common_opt.py +0 -0
  46. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/cli/_subcommand_batch.py +0 -0
  47. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/cli/_subcommand_call.py +0 -0
  48. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/cli/_subcommand_license.py +0 -0
  49. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/const.py +0 -0
  50. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/data/lcmodel/default/model.json +0 -0
  51. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/data/lcmodel/human_95_80/model.json +0 -0
  52. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/data/lcmodel/human_95_80/model_info.tar.gz +0 -0
  53. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/data/lcmodel/human_95_80/weights.npz +0 -0
  54. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/data/lcmodel/human_98_80/model.json +0 -0
  55. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/data/lcmodel/human_98_80/model_info.tar.gz +0 -0
  56. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/data/lcmodel/human_98_80/weights.npz +0 -0
  57. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/data/lcmodel/prim_90_50/model.json +0 -0
  58. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/data/lcmodel/prim_90_50/model_info.tar.gz +0 -0
  59. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/data/lcmodel/prim_90_50/weights.npz +0 -0
  60. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/data/lcmodel/primdiv_85_50/model.json +0 -0
  61. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/data/lcmodel/primdiv_85_50/model_info.tar.gz +0 -0
  62. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/data/lcmodel/primdiv_85_50/weights.npz +0 -0
  63. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/data/tracks/alignment_track_fields.tsv +0 -0
  64. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/data/tracks/variant_track_fields.tsv +0 -0
  65. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/data/workflow/Snakefile +0 -0
  66. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/data/workflow/profiles/pav_default/config.yaml +0 -0
  67. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/data/workflow/rules/data.smk +0 -0
  68. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/data/workflow/rules/pipeline.smk +0 -0
  69. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/data/workflow/rules/tracks.smk +0 -0
  70. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/dev/__init__.py +0 -0
  71. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/dev/imports.py +0 -0
  72. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/dev/inter.py +0 -0
  73. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/dev/path.py +0 -0
  74. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/dev/sm.py +0 -0
  75. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/dev/tables.py +0 -0
  76. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/fig/__init__.py +0 -0
  77. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/fig/const.py +0 -0
  78. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/fig/density.py +0 -0
  79. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/fig/util.py +0 -0
  80. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/inv.py +0 -0
  81. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/io.py +0 -0
  82. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/kde.py +0 -0
  83. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/lgsv/__init__.py +0 -0
  84. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/lgsv/chain.py +0 -0
  85. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/lgsv/io.py +0 -0
  86. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/lgsv/region_kde.py +0 -0
  87. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/lgsv/resources.py +0 -0
  88. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/lgsv/struct.py +0 -0
  89. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/pipeline.py +0 -0
  90. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/region.py +0 -0
  91. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/schema.py +0 -0
  92. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/seq.py +0 -0
  93. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/util.py +0 -0
  94. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/workflow/__init__.py +0 -0
  95. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3/workflow/resource.py +0 -0
  96. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3.egg-info/dependency_links.txt +0 -0
  97. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3.egg-info/entry_points.txt +0 -0
  98. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3.egg-info/requires.txt +0 -0
  99. {pav3-3.0.0.dev18 → pav3-3.0.0.dev20}/src/pav3.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pav3
3
- Version: 3.0.0.dev18
3
+ Version: 3.0.0.dev20
4
4
  Summary: Assembly-based variant discovery
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.12
@@ -31,6 +31,9 @@ Dynamic: license-file
31
31
 
32
32
  Variant caller for assembled genomes.
33
33
 
34
+ Please discuss with authors prior to publishing results using PAV 3. A paper describing PAV 3 is in
35
+ preparation and is expected mid-2026.
36
+
34
37
  ## Development release (Please read)
35
38
 
36
39
  PAV 3 is currently a development release and may contain bugs. Please report problems you encounter.
@@ -6,6 +6,9 @@
6
6
 
7
7
  Variant caller for assembled genomes.
8
8
 
9
+ Please discuss with authors prior to publishing results using PAV 3. A paper describing PAV 3 is in
10
+ preparation and is expected mid-2026.
11
+
9
12
  ## Development release (Please read)
10
13
 
11
14
  PAV 3 is currently a development release and may contain bugs. Please report problems you encounter.
@@ -1,6 +1,6 @@
1
1
  """PAV Python library routines."""
2
2
 
3
- __version__ = '3.0.0.dev18'
3
+ __version__ = '3.0.0.dev20'
4
4
 
5
5
  __all__ = [
6
6
  'align',
@@ -59,7 +59,7 @@ def feature(dtype: type[pl.DataType], name: str = None) -> Callable:
59
59
  self,
60
60
  df: pl.DataFrame,
61
61
  df_qry_fai: Optional[pl.DataFrame] = None,
62
- temp_features: Optional[set[str]] = None
62
+ temp_features: Optional[set[str]] = None,
63
63
  ) -> pl.DataFrame:
64
64
  """Wrap feature functions with a consistent signature.
65
65
 
@@ -12,7 +12,6 @@ __all__ = [
12
12
  'get_affine_by_params',
13
13
  ]
14
14
 
15
- from dataclasses import dataclass, field
16
15
  from abc import ABC, abstractmethod
17
16
  import re
18
17
  from typing import Any, Optional
@@ -20,6 +19,8 @@ from typing import Any, Optional
20
19
  import numpy as np
21
20
  import polars as pl
22
21
 
22
+ from agglovar.meta.decorators import immutable
23
+
23
24
  from . import op
24
25
 
25
26
  AFFINE_SCORE_MATCH: float = 2.0
@@ -29,16 +30,29 @@ AFFINE_SCORE_MISMATCH: float = 4.0
29
30
  """Mismatch score (minimap2 default)"""
30
31
 
31
32
  AFFINE_SCORE_GAP: tuple[tuple[float, float], ...] = ((4.0, 2.0), (24.0, 1.0))
32
- """Multi-affine gap scores as a sequence of gap-open and gap-extend penalties (minimap2 defaults)."""
33
+ """Multi-affine scores as a sequence of gap-open and gap-extend penalties (minimap2 defaults)."""
34
+
35
+ AFFINE_SCORE_TS = (1.0, 50)
36
+ """Template switch score.
37
+
38
+ May be a single float (penalty of a template switch) or a tuple of a multiplier and a gap size
39
+ (e.g. "(1.0, 50)" sets the template switch score to the cost of a 50 bp gap).
40
+ """
33
41
 
34
- AFFINE_SCORE_TS = None
35
- """Template switch score. If None, defaults to 2x the penalty of a 50 bp gap."""
42
+ AFFINE_OFF_VARIANT_PENALTY = 2.0
43
+ """Multiplier for off-variant penalties when scoring.
44
+
45
+ For example, the gap score of query (inserted) bases for a deletion would be multiplied by this
46
+ factor. Used for picking the right variant call type for a set of alignment features.
47
+ """
36
48
 
37
49
  DEFAULT_ALIGN_SCORE_MODEL = (
38
50
  f'affine::'
39
51
  f'match={AFFINE_SCORE_MATCH},'
40
52
  f'mismatch={AFFINE_SCORE_MISMATCH},'
41
- f'gap={";".join([f"{gap_open}:{gap_extend}" for gap_open, gap_extend in AFFINE_SCORE_GAP])}'
53
+ f'gap={";".join([f"{gap_open}:{gap_extend}" for gap_open, gap_extend in AFFINE_SCORE_GAP])},'
54
+ f'ts={str(AFFINE_SCORE_TS).replace(',', ':')},'
55
+ f'off_var={AFFINE_OFF_VARIANT_PENALTY}'
42
56
  )
43
57
  """Default alignment score model specification."""
44
58
 
@@ -54,7 +68,7 @@ class ScoreModel(ABC):
54
68
 
55
69
  :returns: Match score.
56
70
  """
57
- pass
71
+ ...
58
72
 
59
73
  @abstractmethod
60
74
  def mismatch(self, n: int = 1) -> float:
@@ -64,7 +78,7 @@ class ScoreModel(ABC):
64
78
 
65
79
  :returns: Mismatch score.
66
80
  """
67
- pass
81
+ ...
68
82
 
69
83
  @abstractmethod
70
84
  def gap(self, n: int = 1) -> float:
@@ -74,15 +88,26 @@ class ScoreModel(ABC):
74
88
 
75
89
  :returns: Gap score for one operation.
76
90
  """
77
- pass
91
+ ...
78
92
 
79
93
  @abstractmethod
80
- def template_switch(self) -> float:
94
+ def template_switch(self, n: int = 1) -> float:
81
95
  """Score a template switch.
82
96
 
83
97
  :returns: Template switch score.
84
98
  """
85
- pass
99
+ ...
100
+
101
+ @abstractmethod
102
+ def off_variant(self) -> float:
103
+ """Multiplier for off-variant features.
104
+
105
+ For scored features that are incompatible with a variant type, for example reference gaps
106
+ at an insertion site, multiply the gap score by this factor.
107
+
108
+ :returns: Multiplier for off-variant features.
109
+ """
110
+ ...
86
111
 
87
112
  def score_op(self, op_code: int | str, op_len: int) -> float:
88
113
  """Score one alignment operation.
@@ -138,7 +163,7 @@ class ScoreModel(ABC):
138
163
  .select(pl.col('align_ops'))
139
164
  .to_series()
140
165
  .map_elements(op.row_to_arr, return_dtype=pl.Object)
141
- .map_elements(self.score_op_arr, return_dtype=pl.Float32)
166
+ .map_elements(lambda op_arr: np.float32(self.score_op_arr(op_arr)), return_dtype=pl.Float32)
142
167
  )
143
168
 
144
169
  # A pure polars implementation is slower:
@@ -163,7 +188,7 @@ class ScoreModel(ABC):
163
188
 
164
189
  :returns A copy of this score model that does not penalize gaps.
165
190
  """
166
- pass
191
+ ...
167
192
 
168
193
  @abstractmethod
169
194
  def model_param_string(self) -> str:
@@ -171,19 +196,19 @@ class ScoreModel(ABC):
171
196
 
172
197
  :returns: A parameter string for this score model.
173
198
  """
174
- pass
199
+ ...
175
200
 
176
201
  @abstractmethod
177
202
  def __eq__(self, other: 'ScoreModel') -> bool:
178
203
  """Check equality with another :class:`ScoreModel`."""
179
- pass
204
+ ...
180
205
 
181
206
  def __repr__(self) -> str:
182
207
  """Get a string representation of the model."""
183
208
  return 'ScoreModel(Interface)'
184
209
 
185
210
 
186
- @dataclass(frozen=True)
211
+ @immutable
187
212
  class AffineScoreModel(ScoreModel):
188
213
  """Affine score model with default values modeled on minimap2 (2.26) default parameters.
189
214
 
@@ -193,24 +218,42 @@ class AffineScoreModel(ScoreModel):
193
218
  :param score_template_switch: Template switch penalty. If none, defaults to 2x the penalty of a 50 bp gap.
194
219
  """
195
220
 
196
- score_match: float = field(default=AFFINE_SCORE_MATCH)
197
- score_mismatch: float = field(default=AFFINE_SCORE_MISMATCH)
198
- score_affine_gap: tuple[tuple[float, float], ...] = field(default=AFFINE_SCORE_GAP)
199
- score_template_switch: float | None = field(default=AFFINE_SCORE_TS)
200
-
201
- def __post_init__(self):
202
- """Ensure penalties are set correctly."""
203
- object.__setattr__(self, 'score_match', float(abs(self.score_match)))
204
- object.__setattr__(self, 'score_mismatch', float(-abs(self.score_mismatch)))
205
- object.__setattr__(self, 'score_affine_gap', tuple((
206
- (float(-abs(gap_open)), float(-abs(gap_extend)))
207
- for gap_open, gap_extend in self.score_affine_gap
208
- )))
221
+ score_match: float
222
+ score_mismatch: float
223
+ score_affine_gap: tuple[tuple[float, float], ...]
224
+ score_template_switch: float
225
+ mul_off_variant: float
209
226
 
210
- if self.score_template_switch is None:
211
- object.__setattr__(self, 'score_template_switch', 2 * self.gap(50))
227
+ def __init__(
228
+ self,
229
+ score_match: float = AFFINE_SCORE_MATCH,
230
+ score_mismatch: float = AFFINE_SCORE_MISMATCH,
231
+ score_affine_gap: tuple[tuple[float, float], ...] = AFFINE_SCORE_GAP,
232
+ score_template_switch: float | tuple[float, int] = AFFINE_SCORE_TS,
233
+ mul_off_variant: float = AFFINE_OFF_VARIANT_PENALTY,
234
+ ):
235
+ self.score_match = abs(float(score_match))
236
+ self.score_mismatch = -abs(float(score_mismatch))
237
+ self.score_affine_gap = tuple(
238
+ (-abs(float(gap_open)), -abs(float(gap_extend)))
239
+ for gap_open, gap_extend in score_affine_gap
240
+ )
241
+
242
+ if isinstance(score_template_switch, tuple):
243
+ if len(score_template_switch) != 2:
244
+ raise ValueError(f'Template switch score tuple must be 2 elements (multiplier, gap size): {score_template_switch}')
245
+
246
+ self.score_template_switch = (
247
+ abs(float(score_template_switch[0]))
248
+ * self.gap(abs(int(score_template_switch[1])))
249
+ )
212
250
  else:
213
- object.__setattr__(self, 'score_template_switch', float(-abs(self.score_template_switch)))
251
+ self.score_template_switch = -abs(float(score_template_switch))
252
+
253
+ self.mul_off_variant = abs(float(mul_off_variant))
254
+
255
+ if self.mul_off_variant <= 1.0:
256
+ raise ValueError(f'Off-target variant multiplier must be > 1.0: {self.mul_off_variant}')
214
257
 
215
258
  def match(self, n: int = 1) -> float:
216
259
  """Score match.
@@ -247,12 +290,24 @@ class AffineScoreModel(ScoreModel):
247
290
  for gap_open, gap_extend in self.score_affine_gap
248
291
  ])
249
292
 
250
- def template_switch(self) -> float:
293
+ def template_switch(self, n: int = 1) -> float:
251
294
  """Score a template switch.
252
295
 
296
+ :param n: Number of template switches.
297
+
253
298
  :returns: Template switch score.
254
299
  """
255
- return self.score_template_switch
300
+ return self.score_template_switch * n
301
+
302
+ def off_variant(self) -> float:
303
+ """Multiplier for off-variant features.
304
+
305
+ For scored features that are incompatible with a variant type, for example reference gaps
306
+ at an insertion site, multiply the gap score by this factor.
307
+
308
+ :returns: Multiplier for off-variant features.
309
+ """
310
+ return self.mul_off_variant
256
311
 
257
312
  def mismatch_model(self) -> 'ScoreModel':
258
313
  """Create a version of this model that scores only mismatches (ignores gaps).
@@ -266,7 +321,8 @@ class AffineScoreModel(ScoreModel):
266
321
  score_match=self.score_match,
267
322
  score_mismatch=self.score_mismatch,
268
323
  score_affine_gap=((0.0, 0.0),),
269
- score_template_switch=0.0
324
+ score_template_switch=self.score_template_switch,
325
+ mul_off_variant=self.mul_off_variant,
270
326
  )
271
327
 
272
328
  def score_operations(
@@ -292,10 +348,9 @@ class AffineScoreModel(ScoreModel):
292
348
  gap_score[:, 0] = np.max(gap_score, axis=1)
293
349
 
294
350
  return (
295
- np.sum(op_arr[:, 1] * (op_arr[:, 0] == op.EQ) * self.score_match) +
296
- np.sum(op_arr[:, 1] * (op_arr[:, 0] == op.X) * self.score_mismatch) +
297
- # If no gap penalties (i.e. mismatch model), then gap_score is -inf (set to 0.0)
298
- np.nan_to_num(gap_score[:, 0], neginf=0.0).sum()
351
+ np.sum(op_arr[:, 1] * (op_arr[:, 0] == op.EQ) * self.score_match)
352
+ + np.sum(op_arr[:, 1] * (op_arr[:, 0] == op.X) * self.score_mismatch)
353
+ + np.nan_to_num(gap_score[:, 0], neginf=0.0).sum() # If no gap penalties (i.e. mismatch model), then gap_score is -inf (set to 0.0)
299
354
  )
300
355
 
301
356
  def model_param_string(self) -> str:
@@ -305,7 +360,14 @@ class AffineScoreModel(ScoreModel):
305
360
  """
306
361
  gap_str = ";".join([f"{gap_open}:{gap_extend}" for gap_open, gap_extend in self.score_affine_gap])
307
362
 
308
- return f'affine::match={self.score_match},mismatch={self.score_mismatch},gap={gap_str}'
363
+ return (
364
+ f'affine::'
365
+ f'match={self.score_match},'
366
+ f'mismatch={self.score_mismatch},'
367
+ f'gap={gap_str},'
368
+ f'ts={self.score_template_switch},'
369
+ f'off_var={self.mul_off_variant}'
370
+ )
309
371
 
310
372
  def __eq__(self, other: 'ScoreModel') -> bool:
311
373
  """Check equality with another :class:`ScoreModel`."""
@@ -370,14 +432,23 @@ def get_affine_by_params(param_string: str) -> AffineScoreModel:
370
432
  :raises ValueError: If the string cannot be parsed.
371
433
  """
372
434
  # Set defaults
373
- params: list[Any] = [
374
- AFFINE_SCORE_MATCH,
375
- AFFINE_SCORE_MISMATCH,
376
- AFFINE_SCORE_GAP,
377
- AFFINE_SCORE_TS
378
- ]
379
-
380
- keys = ['match', 'mismatch', 'gap', 'ts']
435
+ params: dict[str, Any] = {
436
+ 'score_match': AFFINE_SCORE_MATCH,
437
+ 'score_mismatch': AFFINE_SCORE_MISMATCH,
438
+ 'score_affine_gap': AFFINE_SCORE_GAP,
439
+ 'score_template_switch': AFFINE_SCORE_TS,
440
+ 'mul_off_variant': AFFINE_OFF_VARIANT_PENALTY,
441
+ }
442
+
443
+ key_to_param = {
444
+ 'match': 'score_match',
445
+ 'mismatch': 'score_mismatch',
446
+ 'gap': 'score_affine_gap',
447
+ 'ts': 'score_template_switch',
448
+ 'off_var': 'mul_off_variant',
449
+ }
450
+
451
+ keys = list(key_to_param.keys())
381
452
 
382
453
  # Sanitize parameter string
383
454
  if param_string is not None:
@@ -422,19 +493,15 @@ def get_affine_by_params(param_string: str) -> AffineScoreModel:
422
493
 
423
494
  param_pos += 1
424
495
 
425
- if key in {'match', 'mismatch', 'ts'}:
426
- try:
427
- val = abs(float(val))
428
- except ValueError as e:
429
- raise ValueError(f'Invalid numeric value for parameter "{key}": {val}') from e
430
-
431
- if key == 'match':
432
- params[0] = val
496
+ if not val:
497
+ continue
433
498
 
434
- elif key == 'mismatch':
435
- params[1] = val
499
+ try:
500
+ param_name = key_to_param[key]
501
+ except KeyError:
502
+ raise ValueError(f'Unrecognized parameter key: {key}')
436
503
 
437
- elif key == 'gap':
504
+ if key == 'gap':
438
505
  gap_list = []
439
506
 
440
507
  for gap_pair in val.split(';'):
@@ -454,18 +521,30 @@ def get_affine_by_params(param_string: str) -> AffineScoreModel:
454
521
 
455
522
  gap_list.append((gap_open, gap_extend))
456
523
 
457
- params[2] = tuple(gap_list)
524
+ val = tuple(gap_list)
458
525
 
459
- elif key == 'ts':
460
- params[3] = val
526
+ elif key == 'ts' and val[0] == '(' and val[-1] == ')':
527
+ ts_tuple = val[1:-1].split(':')
528
+
529
+ if len(ts_tuple) != 2:
530
+ raise ValueError(f'Key "ts" requires a set value or a tuple[float, int]: Found {val}')
531
+
532
+ val = (
533
+ float(ts_tuple[0]),
534
+ int(ts_tuple[1]),
535
+ )
461
536
 
462
537
  else:
463
- raise ValueError(f'Unrecognized alignment parameter: {key} (allowed: match, mismatch, gap, ts)')
538
+ try:
539
+ val = abs(float(val))
540
+ except ValueError as e:
541
+ raise ValueError(f'Invalid numeric value for parameter "{key}": {val}') from e
542
+
543
+
544
+ params[param_name] = val
545
+
464
546
 
465
547
  # Return alignment object
466
548
  return AffineScoreModel(
467
- score_match=params[0],
468
- score_mismatch=params[1],
469
- score_affine_gap=params[2],
470
- score_template_switch=params[3]
549
+ **params
471
550
  )
@@ -14,7 +14,6 @@ import os
14
14
 
15
15
  import agglovar
16
16
  import pav3
17
- import snakemake.io
18
17
  import polars as pl
19
18
 
20
19
  global PAV_CONFIG
@@ -35,7 +34,7 @@ global REF_FAI
35
34
  #
36
35
 
37
36
  def align_index_files(
38
- wildcards: snakemake.io.Namedlist | dict[str, str]
37
+ wildcards: dict[str, str]
39
38
  ):
40
39
  """
41
40
  Get a list of reference index files needed by an aligner.