OSCR

Multi-omics reveal phosphatidic acid phosphatases modify Niemann-Pick type C disease severity.

Code ↔ Paper

3 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 3 matches
  1. [1] § STAR★Methods › Method details › Human genome sequence analysis ↔ deepvariant/postprocess_variants.py, lines 611–646 · score 0.71 · Phred scaled, quality score, variant genotype, sequencing, Predictor, VCF
  2. [2] § STAR★Methods › Quantification and statistical analysis ↔ deepvariant/methylation_aware_phasing.cc, lines 231–319 · score 0.52 · Wilcoxon rank sum
  3. [3] § STAR★Methods › Quantification and statistical analysis ↔ deepvariant/methylation_aware_phasing.h, lines 46–108 · score 0.51 · Wilcoxon rank sum

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Python · 2,378 lines · 80 KB · BSD-3-Clause · 1 match

  1. # Copyright 2017 Google LLC.
  2. #
  3. # Redistribution and use in source and binary forms, with or without
  4. # modification, are permitted provided that the following conditions
  5. # are met:
  6. #
  7. # 1. Redistributions of source code must retain the above copyright notice,
  8. # this list of conditions and the following disclaimer.
  9. #
  10. # 2. Redistributions in binary form must reproduce the above copyright
  11. # notice, this list of conditions and the following disclaimer in the
  12. # documentation and/or other materials provided with the distribution.
  13. #
  14. # 3. Neither the name of the copyright holder nor the names of its
  15. # contributors may be used to endorse or promote products derived from this
  16. # software without specific prior written permission.
  17. #
  18. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  19. # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  20. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  21. # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
  22. # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  23. # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  24. # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  25. # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  26. # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  27. # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  28. # POSSIBILITY OF SUCH DAMAGE.
  29. """Postprocess output from call_variants to produce a VCF file."""
  30. # TODO: Add type annotations to this module
  31. import collections
  32. import functools
  33. import itertools
  34. import json
  35. import os
  36. import tempfile
  37. import time
  38. from typing import Iterable, Iterator, Sequence
  39. from absl import flags
  40. from absl import logging
  41. from google.protobuf import json_format
  42. import numpy as np
  43. import pysam
  44. import tensorflow as tf
  45. from deepvariant import calling_regions_utils
  46. from deepvariant import dv_constants
  47. from deepvariant import dv_utils
  48. from deepvariant import dv_vcf_constants
  49. from deepvariant import haplotypes
  50. from deepvariant import logging_level
  51. from deepvariant.protos import deepvariant_pb2
  52. from deepvariant.python import merge_phased_reads as merge_phased_reads_lib
  53. from deepvariant.python import postprocess_variants as postprocess_variants_lib
  54. from deepvariant.small_model import inference as small_model_inference
  55. from absl import app
  56. import multiprocessing
  57. from third_party.nucleus.io import sharded_file_utils
  58. from third_party.nucleus.io import tabix
  59. from third_party.nucleus.io import tfrecord
  60. from third_party.nucleus.io import vcf
  61. from third_party.nucleus.io.python import merge_variants
  62. from third_party.nucleus.io.python import vcf_concat
  63. from third_party.nucleus.protos import range_pb2
  64. from third_party.nucleus.protos import reference_pb2
  65. from third_party.nucleus.protos import variants_pb2
  66. from third_party.nucleus.util import errors
  67. from third_party.nucleus.util import genomics_math
  68. from third_party.nucleus.util import proto_utils
  69. from third_party.nucleus.util import ranges
  70. from third_party.nucleus.util import struct_utils
  71. from third_party.nucleus.util import variant_utils
  72. from third_party.nucleus.util import variantcall_utils
  73. _INFILE = flags.DEFINE_string(
  74. 'infile',
  75. None,
  76. (
  77. 'Required. Path(s) to CallVariantOutput protos in TFRecord format to '
  78. 'postprocess. These should be the complete set of outputs for '
  79. 'call_variants.py.'
  80. ),
  81. )
  82. _OUTFILE = flags.DEFINE_string(
  83. 'outfile',
  84. None,
  85. (
  86. 'Required. Destination path where we will write output variant calls in'
  87. ' VCF format.'
  88. ),
  89. )
  90. _REF = flags.DEFINE_string(
  91. 'ref',
  92. None,
  93. (
  94. 'Required. Genome reference in FAI-indexed FASTA format. Used to'
  95. ' determine the sort order for the emitted variants and the VCF header.'
  96. ),
  97. )
  98. _SMALL_MODEL_CVO_RECORDS = flags.DEFINE_string(
  99. 'small_model_cvo_records',
  100. None,
  101. (
  102. 'Optional. Path(s) to CallVariantOutput protos in TFRecord format that'
  103. ' were called by the small model to include in postprocess .'
  104. ),
  105. )
  106. _QUAL_FILTER = flags.DEFINE_float(
  107. 'qual_filter',
  108. 1.0,
  109. 'Any variant with QUAL < qual_filter will be filtered in the VCF file.',
  110. )
  111. _CNN_HOMREF_CALL_MIN_GQ = flags.DEFINE_float(
  112. 'cnn_homref_call_min_gq',
  113. 20.0,
  114. (
  115. 'All CNN RefCalls whose GQ is less than this value will have ./.'
  116. ' genotype instead of 0/0.'
  117. ),
  118. )
  119. _MULT_ALLELIC_QUAL_FILTER = flags.DEFINE_float(
  120. 'multi_allelic_qual_filter',
  121. 1.0,
  122. 'The qual value below which to filter multi-allelic variants.',
  123. )
  124. _NONVARIANT_SITE_TFRECORD_PATH = flags.DEFINE_string(
  125. 'nonvariant_site_tfrecord_path',
  126. None,
  127. (
  128. 'Optional. Path(s) to the non-variant sites protos in TFRecord format'
  129. ' to convert to gVCF file. This should be the complete set of outputs'
  130. ' from the --gvcf flag of make_examples.py.'
  131. ),
  132. )
  133. _PHASED_READS_INPUT_PATH = flags.DEFINE_string(
  134. 'phased_reads_input_path',
  135. None,
  136. (
  137. 'Optional. Path to a TSV file containing phased read information, '
  138. 'typically an output of the make_examples step. This information '
  139. 'will be used to extend phase blocks in the output VCF.'
  140. ),
  141. )
  142. _CHECKPOINT_JSON = flags.DEFINE_string(
  143. 'checkpoint_json',
  144. None,
  145. 'Optional. Path to the json file containing the flags for postprocessing.',
  146. )
  147. _PHASED_READS_SWITCHES_OUTPUT_PATH = flags.DEFINE_string(
  148. 'phased_reads_switches_output_path',
  149. '/tmp/phased_reads_switches.tsv',
  150. (
  151. 'Optional. Path to a TSV file containing switches information, '
  152. 'typically an output of the make_examples step. This information '
  153. 'will be used to extend phase blocks in the output VCF.'
  154. ),
  155. )
  156. _PHASED_READS_CORRECTED_OUTPUT_PATH = flags.DEFINE_string(
  157. 'phased_reads_corrected_output_path',
  158. '/tmp/phased_reads_corrected.tsv',
  159. (
  160. 'Optional. Path to a TSV file containing phased read information, '
  161. 'typically an output of the make_examples step. This information '
  162. 'will be used to extend phase blocks in the output VCF.'
  163. ),
  164. )
  165. _GVCF_OUTFILE = flags.DEFINE_string(
  166. 'gvcf_outfile',
  167. None,
  168. 'Optional. Destination path where we will write the Genomic VCF output.',
  169. )
  170. _GROUP_VARIANTS = flags.DEFINE_boolean(
  171. 'group_variants',
  172. True,
  173. (
  174. 'If using vcf_candidate_importer and multi-allelic '
  175. 'sites are split across multiple lines in VCF, set to False so that '
  176. 'variants are not grouped when transforming CallVariantsOutput to '
  177. 'Variants.'
  178. ),
  179. )
  180. _VCF_STATS_REPORT = flags.DEFINE_boolean(
  181. 'vcf_stats_report',
  182. False,
  183. 'Deprecated. Use vcf_stats_report.py instead.',
  184. )
  185. _SAMPLE_NAME = flags.DEFINE_string(
  186. 'sample_name',
  187. None,
  188. (
  189. 'Optional. If set, this will only be used if the sample name cannot be '
  190. 'determined from the CallVariantsOutput or non-variant sites protos.'
  191. ),
  192. )
  193. _USE_MULTIALLELIC_MODEL = flags.DEFINE_boolean(
  194. 'use_multiallelic_model',
  195. False,
  196. (
  197. 'If True, use a specialized model for genotype resolution of'
  198. ' multiallelic cases with two alts.'
  199. ),
  200. )
  201. _MULTIALLELIC_MODE = flags.DEFINE_enum(
  202. 'multiallelic_mode',
  203. 'product',
  204. ['min', 'product'],
  205. 'The fusion rule for merging probabilities in multiallelic calling.',
  206. )
  207. _DEBUG_OUTPUT_ALL_CANDIDATES = flags.DEFINE_enum(
  208. 'debug_output_all_candidates',
  209. None,
  210. ['ALT', 'INFO'],
  211. (
  212. 'Outputs all candidates considered by DeepVariant as additional ALT'
  213. ' alleles or as an INFO field. For ALT, filtered candidates are'
  214. ' assigned a GL=0 and added as ALTs alleles, but do not appear in any'
  215. ' sample genotypes. This flag is useful for debugging purposes.'
  216. ' ALT-mode is incompatible with the multiallelic caller.'
  217. ),
  218. )
  219. _ONLY_KEEP_PASS = flags.DEFINE_boolean(
  220. 'only_keep_pass', False, 'If True, only keep PASS calls.'
  221. )
  222. _HAPLOID_CONTIGS = flags.DEFINE_string(
  223. 'haploid_contigs',
  224. None,
  225. (
  226. 'Optional list of non autosomal chromosomes. For all listed chromosomes'
  227. 'HET probabilities are not considered. The list can be either comma '
  228. 'or space-separated.'
  229. ),
  230. )
  231. _CPUS = flags.DEFINE_integer(
  232. 'cpus',
  233. multiprocessing.cpu_count(),
  234. 'Number of worker processes to use. Set --cpus < 2 to disable parallel'
  235. ' processing.',
  236. short_name='j',
  237. required=False,
  238. )
  239. _NUM_PARTITIONS = flags.DEFINE_integer(
  240. 'num_partitions',
  241. 0,
  242. 'Number of partitions to use for parallel or sequential processing. --Set'
  243. ' --num_partitions > --cpus to trade runtime for lower memory usage. Set'
  244. ' --num_partitions < 2 and --cpus < 2 to disable partitioning.',
  245. required=False,
  246. )
  247. _PAR_REGIONS = flags.DEFINE_string(
  248. 'par_regions_bed',
  249. None,
  250. (
  251. 'Optional BED file containing Human Pseudoautosomal Region (PAR) '
  252. 'regions.'
  253. 'Variants within this region are unaffected by genotype reallocation '
  254. 'applied on regions supplied by --haploid_contigs flag.'
  255. ),
  256. )
  257. _REGIONS = flags.DEFINE_string(
  258. 'regions',
  259. '',
  260. (
  261. 'Optional. Space-separated list of regions we want to process. Elements'
  262. ' can be region literals (e.g., chr20:10-20) or paths to BED/BEDPE'
  263. ' files. This should match the flag passed to make_examples.py.'
  264. ),
  265. )
  266. _PROCESS_SOMATIC = flags.DEFINE_boolean(
  267. 'process_somatic',
  268. False,
  269. 'Optional. If specified the input is treated as somatic.',
  270. )
  271. _PON_FILTERING = flags.DEFINE_string(
  272. 'pon_filtering',
  273. None,
  274. (
  275. 'Optional. Only used if --process_somatic is true. '
  276. 'A VCF file with Panel of Normals (PON) data.'
  277. 'If set, the output VCF will be filtered: any variants that appear in '
  278. 'PON will be marked with a PON filter, and PASS filter value will be '
  279. 'removed.'
  280. ),
  281. )
  282. _RESOLVE_CALL_VARIANTS_OUTPUTS_BY_MODEL = flags.DEFINE_bool(
  283. 'resolve_call_variants_outputs_by_model',
  284. False,
  285. '[Experimental] If true, postprocess_variants expects 2 CVO records for'
  286. ' each example, one from each model. One of the CVO record is chosen based'
  287. ' on the value of the `--small_model_gq_threshold` flag. This mirrors the'
  288. ' same behavior that normally happens in `make_examples` during inference.'
  289. ' The purpose is to explore the joint accuracy of the two'
  290. ' models as a function of GQ thresholds.',
  291. )
  292. _SMALL_MODEL_GQ_THRESHOLD = flags.DEFINE_integer(
  293. 'small_model_gq_threshold',
  294. -1,
  295. '[Experimental] The GQ threshold for accepting classifications from the'
  296. ' small model. Used when `--resolve_call_variants_outputs_by_model` is'
  297. ' true.',
  298. )
  299. # Some format fields are indexed by alt allele, such as AD (depth by allele).
  300. # These need to be cleaned up if we remove any alt alleles. Any info field
  301. # listed here will be have its values cleaned up if we've removed any alt
  302. # alleles.
  303. # Each tuple contains: field name, ref_is_zero.
  304. _ALT_ALLELE_INDEXED_FORMAT_FIELDS = frozenset([
  305. ('AD', True),
  306. ('VAF', False),
  307. ('MF', True),
  308. ('MD', True),
  309. ('NAD', True),
  310. ('NAF', False),
  311. ])
  312. # The number of places past the decimal point to round QUAL estimates to.
  313. _QUAL_PRECISION = 7
  314. # When this was set, it's about 20 seconds per log.
  315. _LOG_EVERY_N = 100000
  316. # When outputting all alt alleles, use placeholder value to indicate genotype
  317. # will be soft-filtered.
  318. _FILTERED_ALT_PROB = -9.0
  319. # The number of genotype probabilities in a diploid sample.
  320. _NUM_GENOTYPE_PROBABILITIES = 3
  321. def _extract_single_sample_name(
  322. record: deepvariant_pb2.CallVariantsOutput,
  323. ) -> str:
  324. """Returns the name of the single sample within the CallVariantsOutput file.
  325. Args:
  326. record: A deepvariant_pb2.CallVariantsOutput record.
  327. Returns:
  328. The name of the single individual in the first proto in the file.
  329. Raises:
  330. ValueError: There is not exactly one VariantCall in the proto or the
  331. call_set_name of the VariantCall is not populated.
  332. """
  333. variant = record.variant
  334. call = variant_utils.only_call(variant)
  335. name = call.call_set_name
  336. if not name:
  337. raise ValueError(
  338. 'Error extracting name: no call_set_name set: {}'.format(record)
  339. )
  340. return name
  341. def _pysam_resolve_file_path(file_path: str) -> str:
  342. """Prepends a prefix to the file_path when accessing Google files.
  343. Args:
  344. file_path: str. Full path pointing a specific file to access with pysam.
  345. Returns:
  346. str. The full configured file path for pysam to open.
  347. """
  348. # BEGN_INTERNAL
  349. if (
  350. file_path.startswith('/cns/')
  351. or file_path.startswith('/placer/')
  352. or file_path.startswith('/readahead/')
  353. or file_path.startswith('/bigstore/')
  354. ):
  355. return f'google:{file_path}'
  356. # END_INTERNAL
  357. return file_path
  358. def most_likely_genotype(
  359. predictions: Sequence[float], ploidy: int = 2, n_alleles: int = 2
  360. ) -> tuple[int, list[int]]:
  361. """Gets the most likely genotype from predictions.
  362. From https://samtools.github.io/hts-specs/VCFv4.3.pdf:
  363. Genotype Ordering. In general case of ploidy P and N alternate alleles (0 is
  364. the REF and 1..N the alternate alleles), the ordering of genotypes for the
  365. likelihoods can be expressed by the following pseudocode with as many nested
  366. loops as ploidy:
  367. * Note that we use inclusive for loop boundaries.
  368. for a_P = 0 . . . N
  369. for a_P-1 = 0 . . . aP
  370. . . .
  371. for a_1 = 0 . . . a2
  372. println a1 a2 . . . aP
  373. Alternatively, the same can be achieved recursively with the following
  374. pseudocode:
  375. Ordering (P , N , suffix =""):
  376. for a in 0 . . . N
  377. if (P == 1) println str (a) + suffix
  378. if (P > 1) Ordering (P -1 , a, str (a) + suffix)
  379. Examples:
  380. * for P=2 and N=1, the ordering is 00,01,11
  381. * for P=2 and N=2, the ordering is 00,01,11,02,12,22
  382. * for P=3 and N=2, the ordering is 000,001,011,111,002,012,112,022,122,222
  383. * for P=1, the index of the genotype a is a
  384. * for P=2, the index of the genotype "a/b", where a <= b, is b(b + 1)/2 + a
  385. * for P=2 and arbitrary N, the ordering can be easily derived from a
  386. triangular matrix:
  387. b / a 0 1 2 3
  388. 0 0
  389. 1 1 2
  390. 2 3 4 5
  391. 3 6 7 8 9
  392. Args:
  393. predictions: N element array-like. The real-space probabilities of each
  394. genotype state for this variant. The number of elements in predictions is
  395. related to ploidy and n_alleles is given by N = choose(ploidy + n_alleles
  396. - 1, n_alleles -1) for more information see:
  397. http://genome.sph.umich.edu/wiki/Relationship_between_Ploidy,_Alleles_and_Genotypes
  398. ploidy: int >= 1. The ploidy (e.g., number of chromosomes) of this sample.
  399. n_alleles: int >= 2. The number of alleles (ref + n_alts).
  400. Returns:
  401. Two values. The first is the index of the most likely prediction in
  402. predictions. The second is a list of P elements with the VCF-style genotype
  403. indices corresponding to this index. For example, with P = 2 and an index of
  404. 1, this returns the value (1, [0, 1]).
  405. Raises:
  406. NotImplementedError: if ploidy != 2 as this not yet implemented.
  407. ValueError: If n_alleles < 2.
  408. ValueError: If we cannot determine the genotype given prediction, n_alts,
  409. and ploidy.
  410. """
  411. # TODO: This can be memoized for efficiency.
  412. if ploidy != 2:
  413. raise NotImplementedError('Ploidy != 2 not yet implemented.')
  414. if n_alleles < 2:
  415. raise ValueError('n_alleles must be >= 2 but got', n_alleles)
  416. # TODO: would be nice to add test that predictions has the right
  417. # number of elements. But that would involve calculating the binomial
  418. # coefficient of n_alleles and ploidy, which would be expensive. Probably
  419. # need to memoize the whole function if we are going to add this.
  420. index_of_max = np.argmax(predictions)
  421. # This is the general case solution for fixed ploidy of 2 and arbitrary
  422. # n_alleles. We should generalize this code to the arbitrary ploidy case when
  423. # needed and memoize the mapping here.
  424. index = 0
  425. for h1 in range(0, n_alleles + 1):
  426. for h2 in range(0, h1 + 1):
  427. if index == index_of_max:
  428. return index, [h2, h1]
  429. index += 1
  430. raise ValueError('No corresponding GenotypeType for predictions', predictions)
  431. def uncall_gt_if_no_ad(variant: variants_pb2.Variant) -> None:
  432. """Converts genotype to "./." if sum(AD)=0."""
  433. vcall = variant_utils.only_call(variant)
  434. if sum(variantcall_utils.get_ad(vcall)) == 0:
  435. # Set GT to ./.; GLs set to 0; GQ=0
  436. vcall.genotype[:] = [-1, -1]
  437. vcall.genotype_likelihood[:] = [0, 0]
  438. variantcall_utils.set_gq(vcall, 0)
  439. def uncall_homref_gt_if_lowqual(
  440. variant: variants_pb2.Variant, min_homref_gq: float
  441. ) -> None:
  442. """Converts genotype to "./." if variant is CNN RefCall and has low GQ.
  443. If the variant has "RefCall" filter (which means an example was created for
  444. this site but CNN didn't call this as variant) and if the GQ is less than
  445. the given min_homref_gq threshold, set the genotype of the variant proto
  446. to "./.". See http://internal for more info.
  447. Args:
  448. variant: third_party.nucleus.protos.Variant proto.
  449. min_homref_gq: float.
  450. """
  451. vcall = variant_utils.only_call(variant)
  452. if (
  453. variant.filter == [dv_vcf_constants.DEEP_VARIANT_REF_FILTER]
  454. and variantcall_utils.get_gq(vcall) < min_homref_gq
  455. ):
  456. vcall.genotype[:] = [-1, -1]
  457. variant.filter[:] = [dv_vcf_constants.DEEP_VARIANT_NO_CALL]
  458. # TODO Implement ingration test to test phased output.
  459. def maybe_phase_genotype(
  460. variant: variants_pb2.Variant,
  461. genotype: list[int],
  462. ) -> tuple[bool, list[int]]:
  463. """Phases the genotype if phase information is available.
  464. The `ALT_PS` field contains phases assigned to each allele in range [0..2],
  465. for HP tags 0,1,2. The length of this array is number of alleles + 1, since
  466. REF is added implicitly.
  467. For example, [1,2] means that REF allele is assigned phase 1 and ALT_1 allele
  468. is assigned a phase 2. [2,1] means that REF allele is assigned phase 2 and
  469. ALT_1 allele is assigned phase 1. [2,2,1,1] means REF is phase 2, ALT_1 is
  470. phase 2, ALT_2 is phase 1, and ALT_3 is phase 1, etc.
  471. Args:
  472. variant: third_party.nucleus.protos.Variant proto.
  473. genotype: list of ints. The genotype indices to be written to the VCF.
  474. Returns:
  475. is_phased: bool. Whether it was possible to phase the genotype.
  476. genotype: genotype in the correct phasing order, if phased.
  477. """
  478. if not (
  479. variant_utils.get_info(variant, dv_constants.VARIANT_PHASE_SET)
  480. and variant_utils.get_info(variant, dv_constants.PHASED_GENOTYPE)
  481. ):
  482. return False, genotype
  483. phase_info = [
  484. p.int_value for p in variant.info[dv_constants.PHASED_GENOTYPE].values
  485. ]
  486. if max(genotype) >= len(phase_info):
  487. logging.warning(
  488. (
  489. 'Genotype %s is out of range for phase info %s for variant %s. '
  490. 'Phasing was not applied.'
  491. ),
  492. genotype,
  493. phase_info,
  494. variant,
  495. )
  496. return False, genotype
  497. allele_1_haplotype = phase_info[genotype[0]]
  498. allele_2_haplotype = phase_info[genotype[1]]
  499. is_phased = (
  500. 0 not in (allele_1_haplotype, allele_2_haplotype)
  501. and allele_1_haplotype != allele_2_haplotype
  502. )
  503. if is_phased:
  504. genotype = [
  505. genotype[allele_1_haplotype - 1],
  506. genotype[allele_2_haplotype - 1],
  507. ]
  508. return is_phased, genotype
  509. def add_call_to_variant(
  510. variant: variants_pb2.Variant,
  511. predictions: Sequence[float],
  512. qual_filter: float,
  513. sample_name: str | None,
  514. ) -> variants_pb2.Variant:
  515. """Fills in Variant record using the prediction probabilities.
  516. This functions sets the call[0].genotype, call[0].info['GQ'],
  517. call[0].genotype_probabilities, variant.filter, and variant.quality fields of
  518. variant based on the genotype likelihoods in predictions.
  519. Args:
  520. variant: third_party.nucleus.protos.Variant protobuf to be filled in with
  521. info derived from predictions.
  522. predictions: N element array-like. The real-space probabilities of each
  523. genotype state for this variant.
  524. qual_filter: float. If predictions implies that this isn't a reference call
  525. and the QUAL of the prediction isn't larger than qual_filter variant will
  526. be marked as FILTERed.
  527. sample_name: str. The name of the sample to assign to the Variant proto
  528. call_set_name field.
  529. Returns:
  530. A tuple of the Variant record and its phase information.
  531. Raises:
  532. ValueError: If variant doesn't have exactly one variant.call record.
  533. """
  534. call = variant_utils.only_call(variant)
  535. n_alleles = len(variant.alternate_bases) + 1
  536. index, genotype = most_likely_genotype(predictions, n_alleles=n_alleles)
  537. gq, variant.quality = compute_quals(predictions, index)
  538. call.call_set_name = sample_name
  539. call.is_phased, genotype = maybe_phase_genotype(
  540. variant,
  541. genotype,
  542. )
  543. if is_methylated(call):
  544. mf = variantcall_utils.get_mf(call)
  545. mt = variantcall_utils.determine_methylation_type(mf)
  546. mi = variantcall_utils.get_mi(call)
  547. variantcall_utils.set_mt(call, mt)
  548. variantcall_utils.set_mi(call, mi)
  549. variantcall_utils.set_gt(call, genotype)
  550. variantcall_utils.set_gq(call, gq)
  551. gls = [genomics_math.perror_to_bounded_log10_perror(gp) for gp in predictions]
  552. variantcall_utils.set_gl(call, gls)
  553. uncall_gt_if_no_ad(variant)
  554. variant.filter[:] = dv_vcf_constants.compute_filter_fields(
  555. variant, qual_filter
  556. )
  557. uncall_homref_gt_if_lowqual(variant, _CNN_HOMREF_CALL_MIN_GQ.value)
  558. return variant
  559. def compute_quals(
  560. predictions: Sequence[float], prediction_index: int
  561. ) -> tuple[int, int]:
  562. """Computes GQ and QUAL values from a set of prediction probabilities.
  563. Prediction probabilities are represented as a probability distribution over
  564. the N genotype states (e.g., for 3 genotype states {HOM_REF, HET, HOM_VAR}).
  565. Genotype Quality (or GQ) represents the PHRED scaled confidence in the
  566. particular genotype assignment. Likewise the QUAL representes the PHRED scaled
  567. confidence in variant as compared to reference, that is, P(NON_REF) / P(ALL)
  568. which in the diploid genotype case is P(HET) + P(HOM_VAR) / P(ALL). These
  569. quality scores are capped by _MAX_CONFIDENCE.
  570. Args:
  571. predictions: N element array-like. The real-space probabilities of each
  572. genotype state for this variant.
  573. prediction_index: int. The actual called genotype from the distribution.
  574. Returns:
  575. GQ and QUAL values for output in a Variant record.
  576. """
  577. # GQ is prob(genotype) / prob(all genotypes)
  578. # GQ is rounded to the nearest integer to comply with the VCF spec.
  579. gq = int(
  580. np.around(
  581. genomics_math.ptrue_to_bounded_phred(predictions[prediction_index])
  582. )
  583. )
  584. # QUAL is prob(variant genotype) / prob(all genotypes)
  585. # Taking the min to avoid minor numerical issues than can push sum > 1.0.
  586. # TODO: this is equivalent to the likely better implementation:
  587. # genomics_math.perror_to_phred(max(predictions[0], min_ref_confidence))
  588. # where min_ref_confidence is roughly 1.25e-10 (producing a qual of 99).
  589. qual = genomics_math.ptrue_to_bounded_phred(min(sum(predictions[1:]), 1.0))
  590. rounded_qual = round(qual, _QUAL_PRECISION)
  591. return gq, rounded_qual
  592. def expected_alt_allele_indices(num_alternate_bases: int) -> list[list[int]]:
  593. """Returns (sorted) expected list of alt_allele_indices, given #alt bases."""
  594. num_alleles = num_alternate_bases + 1
  595. alt_allele_indices_list = [
  596. sorted(list(set(x) - {0}))
  597. for x in itertools.combinations(range(num_alleles), 2)
  598. ]
  599. # alt_allele_indices starts from 0, where 0 refers to the first alt allele.
  600. # pylint: disable=g-complex-comprehension
  601. return sorted([
  602. [i - 1 for i in alt_allele_indices]
  603. for alt_allele_indices in alt_allele_indices_list
  604. ])
  605. # pylint: enable=g-complex-comprehension
  606. def _check_alt_allele_indices(
  607. call_variants_outputs: Sequence[deepvariant_pb2.CallVariantsOutput],
  608. ) -> bool:
  609. """Returns True if and only if the alt allele indices are valid."""
  610. all_alt_allele_indices = sorted([
  611. list(call_variants_output.alt_allele_indices.indices)
  612. for call_variants_output in call_variants_outputs
  613. ])
  614. if all_alt_allele_indices != expected_alt_allele_indices(
  615. len(call_variants_outputs[0].variant.alternate_bases)
  616. ):
  617. logging.warning(
  618. (
  619. 'Alt allele indices found from call_variants_outputs for '
  620. 'variant %s is %s, which is invalid.'
  621. ),
  622. call_variants_outputs[0].variant,
  623. all_alt_allele_indices,
  624. )
  625. return False
  626. return True
  627. def variants_are_equal(
  628. variant_1: variants_pb2.Variant,
  629. variant_2: variants_pb2.Variant,
  630. ) -> bool:
  631. """Returns True if the variants are the same, ignoring the calls field.
  632. The `calls` field is ignored because the `VariantCall` object might originate
  633. from the small model (during make_examples) or the CNN model (during
  634. call_variants); what matters is that the `Variant` objects themselves match.
  635. Args:
  636. variant_1: The first variant to compare.
  637. variant_2: The second variant to compare.
  638. Returns:
  639. True if the variants are the same, otherwise False.
  640. """
  641. variant_1_vars = json_format.MessageToDict(variant_1)
  642. variant_2_vars = json_format.MessageToDict(variant_2)
  643. del variant_1_vars['calls']
  644. del variant_2_vars['calls']
  645. if 'info' in variant_1_vars:
  646. del variant_1_vars['info']
  647. if 'info' in variant_2_vars:
  648. del variant_2_vars['info']
  649. return variant_1_vars == variant_2_vars
  650. def is_valid_call_variants_outputs(
  651. call_variants_outputs: Sequence[deepvariant_pb2.CallVariantsOutput],
  652. ) -> bool:
  653. """Returns True if the call_variants_outputs follows our assumptions.
  654. Args:
  655. call_variants_outputs: list of CallVariantsOutput to check.
  656. Returns:
  657. True if the sanity check passes.
  658. """
  659. if not call_variants_outputs:
  660. return True # An empty list is a degenerate case.
  661. if (
  662. not _check_alt_allele_indices(call_variants_outputs)
  663. and not _RESOLVE_CALL_VARIANTS_OUTPUTS_BY_MODEL.value
  664. ):
  665. return False
  666. first_call, other_calls = call_variants_outputs[0], call_variants_outputs[1:]
  667. # Sanity check that all call_variants_outputs have the same `variant`.
  668. for call_to_check in other_calls:
  669. if not variants_are_equal(first_call.variant, call_to_check.variant):
  670. logging.warning(
  671. (
  672. 'Expected all inputs to merge_predictions to have the '
  673. 'same `variant`, but getting %s and %s.'
  674. ),
  675. first_call.variant,
  676. call_to_check.variant,
  677. )
  678. return False
  679. return True
  680. def convert_call_variants_outputs_to_probs_dict(
  681. canonical_variant: variants_pb2.Variant,
  682. call_variants_outputs: Sequence[deepvariant_pb2.CallVariantsOutput],
  683. alt_alleles_to_remove: set[str],
  684. debug_output_all_candidates: str | None = None,
  685. ) -> dict[tuple[str, str], list[float]]:
  686. """Converts a list of CallVariantsOutput to an internal allele probs dict.
  687. Args:
  688. canonical_variant: variants_pb2.Variant.
  689. call_variants_outputs: list of CallVariantsOutput.
  690. alt_alleles_to_remove: set of strings. Alleles to remove.
  691. debug_output_all_candidates: If 'ALT', set low qual alleles to be
  692. soft-filtered.
  693. Returns:
  694. Dictionary of {(allele1, allele2): list of probabilities},
  695. where allele1 and allele2 are strings.
  696. """
  697. flattened_dict = collections.defaultdict(list)
  698. if not call_variants_outputs:
  699. return flattened_dict
  700. for call_variants_output in call_variants_outputs:
  701. allele_set1 = frozenset([canonical_variant.reference_bases])
  702. allele_set2 = frozenset(
  703. canonical_variant.alternate_bases[index]
  704. for index in call_variants_output.alt_allele_indices.indices
  705. )
  706. has_alleles_to_rm = bool(alt_alleles_to_remove.intersection(allele_set2))
  707. if has_alleles_to_rm and debug_output_all_candidates != 'ALT':
  708. continue
  709. if has_alleles_to_rm:
  710. # This block is run when debug_output_all_candidates=ALT
  711. # It sets genotype likelihood to a placeholder value,
  712. # which is later used to set GL=1.0 (prob=0).
  713. p11, p12, p22 = (
  714. _FILTERED_ALT_PROB,
  715. _FILTERED_ALT_PROB,
  716. _FILTERED_ALT_PROB,
  717. )
  718. else:
  719. p11, p12, p22 = call_variants_output.genotype_probabilities
  720. for set1, set2, p in [
  721. (allele_set1, allele_set1, p11),
  722. (allele_set1, allele_set2, p12),
  723. (allele_set2, allele_set2, p22),
  724. ]:
  725. for indices in itertools.product(set1, set2):
  726. flattened_dict[indices].append(p)
  727. return flattened_dict
  728. def get_alt_alleles_to_remove(
  729. call_variants_outputs: Sequence[deepvariant_pb2.CallVariantsOutput],
  730. qual_filter: float,
  731. ) -> set[str]:
  732. """Returns all the alt alleles with quality below qual_filter.
  733. Quality is defined as (1-p(ref/ref)). This removes all alt alleles whose
  734. quality is below the filter value, with the exception that if the set of
  735. removed alt alleles covers everything in the alternate_bases, the single alt
  736. allele where the 1-p(ref/ref) is the highest is retained.
  737. Args:
  738. call_variants_outputs: list of CallVariantsOutput.
  739. qual_filter: double. The qual value below which to filter variants.
  740. Returns:
  741. Set of strings: alt alleles to remove.
  742. """
  743. alt_alleles_to_remove = set() # first alt is represented as 0.
  744. if not qual_filter or not call_variants_outputs:
  745. return alt_alleles_to_remove
  746. max_qual, max_qual_allele = None, None
  747. canonical_variant = call_variants_outputs[0].variant
  748. for call_variants_output in call_variants_outputs:
  749. # Go through the ones where alt_allele_indices has
  750. # exactly one element. There are the pileup images that contains information
  751. # like:
  752. # p00, p01, p11
  753. # or p00, p02, p22
  754. # ...p00, p0N, pNN
  755. if len(call_variants_output.alt_allele_indices.indices) == 1:
  756. # From here, we want to see which ones of these alt alleles (1-N) that we
  757. # can skip. We can use the concept of QUAL in VCF, and filter out ones
  758. # where QUAL < FLAGS.qual_filter. This is because if QUAL is too low,
  759. # it means it is unlikely this has a variant genotype.
  760. _, qual = compute_quals(
  761. call_variants_output.genotype_probabilities, prediction_index=0
  762. )
  763. alt_allele_index = call_variants_output.alt_allele_indices.indices[0]
  764. # Keep track of one alt allele with the highest qual score.
  765. if max_qual is None or max_qual < qual:
  766. max_qual, max_qual_allele = (
  767. qual,
  768. canonical_variant.alternate_bases[alt_allele_index],
  769. )
  770. if qual < qual_filter:
  771. alt_alleles_to_remove.add(
  772. canonical_variant.alternate_bases[alt_allele_index]
  773. )
  774. # If all alt alleles are below `qual_filter`, keep at least one.
  775. if len(alt_alleles_to_remove) == len(canonical_variant.alternate_bases):
  776. alt_alleles_to_remove -= set([max_qual_allele])
  777. return alt_alleles_to_remove
  778. def is_methylated(call: variants_pb2.VariantCall) -> bool:
  779. """Determines if a VariantCall is methylated.
  780. A variant is considered methylated if any of its methylation fractions
  781. (MF) for the reference or alternate alleles is greater than 0.
  782. Args:
  783. call: A `variants_pb2.VariantCall` object.
  784. Returns:
  785. bool: True if any `methylation_fraction` value is greater than 0,
  786. otherwise False.
  787. """
  788. if 'MF' not in call.info:
  789. return False
  790. mf_values = variantcall_utils.get_mf(call)
  791. if any(mf > 0 for mf in mf_values):
  792. return True
  793. return False
  794. class AlleleRemapper:
  795. """Facilitates removing alt alleles from a Variant.
  796. This class provides a one-to-shop for managing the information needed to
  797. remove alternative alleles from Variant. It provides functions and properties
  798. to get the original alts, the new alts, and asking if alleles (strings) or
  799. indices (integers) should be retained or eliminated.
  800. """
  801. def __init__(
  802. self, original_alt_alleles: Sequence[str], alleles_to_remove: set[str]
  803. ):
  804. self.original_alts = list(original_alt_alleles)
  805. self.alleles_to_remove = set(alleles_to_remove)
  806. def keep_index(
  807. self, allele_index: int, ref_is_zero: bool | str = False
  808. ) -> bool:
  809. if ref_is_zero:
  810. return True if allele_index == 0 else self.keep_index(allele_index - 1)
  811. else:
  812. return self.original_alts[allele_index] not in self.alleles_to_remove
  813. def retained_alt_alleles(self) -> Sequence[str]:
  814. return [
  815. alt for alt in self.original_alts if alt not in self.alleles_to_remove
  816. ]
  817. def reindex_allele_indexed_fields(
  818. self, variant: variants_pb2.Variant, fields: frozenset[tuple[str, bool]]
  819. ) -> None:
  820. """Updates variant.call fields indexed by ref + alt_alleles.
  821. Args:
  822. variant: Variant proto. We will update the info fields of the Variant.call
  823. protos.
  824. fields: Iterable of string. Each string should provide a key to an
  825. alternative allele indexed field in VariantCall.info fields. Each field
  826. specified here will be updated to remove values associated with alleles
  827. no longer wanted according to this remapper object.
  828. """
  829. for field_info in fields:
  830. field = field_info[0]
  831. ref_is_zero = field_info[1]
  832. for call in variant.calls:
  833. if field in call.info:
  834. entry = call.info[field]
  835. updated = [
  836. v
  837. for i, v in enumerate(entry.values)
  838. if self.keep_index(i, ref_is_zero=ref_is_zero)
  839. ]
  840. # We cannot do entry.values[:] = updated as the ListValue type "does
  841. # not support assignment" so we have to do this grossness.
  842. del entry.values[:]
  843. entry.values.extend(updated)
  844. def prune_alleles(
  845. variant: variants_pb2.Variant, alt_alleles_to_remove: set[str]
  846. ) -> variants_pb2.Variant:
  847. """Remove the alt alleles in alt_alleles_to_remove from canonical_variant.
  848. Args:
  849. variant: variants_pb2.Variant.
  850. alt_alleles_to_remove: iterable of str. Alt alleles to remove from variant.
  851. Returns:
  852. variants_pb2.Variant with the alt alleles removed from alternate_bases.
  853. """
  854. # If we aren't removing any alt alleles, just return the unmodified variant.
  855. if not alt_alleles_to_remove:
  856. return variant
  857. new_variant = variants_pb2.Variant()
  858. new_variant.CopyFrom(variant)
  859. # Cleanup any VariantCall.info fields indexed by alt allele.
  860. remapper = AlleleRemapper(variant.alternate_bases, alt_alleles_to_remove)
  861. remapper.reindex_allele_indexed_fields(
  862. new_variant, _ALT_ALLELE_INDEXED_FORMAT_FIELDS
  863. )
  864. new_variant.alternate_bases[:] = remapper.retained_alt_alleles()
  865. return new_variant
  866. def get_multiallelic_distributions(
  867. call_variants_outputs: Sequence[deepvariant_pb2.CallVariantsOutput],
  868. pruned_alleles: set[str],
  869. ) -> np.ndarray:
  870. """Return 9 values for 3 distributions from given multiallelic CVOs.
  871. This function is only called for sites with two alt alleles remaining after
  872. pruning. However, call_variants_outputs contains CVOs from pruned and unpruned
  873. alleles, so we ignore the CVOs containing alleles that were pruned.
  874. Args:
  875. call_variants_outputs: list of CVOs for a multiallelic site with exactly two
  876. alts after pruning. For such a site, we would expect 3 CVOs (alt1, alt2,
  877. alt1/2). However, there may be more than 3 CVOs if some alleles were
  878. pruned at this site.
  879. pruned_alleles: set of strings corresponding to pruned alleles. Used to
  880. filter CVOs for pruned alleles.
  881. Returns:
  882. final_probs: array of shape (1, 9). The 9 values correspond to three model
  883. output distributions. The first is from the image containing alt1, the
  884. second is from the image for alt2, the third is from the image with both
  885. alt1 and alt2.
  886. """
  887. alt_allele_indices_to_probs = {}
  888. first_alt_index = None
  889. second_alt_index = None
  890. # Find the CVOs with two alts, corresponding to the image with alt1 and alt2.
  891. for cvo in call_variants_outputs:
  892. indices = cvo.alt_allele_indices.indices[:]
  893. curr_alleles = [cvo.variant.alternate_bases[i] for i in indices]
  894. curr_alleles_pruned = any([a in pruned_alleles for a in curr_alleles])
  895. # Ignore CVOs containing pruned alleles.
  896. if len(indices) == 2 and not curr_alleles_pruned:
  897. first_alt_index = min(indices)
  898. second_alt_index = max(indices)
  899. probs = cvo.genotype_probabilities[:]
  900. alt_allele_indices_to_probs[(first_alt_index, second_alt_index)] = probs
  901. # Find the single alt CVOs.
  902. for cvo in call_variants_outputs:
  903. if len(cvo.alt_allele_indices.indices[:]) == 1:
  904. index = cvo.alt_allele_indices.indices[0]
  905. if index == first_alt_index or index == second_alt_index:
  906. probs = cvo.genotype_probabilities[:]
  907. alt_allele_indices_to_probs[index] = probs
  908. assert len(alt_allele_indices_to_probs) == 3
  909. # Concatenate all probabilities into one array.
  910. final_probs = np.array([
  911. alt_allele_indices_to_probs[first_alt_index]
  912. + alt_allele_indices_to_probs[second_alt_index]
  913. + alt_allele_indices_to_probs[(first_alt_index, second_alt_index)]
  914. ])
  915. return final_probs
  916. @functools.lru_cache
  917. def get_multiallelic_model(
  918. use_multiallelic_model: bool,
  919. ) -> tf.keras.Model | None:
  920. """Loads and returns the model, which must be in saved model format.
  921. Args:
  922. use_multiallelic_model: if True, use a specialized model for genotype
  923. resolution of multiallelic cases with two alts.
  924. Returns:
  925. A keras model instance if use_multiallelic_model, else None.
  926. """
  927. if not use_multiallelic_model:
  928. return None
  929. curr_dir = os.path.dirname(__file__)
  930. multiallelic_model_path = os.path.join(curr_dir, 'multiallelic_model')
  931. return tf.keras.models.load_model(multiallelic_model_path, compile=False)
  932. def normalize_predictions(predictions: Sequence[float]) -> Sequence[float]:
  933. """Normalize predictions and handle soft-filtered alt alleles."""
  934. if sum(predictions) == 0:
  935. predictions = [1.0] * len(predictions)
  936. denominator = (
  937. sum([i if i != _FILTERED_ALT_PROB else 0.0 for i in predictions]) or 1.0
  938. )
  939. normalized_predictions = [
  940. i / denominator if i != _FILTERED_ALT_PROB else 0.0 for i in predictions
  941. ]
  942. return normalized_predictions
  943. def correct_nonautosome_probabilities(
  944. probabilities: list[float],
  945. variant: variants_pb2.Variant,
  946. ) -> Sequence[float]:
  947. """Recalculate probabilities for non-autosome heterozygous calls."""
  948. n_alleles = len(variant.alternate_bases) + 1
  949. # It is assumed that probabilities are stored in the specific order. See
  950. # most_likely_genotype for details.
  951. # Each heterozyhous probability is zeroed. For example, for biallelic case
  952. # the probability of 0/1 genotype becomes zero.
  953. index = 0
  954. for h1 in range(0, n_alleles):
  955. for h2 in range(0, h1 + 1):
  956. if h2 != h1:
  957. if len(probabilities) <= index:
  958. raise ValueError("Probabilties array doesn't match alt alleles.")
  959. probabilities[index] = 0
  960. index += 1
  961. new_sum = sum(probabilities) or 1.0
  962. return list(map(lambda p: p / new_sum, probabilities))
  963. def is_non_autosome(variant: variants_pb2.Variant) -> bool:
  964. """Returns True if variant is non_autosome."""
  965. haploid_contigs_str = _HAPLOID_CONTIGS.value or ''
  966. parts = haploid_contigs_str.split(',')
  967. # pylint: disable=g-complex-comprehension
  968. haploid_contigs = [item for part in parts for item in part.split()]
  969. # pylint: enable=g-complex-comprehension
  970. return haploid_contigs and variant.reference_name in haploid_contigs # pytype: disable=bad-return-type
  971. def is_in_regions(
  972. variant: variants_pb2.Variant, regions: ranges.RangeSet
  973. ) -> bool:
  974. """Returns True of variant overlaps one of the regions."""
  975. if regions:
  976. return regions.variant_overlaps(variant)
  977. else:
  978. return False
  979. @functools.lru_cache
  980. def get_par_regions() -> ranges.RangeSet | None:
  981. """Returns the cached par regions if specified, else None."""
  982. if _PAR_REGIONS.value:
  983. return ranges.RangeSet.from_bed(_PAR_REGIONS.value, enable_logging=False)
  984. else:
  985. return None
  986. def resolve_call_variant_outputs_by_model(
  987. call_variants_outputs: Sequence[deepvariant_pb2.CallVariantsOutput],
  988. gq_threshold: float,
  989. ) -> Sequence[deepvariant_pb2.CallVariantsOutput]:
  990. """Picks one of the CVO pairs by model ID based on the GQ threshold.
  991. In GQ debug mode, CVOs are written by both models. This means for every
  992. candidate <> alt_allele_indices combination, there are 2 CVOs with class
  993. probabilities from each model. For every alt_allele_indices set, one of the
  994. CVOs is picked based on the GQ threshold, following the same logic as happens
  995. in make_examples: if the GQ threshold is met, the CVO from the small model is
  996. picked. Otherwise, the CVO from the deepvariant model is picked. This allows
  997. multiple postprocess_variant commands to produce many VCFs from a single
  998. deepvariant run, allowing for a detailed analysis of which GQ threshold
  999. is optimal.
  1000. Args:
  1001. call_variants_outputs: list of CVOs for a given variant site.
  1002. gq_threshold: GQ threshold.
  1003. Returns:
  1004. A list of CVOs, one for each alt_allele_indices set from one of the models.
  1005. """
  1006. filtered_by_gq = []
  1007. cvos_grouped_by_alt_allele_indices = itertools.groupby(
  1008. call_variants_outputs,
  1009. lambda x: x.alt_allele_indices,
  1010. )
  1011. for _, cvo_pair in cvos_grouped_by_alt_allele_indices:
  1012. # every group should have 2 CVOs, one for each model, which are sorted into
  1013. # alphabetical order by model ID: ["deepvariant", "small_model"]
  1014. deepvariant_call, small_model_call = sorted(
  1015. cvo_pair,
  1016. key=lambda x: variantcall_utils.get_model_id(x.variant.calls[0]),
  1017. )
  1018. if small_model_inference.passes_confidence_threshold(
  1019. small_model_call.genotype_probabilities, gq_threshold
  1020. ):
  1021. filtered_by_gq.append(small_model_call)
  1022. else:
  1023. filtered_by_gq.append(deepvariant_call)
  1024. return filtered_by_gq
  1025. def merge_predictions(
  1026. call_variants_outputs: Sequence[deepvariant_pb2.CallVariantsOutput],
  1027. qual_filter: float | None = None,
  1028. multiallelic_model: tf.keras.Model | None = None,
  1029. debug_output_all_candidates: str | None = None,
  1030. ) -> tuple[variants_pb2.Variant, Sequence[float]]:
  1031. """Merges the predictions from the multi-allelic calls."""
  1032. # See the logic described in the class PileupImageCreator pileup_image.py
  1033. #
  1034. # Because of the logic above, this function expects all cases above to have
  1035. # genotype_predictions that we can combine from.
  1036. if _RESOLVE_CALL_VARIANTS_OUTPUTS_BY_MODEL.value:
  1037. call_variants_outputs = resolve_call_variant_outputs_by_model(
  1038. call_variants_outputs, _SMALL_MODEL_GQ_THRESHOLD.value
  1039. )
  1040. par_regions = get_par_regions()
  1041. if not call_variants_outputs:
  1042. raise ValueError('Expected 1 or more call_variants_outputs.')
  1043. if not is_valid_call_variants_outputs(call_variants_outputs):
  1044. raise ValueError('`call_variants_outputs` did not pass sanity check.')
  1045. first_call, other_calls = call_variants_outputs[0], call_variants_outputs[1:]
  1046. canonical_variant = first_call.variant
  1047. if not other_calls:
  1048. canonical_variant = variant_utils.simplify_variant_alleles(
  1049. canonical_variant
  1050. )
  1051. if is_non_autosome(canonical_variant) and not is_in_regions(
  1052. canonical_variant, par_regions
  1053. ):
  1054. return canonical_variant, correct_nonautosome_probabilities(
  1055. list(first_call.genotype_probabilities), canonical_variant
  1056. )
  1057. return canonical_variant, first_call.genotype_probabilities
  1058. # Special handling of multiallelic variants
  1059. alt_alleles_to_remove = get_alt_alleles_to_remove(
  1060. call_variants_outputs, qual_filter
  1061. )
  1062. # flattened_probs_dict is only used with the multiallelic model
  1063. flattened_probs_dict = convert_call_variants_outputs_to_probs_dict(
  1064. canonical_variant,
  1065. call_variants_outputs,
  1066. alt_alleles_to_remove,
  1067. debug_output_all_candidates,
  1068. )
  1069. if debug_output_all_candidates == 'INFO':
  1070. struct_utils.add_string_field(
  1071. canonical_variant.info,
  1072. 'CANDIDATES',
  1073. '|'.join(canonical_variant.alternate_bases),
  1074. )
  1075. if debug_output_all_candidates != 'ALT':
  1076. canonical_variant = prune_alleles(canonical_variant, alt_alleles_to_remove)
  1077. # Run alternate model for multiallelic cases.
  1078. num_alts = len(canonical_variant.alternate_bases)
  1079. if num_alts == 2 and multiallelic_model is not None:
  1080. # We have 3 CVOs for 2 alts. In this case, there are 6 possible genotypes.
  1081. cvo_probs = get_multiallelic_distributions(
  1082. call_variants_outputs, alt_alleles_to_remove
  1083. )
  1084. normalized_predictions = multiallelic_model(cvo_probs).numpy().tolist()[0]
  1085. elif _MULTIALLELIC_MODE.value == 'product':
  1086. # New logic: "overlap-count" with product fusion.
  1087. # 1. Collect information about each CVO's example.
  1088. example_info = []
  1089. original_variant = call_variants_outputs[0].variant
  1090. for cvo in call_variants_outputs:
  1091. example_alt_alleles = frozenset(
  1092. original_variant.alternate_bases[i]
  1093. for i in cvo.alt_allele_indices.indices
  1094. )
  1095. is_for_pruned_allele = bool(
  1096. alt_alleles_to_remove.intersection(example_alt_alleles)
  1097. )
  1098. if is_for_pruned_allele and debug_output_all_candidates != 'ALT':
  1099. continue
  1100. probs = (
  1101. (_FILTERED_ALT_PROB,) * _NUM_GENOTYPE_PROBABILITIES
  1102. if is_for_pruned_allele
  1103. else cvo.genotype_probabilities
  1104. )
  1105. example_info.append({'probs': probs, 'alts': example_alt_alleles})
  1106. # 2. Calculate raw probability for each possible genotype.
  1107. predictions = []
  1108. genotype_ordering = variant_utils.genotype_ordering_in_likelihoods(
  1109. canonical_variant
  1110. )
  1111. for _, _, allele1_str, allele2_str in genotype_ordering:
  1112. prob_list_for_genotype = []
  1113. for example in example_info:
  1114. # Check each allele of the diploid genotype independently against the
  1115. # example's alternate alleles. This correctly calculates an overlap of 2
  1116. # for homozygous alternate genotypes.
  1117. overlap = int(allele1_str in example['alts']) + int(
  1118. allele2_str in example['alts']
  1119. )
  1120. prob_list_for_genotype.append(example['probs'][overlap])
  1121. # 3. Fuse probabilities with product.
  1122. if _FILTERED_ALT_PROB in prob_list_for_genotype:
  1123. fused_prob = _FILTERED_ALT_PROB
  1124. else:
  1125. fused_prob = np.prod(prob_list_for_genotype)
  1126. predictions.append(fused_prob)
  1127. # 4. Normalize the final predictions.
  1128. normalized_predictions = normalize_predictions(predictions)
  1129. else:
  1130. def min_alt_filter(probs):
  1131. return min([x for x in probs if x != _FILTERED_ALT_PROB] or [0])
  1132. predictions = [
  1133. min_alt_filter(flattened_probs_dict[(m, n)])
  1134. for _, _, m, n in variant_utils.genotype_ordering_in_likelihoods(
  1135. canonical_variant
  1136. )
  1137. ]
  1138. if sum(predictions) == 0:
  1139. predictions = [1.0] * len(predictions)
  1140. normalized_predictions = normalize_predictions(predictions)
  1141. # Note the simplify_variant_alleles call *must* happen after the predictions
  1142. # calculation above. flattened_probs_dict is indexed by alt allele, and
  1143. # simplify can change those alleles so we cannot simplify until afterwards.
  1144. canonical_variant = variant_utils.simplify_variant_alleles(canonical_variant)
  1145. if is_non_autosome(canonical_variant) and not is_in_regions(
  1146. canonical_variant, par_regions
  1147. ):
  1148. return canonical_variant, correct_nonautosome_probabilities(
  1149. normalized_predictions, canonical_variant
  1150. )
  1151. else:
  1152. return canonical_variant, normalized_predictions
  1153. def should_filter(
  1154. variant: variants_pb2.Variant,
  1155. pon_vcf_reader: vcf.VcfReader,
  1156. padding_bases: int = 0,
  1157. ) -> bool:
  1158. """Returns True if the variant should be filtered based on PON."""
  1159. if pon_vcf_reader is None:
  1160. return False
  1161. query_region = ranges.make_range(
  1162. chrom=variant.reference_name,
  1163. start=variant.start - padding_bases,
  1164. end=variant.end + padding_bases,
  1165. )
  1166. pon_variants = list(pon_vcf_reader.query(query_region))
  1167. if not pon_variants:
  1168. return False
  1169. # TODO: Consider improving this logic to directly match the
  1170. # contig name, the position, and REF and ALT directly.
  1171. variant_key = variant_utils.variant_key(variant)
  1172. for pon_variant in pon_variants:
  1173. if variant_key == variant_utils.variant_key(pon_variant):
  1174. return True
  1175. return False
  1176. def add_pon_filter(
  1177. variant_generator: Iterator[variants_pb2.Variant],
  1178. pon_vcf_reader: vcf.VcfReader,
  1179. ) -> Iterator[variants_pb2.Variant]:
  1180. for variant in variant_generator:
  1181. if dv_vcf_constants.DEEP_VARIANT_PASS in variant.filter and should_filter(
  1182. variant, pon_vcf_reader
  1183. ):
  1184. variant.filter.remove(dv_vcf_constants.DEEP_VARIANT_PASS)
  1185. variant.filter.append(dv_vcf_constants.DEEP_VARIANT_PON)
  1186. yield variant
  1187. def write_variants_to_vcf(
  1188. variant_iterable: Iterator[variants_pb2.Variant],
  1189. output_vcf_path: str,
  1190. header: variants_pb2.VcfHeader,
  1191. ):
  1192. """Writes Variant protos to a VCF file.
  1193. Args:
  1194. variant_iterable: iterable. An iterable of sorted Variant protos.
  1195. output_vcf_path: str. Output file in VCF format.
  1196. header: VcfHeader proto. The VCF header to use for writing the variants.
  1197. """
  1198. logging.info('Writing output to VCF file: %s', output_vcf_path)
  1199. with vcf.VcfWriter(
  1200. output_vcf_path, header=header, round_qualities=True
  1201. ) as writer:
  1202. count = 0
  1203. for variant in variant_iterable:
  1204. if not _ONLY_KEEP_PASS.value or variant.filter == [
  1205. dv_vcf_constants.DEEP_VARIANT_PASS
  1206. ]:
  1207. count += 1
  1208. if _PROCESS_SOMATIC.value:
  1209. writer.write_somatic(variant)
  1210. else:
  1211. writer.write(variant)
  1212. logging.log_every_n(
  1213. logging.INFO, '%s variants written.', _LOG_EVERY_N, count
  1214. )
  1215. logging.info('Total variants written: %s', count)
  1216. def _sort_grouped_variants(group: Sequence[deepvariant_pb2.CallVariantsOutput]):
  1217. return sorted(group, key=lambda x: sorted(x.alt_allele_indices.indices))
  1218. def _transform_call_variant_group_to_output_variant(
  1219. call_variant_group: Sequence[deepvariant_pb2.CallVariantsOutput],
  1220. qual_filter: float,
  1221. multi_allelic_qual_filter: float,
  1222. sample_name: str,
  1223. use_multiallelic_model: bool,
  1224. debug_output_all_candidates: str | None,
  1225. ) -> variants_pb2.Variant:
  1226. """Transforms a group of CalVariantOutput to VariantOutput.
  1227. The group of CVOs present in the call_variants_group are converted to the
  1228. Variant proto, with the following filters applied: 1) variants are omitted
  1229. if their quality is lower than the `qual_filter` threshold. 2) multi-allelic
  1230. variants omit individual alleles whose qualities are lower than the
  1231. `multi_allelic_qual_filter` threshold.
  1232. Args:
  1233. call_variant_group: list[CVO]. A group of CallVariantsOutput protos.
  1234. qual_filter: double. The qual value below which to filter variants.
  1235. multi_allelic_qual_filter: double. The qual value below which to filter
  1236. multi-allelic variants.
  1237. sample_name: str. Sample name to write to VCF file.
  1238. use_multiallelic_model: if True, use a specialized model for genotype
  1239. resolution of multiallelic cases with two alts.
  1240. debug_output_all_candidates: if 'ALT', output all alleles considered by
  1241. DeepVariant as ALT alleles.
  1242. Returns:
  1243. the Variant proto.
  1244. """
  1245. multiallelic_model = get_multiallelic_model(
  1246. use_multiallelic_model=use_multiallelic_model
  1247. )
  1248. outputs = _sort_grouped_variants(call_variant_group)
  1249. canonical_variant, predictions = merge_predictions(
  1250. outputs,
  1251. multi_allelic_qual_filter,
  1252. multiallelic_model=multiallelic_model,
  1253. debug_output_all_candidates=debug_output_all_candidates,
  1254. )
  1255. return add_call_to_variant(
  1256. canonical_variant,
  1257. predictions,
  1258. qual_filter=qual_filter,
  1259. sample_name=sample_name,
  1260. )
  1261. def _transform_call_variants_output_to_variants(
  1262. input_sorted_tfrecord_path: str,
  1263. sample_name: str,
  1264. ) -> Iterator[variants_pb2.Variant]:
  1265. """Yields Variant protos in sorted order from CallVariantsOutput protos.
  1266. Args:
  1267. input_sorted_tfrecord_path: str. TFRecord format file containing sorted
  1268. CallVariantsOutput protos.
  1269. sample_name: str. Sample name use in the output VCF and gVCF.
  1270. Yields:
  1271. Variant protos in sorted order representing the CallVariantsOutput calls.
  1272. """
  1273. for call_variant_group in group_call_variants_outputs(
  1274. input_sorted_tfrecord_path, _GROUP_VARIANTS.value
  1275. ):
  1276. yield _transform_call_variant_group_to_output_variant(
  1277. call_variant_group,
  1278. _QUAL_FILTER.value,
  1279. _MULT_ALLELIC_QUAL_FILTER.value,
  1280. sample_name,
  1281. _USE_MULTIALLELIC_MODEL.value,
  1282. _DEBUG_OUTPUT_ALL_CANDIDATES.value,
  1283. )
  1284. def dump_variants_to_temp_file(
  1285. variant_protos: Iterator[variants_pb2.Variant],
  1286. ) -> tempfile._TemporaryFileWrapper:
  1287. temp = tempfile.NamedTemporaryFile()
  1288. tfrecord.write_tfrecords(variant_protos, temp.name)
  1289. return temp
  1290. def group_call_variants_outputs(
  1291. input_sorted_tfrecord_path: str, group_variants: bool
  1292. ) -> Iterator[Sequence[deepvariant_pb2.CallVariantsOutput]]:
  1293. """Yields CallVariantOutputs grouped by their variant range.
  1294. Args:
  1295. input_sorted_tfrecord_path: str. TFRecord format file containing sorted
  1296. CallVariantsOutput protos.
  1297. group_variants: bool. If true, group variants that have same start and end
  1298. position.
  1299. """
  1300. group_fn = None
  1301. if group_variants:
  1302. group_fn = lambda x: variant_utils.variant_range(x.variant)
  1303. for _, group in itertools.groupby(
  1304. tfrecord.read_tfrecords(
  1305. input_sorted_tfrecord_path, proto=deepvariant_pb2.CallVariantsOutput
  1306. ),
  1307. group_fn,
  1308. ):
  1309. yield list(group)
  1310. def _concat_vcf(
  1311. output_file: str, temp_vcf_files: Sequence[tempfile._TemporaryFileWrapper]
  1312. ) -> None:
  1313. """Concatenates a set of temp (g)VCF files."""
  1314. vcf_files_to_concat = [f.name for f in temp_vcf_files]
  1315. vcf_concat.concat(output_file, vcf_files_to_concat)
  1316. def process_contiguous_partition(
  1317. contiguous_range_set: Sequence[range_pb2.Range],
  1318. contigs: Sequence[reference_pb2.ContigInfo],
  1319. cvo_paths: Sequence[str],
  1320. temp_file_name: str,
  1321. sample_name: str,
  1322. ) -> Iterator[variants_pb2.Variant]:
  1323. """Postprocess all CVOs in the given partition and returns an iterator.
  1324. Args:
  1325. contiguous_range_set: set of contiguous ranges to load and transform.
  1326. contigs: all contigs from ref
  1327. cvo_paths: paths to all CVO files
  1328. temp_file_name: path to temp file to variants to.
  1329. sample_name: the sample name to use for the output VCF and gVCF.
  1330. Returns:
  1331. An iterator of processed variants.
  1332. """
  1333. start_time = time.time()
  1334. num_cvo_records = postprocess_variants_lib.process_single_sites_tfrecords(
  1335. contigs,
  1336. cvo_paths,
  1337. temp_file_name,
  1338. contiguous_range_set,
  1339. )
  1340. if contiguous_range_set:
  1341. logging.info(
  1342. 'Processing region %s:%s-%s:%s',
  1343. contiguous_range_set[0].reference_name,
  1344. contiguous_range_set[0].start,
  1345. contiguous_range_set[-1].reference_name,
  1346. contiguous_range_set[-1].end,
  1347. )
  1348. logging.info('CVO sorting took %s minutes', (time.time() - start_time) / 60)
  1349. if num_cvo_records == 0:
  1350. return iter([])
  1351. logging.info('Transforming call_variants_output to variants.')
  1352. independent_variants = _transform_call_variants_output_to_variants(
  1353. input_sorted_tfrecord_path=temp_file_name,
  1354. sample_name=sample_name,
  1355. )
  1356. variant_generator = haplotypes.maybe_resolve_conflicting_variants(
  1357. independent_variants, qual_filter=_QUAL_FILTER.value
  1358. )
  1359. logging.info('Processed %s variants.', num_cvo_records)
  1360. return variant_generator
  1361. def _yield_variants_from_temp_files(
  1362. temp_files: Sequence[tempfile._TemporaryFileWrapper],
  1363. ) -> Iterable[variants_pb2.Variant]:
  1364. """Yields variants from all the temp files in order.
  1365. Args:
  1366. temp_files: a list of NamedTemporaryFiles objects
  1367. Yields:
  1368. variants read in order from the given temp files.
  1369. """
  1370. for temp_file in temp_files:
  1371. for variant in tfrecord.read_tfrecords(
  1372. temp_file.name, proto=variants_pb2.Variant
  1373. ):
  1374. yield variant
  1375. def _decide_to_use_csi(contigs: Sequence[reference_pb2.ContigInfo]) -> bool:
  1376. """Return True if CSI index is to be used over tabix index format.
  1377. If the length of any reference chromosomes exceeds 512M
  1378. (here we use 5e8 to keep a safety margin), we will choose csi
  1379. as the index format. Otherwise we use tbi as default.
  1380. Args:
  1381. contigs: list of contigs.
  1382. Returns:
  1383. A boolean variable indicating if the csi format is to be used or not.
  1384. """
  1385. max_chrom_length = max([c.n_bases for c in contigs])
  1386. return max_chrom_length > 5e8
  1387. def build_index(vcf_file: str, csi: bool = False) -> None:
  1388. """A helper function for indexing VCF files.
  1389. Args:
  1390. vcf_file: string. Path to the VCF file to be indexed.
  1391. csi: bool. If true, index using the CSI format.
  1392. """
  1393. if csi:
  1394. tabix.build_csi_index(vcf_file, min_shift=14)
  1395. else:
  1396. tabix.build_index(vcf_file)
  1397. def get_cvo_paths(cvo_file_spec: str) -> list[str]:
  1398. """Returns sharded filenames for the `cvo_file_spec` parameter."""
  1399. if sharded_file_utils.is_sharded_file_spec(cvo_file_spec):
  1400. # Input is already sharded, so dynamic sharding check is disabled.
  1401. paths = sharded_file_utils.maybe_generate_sharded_filenames(cvo_file_spec)
  1402. else:
  1403. # Input is expected to be dynamically sharded.
  1404. filename_resolver = cvo_file_spec.replace('.tfrecord.gz', '*')
  1405. all_files = sharded_file_utils.glob_list_sharded_file_patterns(
  1406. filename_resolver
  1407. )
  1408. filename_pattern = cvo_file_spec.replace(
  1409. '.tfrecord.gz', '@' + str(len(all_files)) + '.tfrecord.gz'
  1410. )
  1411. paths = sharded_file_utils.maybe_generate_sharded_filenames(
  1412. filename_pattern
  1413. )
  1414. # This check is to make sure all files we glob is exactly the same as the
  1415. # paths we create, otherwise we have multiple file patterns.
  1416. if sorted(all_files) != sorted(paths):
  1417. raise ValueError(
  1418. 'Found multiple file patterns in input filename space: ',
  1419. cvo_file_spec,
  1420. )
  1421. return paths
  1422. def get_first_cvo_record(
  1423. paths: Sequence[str],
  1424. ) -> deepvariant_pb2.CallVariantsOutput | None:
  1425. """Returns the first record from the given paths."""
  1426. return dv_utils.get_one_example_from_examples_path(
  1427. ','.join(paths), proto=deepvariant_pb2.CallVariantsOutput
  1428. )
  1429. def get_sample_name(cvo_paths: Sequence[str]) -> str:
  1430. """Determines the sample name to be used for the output VCF and gVCF.
  1431. We check the following sources to determine the sample name and use the first
  1432. name available:
  1433. 1) CallVariantsOutput
  1434. 2) nonvariant site TFRecords
  1435. 3) --sample_name flag
  1436. 4) default sample name
  1437. Args:
  1438. cvo_paths: file paths to all CVO files.
  1439. Returns:
  1440. sample_name used when writing the output VCF and gVCF.
  1441. """
  1442. record = get_first_cvo_record(cvo_paths)
  1443. gvcf_record = None
  1444. if _NONVARIANT_SITE_TFRECORD_PATH.value:
  1445. gvcf_record = dv_utils.get_one_example_from_examples_path(
  1446. _NONVARIANT_SITE_TFRECORD_PATH.value, proto=variants_pb2.Variant
  1447. )
  1448. if record is not None:
  1449. sample_name = _extract_single_sample_name(record)
  1450. logging.info(
  1451. 'Using sample name from call_variants output. Sample name: %s',
  1452. sample_name,
  1453. )
  1454. if _SAMPLE_NAME.value:
  1455. logging.info('--sample_name is set but was not used.')
  1456. elif (
  1457. _NONVARIANT_SITE_TFRECORD_PATH.value and gvcf_record and gvcf_record.calls
  1458. ):
  1459. sample_name = gvcf_record.calls[0].call_set_name
  1460. logging.info(
  1461. (
  1462. 'call_variants output is empty, so using sample name from TFRecords'
  1463. ' at --nonvariant_site_tfrecord_path. Sample name: %s'
  1464. ),
  1465. sample_name,
  1466. )
  1467. if _SAMPLE_NAME.value:
  1468. logging.info('--sample_name is set but was not used.')
  1469. elif _SAMPLE_NAME.value:
  1470. sample_name = _SAMPLE_NAME.value
  1471. logging.info(
  1472. (
  1473. 'call_variants output and nonvariant TFRecords are empty. Using'
  1474. ' sample name set with --sample_name. Sample name: %s'
  1475. ),
  1476. sample_name,
  1477. )
  1478. else:
  1479. sample_name = dv_constants.DEFAULT_SAMPLE_NAME
  1480. logging.info(
  1481. (
  1482. 'Could not determine sample name and --sample_name is unset. Using'
  1483. ' the default sample name. Sample name: %s'
  1484. ),
  1485. sample_name,
  1486. )
  1487. return sample_name
  1488. def _merge_phasing_blocks(
  1489. input_path: str,
  1490. switches_output_path: str,
  1491. output_path: str,
  1492. ) -> None:
  1493. """Reads the phased reads TSV file and loads them into a dictionary.
  1494. Args:
  1495. input_path: path to the phased reads TSV file.
  1496. switches_output_path: path to the switches output TSV file.
  1497. output_path: path to the output TSV file.
  1498. """
  1499. merger = merge_phased_reads_lib.Merger()
  1500. merger.load_from_files(input_path)
  1501. merger.merge_reads(switches_output_path)
  1502. merger.correct_and_print_read_stats(output_path)
  1503. def _load_phasing_info(
  1504. switches_output_path: str,
  1505. ) -> dict[tuple[str, str], int]:
  1506. """Loads the phasing info from the merge_reads output.
  1507. Args:
  1508. switches_output_path: path to the switches output TSV file.
  1509. Returns:
  1510. A map from (shard, region) to a boolean indicating whether the phasing
  1511. blocks in the shard and region need to be switched.
  1512. """
  1513. phasing_info = {}
  1514. with open(switches_output_path, 'r') as f:
  1515. for line in f:
  1516. shard, region, switch_status = line.split('\t')
  1517. phasing_info[shard, region] = int(switch_status)
  1518. return phasing_info
  1519. def run_postprocess_variants_on_region(
  1520. output_vcf: str,
  1521. output_gvcf: str,
  1522. partition: Sequence[range_pb2.Range],
  1523. contigs: Sequence[reference_pb2.ContigInfo],
  1524. all_cvo_paths: Sequence[str],
  1525. header: variants_pb2.VcfHeader,
  1526. is_empty: bool,
  1527. sample_name: str,
  1528. emit_variants_as_tfrecords: bool,
  1529. output_tfrecord_file: str,
  1530. ) -> None:
  1531. """Runs postprocess_variants on the given partition.
  1532. If the partition is empty, we process all CVO records. If the partition is not
  1533. empty, we process only the CVO records in the partition.
  1534. Args:
  1535. output_vcf: path to the output VCF file.
  1536. output_gvcf: path to the output gVCF file.
  1537. partition: a list of nucleus.genomics.v1.Range protos.
  1538. contigs: all contigs from ref
  1539. all_cvo_paths: paths to all CVO files
  1540. header: the VCF header
  1541. is_empty: if the partition is empty.
  1542. sample_name: the sample name to use for the output VCF and gVCF.
  1543. emit_variants_as_tfrecords: if True, emit variants as TFRecords.
  1544. output_tfrecord_file: path to the output TFRecord file.
  1545. Returns:
  1546. None (the output is written to the output_vcf and output_gvcf files).
  1547. """
  1548. temp = tempfile.NamedTemporaryFile()
  1549. start_time = time.time()
  1550. if not is_empty:
  1551. variant_generator = process_contiguous_partition(
  1552. partition,
  1553. contigs,
  1554. all_cvo_paths,
  1555. temp.name,
  1556. sample_name,
  1557. )
  1558. pon_reader = (
  1559. vcf.VcfReader(_PON_FILTERING.value) if _PON_FILTERING.value else None
  1560. )
  1561. variant_generator = add_pon_filter(variant_generator, pon_reader)
  1562. else:
  1563. logging.info('call_variants_output is empty. Writing out empty VCF.')
  1564. variant_generator = iter([])
  1565. logging.info(
  1566. 'Processing variants (and writing to temporary files) took %s minutes',
  1567. (time.time() - start_time) / 60,
  1568. )
  1569. if emit_variants_as_tfrecords:
  1570. tfrecord.write_tfrecords(
  1571. variant_generator,
  1572. output_tfrecord_file,
  1573. compression_type='',
  1574. )
  1575. else:
  1576. emit_variants_to_vcf(
  1577. output_vcf,
  1578. output_gvcf,
  1579. header,
  1580. partition,
  1581. variant_generator,
  1582. )
  1583. temp.close()
  1584. def emit_variants_to_vcf(
  1585. output_vcf: str,
  1586. output_gvcf: str,
  1587. header: variants_pb2.VcfHeader,
  1588. partition: Sequence[range_pb2.Range],
  1589. variant_generator: Iterator[variants_pb2.Variant],
  1590. variant_tfrecord_path: str = '',
  1591. ) -> None:
  1592. """Writes variants either from an iterator or a TFRecord file to VCF/gVCF.
  1593. Args:
  1594. output_vcf: path to the output VCF file.
  1595. output_gvcf: path to the output gVCF file.
  1596. header: the VCF header
  1597. partition: a list of nucleus.genomics.v1.Range protos.
  1598. variant_generator: an iterator of variants to write to the VCF file.
  1599. variant_tfrecord_path: path to the variant TFRecord file.
  1600. """
  1601. start_time = time.time()
  1602. if not _NONVARIANT_SITE_TFRECORD_PATH.value:
  1603. if _PROCESS_SOMATIC.value:
  1604. logging.info('Writing variants to somatic VCF.')
  1605. else:
  1606. logging.info('Writing variants to VCF.')
  1607. if variant_tfrecord_path:
  1608. variant_generator = tfrecord.read_tfrecords(
  1609. variant_tfrecord_path, variants_pb2.Variant
  1610. )
  1611. write_variants_to_vcf(
  1612. variant_iterable=variant_generator,
  1613. output_vcf_path=output_vcf,
  1614. header=header,
  1615. )
  1616. logging.info(
  1617. 'VCF creation took %s minutes', (time.time() - start_time) / 60
  1618. )
  1619. else:
  1620. tmp_variant_file = None
  1621. if not variant_tfrecord_path:
  1622. tmp_variant_file = dump_variants_to_temp_file(variant_generator)
  1623. variant_tfrecord_path = tmp_variant_file.name
  1624. merge_variants.merge_and_write_variants_and_nonvariants(
  1625. _ONLY_KEEP_PASS.value,
  1626. variant_tfrecord_path,
  1627. tfrecord.expanded_paths_if_sharded(
  1628. _NONVARIANT_SITE_TFRECORD_PATH.value
  1629. ),
  1630. _REF.value,
  1631. output_vcf,
  1632. output_gvcf,
  1633. header,
  1634. partition,
  1635. _PROCESS_SOMATIC.value,
  1636. )
  1637. if tmp_variant_file:
  1638. tmp_variant_file.close()
  1639. logging.info(
  1640. 'VCF and gVCF creation took %s minutes.',
  1641. (time.time() - start_time) / 60,
  1642. )
  1643. def stitch_phase_sets(
  1644. *,
  1645. tfrecord_paths: Sequence[str],
  1646. switches_output_path: str,
  1647. output_tfrecord_paths: Sequence[str],
  1648. ) -> None:
  1649. """Stitches the phase sets of the variants in the tfrecord files."""
  1650. postprocess_variants_lib.stitch_phase_sets(
  1651. tfrecord_paths, switches_output_path, output_tfrecord_paths
  1652. )
  1653. def _process_partitions_in_parallel(
  1654. *,
  1655. contigs: Sequence[reference_pb2.ContigInfo],
  1656. all_cvo_paths: Sequence[str],
  1657. header: variants_pb2.VcfHeader,
  1658. is_empty: bool,
  1659. sample_name: str,
  1660. emit_variants_as_tfrecords: bool,
  1661. temp_vcf_files: Sequence[tempfile._TemporaryFileWrapper],
  1662. temp_gvcf_files: Sequence[tempfile._TemporaryFileWrapper],
  1663. temp_tfrecord_files: Sequence[tempfile._TemporaryFileWrapper],
  1664. temp_tfrecord_output_files: Sequence[tempfile._TemporaryFileWrapper],
  1665. partitions: Sequence[Sequence[range_pb2.Range]],
  1666. num_partitions: int,
  1667. ):
  1668. """Processes multiple partitions in parallel.
  1669. Args:
  1670. contigs: all contigs from ref
  1671. all_cvo_paths: paths to all CVO files
  1672. header: the VCF header
  1673. is_empty: if the partition is empty.
  1674. sample_name: the sample name to use for the output VCF and gVCF.
  1675. emit_variants_as_tfrecords: if True, emit variants as TFRecords.
  1676. temp_vcf_files: temporary VCF files.
  1677. temp_gvcf_files: temporary gVCF files.
  1678. temp_tfrecord_files: temporary TFRecord files.
  1679. temp_tfrecord_output_files: temporary TFRecord output files.
  1680. partitions: the partitions to process.
  1681. num_partitions: the number of partitions.
  1682. Returns:
  1683. None (the output is written to the output_vcf and output_gvcf files).
  1684. """
  1685. logging.info(
  1686. 'Running postprocess_variants with parallelism using %s CPUs over'
  1687. ' %s partitions.',
  1688. _CPUS.value,
  1689. num_partitions,
  1690. )
  1691. with multiprocessing.Pool(_CPUS.value) as pool:
  1692. tasks = []
  1693. for task_id in range(num_partitions):
  1694. tasks.append(
  1695. (
  1696. temp_vcf_files[task_id].name,
  1697. temp_gvcf_files[task_id].name,
  1698. partitions[task_id],
  1699. contigs,
  1700. all_cvo_paths,
  1701. header,
  1702. is_empty,
  1703. sample_name,
  1704. emit_variants_as_tfrecords,
  1705. temp_tfrecord_files[task_id].name,
  1706. ),
  1707. )
  1708. async_result = pool.starmap_async(run_postprocess_variants_on_region, tasks)
  1709. async_result.get()
  1710. if emit_variants_as_tfrecords:
  1711. stitch_phase_sets(
  1712. tfrecord_paths=[t.name for t in temp_tfrecord_files],
  1713. switches_output_path=_PHASED_READS_SWITCHES_OUTPUT_PATH.value,
  1714. output_tfrecord_paths=[t.name for t in temp_tfrecord_output_files],
  1715. )
  1716. tasks = []
  1717. for task_id in range(num_partitions):
  1718. tasks.append((
  1719. temp_vcf_files[task_id].name,
  1720. temp_gvcf_files[task_id].name,
  1721. header,
  1722. partitions[task_id],
  1723. iter([]),
  1724. temp_tfrecord_output_files[task_id].name,
  1725. ))
  1726. async_result = pool.starmap_async(emit_variants_to_vcf, tasks)
  1727. async_result.get()
  1728. def _process_partitions_sequentially(
  1729. *,
  1730. contigs: Sequence[reference_pb2.ContigInfo],
  1731. all_cvo_paths: Sequence[str],
  1732. header: variants_pb2.VcfHeader,
  1733. is_empty: bool,
  1734. sample_name: str,
  1735. emit_variants_as_tfrecords: bool,
  1736. temp_vcf_files: Sequence[tempfile._TemporaryFileWrapper],
  1737. temp_gvcf_files: Sequence[tempfile._TemporaryFileWrapper],
  1738. temp_tfrecord_files: Sequence[tempfile._TemporaryFileWrapper],
  1739. temp_tfrecord_output_files: Sequence[tempfile._TemporaryFileWrapper],
  1740. partitions: Sequence[Sequence[range_pb2.Range]],
  1741. num_partitions: int,
  1742. ):
  1743. """Processes multiple partitions sequentially.
  1744. This mode minimizes the memory usage.
  1745. Args:
  1746. contigs: all contigs from ref
  1747. all_cvo_paths: paths to all CVO files
  1748. header: the VCF header
  1749. is_empty: if the partition is empty.
  1750. sample_name: the sample name to use for the output VCF and gVCF.
  1751. emit_variants_as_tfrecords: if true, emit variants as TFRecords.
  1752. temp_vcf_files: temporary VCF files.
  1753. temp_gvcf_files: temporary gVCF files.
  1754. temp_tfrecord_files: temporary TFRecord files.
  1755. temp_tfrecord_output_files: temporary TFRecord output files.
  1756. partitions: the partitions to process.
  1757. num_partitions: the number of partitions.
  1758. Returns:
  1759. None (the output is written to the output_vcf and output_gvcf files).
  1760. """
  1761. logging.info(
  1762. 'Running postprocess_variants sequentially over %s partitions.',
  1763. num_partitions,
  1764. )
  1765. for task_id in range(num_partitions):
  1766. run_postprocess_variants_on_region(
  1767. temp_vcf_files[task_id].name,
  1768. temp_gvcf_files[task_id].name,
  1769. partitions[task_id],
  1770. contigs,
  1771. all_cvo_paths,
  1772. header,
  1773. is_empty,
  1774. sample_name,
  1775. emit_variants_as_tfrecords,
  1776. temp_tfrecord_files[task_id].name,
  1777. )
  1778. if emit_variants_as_tfrecords:
  1779. stitch_phase_sets(
  1780. tfrecord_paths=[t.name for t in temp_tfrecord_files],
  1781. switches_output_path=_PHASED_READS_SWITCHES_OUTPUT_PATH.value,
  1782. output_tfrecord_paths=[t.name for t in temp_tfrecord_output_files],
  1783. )
  1784. for task_id in range(num_partitions):
  1785. emit_variants_to_vcf(
  1786. temp_vcf_files[task_id].name,
  1787. temp_gvcf_files[task_id].name,
  1788. header,
  1789. partitions[task_id],
  1790. iter([]),
  1791. temp_tfrecord_output_files[task_id].name,
  1792. )
  1793. def run_postprocessing_over_multiple_partitions(
  1794. *,
  1795. contigs: Sequence[reference_pb2.ContigInfo],
  1796. all_cvo_paths: Sequence[str],
  1797. header: variants_pb2.VcfHeader,
  1798. is_empty: bool,
  1799. sample_name: str,
  1800. ) -> None:
  1801. """Runs postprocessing over multiple partitions.
  1802. Args:
  1803. contigs: all contigs from ref
  1804. all_cvo_paths: paths to all CVO files
  1805. header: the VCF header
  1806. is_empty: if the partition is empty.
  1807. sample_name: the sample name to use for the output VCF and gVCF.
  1808. Returns:
  1809. None (the output is written to the output_vcf and output_gvcf files).
  1810. """
  1811. calling_regions = calling_regions_utils.build_calling_regions(
  1812. contigs=contigs,
  1813. regions_to_include=calling_regions_utils.parse_regions_flag(
  1814. _REGIONS.value
  1815. ),
  1816. regions_to_exclude=[],
  1817. ref_n_regions=[],
  1818. )
  1819. num_partitions = max(_NUM_PARTITIONS.value, _CPUS.value)
  1820. partitions = calling_regions_utils.partition_calling_regions(
  1821. calling_regions, num_partitions=num_partitions
  1822. )
  1823. temp_vcf_files = [
  1824. tempfile.NamedTemporaryFile(suffix='.gz') for _ in partitions
  1825. ]
  1826. temp_gvcf_files = [
  1827. tempfile.NamedTemporaryFile(suffix='.gz') for _ in partitions
  1828. ]
  1829. temp_tfrecord_files = [
  1830. tempfile.NamedTemporaryFile(suffix='.tfrecord') for _ in partitions
  1831. ]
  1832. temp_tfrecord_output_files = [
  1833. tempfile.NamedTemporaryFile(suffix='.tfrecord') for _ in partitions
  1834. ]
  1835. emit_variants_as_tfrecords = bool(_PHASED_READS_INPUT_PATH.value)
  1836. if _CPUS.value > 1:
  1837. _process_partitions_in_parallel(
  1838. contigs=contigs,
  1839. all_cvo_paths=all_cvo_paths,
  1840. header=header,
  1841. is_empty=is_empty,
  1842. sample_name=sample_name,
  1843. emit_variants_as_tfrecords=emit_variants_as_tfrecords,
  1844. temp_vcf_files=temp_vcf_files,
  1845. temp_gvcf_files=temp_gvcf_files,
  1846. temp_tfrecord_files=temp_tfrecord_files,
  1847. temp_tfrecord_output_files=temp_tfrecord_output_files,
  1848. partitions=partitions,
  1849. num_partitions=num_partitions,
  1850. )
  1851. else:
  1852. _process_partitions_sequentially(
  1853. contigs=contigs,
  1854. all_cvo_paths=all_cvo_paths,
  1855. header=header,
  1856. is_empty=is_empty,
  1857. sample_name=sample_name,
  1858. emit_variants_as_tfrecords=emit_variants_as_tfrecords,
  1859. temp_vcf_files=temp_vcf_files,
  1860. temp_gvcf_files=temp_gvcf_files,
  1861. temp_tfrecord_files=temp_tfrecord_files,
  1862. temp_tfrecord_output_files=temp_tfrecord_output_files,
  1863. partitions=partitions,
  1864. num_partitions=num_partitions,
  1865. )
  1866. _concat_vcf(_OUTFILE.value, temp_vcf_files)
  1867. if _NONVARIANT_SITE_TFRECORD_PATH.value:
  1868. _concat_vcf(_GVCF_OUTFILE.value, temp_gvcf_files)
  1869. for temp_vcf_file in temp_vcf_files:
  1870. temp_vcf_file.close()
  1871. for temp_gvcf_file in temp_gvcf_files:
  1872. temp_gvcf_file.close()
  1873. for temp_tfrecord_file in temp_tfrecord_files:
  1874. temp_tfrecord_file.close()
  1875. for temp_tfrecord_output_file in temp_tfrecord_output_files:
  1876. temp_tfrecord_output_file.close()
  1877. def run_postprocessing_without_partitioning(
  1878. *,
  1879. contigs: Sequence[reference_pb2.ContigInfo],
  1880. all_cvo_paths: Sequence[str],
  1881. header: variants_pb2.VcfHeader,
  1882. is_empty: bool,
  1883. sample_name: str,
  1884. ) -> None:
  1885. """Runs postprocessing without any partitioning.
  1886. Args:
  1887. contigs: all contigs from ref
  1888. all_cvo_paths: paths to all CVO files
  1889. header: the VCF header
  1890. is_empty: if the partition is empty.
  1891. sample_name: the sample name to use for the output VCF and gVCF.
  1892. Returns:
  1893. None (the output is written to the output_vcf and output_gvcf files).
  1894. """
  1895. logging.info(
  1896. 'Running postprocess_variants without parallelism or partitions.'
  1897. )
  1898. emit_variants_as_tfrecords = bool(_PHASED_READS_INPUT_PATH.value)
  1899. tmp_tfrecord_file = None
  1900. tmp_tfrecord_file_name = ''
  1901. if emit_variants_as_tfrecords:
  1902. tmp_tfrecord_file = tempfile.NamedTemporaryFile(suffix='.tfrecord')
  1903. tmp_tfrecord_file_name = tmp_tfrecord_file.name
  1904. run_postprocess_variants_on_region(
  1905. _OUTFILE.value,
  1906. _GVCF_OUTFILE.value,
  1907. [],
  1908. contigs,
  1909. all_cvo_paths,
  1910. header,
  1911. is_empty,
  1912. sample_name,
  1913. emit_variants_as_tfrecords,
  1914. tmp_tfrecord_file_name,
  1915. )
  1916. if emit_variants_as_tfrecords:
  1917. output_tfrecord_file = tempfile.NamedTemporaryFile(suffix='.tfrecord')
  1918. stitch_phase_sets(
  1919. tfrecord_paths=[tmp_tfrecord_file_name],
  1920. switches_output_path=_PHASED_READS_SWITCHES_OUTPUT_PATH.value,
  1921. output_tfrecord_paths=[output_tfrecord_file.name],
  1922. )
  1923. emit_variants_to_vcf(
  1924. _OUTFILE.value,
  1925. _GVCF_OUTFILE.value,
  1926. header,
  1927. [],
  1928. iter([]),
  1929. output_tfrecord_file.name,
  1930. )
  1931. output_tfrecord_file.close()
  1932. if tmp_tfrecord_file:
  1933. tmp_tfrecord_file.close()
  1934. def apply_flags_for_postprocessing(flags_obj):
  1935. """Read flags for postprocessing from the model.example_info.json file.
  1936. Args:
  1937. flags_obj: The flag values object.
  1938. """
  1939. if not flags_obj.checkpoint_json:
  1940. return
  1941. logging.info(
  1942. 'Reading flags_for_postprocessing from %s', flags_obj.checkpoint_json
  1943. )
  1944. with tf.io.gfile.GFile(flags_obj.checkpoint_json, 'r') as fin:
  1945. flags_map = json.load(fin).get('flags_for_postprocessing', {})
  1946. if flags_map:
  1947. logging.info(
  1948. 'Flags for postprocessing:\n%s',
  1949. '\n'.join([f'{k}: {v}' for k, v in flags_map.items()]),
  1950. )
  1951. for flag_name, flag_value in flags_map.items():
  1952. if flag_name not in flags_obj:
  1953. logging.warning(
  1954. 'Flag "%s" from json is not defined as an application flag.',
  1955. flag_name,
  1956. )
  1957. continue
  1958. flag = flags_obj[flag_name]
  1959. if flag.present:
  1960. if flag.value != flag_value:
  1961. logging.warning(
  1962. 'Flag %s is specified in model.example_info.json [%s] but '
  1963. 'overridden by command line with value [%s]',
  1964. flag_name,
  1965. flag_value,
  1966. flag.value,
  1967. )
  1968. continue
  1969. flag.value = flag_value
  1970. def main(argv=()):
  1971. with errors.clean_commandline_error_exit():
  1972. apply_flags_for_postprocessing(flags.FLAGS)
  1973. if len(argv) > 1:
  1974. errors.log_and_raise(
  1975. 'Command line parsing failure: postprocess_variants does not accept '
  1976. 'positional arguments but some are present on the command line: '
  1977. '"{}".'.format(str(argv)),
  1978. errors.CommandLineError,
  1979. )
  1980. del argv # Unused.
  1981. if (not _NONVARIANT_SITE_TFRECORD_PATH.value) != (not _GVCF_OUTFILE.value):
  1982. errors.log_and_raise(
  1983. (
  1984. 'gVCF creation requires both nonvariant_site_tfrecord_path and '
  1985. 'gvcf_outfile flags to be set.'
  1986. ),
  1987. errors.CommandLineError,
  1988. )
  1989. if (
  1990. _USE_MULTIALLELIC_MODEL.value
  1991. and _DEBUG_OUTPUT_ALL_CANDIDATES.value == 'ALT'
  1992. ):
  1993. errors.log_and_raise(
  1994. (
  1995. 'debug_output_all_candidates=ALT is incompatible with the '
  1996. 'multiallelic model. Use INFO instead.'
  1997. ),
  1998. errors.CommandLineError,
  1999. )
  2000. if _NUM_PARTITIONS.value > 0 and _NUM_PARTITIONS.value < _CPUS.value:
  2001. logging.warning(
  2002. '--num_partitions is less than --cpus. Setting --num_partitions to'
  2003. ' --cpus=%s',
  2004. _CPUS.value,
  2005. )
  2006. proto_utils.uses_fast_cpp_protos_or_die()
  2007. logging_level.set_from_flag()
  2008. fasta_reader = pysam.FastaFile(
  2009. filename=_pysam_resolve_file_path(_REF.value)
  2010. )
  2011. contigs = []
  2012. for reference_index in range(fasta_reader.nreferences):
  2013. contigs.append(
  2014. reference_pb2.ContigInfo(
  2015. name=fasta_reader.references[reference_index],
  2016. n_bases=fasta_reader.lengths[reference_index],
  2017. pos_in_fasta=reference_index,
  2018. )
  2019. )
  2020. cvo_paths = get_cvo_paths(_INFILE.value)
  2021. small_model_cvo_paths = []
  2022. if _SMALL_MODEL_CVO_RECORDS.value:
  2023. small_model_cvo_paths = get_cvo_paths(_SMALL_MODEL_CVO_RECORDS.value)
  2024. all_cvo_paths = cvo_paths + small_model_cvo_paths
  2025. sample_name = get_sample_name(all_cvo_paths)
  2026. header = dv_vcf_constants.deepvariant_header(
  2027. contigs=contigs,
  2028. sample_names=[sample_name],
  2029. add_info_candidates=_DEBUG_OUTPUT_ALL_CANDIDATES.value == 'INFO',
  2030. include_model_id=_SMALL_MODEL_CVO_RECORDS.value is not None,
  2031. include_somatic_fields=_PROCESS_SOMATIC.value,
  2032. )
  2033. if _PROCESS_SOMATIC.value:
  2034. header.filters.append(
  2035. variants_pb2.VcfFilterInfo(
  2036. id=dv_vcf_constants.DEEP_VARIANT_GERMLINE,
  2037. description='Non somatic variants',
  2038. )
  2039. )
  2040. if _PON_FILTERING.value:
  2041. if not _PROCESS_SOMATIC.value:
  2042. raise ValueError(
  2043. 'PON filtering is only supported for somatic variant calling.'
  2044. )
  2045. header.filters.append(
  2046. variants_pb2.VcfFilterInfo(
  2047. id=dv_vcf_constants.DEEP_VARIANT_PON,
  2048. description='Filtered by Panel of Normals (PON)',
  2049. )
  2050. )
  2051. if _PHASED_READS_INPUT_PATH.value:
  2052. logging.info(
  2053. 'Attempting to merge phasing blocks from %s',
  2054. _PHASED_READS_INPUT_PATH.value,
  2055. )
  2056. logging.info(
  2057. 'Writing switches to %s',
  2058. _PHASED_READS_SWITCHES_OUTPUT_PATH.value,
  2059. )
  2060. logging.info(
  2061. 'Writing corrected reads to %s',
  2062. _PHASED_READS_CORRECTED_OUTPUT_PATH.value,
  2063. )
  2064. _merge_phasing_blocks(
  2065. _PHASED_READS_INPUT_PATH.value,
  2066. _PHASED_READS_SWITCHES_OUTPUT_PATH.value,
  2067. _PHASED_READS_CORRECTED_OUTPUT_PATH.value,
  2068. )
  2069. is_empty = get_first_cvo_record(all_cvo_paths) is None
  2070. # Run sequentially in the absence of multiple CPUs or partitions.
  2071. if _CPUS.value < 1 and _NUM_PARTITIONS.value < 1:
  2072. run_postprocessing_without_partitioning(
  2073. contigs=contigs,
  2074. all_cvo_paths=all_cvo_paths,
  2075. header=header,
  2076. is_empty=is_empty,
  2077. sample_name=sample_name,
  2078. )
  2079. else:
  2080. run_postprocessing_over_multiple_partitions(
  2081. contigs=contigs,
  2082. all_cvo_paths=all_cvo_paths,
  2083. header=header,
  2084. is_empty=is_empty,
  2085. sample_name=sample_name,
  2086. )
  2087. start_time = time.time()
  2088. use_csi = _decide_to_use_csi(contigs)
  2089. if str(_OUTFILE.value).endswith('.gz'):
  2090. build_index(_OUTFILE.value, use_csi)
  2091. if _NONVARIANT_SITE_TFRECORD_PATH.value and str(
  2092. _GVCF_OUTFILE.value
  2093. ).endswith('.gz'):
  2094. build_index(_GVCF_OUTFILE.value, use_csi)
  2095. logging.info(
  2096. 'Indexing VCF and gVCF took %s minutes.',
  2097. (time.time() - start_time) / 60,
  2098. )
  2099. if __name__ == '__main__':
  2100. flags.mark_flags_as_required(['infile', 'outfile', 'ref'])
  2101. logging.set_verbosity(logging.INFO)
  2102. logging.get_absl_logger().setLevel(logging.INFO)
  2103. app.run(main)

postprocess_variants.py at commit 45f2627, under BSD-3-Clause · at the source

Overview

Authors: Andrew B Munkacsi1,2, Gabriella E Conway1, Keri Multerer1, Michael D Jackson1, Lauren Csaki3, Tamayanthi Rajakumar1, Jeffrey P Sheridan1, Eliatan Niktab1, Wuh-Liang Hwu4,5, Yin-Hsiu Chien5, Mark Walterfang6, Cintya Del Rio Hernandez1, Robin B Chan7, Bowen Zhou8, Renu Nandakumar7, Peixang Zhang3, Gilbert Di Paolo9, Robert A Maue10, Karen Reue3, Stephen L Sturley11
  1. School of Biological Sciences, Victoria University of Wellington, Wellington, New Zealand
  2. Centre for Biodiscovery, Victoria University of Wellington, Wellington, New Zealand
  3. Department of Human Genetics, University of California, Los Angeles, Los Angeles, CA, USA
  4. Center for Precision Medicine, China Medical University Hospital, Taichung, Taiwan
  5. Department of Pediatrics, National Taiwan University Hospital, Taipei, Taiwan
  6. Department Neuropsychiatry Centre, Royal Melbourne Hospital, Melbourne, VIC, Australia
  7. Department of Pathology and Cell Biology, Columbia University Medical Center, New York, NY, USA
  8. Case Western Reserve University School of Medicine, Cleveland, OH, USA
  9. Denali Therapeutics Inc., San Francisco, CA, USA
  10. Departments of Biochemistry and Cell Biology and of Medical Education, Geisel School of Medicine at Dartmouth, Hanover, NH, USA
  11. Department of Biology, Barnard College at Columbia University, New York, NY, USA
Journal: iScience, volume 29, issue 6, article 116121
Dates: received 26 November 2024; accepted 11 May 2026; published online 28 May 2026
Type: Research article · Language: English
License: CC BY-NC-ND
Identifiers: DOI 10.1016/j.isci.2026.116121 · PMID 42256287 · PMCID PMC13233811 · OpenAlex W7162675976
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: genetics / omics (modality), human (organism), mouse (organism)
Methods: Statistics, Smoothing, state filtering, decompositions, Preprocessing, Graphs
Keywords: Disease, Neurogenetics, Genomics, Lipidomics, Model organism
Topic: Lysosomal Storage Disorders Research (Physiology, Medicine), according to OpenAlex
Funding: National Institutes of Health (T32 HL07343); Dartmouth Geisel School of Medicine (200128); National Institute of Diabetes and Digestive and Kidney Diseases (DK54320); Charles H. Revson Foundation (T32 HL07343); NIDDK NIH HHS (R01 DK128898); National Niemann Pick Disease Foundation Inc; Victoria University of Wellington; Research for Life; Ara Parseghian Medical Research Foundation
Citations: not cited yet (Europe PMC); 94 references in the paper

Abstract

The abstract is not reproduced here: the paper's license (CC BY-NC-ND) does not allow it. Read it in the paper, at the publisher or on Europe PMC.

Repository

Its files are read in the Code ↔ Paper reader above, with 3 matches between paragraphs and lines of code.

google/deepvariant

License: BSD-3-Clause
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: 45f2627504c59785ea2b88d0256a2ec347bce7b4, 18 March 2026
Languages: Python (205), C++ (170), C/C++ (98), Shell (18), Jupyter (1), C (1)
Size: 924 files, 493 scripts
Software Heritage: archived
Found in: the resources table
Holds: README, license file, environment (Dockerfile, Dockerfile.deepsomatic, Dockerfile.deeptrio, Dockerfile.pangenome_aware_deepvariant, Dockerfile.tpu-train, setup.py), tests, documentation, 1 notebook
Not found: CITATION.cff, continuous integration
Tools: TensorFlow (44 files), NumPy (28 files), pandas (6 files), pysam (4 files), Keras (3 files), Pillow (2 files), BCFtools (1 file), JAX (1 file), SAMtools (1 file), scikit-learn (1 file)
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
495 files

The paper's code and data availability statement is in the Data section.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 493 scripts, each with its path and the digest of its content;
  • 3 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

Datasets cited

Code and data availability statement

The paper has a code and data availability statement. Its license (CC BY-NC-ND) does not allow reproducing it here; in short, from what the harvester recognized in it:

  • it points to a dataset: figshare 28049867
  • it says that the data are available on request
  • it says that the code is available on request

Read it in the paper: doi.org/10.1016/j.isci.2026.116121.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 1, 28 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 20 authors, 5 keywords, 9 funders, 92 references.

Cite

This paper

Munkacsi, A. B., Conway, G. E., Multerer, K., Jackson, M. D., Csaki, L., Rajakumar, T., Sheridan, J. P., Niktab, E., Hwu, W.-L., Chien, Y.-H., Walterfang, M., Del Rio Hernandez, C., Chan, R. B., Zhou, B., Nandakumar, R., Zhang, P., Di Paolo, G., Maue, R. A., Reue, K., & Sturley, S. L. (2026). Multi-omics reveal phosphatidic acid phosphatases modify Niemann-Pick type C disease severity. iScience, 29(6), 116121. https://doi.org/10.1016/j.isci.2026.116121

BibTeX

@article{munkacsi2026multi,
author = {Munkacsi, Andrew B and Conway, Gabriella E and Multerer, Keri and Jackson, Michael D and Csaki, Lauren and Rajakumar, Tamayanthi and Sheridan, Jeffrey P and Niktab, Eliatan and Hwu, Wuh-Liang and Chien, Yin-Hsiu and Walterfang, Mark and Del Rio Hernandez, Cintya and Chan, Robin B and Zhou, Bowen and Nandakumar, Renu and Zhang, Peixang and Di Paolo, Gilbert and Maue, Robert A and Reue, Karen and Sturley, Stephen L},
title = {{Multi-omics reveal phosphatidic acid phosphatases modify Niemann-Pick type C disease severity}},
journal = {iScience},
year = {2026},
month = may,
volume = {29},
number = {6},
pages = {116121},
publisher = {Elsevier},
issn = {2589-0042},
doi = {10.1016/j.isci.2026.116121},
url = {https://doi.org/10.1016/j.isci.2026.116121},
pmid = {42256287},
pmcid = {PMC13233811}
}

RIS

TY - JOUR
AU - Munkacsi, Andrew B
AU - Conway, Gabriella E
AU - Multerer, Keri
AU - Jackson, Michael D
AU - Csaki, Lauren
AU - Rajakumar, Tamayanthi
AU - Sheridan, Jeffrey P
AU - Niktab, Eliatan
AU - Hwu, Wuh-Liang
AU - Chien, Yin-Hsiu
AU - Walterfang, Mark
AU - Del Rio Hernandez, Cintya
AU - Chan, Robin B
AU - Zhou, Bowen
AU - Nandakumar, Renu
AU - Zhang, Peixang
AU - Di Paolo, Gilbert
AU - Maue, Robert A
AU - Reue, Karen
AU - Sturley, Stephen L
TI - Multi-omics reveal phosphatidic acid phosphatases modify Niemann-Pick type C disease severity
T2 - iScience
J2 - iScience
PY - 2026
DA - 2026/05/28
VL - 29
IS - 6
SP - 116121
SN - 2589-0042
PB - Elsevier
DO - 10.1016/j.isci.2026.116121
UR - https://doi.org/10.1016/j.isci.2026.116121
LA - en
ER -

CSL-JSON

{
"id": "10.1016/j.isci.2026.116121",
"type": "article-journal",
"title": "Multi-omics reveal phosphatidic acid phosphatases modify Niemann-Pick type C disease severity",
"container-title": "iScience",
"author": [
{
"family": "Munkacsi",
"given": "Andrew B"
},
{
"family": "Conway",
"given": "Gabriella E"
},
{
"family": "Multerer",
"given": "Keri"
},
{
"family": "Jackson",
"given": "Michael D"
},
{
"family": "Csaki",
"given": "Lauren"
},
{
"family": "Rajakumar",
"given": "Tamayanthi"
},
{
"family": "Sheridan",
"given": "Jeffrey P"
},
{
"family": "Niktab",
"given": "Eliatan"
},
{
"family": "Hwu",
"given": "Wuh-Liang"
},
{
"family": "Chien",
"given": "Yin-Hsiu"
},
{
"family": "Walterfang",
"given": "Mark"
},
{
"family": "Del Rio Hernandez",
"given": "Cintya"
},
{
"family": "Chan",
"given": "Robin B"
},
{
"family": "Zhou",
"given": "Bowen"
},
{
"family": "Nandakumar",
"given": "Renu"
},
{
"family": "Zhang",
"given": "Peixang"
},
{
"family": "Di Paolo",
"given": "Gilbert"
},
{
"family": "Maue",
"given": "Robert A"
},
{
"family": "Reue",
"given": "Karen"
},
{
"family": "Sturley",
"given": "Stephen L"
}
],
"container-title-short": "iScience",
"volume": "29",
"issue": "6",
"page": "116121",
"DOI": "10.1016/j.isci.2026.116121",
"PMID": "42256287",
"PMCID": "PMC13233811",
"ISSN": "2589-0042",
"publisher": "Elsevier",
"URL": "https://doi.org/10.1016/j.isci.2026.116121",
"language": "en",
"issued": {
"date-parts": [
[
2026,
5,
28
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1038/s42003-026-10957-8 [code]
Brain defence by the extracellular matrix protein Cochlin.
Journal: Communications biology
In common: BCFtools, JAX, SAMtools, 6 other tools, mouse
[2] doi:10.1038/s41467-026-71790-5 [code]
Recurrent DNA break clusters drive replication-stress-induced copy number variants and genome diversification.
Journal: Nature communications
In common: BCFtools, pysam, SAMtools, 4 other tools, genetics / omics, mouse
[3] doi:10.1007/s00262-026-04390-3 [code]
Identification and prioritisation of tumour antigen candidates from 79 glioblastoma transcriptomes.
Journal: Cancer immunology, immunotherapy : CII
In common: BCFtools, pysam, SAMtools, 3 other tools, genetics / omics, 1 reference
[4] doi:10.1186/s13059-026-04177-w [code]
Genomic sequence evolution underlying human neocortical interareal diversification.
Journal: Genome biology
In common: pysam, SAMtools, TensorFlow, 4 other tools, genetics / omics, mouse
[5] doi:10.1038/s41592-026-03057-2 [code]
CREsted: modeling genomic and synthetic cell-type-specific enhancers across tissues and species.
Journal: Nature methods
In common: pysam, Keras, TensorFlow, 4 other tools, genetics / omics, mouse
[6] doi:10.1016/j.xcrm.2026.102766 [code]
A longitudinal single-cell and spatial multiomic atlas of pediatric high-grade glioma.
Journal: Cell reports. Medicine
In common: pysam, Keras, TensorFlow, 4 other tools, genetics / omics
[7] doi:10.1016/j.isci.2026.116945
Neurodegeneration in the olfactory system in Niemann Pick type C1 disease.
Journal: iScience
In common: mouse, 5 references
[8] doi:10.1038/s44320-026-00208-7 [code]
Interpretable deep generative ensemble learning for single-cell omics with Hydra.
Journal: Molecular systems biology
In common: pysam, Keras, TensorFlow, 3 other tools, 1 reference
[9] doi:10.1038/s44400-026-00094-8 [code]
Haplotype-resolved DNA methylation at the &lt;i&gt;APOE&lt;/i&gt; locus identifies allele-specific epigenetic signatures relevant to Alzheimer's disease risk.
Journal: NPJ dementia
In common: BCFtools, pysam, scikit-learn, 2 other tools, genetics / omics, 1 reference
[10] doi:10.21203/rs.3.rs-9927928/v1 [code]
Genome-wide and allele-resolved maps of the radial architecture of the mouse genome
Journal: Research Square (preprint)
In common: BCFtools, pysam, SAMtools, 2 other tools, genetics / omics, mouse

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.