pav3 3.0.0.dev22__tar.gz → 3.0.0.dev23__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 (107) hide show
  1. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/PKG-INFO +2 -2
  2. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/pyproject.toml +2 -1
  3. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/__init__.py +1 -1
  4. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/data/workflow/Snakefile +1 -1
  5. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/data/workflow/rules/call.smk +2 -2
  6. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/data/workflow/rules/data.smk +1 -2
  7. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/dev/path.py +11 -3
  8. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/pipeline.py +94 -19
  9. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/workflow/call.py +64 -4
  10. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3.egg-info/PKG-INFO +2 -2
  11. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3.egg-info/requires.txt +1 -1
  12. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/CHANGELOG.md +0 -0
  13. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/CODE_OF_CONDUCT.md +0 -0
  14. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/CONTRIBUTING.md +0 -0
  15. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/LICENSE +0 -0
  16. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/MANIFEST.in +0 -0
  17. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/README.md +0 -0
  18. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/setup.cfg +0 -0
  19. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/__main__.py +0 -0
  20. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/align/__init__.py +0 -0
  21. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/align/features.py +0 -0
  22. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/align/lcmodel/__init__.py +0 -0
  23. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/align/lcmodel/_lcmodel.py +0 -0
  24. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/align/lcmodel/_lcmodel_logistic.py +0 -0
  25. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/align/lcmodel/_lcmodel_null.py +0 -0
  26. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/align/lcmodel/_util.py +0 -0
  27. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/align/lift.py +0 -0
  28. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/align/op.py +0 -0
  29. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/align/records.py +0 -0
  30. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/align/score.py +0 -0
  31. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/align/tables.py +0 -0
  32. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/align/trim.py +0 -0
  33. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/anno.py +0 -0
  34. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/call/__init__.py +0 -0
  35. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/call/expr.py +0 -0
  36. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/call/integrate.py +0 -0
  37. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/call/intra.py +0 -0
  38. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/call/util.py +0 -0
  39. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/cli/__init__.py +0 -0
  40. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/cli/_cli.py +0 -0
  41. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/cli/_common_opt.py +0 -0
  42. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/cli/_subcommand_batch.py +0 -0
  43. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/cli/_subcommand_call.py +0 -0
  44. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/cli/_subcommand_license.py +0 -0
  45. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/const.py +0 -0
  46. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/data/lcmodel/default/model.json +0 -0
  47. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/data/lcmodel/human_95_80/model.json +0 -0
  48. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/data/lcmodel/human_95_80/model_info.tar.gz +0 -0
  49. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/data/lcmodel/human_95_80/weights.npz +0 -0
  50. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/data/lcmodel/human_98_80/model.json +0 -0
  51. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/data/lcmodel/human_98_80/model_info.tar.gz +0 -0
  52. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/data/lcmodel/human_98_80/weights.npz +0 -0
  53. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/data/lcmodel/prim_90_50/model.json +0 -0
  54. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/data/lcmodel/prim_90_50/model_info.tar.gz +0 -0
  55. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/data/lcmodel/prim_90_50/weights.npz +0 -0
  56. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/data/lcmodel/primdiv_85_50/model.json +0 -0
  57. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/data/lcmodel/primdiv_85_50/model_info.tar.gz +0 -0
  58. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/data/lcmodel/primdiv_85_50/weights.npz +0 -0
  59. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/data/tracks/alignment_track_fields.tsv +0 -0
  60. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/data/tracks/variant_track_fields.tsv +0 -0
  61. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/data/workflow/profiles/pav_default/config.yaml +0 -0
  62. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/data/workflow/rules/align.smk +0 -0
  63. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/data/workflow/rules/pipeline.smk +0 -0
  64. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/data/workflow/rules/tracks.smk +0 -0
  65. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/dev/__init__.py +0 -0
  66. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/dev/imports.py +0 -0
  67. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/dev/inter.py +0 -0
  68. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/dev/sm.py +0 -0
  69. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/dev/tables.py +0 -0
  70. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/fig/__init__.py +0 -0
  71. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/fig/const.py +0 -0
  72. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/fig/density.py +0 -0
  73. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/fig/tracks.py +0 -0
  74. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/fig/util.py +0 -0
  75. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/inv.py +0 -0
  76. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/io.py +0 -0
  77. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/kde.py +0 -0
  78. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/lgsv/__init__.py +0 -0
  79. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/lgsv/call.py +0 -0
  80. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/lgsv/chain.py +0 -0
  81. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/lgsv/interval.py +0 -0
  82. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/lgsv/io.py +0 -0
  83. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/lgsv/region_kde.py +0 -0
  84. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/lgsv/resources.py +0 -0
  85. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/lgsv/struct.py +0 -0
  86. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/lgsv/variant.py +0 -0
  87. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/params.py +0 -0
  88. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/region.py +0 -0
  89. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/schema.py +0 -0
  90. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/seq.py +0 -0
  91. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/util.py +0 -0
  92. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/vcf.py +0 -0
  93. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/workflow/__init__.py +0 -0
  94. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3/workflow/resource.py +0 -0
  95. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3.egg-info/SOURCES.txt +0 -0
  96. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3.egg-info/dependency_links.txt +0 -0
  97. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3.egg-info/entry_points.txt +0 -0
  98. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/src/pav3.egg-info/top_level.txt +0 -0
  99. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/tests/TESTING_TODO.md +0 -0
  100. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/tests/align/.gitkeep +0 -0
  101. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/tests/align/test_score.py +0 -0
  102. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/tests/align/test_tables.py +0 -0
  103. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/tests/call/.gitkeep +0 -0
  104. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/tests/cli/.gitkeep +0 -0
  105. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/tests/conftest.py +0 -0
  106. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/tests/lgsv/.gitkeep +0 -0
  107. {pav3-3.0.0.dev22 → pav3-3.0.0.dev23}/tox.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pav3
3
- Version: 3.0.0.dev22
3
+ Version: 3.0.0.dev23
4
4
  Summary: Assembly-based variant discovery
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.12
@@ -13,7 +13,7 @@ Requires-Dist: frozendict>=2.4.6
13
13
  Requires-Dist: hmmlearn>=0.3.3
14
14
  Requires-Dist: intervaltree>=3.1.0
15
15
  Requires-Dist: numpy>=2.3.2
16
- Requires-Dist: polars[numpy,pandas,pyarrow]>=1.35.2
16
+ Requires-Dist: polars[numpy,pandas,pyarrow]>=1.41.0
17
17
  Requires-Dist: pyarrow>=21.0.0
18
18
  Requires-Dist: pysam>=0.23.3
19
19
  Requires-Dist: scipy>=1.16.1
@@ -18,7 +18,7 @@ dependencies = [
18
18
  "hmmlearn>=0.3.3",
19
19
  "intervaltree>=3.1.0",
20
20
  "numpy>=2.3.2",
21
- "polars[numpy,pandas,pyarrow]>=1.35.2",
21
+ "polars[numpy,pandas,pyarrow]>=1.41.0",
22
22
  "pyarrow>=21.0.0",
23
23
  "pysam>=0.23.3",
24
24
  "scipy>=1.16.1",
@@ -80,6 +80,7 @@ dev = [
80
80
  "flake8-rst-docstrings>=0.3.1",
81
81
  "ipython>=9.7.0",
82
82
  "ply>=3.11",
83
+ "pygithub>=2.9.1",
83
84
  "pylint>=3.3.7",
84
85
  "pytest>=8.4.1",
85
86
  "pytest-cov>=6.2.1",
@@ -1,6 +1,6 @@
1
1
  """PAV Python library routines."""
2
2
 
3
- __version__ = '3.0.0.dev22'
3
+ __version__ = '3.0.0.dev23'
4
4
 
5
5
  __all__ = [
6
6
  'align',
@@ -23,7 +23,7 @@ PAV_CONFIG = config
23
23
 
24
24
  # Read sample table
25
25
  ASM_TABLE_PATH = pav3.pipeline.get_pav_asm_table_path(
26
- PAV_CONFIG.get('assembly_table', None)
26
+ pav_config=PAV_CONFIG
27
27
  )
28
28
 
29
29
  ASM_TABLE = pav3.pipeline.read_assembly_table(ASM_TABLE_PATH, PAV_CONFIG)
@@ -286,7 +286,7 @@ rule call_tables:
286
286
  ref_path = Path(next(in_file).strip())
287
287
 
288
288
  callsets = tuple(
289
- tuple((
289
+ (
290
290
  (
291
291
  pl.scan_parquet(
292
292
  f'results/{wildcards.asm_name}/call_hap/call_{wildcards.vartype}_{hap}.parquet'
@@ -294,7 +294,7 @@ rule call_tables:
294
294
  .with_row_index('_index')
295
295
  ),
296
296
  f'{wildcards.asm_name}-{hap}',
297
- ))
297
+ )
298
298
  for hap in pav3.pipeline.get_hap_list(wildcards.asm_name, ASM_TABLE)
299
299
  )
300
300
 
@@ -59,8 +59,7 @@ rule align_get_qry_fa:
59
59
  out_filename = f'data/query/{wildcards.asm_name}/query_{wildcards.hap}.fa' # ".gz" is appended as needed
60
60
 
61
61
  if len(input_tuples) == 1 and input_tuples[0][1] == 'fasta':
62
- os.makedirs(os.path.dirname(out_filename), exist_ok=True)
63
- fa_path_list = pav3.pipeline.link_fasta(input_tuples[0][0], out_filename)
62
+ fa_path_list = pav3.pipeline.link_fasta(Path(input_tuples[0][0]), out_filename)
64
63
 
65
64
  else:
66
65
  # Merge/write FASTA from multiple sources and/or GFA files
@@ -1,5 +1,6 @@
1
1
  """Path utilities for development environments."""
2
2
 
3
+ import os
3
4
  from pathlib import Path
4
5
  import tomllib
5
6
 
@@ -19,8 +20,16 @@ def get_pav_dir(
19
20
  directory.
20
21
  """
21
22
 
22
- if path is None:
23
- raise ValueError('path is None')
23
+ org_path = path
24
+
25
+ if path is None or not Path(path).exists():
26
+ path = os.environ.get('PAV_PATH', None)
27
+
28
+ if path is None:
29
+ path = os.environ.get('UV_PROJECT', None)
30
+
31
+ if path is None:
32
+ raise ValueError(f'Path is missing: {org_path}')
24
33
 
25
34
  path = Path(path)
26
35
 
@@ -36,5 +45,4 @@ def get_pav_dir(
36
45
  == 'pav3'
37
46
  ):
38
47
  return parent
39
-
40
48
  raise FileNotFoundError(f'path {path} does not point to a resource within a PAV source directory')
@@ -866,16 +866,25 @@ def link_fasta(
866
866
 
867
867
  # Link FASTA
868
868
  dest_fa_path.unlink(missing_ok=True)
869
- dest_fa_path.symlink_to(source)
869
+ dest_fa_path.parent.mkdir(parents=True, exist_ok=True)
870
+
871
+ dest_fa_path.symlink_to(
872
+ resolve_relative(source, dest_fa_path)
873
+ )
870
874
 
871
875
  # Link indexes (if present)
872
876
  if source_is_indexed:
873
877
  dest_fai_path.unlink(missing_ok=True)
874
- dest_fai_path.symlink_to(source_fai)
878
+
879
+ dest_fai_path.symlink_to(
880
+ resolve_relative(source_fai, dest_fai_path)
881
+ )
875
882
 
876
883
  if dest_gzi_path is not None:
877
884
  dest_gzi_path.unlink(missing_ok=True)
878
- dest_gzi_path.symlink_to(source_gzi)
885
+ dest_gzi_path.symlink_to(
886
+ resolve_relative(source_gzi, dest_gzi_path)
887
+ )
879
888
 
880
889
  else:
881
890
  # Create indexes
@@ -886,6 +895,37 @@ def link_fasta(
886
895
  dest_fa_path, dest_fai_path
887
896
  ] + [dest_gzi_path] if dest_gzi_path is not None else []
888
897
 
898
+ def resolve_relative(
899
+ source_path: str | Path,
900
+ dest_path: str | Path,
901
+ ) -> Path:
902
+ """Resolve a path relative to another path.
903
+
904
+ If both source and destination paths are relative, assume they hare relative to the current
905
+ working directory and return a relative path from the destination path to the source path.
906
+ This can be used to make symbolic links within a directory structure allowing the directories
907
+ to move locations without breaking absolute paths.
908
+
909
+ If either the source or the destination path is absolute, the absolute source path is
910
+ returned.
911
+
912
+ :param source_path: Source path.
913
+ :param dest_path: Destination path.
914
+
915
+ :returns: Resolved path.
916
+ """
917
+ source_path = Path(source_path)
918
+ dest_path = Path(dest_path)
919
+
920
+ if source_path.is_absolute():
921
+ return source_path
922
+
923
+ if dest_path.is_absolute():
924
+ return source_path.absolute()
925
+
926
+ # Return a path relative to the destination path
927
+ return source_path.relative_to(dest_path.parent, walk_up=True)
928
+
889
929
 
890
930
  def get_rules_path(
891
931
  rule_filename: str | Path,
@@ -923,24 +963,32 @@ def get_rules_path(
923
963
 
924
964
  def get_pav_config_path(
925
965
  config_filename: Optional[str | Path] = None,
966
+ root_path: Optional[str | Path] = None,
926
967
  ) -> Path:
927
968
  """Locate the PAV configuration filename.
928
969
 
929
970
  :param config_filename: Path to the config file. If None, the default config file is used.
971
+ :param root_path: Root path to search for the config file. If None, the current directory
972
+ is used.
930
973
 
931
974
  :return: Path to the config file.
932
975
 
933
976
  :raises FileNotFoundError: If the specified configuration (`config_filename`) or default
934
977
  configuration file is not found.
935
978
  """
979
+ if root_path is None:
980
+ root_path = Path('.')
981
+ else:
982
+ root_path = Path(root_path)
983
+
936
984
  default_paths = (
937
- Path('pav.json'),
938
- Path('pav.json.txt'),
985
+ root_path / 'pav.json',
986
+ root_path / 'pav.json.txt',
939
987
  )
940
988
 
941
989
  deprecated_paths = (
942
- Path('config.json'),
943
- Path('config.json.txt'),
990
+ root_path / 'config.json',
991
+ root_path / 'config.json.txt',
944
992
  )
945
993
 
946
994
  if config_filename is not None:
@@ -988,33 +1036,58 @@ def get_pav_config_path(
988
1036
 
989
1037
 
990
1038
  def get_pav_asm_table_path(
991
- asm_table_path: Optional[str | Path] = None,
1039
+ pav_config: Optional[dict] = None,
1040
+ root_path: Optional[str | Path] = Path('.'),
992
1041
  ) -> Path:
993
1042
  """Get a path to the assembly table.
994
1043
 
995
- :param asm_table_path: Table path, if specified.
1044
+ The assembly table path is read from the PAV configuration if it is specified there. If it is
1045
+ not specified in the configuration, the default assembly table paths are searched. All paths
1046
+ are relative to `root_path` (defaults to the current directory).
1047
+
1048
+ :param pav_config: PAV configuration, if specified. Reads the assembly table path from the
1049
+ configuration if it is specified there.
1050
+ :param root_path: Root path to search for the table file. If None, the current directory
1051
+ is used.
996
1052
 
997
1053
  :return: Path to the assembly table.
998
1054
 
999
- :raises FileNotFoundError: If the specified assembly table or default assembly table is not found.
1055
+ :raises FileNotFoundError: If the specified assembly table or default assembly table is not
1056
+ found.
1000
1057
  """
1001
1058
 
1002
- if asm_table_path is not None:
1003
- asm_table_path = Path(asm_table_path)
1059
+ if root_path is None:
1060
+ root_path = Path('.')
1061
+ else:
1062
+ root_path = Path(root_path)
1063
+
1064
+ if pav_config is not None and 'assembly_table' in pav_config:
1065
+ asm_table_path = root_path / str(pav_config['assembly_table'])
1004
1066
 
1005
1067
  if not asm_table_path.is_file():
1006
- raise FileNotFoundError(f'Specified assembly table file not found or not a regular file: {asm_table_path}')
1068
+ raise FileNotFoundError(
1069
+ f'Assembly table file defined in the PAV configuration is not found '
1070
+ f'or not a regular file: {asm_table_path}'
1071
+ )
1007
1072
 
1008
1073
  return asm_table_path
1009
1074
 
1010
- for asm_table_path in (
1011
- Path('assemblies.tsv'),
1012
- Path('assemblies.xlsx')
1013
- ):
1075
+ default_paths = [
1076
+ root_path / 'assemblies.tsv',
1077
+ root_path / 'assemblies.xlsx',
1078
+ ]
1079
+
1080
+ for asm_table_path in default_paths:
1014
1081
  if asm_table_path.is_file():
1015
1082
  return asm_table_path
1016
1083
 
1017
- raise FileNotFoundError('No assembly table file found (tried default "assemblies.tsv" and "assemblies.xlsx")')
1084
+ default_path_str = ', '.join(
1085
+ [f'"{str(p)}"' for p in default_paths]
1086
+ )
1087
+
1088
+ raise FileNotFoundError(
1089
+ f'No assembly table file found (tried default paths: {default_path_str})'
1090
+ )
1018
1091
 
1019
1092
 
1020
1093
  def get_pav_env_path(
@@ -1031,7 +1104,9 @@ def get_pav_env_path(
1031
1104
 
1032
1105
  if env_path is not None:
1033
1106
  if not env_path.is_file():
1034
- raise FileNotFoundError(f'Specified environment file not found or not a regular file: {env_path}')
1107
+ raise FileNotFoundError(
1108
+ f'Specified environment file not found or not a regular file: {env_path}'
1109
+ )
1035
1110
 
1036
1111
  return env_path
1037
1112
 
@@ -24,10 +24,61 @@ from ..call.expr import sort_expr
24
24
  logger = logging.getLogger(__name__)
25
25
 
26
26
 
27
+ def _materialize(
28
+ df: pl.LazyFrame,
29
+ temp_file_container: TempDirContainer,
30
+ label: str,
31
+ ) -> pl.LazyFrame:
32
+ """Write a lazy frame to a temporary Parquet file and return a streaming scan of it.
33
+
34
+ Used as a materialization barrier between lazy operations. It serves two purposes:
35
+
36
+ 1. Memory: Variant tables may be very large. Writing intermediate results to a temporary file and re-reading
37
+ them as streaming scans keeps them off the Python heap instead of accumulating collected frames in memory
38
+ across the integration loop.
39
+
40
+ 2. Correctness: `apply_trim_filter` and `apply_discord_and_inner_filter` each add a row-index column
41
+ (`with_row_index`). When both are chained into a single lazy plan and that plan is sunk, the Polars
42
+ new-streaming engine mis-optimizes the chained row-index operations and raises a spurious
43
+ "duplicate name when building schema" / "column not found" error. The same plan collects correctly with
44
+ the in-memory engine, confirming the logical plan is valid and the fault is in the streaming optimizer.
45
+ Breaking the plan with a Parquet round-trip isolates each helper's row index and avoids the bug.
46
+
47
+ :param df: Lazy frame to materialize.
48
+ :param temp_file_container: Active temporary file container (must be inside its context).
49
+ :param label: Label used in the temporary file name.
50
+
51
+ :return: Streaming scan over the materialized Parquet file.
52
+ """
53
+ temp_file = temp_file_container.next(prefix=f'integrate_{label}_', suffix='.parquet')
54
+ df.sink_parquet(temp_file)
55
+ return pl.scan_parquet(temp_file)
56
+
57
+
27
58
  def integrate_sources(
28
59
  pav_params,
29
60
  input: dict[str, str],
30
61
  output: dict[str, str],
62
+ temp_dir_name: Optional[str] = None,
63
+ ) -> None:
64
+ """Integrate variant calls from all sources into the final per-haplotype call tables.
65
+
66
+ :param pav_params: PAV parameters.
67
+ :param input: Input file dictionary (Snakemake rule input).
68
+ :param output: Output file dictionary (Snakemake rule output).
69
+ :param temp_dir_name: Parent directory for temporary files. Defaults to the system temporary directory.
70
+ """
71
+ # Intermediate variant tables are materialized to a temporary directory and re-read as streaming scans (see
72
+ # `_materialize`). This bounds memory for large callsets and works around a Polars new-streaming engine bug.
73
+ with TempDirContainer(temp_dir_name, prefix='pav_integrate_') as temp_file_container:
74
+ _integrate_sources(pav_params, input, output, temp_file_container)
75
+
76
+
77
+ def _integrate_sources(
78
+ pav_params,
79
+ input: dict[str, str],
80
+ output: dict[str, str],
81
+ temp_file_container: TempDirContainer,
31
82
  ) -> None:
32
83
  inv_min = pav_params.inv_min
33
84
  inv_max = pav_params.inv_max if pav_params.inv_max > 0 else float('inf')
@@ -130,6 +181,11 @@ def integrate_sources(
130
181
  if not (is_lg or is_inv):
131
182
  df = pav3.call.integrate.apply_trim_filter(df, df_trim)
132
183
 
184
+ # Materialization barrier between the two row-index helpers. `apply_trim_filter` and
185
+ # `apply_discord_and_inner_filter` both call `with_row_index`; chaining them into one sunk plan trips a
186
+ # Polars streaming-engine bug (see `_materialize`). The round-trip isolates the two row indices.
187
+ df = _materialize(df, temp_file_container, f'{sourcetype_vartype}_trim')
188
+
133
189
  # Filter DISCORD
134
190
  if filter_discord or filter_inner:
135
191
  df = pav3.call.integrate.apply_discord_and_inner_filter(
@@ -145,6 +201,11 @@ def integrate_sources(
145
201
  existing_ids=collect_list[vartype]
146
202
  )
147
203
 
204
+ # Materialize the fully-processed variant table and continue from a streaming scan. Every downstream use
205
+ # (DISCORD regions, CPX-derived variants, writes, and cross-source aggregation in `collect_list`) reads from
206
+ # this scan. This keeps large tables off the heap and replaces the previous in-memory `df.collect().lazy()`.
207
+ df = _materialize(df, temp_file_container, sourcetype_vartype)
208
+
148
209
  # Read CPX segment table
149
210
  if sourcetype == 'inter':
150
211
  df_segment_var = df_segment.join(df.select(['var_index', 'id']), on='var_index', how='left')
@@ -180,11 +241,10 @@ def integrate_sources(
180
241
  collect_list=collect_list,
181
242
  )
182
243
 
183
- # Aggregate if type is split over multiple inputs
244
+ # Aggregate if type is split over multiple inputs. `df` is already a disk-backed scan (materialized above),
245
+ # so this keeps memory bounded even when several sources accumulate in `collect_list`.
184
246
  if not do_write or len(collect_list[vartype]) > 0:
185
- # Collect here, avoid re-collecting for ID (used as existing IDs) and write.
186
- # Note: May consume significant memory for some callsets, consider writing to temp and re-reading lazy
187
- collect_list[vartype].append(df.collect().lazy())
247
+ collect_list[vartype].append(df)
188
248
 
189
249
  # Write
190
250
  write_list = [update_discord_frame]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pav3
3
- Version: 3.0.0.dev22
3
+ Version: 3.0.0.dev23
4
4
  Summary: Assembly-based variant discovery
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.12
@@ -13,7 +13,7 @@ Requires-Dist: frozendict>=2.4.6
13
13
  Requires-Dist: hmmlearn>=0.3.3
14
14
  Requires-Dist: intervaltree>=3.1.0
15
15
  Requires-Dist: numpy>=2.3.2
16
- Requires-Dist: polars[numpy,pandas,pyarrow]>=1.35.2
16
+ Requires-Dist: polars[numpy,pandas,pyarrow]>=1.41.0
17
17
  Requires-Dist: pyarrow>=21.0.0
18
18
  Requires-Dist: pysam>=0.23.3
19
19
  Requires-Dist: scipy>=1.16.1
@@ -5,7 +5,7 @@ frozendict>=2.4.6
5
5
  hmmlearn>=0.3.3
6
6
  intervaltree>=3.1.0
7
7
  numpy>=2.3.2
8
- polars[numpy,pandas,pyarrow]>=1.35.2
8
+ polars[numpy,pandas,pyarrow]>=1.41.0
9
9
  pyarrow>=21.0.0
10
10
  pysam>=0.23.3
11
11
  scipy>=1.16.1
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes